@polymorphism-tech/morph-spec 8.32.0 → 8.33.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 CHANGED
@@ -120,6 +120,7 @@ When modifying framework components, verify the full scope of related files.
120
120
  | `src/lib/git/pending-prs.js` | os TRÊS consumidores da mesma síntese "aguardando merge": `list.js` (linha sintetizada), `fleet-scanner.js` (entrada `pr-pending`) e `doctor.js` (`auditPullRequests`). Uma feature visível numa superfície e invisível na outra é o bug que este módulo existe para impedir |
121
121
  | `src/lib/git/gh.js` | `pr-index.js` e `finish.js` — as DUAS formas de invocar o `gh` viraram uma; uma segunda cópia de `resolveGh` volta a tornar o caminho de sucesso do índice intestável |
122
122
  | `src/lib/worktree/feature-truth.js` | The four leaders reading the same "which tree owns this feature" predicate: `fleet-scanner.js`, `list.js`, `handoff.js`, and the mutator guard (`approve.js`/`advance.js`/`score.js`/`gate-decision.js`/`gate-check.js`) |
123
+ | `src/lib/tasks/test-runner.js` | `verify-runner.js` (o nó `tests` repassa o `--timeout` e lê `timedOut`), `src/commands/project/worktree.js` (as constantes `WORKTREES_DIR`/`LEGACY_WORKTREES_DIR` que a descoberta ignora — duplicar as strings aqui é como o bug volta), `test/lib/test-runner.test.js` (o canário do ignore de worktrees e o do timeout escalado). Um worktree é o checkout de OUTRA feature: varrê-lo dobra o relógio e reporta sobre código fora da árvore avaliada |
123
124
  | `src/lib/graph/**` | `test/lib/graph/*.test.js`, the committed fixture `test/fixtures/graph/namespace-hierarchy.graph.json` (ADR-003 regression — never hand-edit it), `doctor.js` (`codebase graph` check), `architecture-scanner.js` (centrality section), `setup-infra.js` step 18b |
124
125
  | `src/lib/validators/reuse/**` | `validation-runner.js` (case + push universal), `framework/rubrics/*.json` (o id `reuse` mapeado numa categoria, senão o achado não pesa no score), `test/fixtures/graph/prospectpro-reuse.graph.json` (recorte do grafo real — regerar com `build-reuse-fixture.mjs`, nunca editar à mão) |
125
126
  | o ciclo de reuso (`reuse-map.md`) | `output-schema.js` (+ `npm run generate`), `framework/schemas/tasks.schema.json` + `tasks-json-parser.js` (campo `reuse`), `morph-plan` §1c/§5b, template de dispatch de `morph-implement`/`morph-apply`, personas de `agents.json`, `evals/rubrics/code-quality.md` + `condensed.md`, `morph-review` (insumo do avaliador), `test/framework/graph-fallback.test.js` |
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > Spec-driven development framework for multi-stack projects. Turns feature requests into implementation-ready code through structured, AI-orchestrated phases.
4
4
 
5
5
  **Package:** `@polymorphism-tech/morph-spec`
6
- **Version:** 8.32.0
6
+ **Version:** 8.33.0
7
7
  **Requires:** Node.js 22+, Claude Code
8
8
 
9
9
  ---
@@ -379,4 +379,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
379
379
 
380
380
  ---
381
381
 
382
- *morph-spec v8.32.0 by [Polymorphism Tech](https://polymorphism.tech)*
382
+ *morph-spec v8.33.0 by [Polymorphism Tech](https://polymorphism.tech)*
package/bin/morph-spec.js CHANGED
@@ -365,7 +365,7 @@ program
365
365
  .description('Run the deterministic verification chain (build + tests + validators + e2e) — code before the LLM judge')
366
366
  .option('--json', 'Output as JSON')
367
367
  .option('--rubric <id>', 'Force a rubric (default: by task group → config → default)')
368
- .option('--timeout <ms>', 'Build timeout in ms (default 120000)', (v) => parseInt(v, 10))
368
+ .option('--timeout <ms>', 'Timeout in ms for the build AND tests nodes (build default 120000; tests default 120000 per test project, or project.testTimeoutMs)', (v) => parseInt(v, 10))
369
369
  .option('--skip-tests', 'Skip the test node')
370
370
  .option('--skip-build', 'Skip the build node')
371
371
  .option('--skip-e2e', 'Skip the e2e node (Docker bring-up + Playwright)')
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "8.32.0",
3
+ "version": "8.33.0",
4
4
  "displayName": "MORPH-SPEC Framework",
5
5
  "description": "Spec-driven development with 38 agents and 8-phase workflow for .NET/Blazor/Next.js/Azure",
6
6
  "publisher": "polymorphism-tech",
@@ -186,4 +186,4 @@ Never edited directly — go through CLI commands or `morph-spec update` instead
186
186
 
187
187
  ---
188
188
 
189
- *morph-spec v8.32.0 by Polymorphism Tech*
189
+ *morph-spec v8.33.0 by Polymorphism Tech*
@@ -249,4 +249,4 @@ morph-spec doctor
249
249
 
250
250
  ---
251
251
 
252
- *morph-spec v8.32.0 by Polymorphism Tech*
252
+ *morph-spec v8.33.0 by Polymorphism Tech*
@@ -1,68 +1,68 @@
1
- # morph-spec CLI — Referência Completa
2
-
3
- > Referência completa (flags, semântica, exemplos) dos comandos `morph-spec` usados pela LLM em vez de editar o estado à mão. Resumo rápido em `.morph/framework/CLAUDE.md`; racional dos gates em `.morph/framework/MORPH.md` §5, dispatch de sub-agents em §6, worktree/fleet em §2.2.
4
-
5
- ---
6
-
7
- ## Gates e estado
8
-
9
- - **`morph-spec approve <feature> <gate> --mode auto`** — aprova um gate (`proposal`/`uiux`/`plan`/`review`) em `trust: auto`. **`uiux`** é o único gate cujo `AskUserQuestion` o hook `state-sync.js` não reconhece por texto (só casa "Gate 1/2/3") — aprove-o sempre explicitamente por este comando ou por `advance`, nunca contando com o hook. **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).
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).
11
- - **`morph-spec score <feature> <task> <score> [--dims a,c,q,t]`** — grava `taskScores` no feature.json com timestamp server-side; **nunca edite taskScores à mão**. Em PowerShell, cite o valor de `--dims` entre aspas (`--dims "9.5,9,9,9"`) — sem aspas, a lista pode chegar como tokens separados; o parser aceita ambas as formas, mas citar evita qualquer ambiguidade.
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`, 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).
13
- - **`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`/`score`/`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/score/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
-
16
- ## Features
17
-
18
- - **`morph-spec create <feature> --description "<o quê/porquê>" --type <feature|bug|chore|hotfix> [--request "<txt>"] [--name "<nome>"] [--worktree]`** — cria a estrutura da feature + `feature.json` canônico explicitamente (nascimento explícito no início da proposal; o hook state-sync continua cobrindo o caminho implícito como fallback). **Identidade no nascimento:** `name` (default = slug) e `description` são carimbados; a `--description` é **obrigatória** numa feature nova (ou `--request`, que a semeia) — sem nenhuma das duas o `create` recusa (fim dos scaffolds anônimos). `--type` classifica direto; `--request "<pedido>"` roda o detector determinístico (empate/sem sinal sai com exit 2 e candidatos — a LLM decide e re-roda com `--type` + `--request`/`--description`); `--worktree` já nasce a feature no seu worktree próprio. **Toda feature nasce numa branch `morph/{f}`** (ramificada da default) mesmo sem `--worktree` — por isso o fechamento por `morph-spec finish` é idêntico nos dois fluxos.
19
- - **`morph-spec list [--all] [--json] [--no-branch]`** — o quadro de auditoria: uma linha por feature × workType × status × fase × gates G1/G2/G3 × descrição, com `⚠` para inconsistências (sem descrição, ordem de gate). Uma feature com worktree/branch ativos tem a cópia da RAIZ congelada no instante do nascimento (`create --worktree` grava lá um esqueleto completo e nunca mais atualiza) — por padrão o `list` lê a verdade commitada em `morph/{f}` quando ela diverge do disco desta árvore (mesma leitura do `fleet`), reportando `source: 'branch'|'disk'` e uma coluna **WHERE** (`raiz` / `branch morph/{f}` / `worktree {dir}` / `⚠ worktree ausente`). `--no-branch` volta ao disco puro, sem custo de git — **e também pula a consulta de PR** (nenhuma coluna, nenhuma linha sintetizada, ver abaixo): não é só o modo disk-only de antes, é o modo offline completo. Um ponteiro `worktree.path` que não existe mais aparece com o flag `worktree-missing` (repare com `doctor --worktrees --prune`). Uma feature já arquivada em outra árvore/branch, cujo esqueleto sobrevive aqui, ganha o flag `archived-elsewhere` em vez de sumir da lista (read-only: nunca ressuscita nem poda). Coluna **PR** mostra `#{n} {estado}` (`-` sem PR para a branch da feature). Uma feature cujo `finish --pr` rodou mas cujo PR segue aberto — sem cópia local nesta árvore — aparece como linha **sintetizada**: fase `aguardando-merge`, flag `pr-pending`, WHERE `branch morph/{f}`, descrição "aguardando merge há Nd — sem cópia local" (mesma síntese que `fleet` e `doctor` usam, de `src/lib/git/pending-prs.js` — uma feature visível numa dessas telas e invisível na outra é o bug que esse módulo compartilhado existe para impedir). `--all` inclui as arquivadas; `--json` para scripting. É o complemento do `delete` (enxergar o que está morto antes de remover).
20
- - **`morph-spec delete <feature> [--archive] [--force] [--with-worktree]`** — remove uma feature. Hard por padrão (pasta apagada, git-aware, + entrada podada do índice raiz para não ressuscitar); `--archive` move para `.morph/archive` com status `abandoned` em vez de apagar. Recusa se algum gate estiver aprovado, a menos que `--force`. Recusa TAMBÉM se um worktree vivo estiver em outra árvore (`feature-has-worktree` — apagar o registro da raiz órfão o worktree+branch em silêncio) — `--force` **não** dispensa isto (é consentimento sobre gate, não sobre filesystem); feche primeiro com `finish`/`worktree remove`, ou passe `--with-worktree` para desmontar o worktree (PROVE-or-DECLINE) e apagar num passo só — a branch sobrevive por padrão (`--delete-branch` para descartá-la também). Use para faxina de scaffolds mortos e drafts abandonados (`archive` é para trabalho concluído; `delete` é para trabalho morto).
21
- - **`morph-spec retype <feature> <newType>`** — muda o tipo de trabalho mid-flight; desfaz só os gates carimbados por política, preserva aprovações humanas; bloqueado após o Gate 3.
22
-
23
- ## Worktree / Fleet
24
-
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. `--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}`. Flags: `--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.
26
- - **`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
- - **`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.
28
- - **`morph-spec worktree setup|link|remove|list`** — gerencia o ciclo do worktree por feature (`setup` cria `worktrees/{f}/` na branch `morph/{f}`, aloca o bloco de portas e escreve `.morph/worktree.env`; `link` religa infra compartilhada; `list` mostra fase/gate de cada worktree ativo). **Fechar a feature** continua sendo `morph-spec finish` (acima).
29
- - **`morph-spec worktree remove [feature] [--target <path>] [--discard-ignored] [--delete-branch]`** — a remoção **segura** de um worktree, sem fechar a feature. 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 regenerável (`bin/`, `obj/`, `node_modules/`, `__pycache__/`, `test-results/`, `.auth/`, `*.tsbuildinfo`) é descartado (`discardedIgnored`); o que existe **só ali** é copiado para `.git/morph-spec/finish-backup/` e a remoção **prossegue** (`ignoredBackup` + `backedUp`). 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.
30
- - **`morph-spec branch prune [--remote] [--dry-run] [--json]`** — apaga as branches locais cujo trabalho **já está** na default, **incluindo as squash-merged que o git não enxerga**. O problema que resolve: num repo que faz squash-merge, o commit de merge entra em `origin/master` mas os SHAs da branch não — `git branch --merged` não a lista, `git branch -d` recusa, e sobra o `-D`, indistinguível de descartar trabalho de verdade. Cada branch é classificada com evidência impressa: `merged-ancestral` (o git prova), `merged-squash` (PR `MERGED` pelo `gh`, **ou** árvore idêntica à da default), `open` (PR OPEN — nunca apagada), `in-worktree` (em uso — nunca apagada) e `unmerged` (**não foi possível provar** integração: sem `gh`, sem default resolvível ou trabalho de verdade — todas tratadas igual, de propósito). Só as duas primeiras são apagadas. Branch remota só com `--remote` explícito.
31
- - **`morph-spec handoff <feature> [--here]`** — regenera o documento de retomada (`.morph/features/{f}/handoff.md`): comprimido, derivado, aponta para os artefatos em vez de copiá-los. **Resolve a árvore dona** antes de gerar (mesma lógica do `list`/`fleet`) e escreve LÁ, não necessariamente no cwd — rodar da raiz com o worktree vivo é seguro: o handoff sai correto e no lugar certo, em vez do esqueleto de nascimento da raiz. Sem árvore nenhuma materializada (só existe commitado em `morph/{f}`) recusa com `feature-not-in-tree`, nomeando a branch, em vez de gerar um documento apontando para arquivos inexistentes. `--here` força construir/escrever no cwd mesmo assim. Gerado automaticamente em `setup`/`create --worktree`/`advance`/`approve`.
32
- - **`morph-spec fleet`** — visão da frota: toda feature ativa (raiz + worktrees) com fase, gates pendentes, tasks, **remoto** (`local`/`pushed`/`PR #N`), **claim** (que sessão está na feature), portas, handoff e atividade. Use entre sessões para decidir onde atuar.
33
- - **`morph-spec claim [feature] [--release] [--list] [--all] [--json]`** — posse **advisory** de feature. Cada sessão reclama sua feature ativa automaticamente (hook SessionStart) e renova o heartbeat a cada tool call; o registro vive em `<git-common-dir>/morph-spec/claims.json`, compartilhado por todos os worktrees. **`morph-spec claim <feature>` (sem flags) ADQUIRE** — se esta árvore já tinha claim vivo em outra feature, ele é liberado no mesmo passo (o CLI reporta "movido de X para Y"); é o jeito de trocar de feature no meio da tarefa sem a dança de dois passos `--release` + reclamar. `--list` força a leitura sem adquirir mesmo passando uma feature. **Claims avisam, nunca bloqueiam** — mas se o SessionStart avisar que outra sessão tem claim vivo, **confirme antes de `finish`/`archive`/`delete`**: duas sessões na mesma feature já custaram um recap.md inteiro e um sub-agent (o worktree sumiu debaixo dele). Claim órfão de sessão morta: `morph-spec claim <f> --release`. **O board mostra só claims VIVOS** (dentro do TTL de 30 min): um claim morto não avisa mais nada, e deixá-lo listado transformava o quadro em ruído (features encerradas há 20h e 73h ainda aparecendo). `--all` mostra o registro inteiro; a contagem dos omitidos sai no rodapé, nunca em silêncio.
34
-
35
- ## Validação
36
-
37
- - **`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. `--mermaid` nunca é bloqueado.
38
- - **`morph-spec verify <feature> [task] [--json] [--skip-e2e] [--fresh-e2e]`** — 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.
39
- - **`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. `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.
40
- - **`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.
41
- **Ciclo de vida das features:** ordem de gates, feature sem identidade, pasta órfã, entrada de índice morta, `branch-drift` (a cópia da raiz atrasada em relação ao `morph/{f}` commitado) e `abandoned-scaffold`. Este último é o esqueleto que o `create --worktree` deixa para trás: `feature.json` sozinho, `in_progress`, **0 artefatos, 0 gates aprovados** e `morph/{f}` **sem commit próprio** (a branch ainda apontando para o commit de merge de outra PR). As quatro condições valem juntas e só depois de **24h** — recém-nascida tem exatamente a mesma forma, e alarme em todo `create` é alarme que ninguém lê. Fora de repo git, ou com a default branch indeterminável, o check cala (não dá para provar). Reparo: `morph-spec delete <f> --archive`, ou retome a feature.
42
- **Pull requests:** `pr-awaiting-merge` (PR aberto há mais de 7 dias sem cópia local nesta árvore) e `pr-merged-not-landed` (PR já `MERGED` mas `.morph/archive/{f}` ainda não chegou aqui — sinal de `git pull` faltando). Offline-safe: sem `gh`, sem remoto no GitHub, ou qualquer falha, a checagem sai **não verificado** (nunca `ok`) — `[]` (perguntei, nada encontrado) e `null` (não pude perguntar) nunca se confundem, e o "não verificado" nunca deixa o `doctor` mais barulhento nem muda o exit code.
43
- Ver `--worktrees` abaixo para a reconciliação de worktrees (modo separado).
44
- - **`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). 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.
45
- - **`morph-spec report-check <feature> [task] [--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). 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). Protocolo completo em MORPH.md §6.
46
-
47
- ## Grafo de codebase
48
-
49
- Grafo de símbolos extraído localmente pelo [graphify](https://pypi.org/project/graphifyy/) (versão pinada **0.9.50**, AST via tree-sitter, **zero token**), servido atrás de uma camada de saneamento própria. **Tudo é fail-open:** sem Python, sem grafo, com grafo velho ou corrompido, o framework segue por grep — é assim que ele sempre funcionou, e nenhuma skill pode exigir o grafo (ADR-002).
50
-
51
- O ganho medido **não é recall**: contra o grep, o grafo não achou um único arquivo a mais. É precisão — em 12 símbolos do ProspectPRO, o grep aponta 1.092 arquivos onde existem 266 usos reais (58% de ruído), e o grafo ainda diz *que tipo* de relação é cada um. `IHandler` aparece em 193 arquivos no grep e tem **2** usos reais: é implementada por 98.
52
-
53
- **Por que não consumir o graphify cru.** O resolvedor C# dele não implementa escopo hierárquico de namespace, então um tipo usado por um namespace filho sem `using` explícito vira um *stub* por arquivo — e o blast radius do símbolo real devolve **zero** consumidores onde existem dois. Falso negativo silencioso é o pior modo de falha possível antes de um hotfix. A camada de saneamento coalesce esses stubs (+91,9% de arestas de uso recuperadas no MORPH_GHLBrain), trata como externo só o que **não tem nenhum** nó com arquivo (BCL/NuGet), e devolve as opções quando o símbolo é genuinamente ambíguo em vez de escolher por conta própria.
54
-
55
- - **`morph-spec graph status [-p <path>] [--json]`** — existência, tamanho, **frescor** e saúde do saneamento. Grafo ausente **sai 0**, não 1: não ter grafo é um estado válido do projeto. O frescor compara o `built_at_commit` gravado no `graph.json` com o `HEAD` — e importa porque o hook `post-commit` do graphify rebuilda **em background**: logo depois de um commit, o grafo ainda é o do commit anterior. `--json` devolve `{ available, path, nodes, links, freshness: {state}, version: {installed, pinned, matches}, sanitize: {coalescible, external, ambiguous} }`.
56
- - **`morph-spec graph build [-p <path>] [--json]`** — extração completa (`graphify extract --code-only`). Gera o `.graphifyignore` stack-aware antes de extrair, porque um grafo que nasce com o ruído dentro não se conserta depois. Projeto **sem nenhum arquivo de código** (o caso de um `init` recém-feito) sai **0** com aviso, não erro. Sem graphify no PATH: exit 1 com `reason: graphify-unavailable` e o comando de instalação — nunca stack trace.
57
- - **`morph-spec graph refresh [-p <path>] [--json]`** — atualização incremental (`graphify update`), 2 a 3× mais rápida que o build graças ao cache por hash de conteúdo.
58
- - **`morph-spec graph affected <símbolo> [--prod] [--uses-only] [-p <path>] [--json]`** — blast radius saneado. `--uses-only` exclui `inherits`/`implements`/`extends`, que é o que separa *"20 implementam"* de *"2 usam"*; `--prod` descarta consumidores em projeto de teste. Erros são estados, não exceções: `no-graph`, `not-found` e `ambiguous` (este devolvendo as opções com path e id) saem 1 com a dica de fallback para grep.
59
- - **`morph-spec graph explain <símbolo> [-p <path>] [--json]`** — vizinhança com citação `arquivo:linha`, separando quem depende dele de do que ele depende. **Prefira `explain` a `query`** nas personas: no eval, `explain` foi consistentemente mais preciso, enquanto `query` trunca por budget e traz ruído. Cada aresta traz a procedência em `via` (`direct`, `coalesced`, `language-disambiguated`), então dá para ver exatamente o que o saneamento recuperou.
60
-
61
- **O que o resultado significa muda com a stack.** Em backend, blast radius é *quem chama*. Em frontend, o grafo é estrutura de módulos e não call graph (`imports: 78` contra `calls: 3` no LP_Polymorphism), então ali ele responde *quem importa*. Ler um como o outro leva a conclusão errada, e é por isso que as personas dizem isso no próprio texto.
62
-
63
- O servidor MCP (`graphify-mcp`, 10 tools, todas aceitando `project_path`) está registrado em `framework/mcp/registry.json` para exploração interativa e consulta cross-repo — mas ele devolve o grafo **cru**, sem saneamento. Para qualquer decisão, prefira os comandos acima.
64
-
65
- ## Observabilidade
66
-
67
- - **`morph-spec telemetry [--json] [--feature <f>] [--limit <n>]`** — reconstrói o caminho de execução da sessão a partir de `.morph/logs/events.jsonl` (skills invocadas, personas despachadas + prompts, standards/base de conhecimento lidos, mix de tools, prompts do usuário). Capturado silenciosamente pelo hook `telemetry-log` em todo tool call e todo UserPromptSubmit.
68
- - **`morph-spec cost [feature] [--by task|phase] [--all-trees] [--json]`** — custo da execução a partir do mesmo `events.jsonl`: duração por task (TaskStart→TaskEnd), bytes de contexto re-ingerido por tipo (`rubric`/`standard`/`skill`/`agents`/`plan-artifact`/`code`), dispatches + tamanho de prompt e mix de tools. Bytes re-ingeridos são **proxy determinística** de custo de token (tokens reais não eram observáveis por hook até o evento `SessionUsage` — quando presente no stream, `inputTokens`/`outputTokens`/`cacheReadTokens`/`cacheCreationTokens` são somados e exibidos à parte como "tokens reais", nunca misturados com a proxy de bytes). `--all-trees` agrega `.morph/logs/events.jsonl` de **toda árvore git** que tiver um (não só a corrente) — reusa a mesma enumeração de worktree do `doctor --worktrees`/`fleet`; mostra um resumo por árvore + o total combinado (`--by` aplica ao agregado combinado). Use antes/depois de mudar skill ou fluxo para medir o efeito.
1
+ # morph-spec CLI — Referência Completa
2
+
3
+ > Referência completa (flags, semântica, exemplos) dos comandos `morph-spec` usados pela LLM em vez de editar o estado à mão. Resumo rápido em `.morph/framework/CLAUDE.md`; racional dos gates em `.morph/framework/MORPH.md` §5, dispatch de sub-agents em §6, worktree/fleet em §2.2.
4
+
5
+ ---
6
+
7
+ ## Gates e estado
8
+
9
+ - **`morph-spec approve <feature> <gate> --mode auto`** — aprova um gate (`proposal`/`uiux`/`plan`/`review`) em `trust: auto`. **`uiux`** é o único gate cujo `AskUserQuestion` o hook `state-sync.js` não reconhece por texto (só casa "Gate 1/2/3") — aprove-o sempre explicitamente por este comando ou por `advance`, nunca contando com o hook. **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).
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).
11
+ - **`morph-spec score <feature> <task> <score> [--dims a,c,q,t]`** — grava `taskScores` no feature.json com timestamp server-side; **nunca edite taskScores à mão**. Em PowerShell, cite o valor de `--dims` entre aspas (`--dims "9.5,9,9,9"`) — sem aspas, a lista pode chegar como tokens separados; o parser aceita ambas as formas, mas citar evita qualquer ambiguidade.
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`, 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).
13
+ - **`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`/`score`/`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/score/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
+
16
+ ## Features
17
+
18
+ - **`morph-spec create <feature> --description "<o quê/porquê>" --type <feature|bug|chore|hotfix> [--request "<txt>"] [--name "<nome>"] [--worktree]`** — cria a estrutura da feature + `feature.json` canônico explicitamente (nascimento explícito no início da proposal; o hook state-sync continua cobrindo o caminho implícito como fallback). **Identidade no nascimento:** `name` (default = slug) e `description` são carimbados; a `--description` é **obrigatória** numa feature nova (ou `--request`, que a semeia) — sem nenhuma das duas o `create` recusa (fim dos scaffolds anônimos). `--type` classifica direto; `--request "<pedido>"` roda o detector determinístico (empate/sem sinal sai com exit 2 e candidatos — a LLM decide e re-roda com `--type` + `--request`/`--description`); `--worktree` já nasce a feature no seu worktree próprio. **Toda feature nasce numa branch `morph/{f}`** (ramificada da default) mesmo sem `--worktree` — por isso o fechamento por `morph-spec finish` é idêntico nos dois fluxos.
19
+ - **`morph-spec list [--all] [--json] [--no-branch]`** — o quadro de auditoria: uma linha por feature × workType × status × fase × gates G1/G2/G3 × descrição, com `⚠` para inconsistências (sem descrição, ordem de gate). Uma feature com worktree/branch ativos tem a cópia da RAIZ congelada no instante do nascimento (`create --worktree` grava lá um esqueleto completo e nunca mais atualiza) — por padrão o `list` lê a verdade commitada em `morph/{f}` quando ela diverge do disco desta árvore (mesma leitura do `fleet`), reportando `source: 'branch'|'disk'` e uma coluna **WHERE** (`raiz` / `branch morph/{f}` / `worktree {dir}` / `⚠ worktree ausente`). `--no-branch` volta ao disco puro, sem custo de git — **e também pula a consulta de PR** (nenhuma coluna, nenhuma linha sintetizada, ver abaixo): não é só o modo disk-only de antes, é o modo offline completo. Um ponteiro `worktree.path` que não existe mais aparece com o flag `worktree-missing` (repare com `doctor --worktrees --prune`). Uma feature já arquivada em outra árvore/branch, cujo esqueleto sobrevive aqui, ganha o flag `archived-elsewhere` em vez de sumir da lista (read-only: nunca ressuscita nem poda). Coluna **PR** mostra `#{n} {estado}` (`-` sem PR para a branch da feature). Uma feature cujo `finish --pr` rodou mas cujo PR segue aberto — sem cópia local nesta árvore — aparece como linha **sintetizada**: fase `aguardando-merge`, flag `pr-pending`, WHERE `branch morph/{f}`, descrição "aguardando merge há Nd — sem cópia local" (mesma síntese que `fleet` e `doctor` usam, de `src/lib/git/pending-prs.js` — uma feature visível numa dessas telas e invisível na outra é o bug que esse módulo compartilhado existe para impedir). `--all` inclui as arquivadas; `--json` para scripting. É o complemento do `delete` (enxergar o que está morto antes de remover).
20
+ - **`morph-spec delete <feature> [--archive] [--force] [--with-worktree]`** — remove uma feature. Hard por padrão (pasta apagada, git-aware, + entrada podada do índice raiz para não ressuscitar); `--archive` move para `.morph/archive` com status `abandoned` em vez de apagar. Recusa se algum gate estiver aprovado, a menos que `--force`. Recusa TAMBÉM se um worktree vivo estiver em outra árvore (`feature-has-worktree` — apagar o registro da raiz órfão o worktree+branch em silêncio) — `--force` **não** dispensa isto (é consentimento sobre gate, não sobre filesystem); feche primeiro com `finish`/`worktree remove`, ou passe `--with-worktree` para desmontar o worktree (PROVE-or-DECLINE) e apagar num passo só — a branch sobrevive por padrão (`--delete-branch` para descartá-la também). Use para faxina de scaffolds mortos e drafts abandonados (`archive` é para trabalho concluído; `delete` é para trabalho morto).
21
+ - **`morph-spec retype <feature> <newType>`** — muda o tipo de trabalho mid-flight; desfaz só os gates carimbados por política, preserva aprovações humanas; bloqueado após o Gate 3.
22
+
23
+ ## Worktree / Fleet
24
+
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. `--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}`. Flags: `--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.
26
+ - **`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
+ - **`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.
28
+ - **`morph-spec worktree setup|link|remove|list`** — gerencia o ciclo do worktree por feature (`setup` cria `worktrees/{f}/` na branch `morph/{f}`, aloca o bloco de portas e escreve `.morph/worktree.env`; `link` religa infra compartilhada; `list` mostra fase/gate de cada worktree ativo). **Fechar a feature** continua sendo `morph-spec finish` (acima).
29
+ - **`morph-spec worktree remove [feature] [--target <path>] [--discard-ignored] [--delete-branch]`** — a remoção **segura** de um worktree, sem fechar a feature. 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 regenerável (`bin/`, `obj/`, `node_modules/`, `__pycache__/`, `test-results/`, `.auth/`, `*.tsbuildinfo`) é descartado (`discardedIgnored`); o que existe **só ali** é copiado para `.git/morph-spec/finish-backup/` e a remoção **prossegue** (`ignoredBackup` + `backedUp`). 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.
30
+ - **`morph-spec branch prune [--remote] [--dry-run] [--json]`** — apaga as branches locais cujo trabalho **já está** na default, **incluindo as squash-merged que o git não enxerga**. O problema que resolve: num repo que faz squash-merge, o commit de merge entra em `origin/master` mas os SHAs da branch não — `git branch --merged` não a lista, `git branch -d` recusa, e sobra o `-D`, indistinguível de descartar trabalho de verdade. Cada branch é classificada com evidência impressa: `merged-ancestral` (o git prova), `merged-squash` (PR `MERGED` pelo `gh`, **ou** árvore idêntica à da default), `open` (PR OPEN — nunca apagada), `in-worktree` (em uso — nunca apagada) e `unmerged` (**não foi possível provar** integração: sem `gh`, sem default resolvível ou trabalho de verdade — todas tratadas igual, de propósito). Só as duas primeiras são apagadas. Branch remota só com `--remote` explícito.
31
+ - **`morph-spec handoff <feature> [--here]`** — regenera o documento de retomada (`.morph/features/{f}/handoff.md`): comprimido, derivado, aponta para os artefatos em vez de copiá-los. **Resolve a árvore dona** antes de gerar (mesma lógica do `list`/`fleet`) e escreve LÁ, não necessariamente no cwd — rodar da raiz com o worktree vivo é seguro: o handoff sai correto e no lugar certo, em vez do esqueleto de nascimento da raiz. Sem árvore nenhuma materializada (só existe commitado em `morph/{f}`) recusa com `feature-not-in-tree`, nomeando a branch, em vez de gerar um documento apontando para arquivos inexistentes. `--here` força construir/escrever no cwd mesmo assim. Gerado automaticamente em `setup`/`create --worktree`/`advance`/`approve`.
32
+ - **`morph-spec fleet`** — visão da frota: toda feature ativa (raiz + worktrees) com fase, gates pendentes, tasks, **remoto** (`local`/`pushed`/`PR #N`), **claim** (que sessão está na feature), portas, handoff e atividade. Use entre sessões para decidir onde atuar.
33
+ - **`morph-spec claim [feature] [--release] [--list] [--all] [--json]`** — posse **advisory** de feature. Cada sessão reclama sua feature ativa automaticamente (hook SessionStart) e renova o heartbeat a cada tool call; o registro vive em `<git-common-dir>/morph-spec/claims.json`, compartilhado por todos os worktrees. **`morph-spec claim <feature>` (sem flags) ADQUIRE** — se esta árvore já tinha claim vivo em outra feature, ele é liberado no mesmo passo (o CLI reporta "movido de X para Y"); é o jeito de trocar de feature no meio da tarefa sem a dança de dois passos `--release` + reclamar. `--list` força a leitura sem adquirir mesmo passando uma feature. **Claims avisam, nunca bloqueiam** — mas se o SessionStart avisar que outra sessão tem claim vivo, **confirme antes de `finish`/`archive`/`delete`**: duas sessões na mesma feature já custaram um recap.md inteiro e um sub-agent (o worktree sumiu debaixo dele). Claim órfão de sessão morta: `morph-spec claim <f> --release`. **O board mostra só claims VIVOS** (dentro do TTL de 30 min): um claim morto não avisa mais nada, e deixá-lo listado transformava o quadro em ruído (features encerradas há 20h e 73h ainda aparecendo). `--all` mostra o registro inteiro; a contagem dos omitidos sai no rodapé, nunca em silêncio.
34
+
35
+ ## Validação
36
+
37
+ - **`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. `--mermaid` nunca é bloqueado.
38
+ - **`morph-spec verify <feature> [task] [--json] [--timeout <ms>] [--skip-e2e] [--fresh-e2e]`** — 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.
39
+ - **`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. `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.
40
+ - **`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.
41
+ **Ciclo de vida das features:** ordem de gates, feature sem identidade, pasta órfã, entrada de índice morta, `branch-drift` (a cópia da raiz atrasada em relação ao `morph/{f}` commitado) e `abandoned-scaffold`. Este último é o esqueleto que o `create --worktree` deixa para trás: `feature.json` sozinho, `in_progress`, **0 artefatos, 0 gates aprovados** e `morph/{f}` **sem commit próprio** (a branch ainda apontando para o commit de merge de outra PR). As quatro condições valem juntas e só depois de **24h** — recém-nascida tem exatamente a mesma forma, e alarme em todo `create` é alarme que ninguém lê. Fora de repo git, ou com a default branch indeterminável, o check cala (não dá para provar). Reparo: `morph-spec delete <f> --archive`, ou retome a feature.
42
+ **Pull requests:** `pr-awaiting-merge` (PR aberto há mais de 7 dias sem cópia local nesta árvore) e `pr-merged-not-landed` (PR já `MERGED` mas `.morph/archive/{f}` ainda não chegou aqui — sinal de `git pull` faltando). Offline-safe: sem `gh`, sem remoto no GitHub, ou qualquer falha, a checagem sai **não verificado** (nunca `ok`) — `[]` (perguntei, nada encontrado) e `null` (não pude perguntar) nunca se confundem, e o "não verificado" nunca deixa o `doctor` mais barulhento nem muda o exit code.
43
+ Ver `--worktrees` abaixo para a reconciliação de worktrees (modo separado).
44
+ - **`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). 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.
45
+ - **`morph-spec report-check <feature> [task] [--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). 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). Protocolo completo em MORPH.md §6.
46
+
47
+ ## Grafo de codebase
48
+
49
+ Grafo de símbolos extraído localmente pelo [graphify](https://pypi.org/project/graphifyy/) (versão pinada **0.9.50**, AST via tree-sitter, **zero token**), servido atrás de uma camada de saneamento própria. **Tudo é fail-open:** sem Python, sem grafo, com grafo velho ou corrompido, o framework segue por grep — é assim que ele sempre funcionou, e nenhuma skill pode exigir o grafo (ADR-002).
50
+
51
+ O ganho medido **não é recall**: contra o grep, o grafo não achou um único arquivo a mais. É precisão — em 12 símbolos do ProspectPRO, o grep aponta 1.092 arquivos onde existem 266 usos reais (58% de ruído), e o grafo ainda diz *que tipo* de relação é cada um. `IHandler` aparece em 193 arquivos no grep e tem **2** usos reais: é implementada por 98.
52
+
53
+ **Por que não consumir o graphify cru.** O resolvedor C# dele não implementa escopo hierárquico de namespace, então um tipo usado por um namespace filho sem `using` explícito vira um *stub* por arquivo — e o blast radius do símbolo real devolve **zero** consumidores onde existem dois. Falso negativo silencioso é o pior modo de falha possível antes de um hotfix. A camada de saneamento coalesce esses stubs (+91,9% de arestas de uso recuperadas no MORPH_GHLBrain), trata como externo só o que **não tem nenhum** nó com arquivo (BCL/NuGet), e devolve as opções quando o símbolo é genuinamente ambíguo em vez de escolher por conta própria.
54
+
55
+ - **`morph-spec graph status [-p <path>] [--json]`** — existência, tamanho, **frescor** e saúde do saneamento. Grafo ausente **sai 0**, não 1: não ter grafo é um estado válido do projeto. O frescor compara o `built_at_commit` gravado no `graph.json` com o `HEAD` — e importa porque o hook `post-commit` do graphify rebuilda **em background**: logo depois de um commit, o grafo ainda é o do commit anterior. `--json` devolve `{ available, path, nodes, links, freshness: {state}, version: {installed, pinned, matches}, sanitize: {coalescible, external, ambiguous} }`.
56
+ - **`morph-spec graph build [-p <path>] [--json]`** — extração completa (`graphify extract --code-only`). Gera o `.graphifyignore` stack-aware antes de extrair, porque um grafo que nasce com o ruído dentro não se conserta depois. Projeto **sem nenhum arquivo de código** (o caso de um `init` recém-feito) sai **0** com aviso, não erro. Sem graphify no PATH: exit 1 com `reason: graphify-unavailable` e o comando de instalação — nunca stack trace.
57
+ - **`morph-spec graph refresh [-p <path>] [--json]`** — atualização incremental (`graphify update`), 2 a 3× mais rápida que o build graças ao cache por hash de conteúdo.
58
+ - **`morph-spec graph affected <símbolo> [--prod] [--uses-only] [-p <path>] [--json]`** — blast radius saneado. `--uses-only` exclui `inherits`/`implements`/`extends`, que é o que separa *"20 implementam"* de *"2 usam"*; `--prod` descarta consumidores em projeto de teste. Erros são estados, não exceções: `no-graph`, `not-found` e `ambiguous` (este devolvendo as opções com path e id) saem 1 com a dica de fallback para grep.
59
+ - **`morph-spec graph explain <símbolo> [-p <path>] [--json]`** — vizinhança com citação `arquivo:linha`, separando quem depende dele de do que ele depende. **Prefira `explain` a `query`** nas personas: no eval, `explain` foi consistentemente mais preciso, enquanto `query` trunca por budget e traz ruído. Cada aresta traz a procedência em `via` (`direct`, `coalesced`, `language-disambiguated`), então dá para ver exatamente o que o saneamento recuperou.
60
+
61
+ **O que o resultado significa muda com a stack.** Em backend, blast radius é *quem chama*. Em frontend, o grafo é estrutura de módulos e não call graph (`imports: 78` contra `calls: 3` no LP_Polymorphism), então ali ele responde *quem importa*. Ler um como o outro leva a conclusão errada, e é por isso que as personas dizem isso no próprio texto.
62
+
63
+ O servidor MCP (`graphify-mcp`, 10 tools, todas aceitando `project_path`) está registrado em `framework/mcp/registry.json` para exploração interativa e consulta cross-repo — mas ele devolve o grafo **cru**, sem saneamento. Para qualquer decisão, prefira os comandos acima.
64
+
65
+ ## Observabilidade
66
+
67
+ - **`morph-spec telemetry [--json] [--feature <f>] [--limit <n>]`** — reconstrói o caminho de execução da sessão a partir de `.morph/logs/events.jsonl` (skills invocadas, personas despachadas + prompts, standards/base de conhecimento lidos, mix de tools, prompts do usuário). Capturado silenciosamente pelo hook `telemetry-log` em todo tool call e todo UserPromptSubmit.
68
+ - **`morph-spec cost [feature] [--by task|phase] [--all-trees] [--json]`** — custo da execução a partir do mesmo `events.jsonl`: duração por task (TaskStart→TaskEnd), bytes de contexto re-ingerido por tipo (`rubric`/`standard`/`skill`/`agents`/`plan-artifact`/`code`), dispatches + tamanho de prompt e mix de tools. Bytes re-ingeridos são **proxy determinística** de custo de token (tokens reais não eram observáveis por hook até o evento `SessionUsage` — quando presente no stream, `inputTokens`/`outputTokens`/`cacheReadTokens`/`cacheCreationTokens` são somados e exibidos à parte como "tokens reais", nunca misturados com a proxy de bytes). `--all-trees` agrega `.morph/logs/events.jsonl` de **toda árvore git** que tiver um (não só a corrente) — reusa a mesma enumeração de worktree do `doctor --worktrees`/`fleet`; mostra um resumo por árvore + o total combinado (`--by` aplica ao agregado combinado). Use antes/depois de mudar skill ou fluxo para medir o efeito.
@@ -4,7 +4,7 @@ description: Guia a criação de uma landing page nova ponta-a-ponta a partir do
4
4
  version: core
5
5
  when-to-invoke: ao iniciar um novo projeto de LP com o template Umbraco genérico
6
6
  user-invokable: true
7
- cliVersion: "8.32.0"
7
+ cliVersion: "8.33.0"
8
8
  stacks:
9
9
  - umbraco
10
10
  ---
@@ -4,7 +4,7 @@ description: Migra uma landing page existente (React/Vite, Next.js estático ou
4
4
  version: core
5
5
  when-to-invoke: ao receber instrução "use /migrate-lp-to-umbraco {projeto}" do usuário, ou quando o usuário descrever necessidade de migrar uma LP para Umbraco
6
6
  user-invokable: true
7
- cliVersion: "8.32.0"
7
+ cliVersion: "8.33.0"
8
8
  stacks:
9
9
  - umbraco
10
10
  ---
@@ -4,7 +4,7 @@ description: Orquestra a execução autônoma de tasks de uma feature morph-spec
4
4
  version: core
5
5
  when-to-invoke: após Gate 2 aprovado, ao receber instrução "use /morph-apply" ou "implementar feature"
6
6
  user-invokable: true
7
- cliVersion: "8.32.0"
7
+ cliVersion: "8.33.0"
8
8
  ---
9
9
 
10
10
  # morph-apply
@@ -4,7 +4,7 @@ description: Entende o negócio, os usuários e a arquitetura antes de qualquer
4
4
  version: core
5
5
  when-to-invoke: ao receber um novo request de feature, antes de morph-plan
6
6
  user-invokable: true
7
- cliVersion: "8.32.0"
7
+ cliVersion: "8.33.0"
8
8
  ---
9
9
 
10
10
  # morph-brainstorming
@@ -4,7 +4,7 @@ description: Avalia a qualidade de uma tarefa ou fase com rubricas graduadas 0-1
4
4
  version: core
5
5
  when-to-invoke: ao completar uma tarefa ou ao finalizar uma fase de implementação
6
6
  user-invokable: false
7
- cliVersion: "8.32.0"
7
+ cliVersion: "8.33.0"
8
8
  ---
9
9
 
10
10
  # morph-eval
@@ -4,7 +4,7 @@ description: ADW cirúrgico de hotfix — resolve uma queda/bug crítico de prod
4
4
  version: core
5
5
  when-to-invoke: ao receber "/morph-hotfix {feature}", quando a morph-proposal classifica o pedido como workType hotfix (Passo 0), ou o usuário diz "produção caiu / hotfix / urgente"
6
6
  user-invokable: true
7
- cliVersion: "8.32.0"
7
+ cliVersion: "8.33.0"
8
8
  ---
9
9
 
10
10
  # morph-hotfix
@@ -4,7 +4,7 @@ description: Guia a execução de cada tarefa de implementação com standard e
4
4
  version: core
5
5
  when-to-invoke: ao iniciar cada task de implementação listada em tasks.json
6
6
  user-invokable: false
7
- cliVersion: "8.32.0"
7
+ cliVersion: "8.33.0"
8
8
  ---
9
9
 
10
10
  # morph-implement
@@ -4,7 +4,7 @@ description: Guia a produção dos artefatos de planejamento de uma feature (use
4
4
  version: core
5
5
  when-to-invoke: ao iniciar a fase plan de uma feature, após o Gate 1 (proposal aprovado)
6
6
  user-invokable: true
7
- cliVersion: "8.32.0"
7
+ cliVersion: "8.33.0"
8
8
  ---
9
9
 
10
10
  # morph-plan
@@ -4,7 +4,7 @@ description: Orquestra a fase de proposta+plano de uma feature morph-spec. Encad
4
4
  version: core
5
5
  when-to-invoke: ao receber instrução "use /morph-proposal" do usuário, ou ao iniciar uma nova feature
6
6
  user-invokable: true
7
- cliVersion: "8.32.0"
7
+ cliVersion: "8.33.0"
8
8
  ---
9
9
 
10
10
  # morph-proposal
@@ -4,7 +4,7 @@ description: Conduz a fase de revisão e produz o review-report.md para o Gate 3
4
4
  version: core
5
5
  when-to-invoke: ao iniciar a fase review, após todas as tasks de tasks.json estarem com status `done`
6
6
  user-invokable: true
7
- cliVersion: "8.32.0"
7
+ cliVersion: "8.33.0"
8
8
  ---
9
9
 
10
10
  # morph-review
@@ -4,7 +4,7 @@ description: Varre o codebase existente, detecta stack atual, compara com o stac
4
4
  version: core
5
5
  when-to-invoke: ao iniciar uma feature em projeto brownfield (codebase já existente)
6
6
  user-invokable: true
7
- cliVersion: "8.32.0"
7
+ cliVersion: "8.33.0"
8
8
  ---
9
9
 
10
10
  # morph-stack-scan
@@ -4,7 +4,7 @@ description: Localiza um standard específico via busca server-side (morph-spec
4
4
  version: core
5
5
  when-to-invoke: quando precisar consultar um standard específico durante a implementação
6
6
  user-invokable: true
7
- cliVersion: "8.32.0"
7
+ cliVersion: "8.33.0"
8
8
  ---
9
9
 
10
10
  # morph-standards
@@ -4,7 +4,7 @@ description: Conduz a fase opcional de UI/UX — design system, mockups, compone
4
4
  version: core
5
5
  when-to-invoke: ao tomar o ramo uiux (morph-spec advance --design), depois do Gate 1 e antes do morph-plan
6
6
  user-invokable: true
7
- cliVersion: "8.32.0"
7
+ cliVersion: "8.33.0"
8
8
  ---
9
9
 
10
10
  # morph-uiux
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "8.32.0",
3
+ "version": "8.33.0",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -8,32 +8,46 @@
8
8
  * .sln) → "dotnet test <proj> --nologo -v q" for each match
9
9
  * 4. package.json present AND has scripts.test → "npm test"
10
10
  * 5. null (no test suite detected)
11
+ *
12
+ * Step 3 deliberately does NOT see linked git worktrees. A worktree is another
13
+ * feature's checkout of the SAME repo: its `tests/X.Tests.csproj` is a copy of
14
+ * the one already matched at the root, so including it both doubles the wall
15
+ * clock and reports on code that is not the tree under verification. In the
16
+ * field this produced a red `tests` node — "fail" at exactly the timeout, with
17
+ * the first run's own summary reading "Com falha: 0" — while nothing had
18
+ * failed. Two independent filters carry that rule (see collectDotnetTestProjects):
19
+ * the CONVENTION path (worktrees/, .worktrees/) and, for worktrees created
20
+ * anywhere else, the registry git itself keeps.
11
21
  */
12
22
 
13
- import { execSync } from 'child_process';
23
+ import { execSync, execFileSync } from 'child_process';
14
24
  import { existsSync, readFileSync, readdirSync } from 'fs';
15
- import { join } from 'path';
25
+ import { join, resolve, relative, isAbsolute } from 'path';
16
26
  import { globSync } from 'glob';
17
27
 
28
+ import { WORKTREES_DIR, LEGACY_WORKTREES_DIR } from '../../commands/project/worktree.js';
29
+
18
30
  /**
19
- * Resolve the test command for a project.
31
+ * Timeout budget granted to ONE test project. The chained `a && b` command is
32
+ * given this much per link: a fixed cap made every multi-project chain born
33
+ * condemned — it timed out for being long, not for being broken.
34
+ */
35
+ export const DEFAULT_TEST_TIMEOUT_PER_PROJECT = 120_000;
36
+
37
+ /**
38
+ * Resolve the test command for a project, together with how many test projects
39
+ * it chains — the timeout budget is derived from that count.
20
40
  *
21
41
  * @param {string} [projectPath] - Project root directory (defaults to '.')
22
- * @returns {string | null} The test command to run, or null if none found
42
+ * @returns {{ command: string | null, projectCount: number, source: string }}
43
+ * `source` is one of 'config' | 'dotnet-root' | 'dotnet-scan' | 'npm' | 'none'.
23
44
  */
24
- export function resolveTestCommand(projectPath = '.') {
45
+ export function resolveTestPlan(projectPath = '.') {
25
46
  // 1. Read from .morph/config/config.json
26
- const configPath = join(projectPath, '.morph', 'config', 'config.json');
27
- if (existsSync(configPath)) {
28
- try {
29
- const config = JSON.parse(readFileSync(configPath, 'utf8'));
30
- const cmd = config?.project?.testCommand;
31
- if (cmd && typeof cmd === 'string') {
32
- return cmd;
33
- }
34
- } catch {
35
- // Malformed config — fall through to detection
36
- }
47
+ const config = readConfig(projectPath);
48
+ const configured = config?.project?.testCommand;
49
+ if (configured && typeof configured === 'string') {
50
+ return { command: configured, projectCount: 1, source: 'config' };
37
51
  }
38
52
 
39
53
  // 2. Detect .NET project (sln/csproj at root) → dotnet test
@@ -43,7 +57,7 @@ export function resolveTestCommand(projectPath = '.') {
43
57
  (entry) => entry.endsWith('.sln') || entry.endsWith('.csproj')
44
58
  );
45
59
  if (hasDotnetProject) {
46
- return 'dotnet test';
60
+ return { command: 'dotnet test', projectCount: 1, source: 'dotnet-root' };
47
61
  }
48
62
  } catch {
49
63
  // Unreadable project directory — fall through to next detection
@@ -53,19 +67,13 @@ export function resolveTestCommand(projectPath = '.') {
53
67
  // (e.g. tests/MyApp.Tests/MyApp.Tests.csproj), common in monorepos that
54
68
  // have no root .sln. Run each matched project explicitly rather than a
55
69
  // bare `dotnet test` (which finds nothing to build at the root).
56
- try {
57
- const testProjects = globSync('**/*Tests.csproj', {
58
- cwd: projectPath,
59
- ignore: ['**/bin/**', '**/obj/**', '**/node_modules/**'],
60
- });
61
- if (testProjects.length > 0) {
62
- return testProjects
63
- .sort()
64
- .map((p) => `dotnet test "${p}" --nologo -v q`)
65
- .join(' && ');
66
- }
67
- } catch {
68
- // Glob failure — fall through to next detection
70
+ const testProjects = collectDotnetTestProjects(projectPath);
71
+ if (testProjects.length > 0) {
72
+ return {
73
+ command: testProjects.map((p) => `dotnet test "${p}" --nologo -v q`).join(' && '),
74
+ projectCount: testProjects.length,
75
+ source: 'dotnet-scan',
76
+ };
69
77
  }
70
78
 
71
79
  // 4. package.json present AND has scripts.test → npm test
@@ -74,7 +82,7 @@ export function resolveTestCommand(projectPath = '.') {
74
82
  try {
75
83
  const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
76
84
  if (pkg?.scripts?.test) {
77
- return 'npm test';
85
+ return { command: 'npm test', projectCount: 1, source: 'npm' };
78
86
  }
79
87
  } catch {
80
88
  // Malformed package.json — fall through to null
@@ -82,43 +90,197 @@ export function resolveTestCommand(projectPath = '.') {
82
90
  }
83
91
 
84
92
  // 5. Nothing found
85
- return null;
93
+ return { command: null, projectCount: 0, source: 'none' };
94
+ }
95
+
96
+ /**
97
+ * Resolve the test command for a project.
98
+ *
99
+ * @param {string} [projectPath] - Project root directory (defaults to '.')
100
+ * @returns {string | null} The test command to run, or null if none found
101
+ */
102
+ export function resolveTestCommand(projectPath = '.') {
103
+ return resolveTestPlan(projectPath).command;
104
+ }
105
+
106
+ /**
107
+ * Find the *Tests.csproj files that belong to THIS tree — never to a linked
108
+ * worktree of the same repo.
109
+ *
110
+ * Two filters, because one is not enough:
111
+ * - the glob ignore covers worktrees created by convention (`worktrees/{f}`
112
+ * and the legacy `.worktrees/{f}`), which is where `morph-spec worktree
113
+ * setup` puts them;
114
+ * - `git worktree list --porcelain` covers every OTHER location, including a
115
+ * worktree someone created by hand outside the convention. Fail-open: no
116
+ * git, no answer, no filter — the convention filter still stands.
117
+ *
118
+ * @param {string} projectPath
119
+ * @returns {string[]} Project-relative csproj paths, sorted
120
+ */
121
+ function collectDotnetTestProjects(projectPath) {
122
+ let matches;
123
+ try {
124
+ matches = globSync('**/*Tests.csproj', {
125
+ cwd: projectPath,
126
+ ignore: [
127
+ '**/bin/**',
128
+ '**/obj/**',
129
+ '**/node_modules/**',
130
+ `${WORKTREES_DIR}/**`,
131
+ `${LEGACY_WORKTREES_DIR}/**`,
132
+ ],
133
+ });
134
+ } catch {
135
+ // Glob failure — caller falls through to the next detection step
136
+ return [];
137
+ }
138
+
139
+ const linked = listLinkedWorktreePaths(projectPath);
140
+ if (linked.length > 0) {
141
+ matches = matches.filter(
142
+ (p) => !linked.some((wt) => isInside(resolve(projectPath, p), wt))
143
+ );
144
+ }
145
+
146
+ return matches.sort();
147
+ }
148
+
149
+ /**
150
+ * Absolute paths of the git worktrees linked to this repo, excluding the tree
151
+ * being verified. Fail-open — a repo without git (or a git that errors) yields
152
+ * an empty list, and discovery proceeds on the convention filter alone.
153
+ *
154
+ * @param {string} projectPath
155
+ * @returns {string[]}
156
+ */
157
+ function listLinkedWorktreePaths(projectPath) {
158
+ let out;
159
+ try {
160
+ out = execFileSync('git', ['worktree', 'list', '--porcelain'], {
161
+ cwd: projectPath,
162
+ stdio: 'pipe',
163
+ encoding: 'utf8',
164
+ windowsHide: true,
165
+ });
166
+ } catch {
167
+ return [];
168
+ }
169
+
170
+ const self = resolve(projectPath);
171
+ return out
172
+ .split('\n')
173
+ .filter((line) => line.startsWith('worktree '))
174
+ .map((line) => resolve(line.slice('worktree '.length).trim()))
175
+ .filter((p) => p && p !== self);
176
+ }
177
+
178
+ /** True when `child` is `parent` or lives under it. */
179
+ function isInside(child, parent) {
180
+ const rel = relative(parent, child);
181
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
182
+ }
183
+
184
+ function readConfig(projectPath) {
185
+ const configPath = join(projectPath, '.morph', 'config', 'config.json');
186
+ if (!existsSync(configPath)) return null;
187
+ try {
188
+ return JSON.parse(readFileSync(configPath, 'utf8'));
189
+ } catch {
190
+ // Malformed config — treated as absent by every caller
191
+ return null;
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Resolve the timeout budget for the test node.
197
+ *
198
+ * Precedence: explicit override (`morph-spec verify --timeout`) >
199
+ * `config.project.testTimeoutMs` > default scaled by the number of chained
200
+ * test projects.
201
+ *
202
+ * @param {string} projectPath
203
+ * @param {number} projectCount - How many test projects the command chains
204
+ * @param {number} [override] - Explicit override in ms, if the caller has one
205
+ * @returns {number} Timeout in ms
206
+ */
207
+ export function resolveTestTimeout(projectPath, projectCount, override) {
208
+ if (Number.isFinite(override) && override > 0) return override;
209
+
210
+ const configured = readConfig(projectPath)?.project?.testTimeoutMs;
211
+ if (Number.isFinite(configured) && configured > 0) return configured;
212
+
213
+ return DEFAULT_TEST_TIMEOUT_PER_PROJECT * Math.max(1, projectCount);
214
+ }
215
+
216
+ /**
217
+ * Count the test runs that reported a summary before the command was cut off.
218
+ * Heuristic by construction (the summary line is localized), and used only to
219
+ * name WHICH link of the chain was still running when the clock ran out.
220
+ */
221
+ function countCompletedRuns(output) {
222
+ const matches = (output || '').match(
223
+ /(?:^|\n)\s*(?:Aprovado|Passed|Failed|Reprovado|Com falha|Erro)!/gi
224
+ );
225
+ return matches ? matches.length : 0;
86
226
  }
87
227
 
88
228
  /**
89
229
  * Run the project's test suite.
90
230
  *
91
231
  * @param {string} [projectPath] - Project root directory (defaults to '.')
92
- * @param {object} [opts] - Optional overrides for testing
232
+ * @param {object} [opts]
233
+ * @param {number} [opts.timeout] - Explicit timeout in ms (highest precedence)
93
234
  * @param {Function} [opts._execSync] - Override execSync for unit testing
94
- * @returns {{ passed?: boolean, output?: string, exitCode?: number, skipped?: boolean, reason?: string }}
235
+ * @returns {{ passed?: boolean, output?: string, exitCode?: number, skipped?: boolean, reason?: string, timedOut?: boolean, timeoutMs?: number, message?: string }}
95
236
  */
96
237
  export function runTestSuite(projectPath = '.', opts = {}) {
97
- const cmd = resolveTestCommand(projectPath);
238
+ const { command: cmd, projectCount } = resolveTestPlan(projectPath);
98
239
 
99
240
  if (!cmd) {
100
241
  return { skipped: true, reason: 'No testCommand configured and no known project file detected' };
101
242
  }
102
243
 
103
244
  const exec = opts._execSync ?? execSync;
245
+ const timeout = resolveTestTimeout(projectPath, projectCount, opts.timeout);
104
246
 
105
247
  try {
106
248
  const output = exec(cmd, {
107
249
  cwd: projectPath,
108
- timeout: 120_000,
250
+ timeout,
109
251
  encoding: 'utf8',
110
252
  stdio: 'pipe'
111
253
  });
112
- return { passed: true, output: output ?? '', exitCode: 0 };
254
+ return { passed: true, output: output ?? '', exitCode: 0, timeoutMs: timeout };
113
255
  } catch (err) {
114
256
  const output = (err.stdout || '') + (err.stderr || '');
257
+
258
+ // A timeout is NOT a red suite: the command was interrupted, so no verdict
259
+ // exists. Saying so by name is the whole point — the field diagnosis of
260
+ // this cost a reading of durationMs against a truncated log.
261
+ if (err.code === 'ETIMEDOUT' || err.killed === true) {
262
+ const total = Math.max(1, projectCount);
263
+ const at = Math.min(countCompletedRuns(output) + 1, total);
264
+ const where = total > 1 ? ` na execução ${at} de ${total}` : '';
265
+ return {
266
+ passed: false,
267
+ timedOut: true,
268
+ timeoutMs: timeout,
269
+ buildFailed: false,
270
+ message: `timeout após ${timeout} ms${where} — nenhum teste falhou, o comando foi interrompido`,
271
+ output,
272
+ exitCode: err.status ?? null,
273
+ };
274
+ }
275
+
115
276
  const BUILD_FAIL = /Build FAILED|error MSB\d+|error CS\d+|Could not find.*\.csproj/i;
116
277
  const buildFailed = BUILD_FAIL.test(output);
117
278
  return {
118
279
  passed: false,
119
280
  buildFailed,
120
281
  output,
121
- exitCode: err.status ?? 1
282
+ exitCode: err.status ?? 1,
283
+ timeoutMs: timeout,
122
284
  };
123
285
  }
124
286
  }
@@ -48,7 +48,8 @@ const OUTPUT_TAIL_LINES = 20;
48
48
  * @param {string|null} [taskId] - Task ID for task-scoped verify, or null/undefined for feature scope
49
49
  * @param {Object} [options]
50
50
  * @param {string} [options.rubric] - Force a rubric id (highest precedence)
51
- * @param {number} [options.timeout] - Build timeout in ms (default 120000)
51
+ * @param {number} [options.timeout] - Timeout in ms for the build AND tests nodes
52
+ * (default: 120000 for build; for tests, see resolveTestTimeout())
52
53
  * @param {boolean} [options.skipTests] - Skip the tests node
53
54
  * @param {boolean} [options.skipBuild] - Skip the build node
54
55
  * @param {boolean} [options.skipE2e] - Skip the e2e node
@@ -86,7 +87,18 @@ export async function runVerify(projectPath, featureName, taskId, options = {})
86
87
  appendBuildFeedback(feedback, nodes.build);
87
88
 
88
89
  // 2. tests — skipped entirely (no invocation) when build already failed.
89
- nodes.tests = await runTestsNode({ projectPath, skipTests, buildNode: nodes.build, _runTestSuite });
90
+ // The tests node gets the SAME --timeout the user typed, but only when they
91
+ // typed one: `timeout` above is defaulted for the build, and passing that
92
+ // default down would silently outrank config.project.testTimeoutMs and the
93
+ // per-project scaling (see resolveTestTimeout()).
94
+ const explicitTimeout = Number.isFinite(options.timeout) ? options.timeout : undefined;
95
+ nodes.tests = await runTestsNode({
96
+ projectPath,
97
+ skipTests,
98
+ buildNode: nodes.build,
99
+ timeout: explicitTimeout,
100
+ _runTestSuite,
101
+ });
90
102
  appendTestsFeedback(feedback, nodes.tests);
91
103
 
92
104
  // 3. validators — ALWAYS runs, even on a red build (free static feedback).
@@ -174,7 +186,7 @@ function appendBuildFeedback(feedback, buildNode) {
174
186
  // Tests node
175
187
  // ============================================================================
176
188
 
177
- async function runTestsNode({ projectPath, skipTests, buildNode, _runTestSuite }) {
189
+ async function runTestsNode({ projectPath, skipTests, buildNode, timeout, _runTestSuite }) {
178
190
  const command = resolveTestCommand(projectPath);
179
191
 
180
192
  if (skipTests) {
@@ -192,7 +204,7 @@ async function runTestsNode({ projectPath, skipTests, buildNode, _runTestSuite }
192
204
  }
193
205
 
194
206
  const start = Date.now();
195
- const result = await _runTestSuite(projectPath);
207
+ const result = await _runTestSuite(projectPath, { timeout });
196
208
  const durationMs = Date.now() - start;
197
209
 
198
210
  if (result.skipped) {
@@ -205,6 +217,20 @@ async function runTestsNode({ projectPath, skipTests, buildNode, _runTestSuite }
205
217
  };
206
218
  }
207
219
 
220
+ // A timeout has no verdict to report: the tail of a truncated log reads like
221
+ // a passing run cut mid-sentence. Lead with what actually happened.
222
+ if (result.timedOut) {
223
+ const tail = tailLines(result.output, OUTPUT_TAIL_LINES);
224
+ return {
225
+ status: 'fail',
226
+ command,
227
+ durationMs,
228
+ timedOut: true,
229
+ reason: result.message,
230
+ output: tail ? `${result.message}\n${tail}` : result.message,
231
+ };
232
+ }
233
+
208
234
  return {
209
235
  status: result.passed ? 'pass' : 'fail',
210
236
  command,