@polymorphism-tech/morph-spec 8.31.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 +6 -0
- package/README.md +2 -2
- package/bin/morph-spec.js +20 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -1
- package/framework/MORPH.md +2 -2
- package/framework/docs/CLI.md +10 -5
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +3 -2
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
- package/framework/skills/core/morph-implement/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +2 -2
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/commands/project/branch.js +170 -0
- package/src/commands/project/doctor.js +137 -9
- package/src/commands/project/fleet.js +10 -3
- package/src/commands/project/worktree.js +73 -50
- package/src/commands/state/finish.js +41 -26
- package/src/commands/state/list.js +79 -6
- package/src/core/state/lifecycle-audit.js +127 -0
- package/src/lib/git/branch-classifier.js +177 -0
- package/src/lib/git/gh.js +30 -0
- package/src/lib/git/pending-prs.js +89 -0
- package/src/lib/git/pr-index.js +70 -0
- package/src/lib/tasks/test-runner.js +200 -38
- package/src/lib/validators/verify-runner.js +30 -4
- package/src/lib/worktree/fleet-scanner.js +51 -21
- package/src/lib/worktree/ignored-triage.js +197 -0
- package/src/lib/worktree/worktree-audit.js +200 -35
package/CLAUDE.md
CHANGED
|
@@ -114,7 +114,13 @@ When modifying framework components, verify the full scope of related files.
|
|
|
114
114
|
| `framework/templates/**` | REGISTRY.json (template registered), valid Handlebars helpers |
|
|
115
115
|
| `src/core/paths/output-schema.js`| Run `npm run generate` to update phase-utils.js |
|
|
116
116
|
| `framework/MORPH.md` | All core skills (cross-references), CLAUDE.md (slash command list) |
|
|
117
|
+
| `src/lib/worktree/ignored-triage.js` | `worktree.js` (`classifyIgnoredLeftovers`), `test/lib/worktree/ignored-triage.test.js`, e os canários de `test/commands/finish.test.js` que pinam "o exemplar único nunca morre" — mudar um bucket muda o que o `finish` apaga |
|
|
118
|
+
| `src/lib/git/pr-index.js` | os QUATRO leitores do mesmo fato "esta branch tem PR": `fleet-scanner.js` (coluna PR), `branch-classifier.js` (o que NÃO apagar), `worktree-audit.js` (o que NÃO sugerir remover) e `pending-prs.js` (a síntese "aguardando merge"). `null` é UNKNOWN, nunca "sem PR" |
|
|
119
|
+
| `src/lib/git/branch-classifier.js` | `src/commands/project/branch.js`, `worktree-audit.js` (`unintegratedWork` reusa `classifyBranch` — o `doctor` e o `branch prune` não podem discordar sobre a mesma branch), `test/commands/branch-prune.test.js`, e `lifecycle-audit.js` (`abandoned-scaffold` conta commit próprio pelo mesmo `resolveBaseRef` + `commitsAhead`) |
|
|
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
|
+
| `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 |
|
|
117
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 |
|
|
118
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 |
|
|
119
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) |
|
|
120
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.
|
|
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.
|
|
382
|
+
*morph-spec v8.33.0 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/bin/morph-spec.js
CHANGED
|
@@ -24,6 +24,7 @@ import { updateCommand } from '../src/commands/project/update.js';
|
|
|
24
24
|
import { doctorCommand } from '../src/commands/project/doctor.js';
|
|
25
25
|
import { statusCommand } from '../src/commands/project/status.js';
|
|
26
26
|
import { worktreeSetupCommand, worktreeLinkCommand, worktreeListCommand, worktreeRemoveCommand } from '../src/commands/project/worktree.js';
|
|
27
|
+
import { branchPruneCommand } from '../src/commands/project/branch.js';
|
|
27
28
|
import { fleetCommand } from '../src/commands/project/fleet.js';
|
|
28
29
|
import { claimCommand } from '../src/commands/project/claim.js';
|
|
29
30
|
import { standardsCommand } from '../src/commands/project/standards.js';
|
|
@@ -364,7 +365,7 @@ program
|
|
|
364
365
|
.description('Run the deterministic verification chain (build + tests + validators + e2e) — code before the LLM judge')
|
|
365
366
|
.option('--json', 'Output as JSON')
|
|
366
367
|
.option('--rubric <id>', 'Force a rubric (default: by task group → config → default)')
|
|
367
|
-
.option('--timeout <ms>', '
|
|
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))
|
|
368
369
|
.option('--skip-tests', 'Skip the test node')
|
|
369
370
|
.option('--skip-build', 'Skip the build node')
|
|
370
371
|
.option('--skip-e2e', 'Skip the e2e node (Docker bring-up + Playwright)')
|
|
@@ -472,7 +473,7 @@ worktreeCommand
|
|
|
472
473
|
.command('remove [feature]')
|
|
473
474
|
.description('Safely tear down a worktree (drops infra junctions first, PROVE-or-DECLINE) — does NOT close the feature; that is `finish`')
|
|
474
475
|
.option('--target <path>', 'Remove the worktree at this path (for worktrees with no feature registration)')
|
|
475
|
-
.option('--discard-ignored', '
|
|
476
|
+
.option('--discard-ignored', 'Skip the backup: gitignored files unique to the worktree are deleted instead of copied to .git/morph-spec/finish-backup (by default they are backed up and the removal proceeds anyway)')
|
|
476
477
|
.option('--delete-branch', 'Also delete the branch (default: kept — unintegrated commits live there)')
|
|
477
478
|
.action((feature, options) => worktreeRemoveCommand(feature, options));
|
|
478
479
|
|
|
@@ -482,6 +483,23 @@ worktreeCommand
|
|
|
482
483
|
.option('--json', 'Output as JSON')
|
|
483
484
|
.action((options) => worktreeListCommand(options));
|
|
484
485
|
|
|
486
|
+
// ─── Branch housekeeping ──────────────────────────────────────────────────
|
|
487
|
+
// `git branch --merged` cannot see a squash-merge, so the only command left for
|
|
488
|
+
// a squash-merged branch is `git branch -D` — indistinguishable from discarding
|
|
489
|
+
// real work. `prune` makes the distinction itself and deletes only what it
|
|
490
|
+
// proved is already integrated.
|
|
491
|
+
const branchCommand = program
|
|
492
|
+
.command('branch')
|
|
493
|
+
.description('Local branch housekeeping');
|
|
494
|
+
|
|
495
|
+
branchCommand
|
|
496
|
+
.command('prune')
|
|
497
|
+
.description('Delete local branches whose work is already in the default branch — including squash-merged ones git cannot see. Never touches a branch with an OPEN PR, one in use by a worktree, or one whose integration could not be proven')
|
|
498
|
+
.option('--remote', 'Also delete origin/<branch> for each pruned branch (default: remote branches are left alone)')
|
|
499
|
+
.option('--dry-run', 'Classify and report without deleting anything')
|
|
500
|
+
.option('--json', 'Output as JSON')
|
|
501
|
+
.action((options) => branchPruneCommand(options));
|
|
502
|
+
|
|
485
503
|
// ─── Fleet ────────────────────────────────────────────────────────────────
|
|
486
504
|
program
|
|
487
505
|
.command('fleet')
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
package/framework/CLAUDE.md
CHANGED
|
@@ -58,9 +58,10 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
|
58
58
|
| `list [--all] [--json]` | Quadro de auditoria: feature × workType × status × gates |
|
|
59
59
|
| `delete <f> [--archive] [--force]` | Remove feature; recusa se gate aprovado sem `--force` |
|
|
60
60
|
| `retype <f> <tipo>` | Muda o workType mid-flight; bloqueado após o Gate 3 |
|
|
61
|
-
| `finish <f> --pr\|--merge` | Encerramento único: arquiva, promove use cases,
|
|
61
|
+
| `finish <f> --pr\|--merge` | Encerramento único: arquiva, promove use cases, desmonta worktree. Integra só no `--merge`; `--pr` abre PR e fica `aguardando-merge` até alguém mergear |
|
|
62
62
|
| `archive <f>` | Arquiva sem integrar (legadas/faxina) |
|
|
63
63
|
| `worktree setup\|link\|remove\|list` | Ciclo do worktree; `remove` é a única remoção segura |
|
|
64
|
+
| `branch prune [--dry-run]` | Apaga branch já integrada, inclusive squash-merge; nunca a com PR aberto |
|
|
64
65
|
| `handoff <f>` | Regenera o documento de retomada da feature |
|
|
65
66
|
| `fleet` | Visão agregada de toda feature ativa (raiz + worktrees) |
|
|
66
67
|
| `claim [f] [--release] [--list]` | Posse advisory — confirme antes de `finish`/`archive`/`delete` se outra sessão tiver claim vivo |
|
package/framework/MORPH.md
CHANGED
|
@@ -110,7 +110,7 @@ Nem todo trabalho merece o pipeline completo. Cada feature nasce com **identidad
|
|
|
110
110
|
|
|
111
111
|
**Compat:** feature sem `workType` (pré-v8) é tratada como `feature`. Mudança mid-flight: `morph-spec retype <f> <tipo>` (desfaz só carimbos de política, preserva aprovações humanas; bloqueado após o Gate 3).
|
|
112
112
|
|
|
113
|
-
**Nascimento numa branch:** toda feature — com ou sem `--worktree` — nasce em `morph/{f}` ramificada da default (comum: desenvolve direto na raiz; worktree: isola em `worktrees/{f}`).
|
|
113
|
+
**Nascimento numa branch:** toda feature — com ou sem `--worktree` — nasce em `morph/{f}` ramificada da default (comum: desenvolve direto na raiz; worktree: isola em `worktrees/{f}`). Fecha-se igual nos dois com `morph-spec finish <f> --pr|--merge` (§2.2). Features pré-modelo (sem branch `morph/{f}`) fecham por `morph-spec archive <f>`, não por `finish`.
|
|
114
114
|
|
|
115
115
|
### 2.2 Desenvolvimento paralelo por worktree (frota)
|
|
116
116
|
|
|
@@ -126,7 +126,7 @@ Features independentes podem ser desenvolvidas **em paralelo, cada uma no seu gi
|
|
|
126
126
|
- **Handoff entre sessões:** `.morph/features/{f}/handoff.md` — retomada comprimida, regenerável (`morph-spec handoff <f>`, resolve a árvore dona sozinho), **aponta** para os artefatos em vez de copiá-los. Regenerado em `setup`/`create --worktree`/`advance`/`approve`. **Leia-o antes de re-derivar contexto à mão** — nunca edite manualmente.
|
|
127
127
|
- **Visão da frota:** `fleet` agrega raiz + worktrees (fase, gates, tasks, portas, handoff, commit, sujeira); `worktree list` é a visão enxuta por worktree.
|
|
128
128
|
- **Ownership de arquivos:** sem lock runtime — colisão resolve **em plan-time** (`dag` detecta overlap de `outputs`, recomenda `isolation: "worktree"`) e **por construção** (uma feature = um worktree = um escritor). Arquivos compartilhados convergem no merge do `finish`; conflito ali é escopo sobreposto, não bug.
|
|
129
|
-
- **Fechamento:** aprove o Gate 3, depois **`morph-spec finish <f> --pr|--merge`** — encerramento único
|
|
129
|
+
- **Fechamento:** aprove o Gate 3, depois **`morph-spec finish <f> --pr|--merge`** — encerramento único; `--merge` integra na hora, **`--pr` não** — só abre o PR. **Arquiva** commitando `features/{f} → archive/{f}` **dentro de `morph/{f}`**: `--merge` faz `--no-ff` **local** (sem push; recusa se sujo; aborta em conflito); `--pr` faz `push` + `gh pr create` (exige `origin`+`gh`, checados **antes** de arquivar) — fica `aguardando-merge` até o merge, **nunca "entregue" só por `finished: true`**. Worktree desmontado (PROVE-or-DECLINE), portas liberadas; no `--pr` a branch é **preservada**. `morph-spec archive` fica para arquivar-sem-integrar (features legadas, faxina).
|
|
130
130
|
|
|
131
131
|
---
|
|
132
132
|
|
package/framework/docs/CLI.md
CHANGED
|
@@ -16,17 +16,18 @@
|
|
|
16
16
|
## Features
|
|
17
17
|
|
|
18
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. 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). `--all` inclui as arquivadas; `--json` para scripting. É o complemento do `delete` (enxergar o que está morto antes de remover).
|
|
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
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
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
22
|
|
|
23
23
|
## Worktree / Fleet
|
|
24
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. 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.
|
|
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
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
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
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.
|
|
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.
|
|
30
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`.
|
|
31
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.
|
|
32
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,9 +35,13 @@
|
|
|
34
35
|
## Validação
|
|
35
36
|
|
|
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.
|
|
37
|
-
- **`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.
|
|
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.
|
|
38
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.
|
|
39
|
-
- **`morph-spec doctor --
|
|
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.
|
|
40
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.
|
|
41
46
|
|
|
42
47
|
## Grafo de codebase
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
7
|
+
cliVersion: "8.33.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-apply
|
|
@@ -228,6 +228,7 @@ re-apresente o gate**: siga direto para o encerramento (§6) conforme a decisão
|
|
|
228
228
|
|
|
229
229
|
- 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`).
|
|
230
230
|
- **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.
|
|
231
|
+
- **`--pr` não encerra a feature; quem encerra é o merge.** O comando arquiva e termina (`finished: true`) nos dois modos, mas só `--merge` integra na hora (`integrated: true`); `--pr` sai com `integrated: false`, `awaiting: "merge"` e `pr: {number, url}` — o archive viaja no diff do PR e só chega na branch default quando alguém mergeia. Até lá a feature aparece em `morph-spec list`/`fleet` como `aguardando-merge`, sem cópia local. Nunca registre a feature como entregue só porque `finish --pr` retornou `finished: true`.
|
|
231
232
|
|
|
232
233
|
## Anti-padrões
|
|
233
234
|
|
|
@@ -245,7 +246,7 @@ re-apresente o gate**: siga direto para o encerramento (§6) conforme a decisão
|
|
|
245
246
|
- `.morph/features/{feature}/3-implement/recap.md` produzido por morph-implement.
|
|
246
247
|
- `.morph/features/{feature}/4-review/review-report.md` produzido por morph-review.
|
|
247
248
|
- Gate 3 registrado aprovado em `.morph/features/{feature}/feature.json` (autoritativo) — via `morph-spec approve` em auto, ou pelo hook state-sync no caminho manual.
|
|
248
|
-
- Feature com Gate 3 aprovado e **fechada** por `morph-spec finish --pr|--merge` (arquiva carimbando `status: "completed"` + `archivedAt
|
|
249
|
+
- Feature com Gate 3 aprovado e **fechada** por `morph-spec finish --pr|--merge` (arquiva carimbando `status: "completed"` + `archivedAt` e desmonta num passo; `--merge` integra na hora, `--pr` só abre o PR — `aguardando-merge` até o merge) — ou, se legada, por `morph-spec archive`.
|
|
249
250
|
|
|
250
251
|
## Referências
|
|
251
252
|
|
|
@@ -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.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.
|
|
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.
|
|
7
|
+
cliVersion: "8.33.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-hotfix
|
|
@@ -109,7 +109,7 @@ morph-spec approve {feature} review --approver "<nome de quem aprovou>"
|
|
|
109
109
|
### 8. Pós-gate (o "test loop → ship" de `MORPH.md` §2.1)
|
|
110
110
|
|
|
111
111
|
- Rode a suíte completa uma vez: `morph-spec verify {feature}` (sem escopo de task — cobertura da feature inteira, agora que o Gate 3 está aprovado).
|
|
112
|
-
- Feche com `morph-spec finish {feature} --pr|--merge` — o mesmo encerramento único das demais features (arquiva commitando dentro de `morph/{f}`,
|
|
112
|
+
- Feche com `morph-spec finish {feature} --pr|--merge` — o mesmo encerramento único das demais features (arquiva commitando dentro de `morph/{f}`, desmonta). Num hotfix urgente o `--merge` (local, direto na default, integra na hora) costuma ser o caminho; use `--pr` quando o fix precisa passar por CI/review antes do merge — mas `--pr` **não integra**: sai com `finished: true` e `integrated: false`, `awaiting: "merge"`. Um hotfix "fechado" por `--pr` ainda não chegou na produção — não relate o incidente como resolvido até o merge. Ver `MORPH.md` §2.2.
|
|
113
113
|
- Não integre automaticamente — o usuário decide quando e por qual modo (`finish` toca a default ou o remoto). Feature legada sem `morph/{f}`: `finish` roteia para `morph-spec archive {feature}`.
|
|
114
114
|
|
|
115
115
|
## Anti-padrões
|
|
@@ -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.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.
|
|
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.
|
|
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.
|
|
7
|
+
cliVersion: "8.33.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-review
|
|
@@ -202,7 +202,7 @@ e o `recap.md` estar atualizado. Normalmente invocada pela skill orquestradora `
|
|
|
202
202
|
|
|
203
203
|
No caminho manual, o hook state-sync detecta a aprovação em answers de AskUserQuestion e grava `approvalGates.review = { approved: true, timestamp, approvedBy: "manual" }` no `feature.json` autoritativo da feature — **a atribuição vem do nome da feature na pergunta** (o template acima o carrega). Pergunta sem nome, com mais de uma feature na árvore: o hook recusa a gravar e avisa; aprove então por `morph-spec approve {feature} review`. Em auto sem AskUserQuestion, aprove via `morph-spec approve {feature} review --mode auto` — nunca via Edit à mão de state.json.
|
|
204
204
|
|
|
205
|
-
**Próximo passo (não é desta skill):** com o Gate 3 aprovado, a feature fecha por `morph-spec finish {feature} --pr|--merge` — a `morph-apply` §6 conduz a escolha PR vs merge. Não arquive nem integre aqui.
|
|
205
|
+
**Próximo passo (não é desta skill):** com o Gate 3 aprovado, a feature fecha por `morph-spec finish {feature} --pr|--merge` — a `morph-apply` §6 conduz a escolha PR vs merge. Não arquive nem integre aqui. **`--pr` não encerra a feature** — arquiva e termina, mas só integra no merge (`integrated: false`, `awaiting: "merge"` no payload); até lá a feature fica `aguardando-merge` em `morph-spec list`/`fleet`. `finished: true` não é sinônimo de entregue.
|
|
206
206
|
|
|
207
207
|
## Output esperado
|
|
208
208
|
- `.morph/features/{feature}/4-review/evaluator-report.md` — relatório bruto e autoritativo do
|
|
@@ -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.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.
|
|
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.
|
|
7
|
+
cliVersion: "8.33.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-uiux
|
package/package.json
CHANGED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* branch.js — `morph-spec branch prune`.
|
|
3
|
+
*
|
|
4
|
+
* The gap this closes: after a few features close by squash-merge, the local
|
|
5
|
+
* repo accumulates branches git considers unmerged. `git branch --merged` lists
|
|
6
|
+
* none of them, `git branch -d` refuses each one, and the only command left is
|
|
7
|
+
* `git branch -D` — which is EXACTLY the command for throwing away real work.
|
|
8
|
+
* Once a user has typed `-D` on five safe branches, the sixth (an actual
|
|
9
|
+
* unintegrated feature) gets the same keystroke.
|
|
10
|
+
*
|
|
11
|
+
* So this command never asks the user to make that distinction: it makes it
|
|
12
|
+
* itself, prints the evidence for each branch, and deletes only what it PROVED
|
|
13
|
+
* is already integrated (see branch-classifier.js for the five classes and
|
|
14
|
+
* their order). Everything else is listed with the reason it survives.
|
|
15
|
+
*
|
|
16
|
+
* The remote is a separate decision: `--remote` is required to touch
|
|
17
|
+
* `origin/<branch>`, because a local branch is a local artifact while the remote
|
|
18
|
+
* one may be what someone else, or an open review, is looking at.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { execFileSync } from 'child_process';
|
|
22
|
+
import chalk from 'chalk';
|
|
23
|
+
import { isGitRepo, resolvePrimaryRoot } from './worktree.js';
|
|
24
|
+
import { detectDefaultBranch } from '../../lib/git/default-branch.js';
|
|
25
|
+
import { buildPrIndex } from '../../lib/git/pr-index.js';
|
|
26
|
+
import { classifyBranches } from '../../lib/git/branch-classifier.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Delete one local branch.
|
|
30
|
+
*
|
|
31
|
+
* `-D`, not `-d`, and that is the whole point of the classifier: `-d` refuses
|
|
32
|
+
* every squash-merged branch, so a command that used it would be unable to do
|
|
33
|
+
* its one job. The safety does not come from git's own check (which cannot see
|
|
34
|
+
* a squash-merge) — it comes from only ever reaching this function with a
|
|
35
|
+
* branch whose integration was positively proven.
|
|
36
|
+
*/
|
|
37
|
+
function deleteLocal(name, cwd) {
|
|
38
|
+
try {
|
|
39
|
+
execFileSync('git', ['branch', '-D', name], { cwd, stdio: 'pipe' });
|
|
40
|
+
return { deleted: true };
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return { deleted: false, error: (err.stderr?.toString() || err.message || 'unknown').trim().slice(0, 200) };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function deleteRemote(name, cwd) {
|
|
47
|
+
try {
|
|
48
|
+
execFileSync('git', ['push', 'origin', '--delete', name], { cwd, stdio: 'pipe', timeout: 20000 });
|
|
49
|
+
return { deleted: true };
|
|
50
|
+
} catch (err) {
|
|
51
|
+
return { deleted: false, error: (err.stderr?.toString() || err.message || 'unknown').trim().slice(0, 200) };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Does `origin/<name>` exist locally as a remote-tracking ref? */
|
|
56
|
+
function remoteRefExists(name, cwd) {
|
|
57
|
+
try {
|
|
58
|
+
execFileSync('git', ['show-ref', '--verify', '--quiet', `refs/remotes/origin/${name}`], { cwd, stdio: 'pipe' });
|
|
59
|
+
return true;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Command handler for `morph-spec branch prune [--remote] [--dry-run] [--json]`.
|
|
67
|
+
*
|
|
68
|
+
* Fail-open outside a git repo (exit 0, empty result) — the same discipline the
|
|
69
|
+
* rest of the worktree suite follows: a housekeeping command must never be the
|
|
70
|
+
* reason a script dies.
|
|
71
|
+
*
|
|
72
|
+
* @param {{ remote?: boolean, dryRun?: boolean, json?: boolean, cwd?: string }} [options]
|
|
73
|
+
*/
|
|
74
|
+
export function branchPruneCommand(options = {}) {
|
|
75
|
+
const cwd = options.cwd || process.cwd();
|
|
76
|
+
const emit = (payload, code = 0) => {
|
|
77
|
+
if (options.json) console.log(JSON.stringify(payload, null, 2));
|
|
78
|
+
else printReport(payload, options);
|
|
79
|
+
process.exitCode = code;
|
|
80
|
+
return payload;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (!isGitRepo(cwd)) {
|
|
84
|
+
return emit({ pruned: [], kept: [], error: 'not-a-git-repo' });
|
|
85
|
+
}
|
|
86
|
+
const root = resolvePrimaryRoot(cwd);
|
|
87
|
+
const defaultBranch = detectDefaultBranch(root);
|
|
88
|
+
// null = UNKNOWN, never "no PR" — the classifier treats a branch with no
|
|
89
|
+
// positive evidence as unmerged, so a missing `gh` costs conservatism, not
|
|
90
|
+
// safety. Reported so the user can tell "no PR" from "could not ask".
|
|
91
|
+
const prIndex = buildPrIndex(root);
|
|
92
|
+
|
|
93
|
+
const classified = classifyBranches({ cwd: root, defaultBranch, prIndex });
|
|
94
|
+
const pruned = [];
|
|
95
|
+
const kept = [];
|
|
96
|
+
|
|
97
|
+
for (const b of classified) {
|
|
98
|
+
if (!b.deletable) { kept.push(b); continue; }
|
|
99
|
+
if (options.dryRun) {
|
|
100
|
+
pruned.push({ ...b, deleted: false, dryRun: true, remoteDeleted: false });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const local = deleteLocal(b.name, root);
|
|
104
|
+
let remoteDeleted = false;
|
|
105
|
+
let remoteError;
|
|
106
|
+
if (local.deleted && options.remote && remoteRefExists(b.name, root)) {
|
|
107
|
+
const r = deleteRemote(b.name, root);
|
|
108
|
+
remoteDeleted = r.deleted;
|
|
109
|
+
if (!r.deleted) remoteError = r.error;
|
|
110
|
+
}
|
|
111
|
+
if (local.deleted) pruned.push({ ...b, deleted: true, remoteDeleted, ...(remoteError ? { remoteError } : {}) });
|
|
112
|
+
else kept.push({ ...b, kind: 'delete-failed', reason: `git branch -D falhou: ${local.error}`, deletable: false });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return emit({
|
|
116
|
+
defaultBranch,
|
|
117
|
+
prIndexAvailable: prIndex !== null,
|
|
118
|
+
dryRun: options.dryRun === true,
|
|
119
|
+
remote: options.remote === true,
|
|
120
|
+
pruned,
|
|
121
|
+
kept,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const KIND_LABEL = {
|
|
126
|
+
'merged-ancestral': chalk.green('merged'),
|
|
127
|
+
'merged-squash': chalk.green('squash-merged'),
|
|
128
|
+
open: chalk.yellow('PR aberto'),
|
|
129
|
+
'in-worktree': chalk.cyan('em uso'),
|
|
130
|
+
unmerged: chalk.red('não integrada'),
|
|
131
|
+
'delete-failed': chalk.red('falhou'),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
function printReport(payload, options) {
|
|
135
|
+
if (payload.error === 'not-a-git-repo') {
|
|
136
|
+
console.log(chalk.gray('não é um repositório git — nada a fazer'));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const verb = payload.dryRun ? 'seriam apagadas' : 'apagadas';
|
|
140
|
+
console.log(chalk.bold(`\nbranch prune — default: ${payload.defaultBranch || chalk.red('não resolvida')}`));
|
|
141
|
+
if (!payload.prIndexAvailable) {
|
|
142
|
+
// Load-bearing caveat, not noise: without `gh` a squash-merge is only
|
|
143
|
+
// detectable by the tree comparison, so branches that WERE merged may
|
|
144
|
+
// legitimately show up as unmerged here.
|
|
145
|
+
console.log(chalk.gray(' (sem `gh`/GitHub: squash-merge só é detectado por árvore idêntica)'));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (payload.pruned.length) {
|
|
149
|
+
console.log(chalk.bold(`\n ${payload.pruned.length} ${verb}:`));
|
|
150
|
+
for (const b of payload.pruned) {
|
|
151
|
+
const remote = b.remoteDeleted ? chalk.gray(' + origin') : '';
|
|
152
|
+
console.log(` ${KIND_LABEL[b.kind] || b.kind} ${chalk.bold(b.name)}${remote}`);
|
|
153
|
+
console.log(chalk.gray(` ${b.reason}`));
|
|
154
|
+
if (b.remoteError) console.log(chalk.yellow(` origin não apagada: ${b.remoteError}`));
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
console.log(chalk.gray('\n nada a apagar'));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (payload.kept.length) {
|
|
161
|
+
console.log(chalk.bold(`\n ${payload.kept.length} mantida(s):`));
|
|
162
|
+
for (const b of payload.kept) {
|
|
163
|
+
console.log(` ${KIND_LABEL[b.kind] || b.kind} ${chalk.bold(b.name)}${b.ahead ? chalk.gray(` (+${b.ahead})`) : ''}`);
|
|
164
|
+
console.log(chalk.gray(` ${b.reason}`));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (payload.dryRun) console.log(chalk.gray('\n --dry-run: nada foi apagado'));
|
|
168
|
+
if (!options.remote && payload.pruned.length) console.log(chalk.gray(' branches remotas intactas (use --remote para apagá-las também)'));
|
|
169
|
+
console.log();
|
|
170
|
+
}
|