@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.
- package/README.md +2 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +45 -43
- package/framework/MORPH.md +119 -167
- package/framework/docs/CLI.md +40 -0
- package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -1
- package/framework/skills/core/morph-apply/SKILL.md +3 -7
- package/framework/skills/core/morph-brainstorming/SKILL.md +2 -12
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +2 -8
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +2 -14
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/commands/project/doctor.js +11 -4
- package/src/commands/project/init.js +1 -1
- package/src/commands/project/update.js +91 -7
- package/src/lib/stack-filter.js +7 -2
- package/src/scripts/setup-infra.js +27 -8
- package/src/utils/skills-installer.js +28 -15
|
@@ -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.
|
|
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.
|
|
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.
|
|
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** (
|
|
235
|
-
- **Fechar a feature** com `morph-spec finish {feature} --pr|--merge
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 —
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
7
|
+
cliVersion: "8.18.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-standards
|
package/package.json
CHANGED
|
@@ -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: '
|
|
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/,
|
|
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
|
-
|
|
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
|
-
//
|
|
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 = '
|
|
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
|
-
|
|
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)
|
|
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');
|
package/src/lib/stack-filter.js
CHANGED
|
@@ -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]
|
|
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.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
//
|
|
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.
|
|
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:
|
|
186
|
+
return { installed: installedNow.size };
|
|
174
187
|
}
|