@polymorphism-tech/morph-spec 8.17.0 → 8.18.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.
@@ -0,0 +1,40 @@
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`/`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).
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-decision <feature> <gate> --pause <bool> [--reason <txt>] [--signals <json>]`** — 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.
13
+
14
+ ## Features
15
+
16
+ - **`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.
17
+ - **`morph-spec list [--all] [--json]`** — 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). `--all` inclui as arquivadas; `--json` para scripting. É o complemento do `delete` (enxergar o que está morto antes de remover).
18
+ - **`morph-spec delete <feature> [--archive] [--force]`** — 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`. Use para faxina de scaffolds mortos e drafts abandonados (`archive` é para trabalho concluído; `delete` é para trabalho morto).
19
+ - **`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.
20
+
21
+ ## Worktree / Fleet
22
+
23
+ - **`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` 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, `--force` dispensa o Gate 3, `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados.
24
+ - **`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.
25
+ - **`morph-spec worktree setup|link|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). O fechamento saiu daqui — use `morph-spec finish` (acima).
26
+ - **`morph-spec handoff <feature>`** — regenera o documento de retomada (`.morph/features/{f}/handoff.md`): comprimido, derivado, aponta para os artefatos em vez de copiá-los. Gerado automaticamente em `setup`/`create --worktree`/`advance`.
27
+ - **`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.
28
+ - **`morph-spec claim [feature] [--release] [--list] [--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`.
29
+
30
+ ## Validação
31
+
32
+ - **`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.
33
+ - **`morph-spec verify <feature> [task] [--json]`** — cadeia determinística build+testes+validadores (código antes do juiz LLM); escopo de task quando informado, senão a feature inteira.
34
+ - **`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 o único reparo seguro (`git worktree prune` — registros de pastas que já sumiram); apagar pasta que ainda existe é reportado com o comando exato, nunca executado.
35
+ - **`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. O comando responde a pergunta certa (`work-landed-report-missing` → aceite degradado, NÃO re-despache; `no-work-no-report` → re-despache uma vez). Protocolo completo em MORPH.md §6.
36
+
37
+ ## Observabilidade
38
+
39
+ - **`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.
40
+ - **`morph-spec cost [feature] [--by task|phase] [--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 são observáveis por hook). Use antes/depois de mudar skill ou fluxo para medir o efeito.
@@ -4,7 +4,9 @@ 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.17.0"
7
+ cliVersion: "8.18.0"
8
+ stacks:
9
+ - umbraco
8
10
  ---
9
11
 
10
12
  # create-umbraco-lp
@@ -4,7 +4,9 @@ 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.17.0"
7
+ cliVersion: "8.18.0"
8
+ stacks:
9
+ - umbraco
8
10
  ---
9
11
 
10
12
  # migrate-lp-to-umbraco
@@ -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.17.0"
7
+ cliVersion: "8.18.0"
8
8
  ---
9
9
 
10
10
  # morph-apply
@@ -231,12 +231,8 @@ O comando grava `gateDecisions.review = { pause, reason, signals, trust, decided
231
231
 
232
232
  ### 6. Pós Gate 3 — encerramento
233
233
 
234
- - Aprovar o Gate 3: em auto, `morph-spec approve {feature} review --mode auto`; em manual, o hook state-sync registra a resposta "aprovado" em AskUserQuestion — **desde que a pergunta nomeie a feature** (o template acima nomeia; se você improvisar sem o nome e houver mais de uma feature na árvore, o hook recusa a gravar e avisa, e você aprova por `morph-spec approve`). O gate fica gravado no `feature.json` autoritativo da feature — nunca via Edit à mão de `state.json`.
235
- - **Fechar a feature** com `morph-spec finish {feature} --pr|--merge` — um passo só: arquiva commitando `features/{f} → archive/{f}` dentro de `morph/{f}`, integra e desmonta o worktree (se houver). Escolha o modo com `AskUserQuestion` (header `"Integração"`):
236
- - **Abrir PR** (`--pr`): faz `push` de `morph/{f}` + `gh pr create` com base na default — para review por outro humano/CI antes do merge. Exige `origin` + `gh` autenticado (checados **antes** de arquivar; se faltarem, `finish` recusa sem efeito colateral — caia para `--merge`). A branch é preservada e a sessão fica em `morph/{f}`.
237
- - **Merge local** (`--merge`): merge `--no-ff` na default **localmente**, sem push — para o fluxo single-user direto. Remove a branch e o worktree.
238
- - Não integre automaticamente sem a escolha do modo: `finish` toca o remoto (`--pr`) ou a branch default (`--merge`) — é uma ação de saída que pede confirmação.
239
- - Feature **legada** (criada antes do modelo "nasce numa branch", sem `morph/{f}`): `finish` recusa e roteia para `morph-spec archive {feature}` (arquivar-sem-integrar); nesse caso, arquive.
234
+ - Aprovar o Gate 3: em auto, `morph-spec approve {feature} review --mode auto`; em manual, o hook state-sync registra a resposta "aprovado" em `AskUserQuestion` — **desde que a pergunta nomeie a feature** (senão aprove via `morph-spec approve`).
235
+ - **Fechar a feature** com `morph-spec finish {feature} --pr|--merge`, modo escolhido via `AskUserQuestion` (header `"Integração"`): `--pr` (push + PR, review antes do merge) vs `--merge` (integra local, single-user direto). Não integre automaticamente sem essa escolha. Mecânica completa (o que cada modo faz, feature legada sem `morph/{f}`): `MORPH.md` §2.1/§2.2.
240
236
 
241
237
  ## Anti-padrões
242
238
 
@@ -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.17.0"
7
+ cliVersion: "8.18.0"
8
8
  ---
9
9
 
10
10
  # morph-brainstorming
@@ -17,17 +17,7 @@ Ao iniciar uma nova feature — antes de qualquer especificação técnica ou pl
17
17
 
18
18
  ## Stack padrão (baked in)
19
19
 
20
- O harness conhece o stack por padrão. Não pergunte sobre tecnologia a menos que haja ambiguidade real.
21
-
22
- | Camada | Stack |
23
- |--------|-------|
24
- | Backend | .NET 10, VSA (feature-folder: handler + validator + endpoint + errors + tests) |
25
- | ORM | EF Core + Neon (Postgres), `Guid.CreateVersion7()`, `sealed` classes, `result.Match()` |
26
- | IA | Microsoft Agent Framework (MAF) — quando a feature envolve agentes |
27
- | Frontend | Next.js App Router + Tailwind + Framer Motion + Shadcn |
28
- | Jobs async | Hangfire — apenas se a feature requer processamento em background |
29
-
30
- **Padrões proibidos:** `Guid.NewGuid()`, classes não-sealed sem justificativa, exceptions sem result pattern, camada de Service/Manager entre handler e dados, repository genérico (`IRepository<T>`/`IUnitOfWork`), SQL cru sem justificativa. Acesso a dados em VSA: `ApplicationDbContext` direto no slice (`DbSet<T>` já é repository, `DbContext` já é unit of work); para concorrência ou SQL específico, store dedicado hand-written com justificativa em `decisions.md`.
20
+ O harness conhece o stack por padrão (backend .NET 10 + VSA + EF Core/Neon, IA via MAF, frontend Next.js/Tailwind/Shadcn) e a lista de padrões proibidos (`Guid.NewGuid()`, classes não-sealed, Service/Manager layer, repository genérico, SQL cru). **Não pergunte sobre tecnologia a menos que haja ambiguidade real** — stack nunca entra na árvore de decisões. Regra canônica: `MORPH.md` § "Stack baked-in".
31
21
 
32
22
  ## Modo bug-lean (`workType: bug`)
33
23
 
@@ -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.17.0"
7
+ cliVersion: "8.18.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.17.0"
7
+ cliVersion: "8.18.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.17.0"
7
+ cliVersion: "8.18.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.17.0"
7
+ cliVersion: "8.18.0"
8
8
  ---
9
9
 
10
10
  # morph-plan
@@ -88,13 +88,7 @@ Ao iniciar a fase plan — após o `proposal.md` ser aprovado e antes de qualque
88
88
  4. Produza `mandate.md` em `.morph/features/{feature}/2-plan/`:
89
89
  - Stack exata do projeto (leia de `.morph/config/config.json` → `project.stack` ou do CLAUDE.md).
90
90
  - Standards obrigatórios: liste os IDs relevantes de `.morph/framework/standards/STANDARDS.json`.
91
- - Patterns proibidos — inclua sempre os defaults abaixo mais qualquer específico da feature:
92
- - `Guid.NewGuid()` → usar `Guid.CreateVersion7()`
93
- - Classes não-sealed sem justificativa documentada
94
- - Exceptions como fluxo de controle → usar result pattern com `result.Match()`
95
- - Camada de Service/Manager entre handler e dados → handler usa `ApplicationDbContext` direto no slice
96
- - Repository genérico (`IRepository<T>`/`IUnitOfWork`) → redundante sobre EF Core: `DbSet<T>` já é repository, `DbContext` já é unit of work
97
- - SQL cru (`FromSqlRaw`, `ExecuteQuery(string)`) sem justificativa em `decisions.md` → usar LINQ
91
+ - Patterns proibidos — **copie por extenso no mandate.md** a lista canônica de `MORPH.md` § "Stack baked-in" (`Guid.NewGuid()`→`Guid.CreateVersion7()`, classes não-sealed sem justificativa, exceptions como fluxo de controle, camada de Service/Manager entre handler e dados, repository genérico `IRepository<T>`/`IUnitOfWork`, SQL cru sem justificativa) mais qualquer proibição específica da feature. Copie o texto completo — o mandate.md é lido por implementadores em contexto fresco, sem `MORPH.md` carregado, então um ponteiro sozinho não basta aqui.
98
92
  - Política de acesso a dados (registre no mandate): `ApplicationDbContext` injetado direto no slice — para CRUD e para queries específicas (joins, projeção, paginação). Para concorrência ou SQL específico, store dedicado hand-written com justificativa em `decisions.md`.
99
93
  - **Features com agentes de IA (MAF):** se a feature envolve um agente (Microsoft Agent Framework), **use a tool Read** nos standards relevantes de `framework/standards/ai-agents/*.md` (ex.: `setup`, `agent-session`, `middleware-patterns`, `mcp-server`, `mcp-tools`, `multi-agent-patterns`, `production`) — filtre pelo que a feature exige via STANDARDS.json. Esses standards carregam as **convenções de projeto** (single-agent por padrão — Workflows só para fan-in/out real; provider, caching, observability, A2A) que uma fonte externa de docs (ex.: context7) não conhece. Padrão de divisão: **superfície da API** pode vir de docs ao vivo/context7 (mais frescas para um SDK em evolução), mas **as convenções vêm dos standards** — registre ambas no mandate. Não pule os `ai-agents/*.md` só porque o STANDARDS.json index lista que eles existem.
100
94
  - Checklist de done com critérios verificáveis por tarefa.
@@ -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.17.0"
7
+ cliVersion: "8.18.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.17.0"
7
+ cliVersion: "8.18.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.17.0"
7
+ cliVersion: "8.18.0"
8
8
  ---
9
9
 
10
10
  # morph-stack-scan
@@ -17,19 +17,7 @@ Quando o projeto já tem código existente e você precisa entender o estado atu
17
17
 
18
18
  ## Stack padrão de referência
19
19
 
20
- | Camada | Esperado |
21
- |--------|----------|
22
- | Runtime | .NET 10 |
23
- | Arquitetura | VSA — feature-folder: `{Feature}/` com Handler, Validator, Endpoint, Errors, Tests |
24
- | ORM | EF Core + Neon (Postgres) |
25
- | IDs | `Guid.CreateVersion7()` |
26
- | Classes | `sealed` por padrão |
27
- | Erros | Result pattern (`result.Match()`), não exceptions não tratadas |
28
- | IA | Microsoft Agent Framework (MAF) |
29
- | Frontend | Next.js App Router + Tailwind + Framer Motion + Shadcn |
30
- | Jobs | Hangfire (apenas quando necessário) |
31
-
32
- **Padrões proibidos:** `Guid.NewGuid()`, classes não-sealed sem justificativa, exceptions como fluxo de controle, camada de Service/Manager entre handler e dados, repository genérico (`IRepository<T>`/`IUnitOfWork`), SQL cru sem justificativa. Acesso a dados em VSA: `ApplicationDbContext` direto no slice (`DbSet<T>` já é repository, `DbContext` já é unit of work); para concorrência ou SQL específico, store dedicado hand-written com justificativa em `decisions.md`.
20
+ O "Esperado" da comparação do passo 2 é o stack baked-in (runtime .NET 10 + VSA, EF Core/Neon, `Guid.CreateVersion7()`, `sealed`, result pattern, MAF, Next.js/Tailwind/Shadcn, Hangfire) e a lista de padrões proibidos (`Guid.NewGuid()`, classes não-sealed, Service/Manager layer, repository genérico, SQL cru sem justificativa) — regra canônica em `MORPH.md` § "Stack baked-in". Os templates de `stack-scan.md` abaixo (greenfield e brownfield) escrevem essa lista por extenso no artefato da feature, para que quem ler o `stack-scan.md` depois não precise reabrir o `MORPH.md`.
33
21
 
34
22
  ## Greenfield short-circuit
35
23
 
@@ -4,7 +4,7 @@ description: Localiza e carrega um standard específico do STANDARDS.json sob de
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.17.0"
7
+ cliVersion: "8.18.0"
8
8
  ---
9
9
 
10
10
  # morph-standards
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "8.17.0",
3
+ "version": "8.18.0",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -826,13 +826,20 @@ export async function doctorCommand(options = {}) {
826
826
  hasErrors = true;
827
827
  }
828
828
 
829
- // Check templates
829
+ // Check templates — as of the R3 perf plan (Task 2), setup-infra/update no
830
+ // longer pre-populate .morph/framework/templates/: template resolution is
831
+ // layered (template-registry.js / architecture-scanner.js resolveTemplatePath)
832
+ // and falls back to the npm package's own framework/templates/ when the
833
+ // project-local copy is absent, which is now the expected default rather
834
+ // than an error. The project-local copy remains a supported override — if
835
+ // present, report it as such. The "key templates" check further below
836
+ // verifies the package copy itself is intact, which is the check that
837
+ // actually matters now.
830
838
  const templatesPath = join(morphPath, 'framework', 'templates');
831
839
  if (await pathExists(templatesPath)) {
832
- checks.push({ name: 'templates/', status: 'ok' });
840
+ checks.push({ name: 'templates/', status: 'ok', msg: 'local override present' });
833
841
  } else {
834
- checks.push({ name: 'templates/', status: 'missing' });
835
- hasErrors = true;
842
+ checks.push({ name: 'templates/', status: 'ok', msg: 'resolved from package (no local override)' });
836
843
  }
837
844
 
838
845
  // Check .claude folder
@@ -486,7 +486,7 @@ export async function initCommand(options) {
486
486
  logger.info('Files installed:');
487
487
  logger.dim(` ✓ CLAUDE.md`);
488
488
  logger.dim(` ✓ .morph/config/ (config.json)`);
489
- logger.dim(` ✓ .morph/framework/ (agents.json, standards/, templates/, hooks/)`);
489
+ logger.dim(` ✓ .morph/framework/ (agents.json, standards/, hooks/)`);
490
490
  logger.dim(` ✓ .morph/context/ (project context)`);
491
491
  logger.dim(` ✓ .morph/features/ (feature outputs)`);
492
492
  if (commandsCopied) {
@@ -24,7 +24,8 @@ import {
24
24
  checkProjectOutdated,
25
25
  saveProjectMorphVersion,
26
26
  getUpdateInstructions,
27
- detectInstallMethod
27
+ detectInstallMethod,
28
+ compareVersions
28
29
  } from '../../utils/version-checker.js';
29
30
  import { installClaudeHooks, installGlobalStatusline } from '../../utils/claude-settings-manager.js';
30
31
  import { installSkills } from '../../utils/skills-installer.js';
@@ -59,7 +60,12 @@ async function backupUserConfig(morphPath) {
59
60
  async function cleanFrameworkDirs(morphPath, targetPath) {
60
61
  const claudeDir = join(targetPath, '.claude');
61
62
  const dirsToClean = [
62
- join(morphPath, 'framework', 'templates'),
63
+ // NOTE: framework/templates is intentionally NOT in this list (R3 perf
64
+ // plan, Task 2). It's no longer copied/refreshed here — see
65
+ // reconcileLegacyTemplatesCopy() below, which decides per-run whether a
66
+ // pre-existing local copy is safe to remove or must be preserved as a
67
+ // user override. Blindly deleting it here (the old behavior) destroyed
68
+ // any customization on every single `update` run, override or not.
63
69
  join(morphPath, 'framework', 'standards'),
64
70
  join(morphPath, 'framework', 'hooks'),
65
71
  join(morphPath, 'framework', 'schemas'),
@@ -83,6 +89,67 @@ async function cleanFrameworkDirs(morphPath, targetPath) {
83
89
  }
84
90
  }
85
91
 
92
+ /**
93
+ * Last CLI version whose `update`/`setup-infra` still wrote
94
+ * `.morph/framework/templates/` unconditionally, before Task 2 of the R3
95
+ * perf plan removed that copy. Used as the decision boundary in
96
+ * `reconcileLegacyTemplatesCopy` below — see its doc comment for why a
97
+ * version check is used instead of diffing file content.
98
+ */
99
+ const LAST_VERSION_THAT_COPIED_TEMPLATES = '8.17.0';
100
+
101
+ /**
102
+ * Reconcile a pre-existing `.morph/framework/templates/` copy now that
103
+ * `update` no longer writes it (Task 2, R3 perf plan — resolution falls
104
+ * back to the npm package's own framework/templates/, see
105
+ * template-registry.js / architecture-scanner.js's layered resolver).
106
+ *
107
+ * Decided from the project's PRIOR recorded CLI version (before THIS
108
+ * update writes a new one), not by diffing content against the package.
109
+ * A content diff can't tell "stale copy from an older CLI version" apart
110
+ * from "deliberate user override" — and staleness is the dominant case for
111
+ * anyone who actually runs `update`, since template content does change
112
+ * between releases. The version boundary is unambiguous instead:
113
+ *
114
+ * - No local copy → nothing to do.
115
+ * - Prior version is unrecorded, OR <= LAST_VERSION_THAT_COPIED_TEMPLATES
116
+ * → the copy could ONLY have been put there by the OLD installer:
117
+ * `.morph/framework/**` is Edit-denied to the agent (MORPH_DENY in
118
+ * hooks-installer.js — "installer-managed, immutable at runtime"),
119
+ * the directory is gitignored, and every `update` before this fix
120
+ * unconditionally wiped + recopied it. There is no path by which a
121
+ * project on an old version could have a genuine override sitting
122
+ * here. Safe to delete unconditionally — it is dead weight, not data.
123
+ * - Prior version > LAST_VERSION_THAT_COPIED_TEMPLATES → `update` and
124
+ * `setup-infra` have never written this directory for this project.
125
+ * Anything found here was placed deliberately (by the user or other
126
+ * tooling) — this is the forward-looking override use case the
127
+ * layered resolver exists for. Preserve untouched, unconditionally,
128
+ * with no diffing.
129
+ *
130
+ * @param {string} templatesDest - .morph/framework/templates (may not exist)
131
+ * @param {string|null} priorProjectVersion - project's recorded morph-spec
132
+ * version BEFORE this update run (checkProjectOutdated().current)
133
+ * @returns {'absent'|'removed-legacy'|'preserved-override'}
134
+ */
135
+ export function reconcileLegacyTemplatesCopy(templatesDest, priorProjectVersion) {
136
+ if (!existsSync(templatesDest)) return 'absent';
137
+
138
+ const isLegacy = !priorProjectVersion
139
+ || compareVersions(priorProjectVersion, LAST_VERSION_THAT_COPIED_TEMPLATES) <= 0;
140
+
141
+ if (!isLegacy) return 'preserved-override';
142
+
143
+ try {
144
+ fs.removeSync(templatesDest);
145
+ return 'removed-legacy';
146
+ } catch {
147
+ // Couldn't remove — don't risk a half-broken state; leave it and let
148
+ // the layered resolver's override-first check pick it up as before.
149
+ return 'preserved-override';
150
+ }
151
+ }
152
+
86
153
  /**
87
154
  * Restore user's config.json after reinstallation
88
155
  * @param {string} morphPath - Path to .morph directory
@@ -240,12 +307,14 @@ export async function updateCommand(options) {
240
307
  const updateTemplates = !options.standards || options.templates;
241
308
  const updateStandards = !options.templates || options.standards;
242
309
 
243
- // Update templates
310
+ // Templates are no longer copied to .morph/framework/templates/ (Task 2,
311
+ // R3 perf plan) — see reconcileLegacyTemplatesCopy()'s doc comment for
312
+ // why. Only reconcile a leftover copy from a pre-R3 install.
313
+ let templatesReconcile = 'absent';
244
314
  if (updateTemplates) {
245
- updateSpinner.text = 'Updating templates...';
246
- const templatesSrc = join(contentDir, 'framework', 'templates');
315
+ updateSpinner.text = 'Checking for a legacy templates copy...';
247
316
  const templatesDest = join(morphPath, 'framework', 'templates');
248
- await copyDirectory(templatesSrc, templatesDest);
317
+ templatesReconcile = reconcileLegacyTemplatesCopy(templatesDest, projectCheck.current);
249
318
  }
250
319
 
251
320
  // Update standards
@@ -289,6 +358,13 @@ export async function updateCommand(options) {
289
358
  await copyDirectory(src, join(morphPath, 'framework', dir));
290
359
  }
291
360
  }
361
+ // CLI.md vive em framework/docs/ na fonte mas instala achatado em
362
+ // .morph/framework/CLI.md (o CLAUDE.md residente aponta para lá) —
363
+ // por isso não entra no loop de nomes acima.
364
+ const cliMdSrc = join(canonicalFrameworkDir, 'docs', 'CLI.md');
365
+ if (await pathExists(cliMdSrc)) {
366
+ await copyFile(cliMdSrc, join(morphPath, 'framework', 'CLI.md'));
367
+ }
292
368
 
293
369
  // Regenerate derived files (phase-utils.js, skill output tables) from output-schema.js
294
370
  // Only runs when the generator script is present (framework dev installs via npm link)
@@ -415,7 +491,15 @@ export async function updateCommand(options) {
415
491
 
416
492
  logger.blank();
417
493
  logger.info('Updated files:');
418
- if (updateTemplates) logger.dim(' ✓ .morph/framework/templates/');
494
+ if (updateTemplates) {
495
+ if (templatesReconcile === 'removed-legacy') {
496
+ logger.dim(' ✓ .morph/framework/templates/ (legacy installer copy removed — resolved from package)');
497
+ } else if (templatesReconcile === 'preserved-override') {
498
+ logger.dim(' ⚠ .morph/framework/templates/ (local override present — preserved untouched, not updated)');
499
+ } else {
500
+ logger.dim(' · .morph/framework/templates/ (not present — resolved from package, no local override)');
501
+ }
502
+ }
419
503
  if (updateStandards) logger.dim(' ✓ .morph/framework/standards/');
420
504
  logger.dim(' ✓ .morph/framework/hooks/');
421
505
  logger.dim(' ✓ .morph/framework/agents.json');
@@ -16,10 +16,15 @@ export function parseStacks(content) {
16
16
 
17
17
  const fm = fmMatch[1];
18
18
 
19
- // Try inline format: stacks: [dotnet, blazor]
19
+ // Try inline format: stacks: [dotnet, blazor] — quotes stripped like the
20
+ // list branch below; `["umbraco"]` sem isso parseava como a string literal
21
+ // `"umbraco"` (com aspas) e nunca casava tag nenhuma.
20
22
  const inlineMatch = fm.match(/^stacks:\s*\[([^\]]*)\]/m);
21
23
  if (inlineMatch) {
22
- const items = inlineMatch[1].split(',').map(s => s.trim()).filter(Boolean);
24
+ const items = inlineMatch[1]
25
+ .split(',')
26
+ .map(s => s.replace(/["']/g, '').trim())
27
+ .filter(Boolean);
23
28
  return items.length > 0 ? items : ['*'];
24
29
  }
25
30
 
@@ -264,14 +264,23 @@ export async function setupInfra(targetPath, { _exec = execSync, reinstall = fal
264
264
  await writeFile(contextReadmePath, readmeContent);
265
265
  }
266
266
 
267
- // --- 5. Copy framework/templates → .morph/framework/templates ---
268
- log('Step 5: Copying framework templates...');
269
- const frameworkTemplatesSrc = join(FRAMEWORK_DIR, 'templates');
270
- const templatesDest = join(frameworkDestDir, 'templates');
271
- if (await pathExists(frameworkTemplatesSrc)) {
272
- await copyDirectory(frameworkTemplatesSrc, templatesDest);
273
- } else {
274
- await ensureDir(templatesDest);
267
+ // --- 5. framework/templates is intentionally NOT copied to
268
+ // .morph/framework/templates anymore (R3 perf plan, Task 2). Template
269
+ // resolution is layered (template-registry.js, architecture-scanner.js
270
+ // resolveTemplatePath): a project-local .morph/framework/templates/{path}
271
+ // is still checked FIRST as an explicit override, then falls back to the
272
+ // npm package's own framework/templates/ — which is what every template
273
+ // actually rendered from until now anyway, since the installed copy was
274
+ // never read as anything but that override. Pre-populating it was a
275
+ // byte-for-byte redundant 348KB copy on every install/update.
276
+ log('Step 5: Skipping framework templates copy (resolved from package; local override still supported)...');
277
+ // Cópia legada de instalação pré-R3: se existir, ela SOMBREIA os templates
278
+ // do pacote (override-first). O `morph-spec update` reconcilia (remove cópia
279
+ // stale por fronteira de versão); aqui só avisamos — init não conhece a
280
+ // versão anterior do projeto para decidir com segurança.
281
+ const legacyTemplatesDir = join(frameworkDestDir, 'templates');
282
+ if (await pathExists(legacyTemplatesDir)) {
283
+ log(' ⚠ .morph/framework/templates/ existe (instalação antiga) e tem precedência sobre os templates do pacote — rode `morph-spec update` para reconciliar, ou apague se não for customização sua.');
275
284
  }
276
285
 
277
286
  // --- 6. Copy framework/standards → .morph/framework/standards ---
@@ -324,6 +333,16 @@ export async function setupInfra(targetPath, { _exec = execSync, reinstall = fal
324
333
  await copyFile(morphMdSrc, morphMdDest);
325
334
  }
326
335
 
336
+ // --- 8b2. Copy framework/docs/CLI.md → .morph/framework/CLI.md ---
337
+ // Referência completa dos comandos (flags, semântica, exemplos) — o
338
+ // CLAUDE.md residente carrega só a tabela compacta e aponta para cá.
339
+ log('Step 8b2: Copying CLI.md (command reference)...');
340
+ const cliMdSrc = join(FRAMEWORK_DIR, 'docs', 'CLI.md');
341
+ const cliMdDest = join(frameworkDestDir, 'CLI.md');
342
+ if (await pathExists(cliMdSrc)) {
343
+ await copyFile(cliMdSrc, cliMdDest);
344
+ }
345
+
327
346
  // --- 8c. Copy framework/phases.json → .morph/framework/phases.json ---
328
347
  log('Step 8c: Copying phases.json...');
329
348
  const phasesSrc = join(FRAMEWORK_DIR, 'phases.json');
@@ -75,8 +75,14 @@ function copyDirectory(src, dest) {
75
75
  *
76
76
  * @param {string} srcDir - Source directory to walk
77
77
  * @param {string} destDir - Destination base directory (.claude/skills/)
78
+ * @param {string[]} projectTags - Stack tags from config
79
+ * @param {Set<string>} installedNow - Accumulator: skill names this call
80
+ * actually (re)installed. NOT derived by re-scanning destDir afterwards —
81
+ * a disk scan can't distinguish "installed this run" from "leftover from a
82
+ * previous run with different projectTags", which is exactly the stale
83
+ * entry a stack change needs cleaned up.
78
84
  */
79
- function installSkillsFromDir(srcDir, destDir, projectTags) {
85
+ function installSkillsFromDir(srcDir, destDir, projectTags, installedNow) {
80
86
  const entries = readdirSync(srcDir);
81
87
  for (const entry of entries) {
82
88
  const srcPath = join(srcDir, entry);
@@ -90,9 +96,10 @@ function installSkillsFromDir(srcDir, destDir, projectTags) {
90
96
  if (!shouldInstallSkill(content, projectTags)) continue;
91
97
  const skillDestDir = join(destDir, entry);
92
98
  copyDirectory(srcPath, skillDestDir);
99
+ installedNow.add(entry);
93
100
  } else {
94
101
  // Level/category directory — recurse
95
- installSkillsFromDir(srcPath, destDir, projectTags);
102
+ installSkillsFromDir(srcPath, destDir, projectTags, installedNow);
96
103
  }
97
104
  } else if (entry.endsWith('.md') && entry !== 'README.md') {
98
105
  // Legacy flat .md skill — check stack filter before copying
@@ -102,6 +109,7 @@ function installSkillsFromDir(srcDir, destDir, projectTags) {
102
109
  const skillDir = join(destDir, skillName);
103
110
  mkdirSync(skillDir, { recursive: true });
104
111
  copyFileSync(srcPath, join(skillDir, 'SKILL.md'));
112
+ installedNow.add(skillName);
105
113
  }
106
114
  }
107
115
  }
@@ -147,28 +155,33 @@ export async function installSkills(projectDir, { projectTags = [] } = {}) {
147
155
  const claudeSkillsDir = join(projectDir, '.claude', 'skills');
148
156
  mkdirSync(claudeSkillsDir, { recursive: true });
149
157
 
158
+ // Skill names this call actually (re)installs, given projectTags. Tracked
159
+ // as we go (NOT by re-scanning claudeSkillsDir afterwards) — a disk scan
160
+ // can't tell "installed this run" apart from "leftover from a previous
161
+ // run with different projectTags", which is exactly the stale entry a
162
+ // stack change (or a re-run that narrows the stack) needs cleaned up.
163
+ const installedNow = new Set();
150
164
  for (const level of SKILL_LEVELS_TO_INSTALL) {
151
165
  const levelDir = join(FRAMEWORK_SKILLS_DIR, level);
152
166
  if (!existsSync(levelDir)) continue;
153
- installSkillsFromDir(levelDir, claudeSkillsDir, projectTags);
154
- }
155
-
156
- // Collect installed morph-* skill names
157
- const installed = new Set();
158
- for (const entry of readdirSync(claudeSkillsDir)) {
159
- if (existsSync(join(claudeSkillsDir, entry, 'SKILL.md'))) {
160
- installed.add(entry);
161
- }
167
+ installSkillsFromDir(levelDir, claudeSkillsDir, projectTags, installedNow);
162
168
  }
163
169
 
164
- // Cleanup orphan morph-* skill dirs that exist in source but were filtered out
170
+ // Clean-slate: remove any skill dir that is a KNOWN framework skill (still
171
+ // present in framework/skills/core/) but did NOT get (re)installed this
172
+ // run — i.e. it was filtered out by a `stacks:` mismatch (project.stack
173
+ // changed, or a re-run narrowed the tags). Previously scoped to `morph-`
174
+ // prefixed names only, which silently exempted every non-`morph-` skill
175
+ // (the Umbraco pair) from ever being cleaned up: install once with
176
+ // `umbraco` in the stack, drop the stack later, and they'd survive every
177
+ // reinstall/update forever. Names outside allSourceSkills (user-added,
178
+ // non-framework skills) are never touched.
165
179
  const allSourceSkills = collectSourceSkillNames();
166
180
  for (const entry of readdirSync(claudeSkillsDir)) {
167
- if (!entry.startsWith('morph-')) continue;
168
- if (!installed.has(entry) && allSourceSkills.has(entry)) {
181
+ if (!installedNow.has(entry) && allSourceSkills.has(entry)) {
169
182
  rmSync(join(claudeSkillsDir, entry), { recursive: true, force: true });
170
183
  }
171
184
  }
172
185
 
173
- return { installed: installed.size };
186
+ return { installed: installedNow.size };
174
187
  }