@polymorphism-tech/morph-spec 8.27.0 → 8.28.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 +1 -0
- package/README.md +2 -2
- package/bin/morph-spec.js +9 -4
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +3 -2
- package/docs/QUICKSTART.md +1 -1
- package/framework/MORPH.md +8 -8
- package/framework/docs/CLI.md +6 -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 +1 -1
- 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 +1 -1
- 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 +1 -1
- 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/doctor.js +9 -4
- package/src/commands/project/worktree.js +45 -3
- package/src/commands/state/advance.js +12 -0
- package/src/commands/state/approve.js +20 -0
- package/src/commands/state/create.js +4 -1
- package/src/commands/state/delete.js +63 -4
- package/src/commands/state/finish.js +20 -2
- package/src/commands/state/gate-check.js +13 -0
- package/src/commands/state/gate-decision.js +9 -0
- package/src/commands/state/handoff.js +48 -6
- package/src/commands/state/list.js +124 -10
- package/src/commands/state/score.js +9 -0
- package/src/lib/generators/handoff-generator.js +11 -4
- package/src/lib/worktree/feature-truth.js +248 -0
- package/src/lib/worktree/fleet-scanner.js +1 -65
- package/src/lib/worktree/worktree-audit.js +103 -17
package/CLAUDE.md
CHANGED
|
@@ -114,6 +114,7 @@ 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/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`) |
|
|
117
118
|
|
|
118
119
|
### Test Coverage
|
|
119
120
|
|
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.28.0
|
|
7
7
|
**Requires:** Node.js 22+, Claude Code
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -368,4 +368,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
|
|
|
368
368
|
|
|
369
369
|
---
|
|
370
370
|
|
|
371
|
-
*morph-spec v8.
|
|
371
|
+
*morph-spec v8.28.0 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/bin/morph-spec.js
CHANGED
|
@@ -126,7 +126,7 @@ program
|
|
|
126
126
|
.option('--full', 'Run full health check')
|
|
127
127
|
.option('--mcp', 'Check MCP server configuration')
|
|
128
128
|
.option('--worktrees', "Reconcile git's worktree registry × the directories on disk × known features")
|
|
129
|
-
.option('--prune',
|
|
129
|
+
.option('--prune', "With --worktrees: drop git records for worktree directories that are already gone, and clear any feature's root worktree registration pointing at one of them")
|
|
130
130
|
.option('--json', 'With --worktrees: output as JSON')
|
|
131
131
|
.option('--reset', 'Remove morph-managed entries from .claude/settings.local.json')
|
|
132
132
|
.action(doctorCommand);
|
|
@@ -144,6 +144,7 @@ program
|
|
|
144
144
|
.description('List every feature × status × gates (G1/G2/G3) × workType — the audit board')
|
|
145
145
|
.option('--all', 'Include archived features (.morph/archive)')
|
|
146
146
|
.option('--archived', 'Include archived features (alias of --all)')
|
|
147
|
+
.option('--no-branch', 'Skip reading morph/{feature} for truth — report only what is on disk in this tree')
|
|
147
148
|
.option('--json', 'Output as JSON')
|
|
148
149
|
.action((options) => listCommand(options));
|
|
149
150
|
|
|
@@ -187,9 +188,12 @@ program
|
|
|
187
188
|
|
|
188
189
|
program
|
|
189
190
|
.command('delete <feature>')
|
|
190
|
-
.description('Delete a feature (hard by default; --archive marks it abandoned). Refuses if a gate is approved unless --force')
|
|
191
|
+
.description('Delete a feature (hard by default; --archive marks it abandoned). Refuses if a gate is approved unless --force. Also refuses if a worktree is alive elsewhere, unless --with-worktree')
|
|
191
192
|
.option('--archive', 'Soft delete: move to .morph/archive with status "abandoned" instead of removing')
|
|
192
|
-
.option('--force', 'Delete even when a gate is approved')
|
|
193
|
+
.option('--force', 'Delete even when a gate is approved (does NOT bypass the live-worktree refusal — see --with-worktree)')
|
|
194
|
+
.option('--with-worktree', 'Tear down the live worktree first (PROVE-or-DECLINE); only deletes if that succeeds. Branch survives unless --delete-branch')
|
|
195
|
+
.option('--delete-branch', 'With --with-worktree, also delete the feature branch (default: kept — it is the work)')
|
|
196
|
+
.option('--discard-ignored', 'With --with-worktree, discard gitignored files unique to the worktree instead of declining removal')
|
|
193
197
|
.option('--no-git', 'Skip git staging of the removal')
|
|
194
198
|
.option('--json', 'Output as JSON')
|
|
195
199
|
.action((feature, options) => deleteCommand(feature, options));
|
|
@@ -255,8 +259,9 @@ program
|
|
|
255
259
|
|
|
256
260
|
program
|
|
257
261
|
.command('handoff <feature>')
|
|
258
|
-
.description('Generate the compressed session-handoff document (.morph/features/{f}/handoff.md) — pointers, not copies')
|
|
262
|
+
.description('Generate the compressed session-handoff document (.morph/features/{f}/handoff.md) — pointers, not copies. Resolves the tree that owns the feature\'s state (worktree, or morph/{feature} if only committed) and writes there — not necessarily cwd.')
|
|
259
263
|
.option('--json', 'Output as JSON')
|
|
264
|
+
.option('--here', 'Force building/writing in the current directory, even if another tree owns this feature')
|
|
260
265
|
.action((feature, options) => handoffCommand(feature, options));
|
|
261
266
|
|
|
262
267
|
// ─── Use cases ────────────────────────────────────────────────────────────
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
|
@@ -121,7 +121,8 @@ morph-spec worktree setup {feature} # create worktrees/{feature}/ on bran
|
|
|
121
121
|
morph-spec worktree link [--all] # relink shared infra (node_modules[, .claude, .morph/framework])
|
|
122
122
|
morph-spec worktree list [--json] # active feature worktrees with phase/gate status
|
|
123
123
|
# Close a worktree feature with `morph-spec finish {feature} --pr|--merge` (see above) — it absorbed `worktree finish`.
|
|
124
|
-
morph-spec handoff {feature} [--json]
|
|
124
|
+
morph-spec handoff {feature} [--json] [--here] # regenerate the handoff doc — resolves the owning tree and writes THERE, safe to run from the root
|
|
125
|
+
morph-spec doctor --worktrees [--prune] # reconcile git's registry × disk × features; --prune also clears a dangling root worktree.path pointer
|
|
125
126
|
morph-spec fleet [--json] # every active feature (root + worktrees) with phase/gates/ports/handoff/activity
|
|
126
127
|
```
|
|
127
128
|
|
|
@@ -185,4 +186,4 @@ Never edited directly — go through CLI commands or `morph-spec update` instead
|
|
|
185
186
|
|
|
186
187
|
---
|
|
187
188
|
|
|
188
|
-
*morph-spec v8.
|
|
189
|
+
*morph-spec v8.28.0 by Polymorphism Tech*
|
package/docs/QUICKSTART.md
CHANGED
package/framework/MORPH.md
CHANGED
|
@@ -113,18 +113,18 @@ Nem todo trabalho merece o pipeline completo. Cada feature nasce com **identidad
|
|
|
113
113
|
|
|
114
114
|
### 2.2 Desenvolvimento paralelo por worktree (frota)
|
|
115
115
|
|
|
116
|
-
Features independentes podem ser desenvolvidas **em paralelo, cada uma no seu git worktree**, uma instância de Claude Code por worktree. `morph-spec create <f> --description "<o quê/porquê>" --worktree` (ou `
|
|
116
|
+
Features independentes podem ser desenvolvidas **em paralelo, cada uma no seu git worktree**, uma instância de Claude Code por worktree. `morph-spec create <f> --description "<o quê/porquê>" --worktree` (ou `worktree setup <f>`) cria `worktrees/{f}/` na branch `morph/{f}`. Contrato:
|
|
117
117
|
|
|
118
118
|
- **Uma feature = um worktree = uma instância.** Nunca abra duas instâncias na mesma feature (setup recusa, exit 3).
|
|
119
|
-
- `feature.json` viaja **commitado na branch** `morph/{f}
|
|
119
|
+
- `feature.json` viaja **commitado na branch** `morph/{f}`; a cópia da raiz é esqueleto do nascimento, nunca mais atualizado — leitores (`list`/`handoff`/`fleet`) seguem a branch, mutadores recusam gravar fora da árvore dona (detalhes: CLI.md). `state.json` do worktree é índice derivável — self-heal, nunca sincronizado à mão.
|
|
120
120
|
- Infra compartilhada (`node_modules`, `.claude`, `.morph/framework`) linkada por junction; `memory/`/`logs/` são locais de cada worktree. Faltou infra (worktree aberto com `git worktree add` puro) → `morph-spec worktree link --all`.
|
|
121
121
|
- **Só o morph remove worktree.** Remoção genérica — `git worktree remove` (com OU sem `--force`), `rm -rf`, `Remove-Item -Recurse`, explorador/IDE — **atravessa a junction** e apaga o conteúdo real compartilhado da raiz (`.claude`/`node_modules`/`.morph/framework`), não só o link, saindo com sucesso. Foi assim que uma raiz teve `.claude/` e `.morph/framework/` esvaziados. Só `morph-spec` desmonta a junction ANTES de remover (PROVE-or-DECLINE): **fechar a feature** = `morph-spec finish <f> --pr|--merge`; **só desmontar o worktree** (feature segue aberta) = `morph-spec worktree remove <f>` — ou `--target <path>` para worktree sem feature. O hook `block-destructive-worktree-removal` barra a versão crua, mas a regra vale antes dele.
|
|
122
|
-
- **Ambiente separado (portas):**
|
|
123
|
-
- **Banco de dados:** compartilhado por padrão
|
|
124
|
-
- **Limitação conhecida:** `node_modules` compartilhado (junction) —
|
|
125
|
-
- **Handoff entre sessões:** `.morph/features/{f}/handoff.md` —
|
|
126
|
-
- **Visão da frota:** `
|
|
127
|
-
- **Ownership de arquivos:** sem lock
|
|
122
|
+
- **Ambiente separado (portas):** setup aloca **bloco de portas determinístico** por feature em `.morph/worktree.env` (`PORT`, `MORPH_PORT_RANGE`; `ASPNETCORE_URLS` se dotnet). **Suba dev servers SEMPRE nas portas do bloco** — as padrão (3000/5000) pertencem à raiz. Local, git-invisível; SessionStart injeta as portas. Override: `config.json → fleet.basePort/blockSize`.
|
|
123
|
+
- **Banco de dados:** compartilhado por padrão; isolar via Neon MCP: branch de DB `morph/{f}` + connection string em `.morph/worktree.env` (sem Neon, prefixe schema/DB pelo nome da feature). Migration destrutiva → integre antes de abrir worktrees irmãos.
|
|
124
|
+
- **Limitação conhecida:** `node_modules` compartilhado (junction) — dependência mudada afeta os irmãos; integre (`finish`) antes de seguir em paralelo.
|
|
125
|
+
- **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.
|
|
126
|
+
- **Visão da frota:** `fleet` agrega raiz + worktrees (fase, gates, tasks, portas, handoff, commit, sujeira); `worktree list` é a visão enxuta por worktree.
|
|
127
|
+
- **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.
|
|
128
128
|
- **Fechamento:** aprove o Gate 3, depois **`morph-spec finish <f> --pr|--merge`** — encerramento único e simétrico. **Arquiva** commitando `features/{f} → archive/{f}` **dentro de `morph/{f}`** e integra: `--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). Worktree desmontado (PROVE-or-DECLINE), portas liberadas; no `--pr` a branch é **preservada**. `morph-spec archive` fica para arquivar-sem-integrar (features legadas, faxina).
|
|
129
129
|
|
|
130
130
|
---
|
package/framework/docs/CLI.md
CHANGED
|
@@ -11,22 +11,23 @@
|
|
|
11
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
12
|
- **`morph-spec gate-check <feature> <gate> [--signals <json>] [--trust <mode>] [--json]`** — o caminho **default**: coleta do disco os sinais que `shouldPauseGate` (risk-detector.js) já sabe avaliar (`workType`, `taskCount`, verificação, sha/diffstat), decide a pausa e persiste `gateDecisions.{gate}`. `--signals` aceita só as chaves que o disco genuinamente não expõe sozinho — `designSystemDivergence`, `standardsViolations`, `evalScore` — qualquer outra chave é ignorada com aviso. No **review**, o `evalScore` real vem do token `**Composite-Score:** {n.n}` do `evaluator-report.md` (o avaliador o grava por contrato). **Score que não venha desse token PAUSA o Gate 3**, qualquer que seja o valor: o proxy (média das `taskScores`) e o override de `--signals` medem auto-avaliação de quem implementou, não qualidade da feature — em campo, 7 tasks com auto-score 9.0-9.3 auto-passaram um produto que o avaliador independente pontuou 7.2 e reprovou com 3 P1. `evalScoreSource` registra a procedência (`evaluator-report` / `taskScores-proxy` / `signals-override`). Para forçar a passagem mesmo assim, use `gate-decision --pause false` — explícito e registrado. Fora do review a regra não vale: antes de haver implementação não existe avaliador independente. Um stamp de `verification` com status `fail` (último `morph-spec verify` vermelho) **força pausa** em qualquer gate. Um stamp `pass` mas **stale** (sha diferente do HEAD atual — código mudou depois do último verify) força pausa **só no ship** (`review`); em `plan`/`uiux`/`proposal` a staleness é computada e reportada em `signals`, mas não pausa nada — é regra de ship, não de gate genérico. Stamp **ausente** nunca pausa por isso (features legadas sem stamp).
|
|
13
13
|
- **`morph-spec gate-decision <feature> <gate> --pause <bool> [--reason <txt>] [--signals <json>]`** — o **escape manual**: registra uma decisão já tomada pela sessão (ex.: `discoveryMode: "interview"`, sinal que `shouldPauseGate` nem representa). Mesmo write path do `gate-check` — grava `gateDecisions` no feature.json com timestamp server-side (shape em `framework/schemas/feature.schema.json`); **nunca edite gateDecisions à mão** — o Edit manual colide com o hook state-sync.
|
|
14
|
+
- **Guarda de árvore dona:** `approve`/`unapprove`/`advance`/`score`/`gate-decision`/`gate-check` recusam (exit 1) quando um worktree vivo é dono da feature e a sessão está em outra árvore — sem isso o comando carimbaria o gate/score/decisão na cópia congelada da raiz, criando o drift em vez de só herdá-lo. A recusa nomeia o worktree e o comando exato para rodar lá; `--force` **não** dispensa (é sobre gate order, não sobre árvore). Rodando de dentro do worktree dono, ou numa feature sem worktree algum, nada muda.
|
|
14
15
|
|
|
15
16
|
## Features
|
|
16
17
|
|
|
17
18
|
- **`morph-spec create <feature> --description "<o quê/porquê>" --type <feature|bug|chore|hotfix> [--request "<txt>"] [--name "<nome>"] [--worktree]`** — cria a estrutura da feature + `feature.json` canônico explicitamente (nascimento explícito no início da proposal; o hook state-sync continua cobrindo o caminho implícito como fallback). **Identidade no nascimento:** `name` (default = slug) e `description` são carimbados; a `--description` é **obrigatória** numa feature nova (ou `--request`, que a semeia) — sem nenhuma das duas o `create` recusa (fim dos scaffolds anônimos). `--type` classifica direto; `--request "<pedido>"` roda o detector determinístico (empate/sem sinal sai com exit 2 e candidatos — a LLM decide e re-roda com `--type` + `--request`/`--description`); `--worktree` já nasce a feature no seu worktree próprio. **Toda feature nasce numa branch `morph/{f}`** (ramificada da default) mesmo sem `--worktree` — por isso o fechamento por `morph-spec finish` é idêntico nos dois fluxos.
|
|
18
|
-
- **`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).
|
|
19
|
-
- **`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 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).
|
|
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).
|
|
20
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.
|
|
21
22
|
|
|
22
23
|
## Worktree / Fleet
|
|
23
24
|
|
|
24
|
-
- **`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — promovendo junto, no mesmo commit, os use cases da feature para o acervo permanente `.morph/specs/usecases/` com `status: implemented` (fail-open: sem `2-plan/usecases/`, no-op silencioso) — depois integra. 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, `--force` dispensa o Gate 3
|
|
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
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.
|
|
26
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.
|
|
27
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).
|
|
28
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 handoff <feature
|
|
30
|
+
- **`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`.
|
|
30
31
|
- **`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.
|
|
31
32
|
- **`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.
|
|
32
33
|
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
- **`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.
|
|
36
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.
|
|
37
38
|
- **`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.
|
|
38
|
-
- **`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.
|
|
39
|
+
- **`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) **e** limpa o registro `worktree` de qualquer feature cujo `.path` aponte para um desses diretórios já sumidos (`dangling-registration` — a direção simétrica do `no-feature`: não é o worktree sem feature, é a feature com um ponteiro morto na raiz); apagar pasta que ainda existe é reportado com o comando exato, nunca executado.
|
|
39
40
|
- **`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.
|
|
40
41
|
|
|
41
42
|
## Observabilidade
|
|
@@ -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.28.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.28.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.28.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-apply
|
|
@@ -4,7 +4,7 @@ description: Entende o negócio, os usuários e a arquitetura antes de qualquer
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber um novo request de feature, antes de morph-plan
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.28.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.28.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.28.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.28.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.28.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.28.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.28.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.28.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.28.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.28.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-uiux
|
package/package.json
CHANGED
|
@@ -564,10 +564,15 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
|
|
|
564
564
|
const { auditWorktrees, pruneWorktrees } = await import('../../lib/worktree/worktree-audit.js');
|
|
565
565
|
|
|
566
566
|
if (options.prune) {
|
|
567
|
-
const
|
|
568
|
-
const
|
|
569
|
-
|
|
567
|
+
const beforeIssues = auditWorktrees(targetPath);
|
|
568
|
+
const beforeMissing = beforeIssues.filter((i) => i.type === 'registered-missing').length;
|
|
569
|
+
const beforeDangling = beforeIssues.filter((i) => i.type === 'dangling-registration').length;
|
|
570
|
+
const result = await pruneWorktrees(targetPath);
|
|
571
|
+
if (result.pruned) logger.success(`git worktree prune: ${beforeMissing} registro(s) órfão(s) removido(s).`);
|
|
570
572
|
else logger.warn(`prune falhou: ${result.error}`);
|
|
573
|
+
if (result.clearedRegistrations.length) {
|
|
574
|
+
logger.success(`registro de worktree limpo em ${result.clearedRegistrations.length}/${beforeDangling} feature(s): ${result.clearedRegistrations.map((r) => r.feature).join(', ')}`);
|
|
575
|
+
}
|
|
571
576
|
}
|
|
572
577
|
|
|
573
578
|
const issues = auditWorktrees(targetPath);
|
|
@@ -589,7 +594,7 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
|
|
|
589
594
|
console.log(` ${i.detail}`);
|
|
590
595
|
console.log(` → ${i.fix}`);
|
|
591
596
|
}
|
|
592
|
-
if (issues.some((i) => i.type === 'registered-missing') && !options.prune) {
|
|
597
|
+
if (issues.some((i) => i.type === 'registered-missing' || i.type === 'dangling-registration') && !options.prune) {
|
|
593
598
|
logger.dim('\n Rode `morph-spec doctor --worktrees --prune` para limpar os registros órfãos.');
|
|
594
599
|
}
|
|
595
600
|
return false;
|
|
@@ -443,7 +443,13 @@ export async function runWorktreeSetup(feature, options = {}) {
|
|
|
443
443
|
const normalizedExisting = existingPath.replace(/\\/g, '/');
|
|
444
444
|
const normalizedNew = worktreePath.replace(/\\/g, '/');
|
|
445
445
|
if (normalizedExisting !== normalizedNew && existsSync(existingPath)) {
|
|
446
|
-
return {
|
|
446
|
+
return {
|
|
447
|
+
ok: false, exitCode: 3, created: false, error: 'worktree-exists-elsewhere', existingPath, feature,
|
|
448
|
+
hint: `A different live worktree is already registered for "${feature}". `
|
|
449
|
+
+ `If it's stale (removed by hand, or by \`git worktree prune\` directly, outside \`morph-spec finish\`/\`worktree remove\`), `
|
|
450
|
+
+ `run \`morph-spec doctor --worktrees --prune\` to clear the registration, or `
|
|
451
|
+
+ `\`morph-spec worktree remove --target "${existingPath}"\` if the directory is still there but unwanted.`,
|
|
452
|
+
};
|
|
447
453
|
}
|
|
448
454
|
}
|
|
449
455
|
}
|
|
@@ -519,7 +525,10 @@ export async function runWorktreeSetup(feature, options = {}) {
|
|
|
519
525
|
try {
|
|
520
526
|
if (existsSync(join(worktreePath, '.morph', 'features', feature, 'feature.json'))) {
|
|
521
527
|
const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
|
|
522
|
-
|
|
528
|
+
// `worktree` is ROOT-OWNED bookkeeping, absent from this branch-local
|
|
529
|
+
// copy by contract — pass it explicitly so the doc built FROM INSIDE
|
|
530
|
+
// the worktree still says "Local: worktree", not "raiz primária".
|
|
531
|
+
writeHandoff(feature, worktreePath, { worktree: { path: worktreePath, branch } });
|
|
523
532
|
}
|
|
524
533
|
} catch { /* fail-open */ }
|
|
525
534
|
|
|
@@ -1018,7 +1027,7 @@ export function primaryRootUnexplainedDirt(root, feature, opts = {}) {
|
|
|
1018
1027
|
* @param {string} root
|
|
1019
1028
|
* @param {string} feature
|
|
1020
1029
|
*/
|
|
1021
|
-
async function clearRootWorktreeRegistration(root, feature) {
|
|
1030
|
+
export async function clearRootWorktreeRegistration(root, feature) {
|
|
1022
1031
|
const featPathspec = `.morph/features/${feature}`;
|
|
1023
1032
|
try {
|
|
1024
1033
|
const rootFjPath = join(root, '.morph', 'features', feature, 'feature.json');
|
|
@@ -1044,6 +1053,39 @@ async function clearRootWorktreeRegistration(root, feature) {
|
|
|
1044
1053
|
} catch { /* fail-open — a dirty root is a nuisance, not a reason to fail */ }
|
|
1045
1054
|
}
|
|
1046
1055
|
|
|
1056
|
+
/**
|
|
1057
|
+
* Clear a feature's `worktree` registration ONLY when it's dangling — the
|
|
1058
|
+
* path it names no longer exists on disk (the directory was removed outside
|
|
1059
|
+
* `morph-spec finish`/`worktree remove`: by hand, by `git worktree prune`, or
|
|
1060
|
+
* by any tool that doesn't know about this pointer).
|
|
1061
|
+
*
|
|
1062
|
+
* Deliberately narrower than `clearRootWorktreeRegistration`: a registration
|
|
1063
|
+
* naming a worktree that's still ALIVE is not a bug to repair — it's the
|
|
1064
|
+
* anti-dupe guard's only signal (`runWorktreeSetup` Guard 3) and `verify`'s
|
|
1065
|
+
* only signal for which cwd may persist (`decidePersistence`). Clearing a
|
|
1066
|
+
* live registration would trade a true pointer for a false negative on both.
|
|
1067
|
+
*
|
|
1068
|
+
* Reuses `clearRootWorktreeRegistration` for the actual mutation (same
|
|
1069
|
+
* atomic-write + scoped-commit contract) once the dangling predicate holds.
|
|
1070
|
+
*
|
|
1071
|
+
* @param {string} root
|
|
1072
|
+
* @param {string} feature
|
|
1073
|
+
* @returns {Promise<{ cleared: boolean }>}
|
|
1074
|
+
*/
|
|
1075
|
+
export async function clearDanglingWorktreeRegistration(root, feature) {
|
|
1076
|
+
try {
|
|
1077
|
+
const rootFjPath = join(root, '.morph', 'features', feature, 'feature.json');
|
|
1078
|
+
if (!existsSync(rootFjPath)) return { cleared: false };
|
|
1079
|
+
const fj = JSON.parse(readFileSync(rootFjPath, 'utf8'));
|
|
1080
|
+
const path = fj?.worktree?.path;
|
|
1081
|
+
if (!path || existsSync(path)) return { cleared: false };
|
|
1082
|
+
await clearRootWorktreeRegistration(root, feature);
|
|
1083
|
+
return { cleared: true };
|
|
1084
|
+
} catch {
|
|
1085
|
+
return { cleared: false }; // fail-open — malformed feature.json must never throw
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1047
1089
|
/**
|
|
1048
1090
|
* Why `git worktree remove` failed, from its stderr.
|
|
1049
1091
|
*
|
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
import { getFeature, setApprovalGate, derivePhase } from '../../core/state/state-manager.js';
|
|
6
6
|
import { unmetPrereqs } from '../../core/state/gate-order.js';
|
|
7
7
|
import { OUTPUT_SCHEMA } from '../../core/paths/output-schema.js';
|
|
8
|
+
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
8
9
|
|
|
9
10
|
const FRAMEWORK_DIR = fileURLToPath(new URL('../../../framework/', import.meta.url));
|
|
10
11
|
|
|
@@ -111,6 +112,17 @@ export async function advanceCommand(featureName, options = {}) {
|
|
|
111
112
|
process.exit(1);
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
// Owner-tree guard: a live worktree elsewhere owns this feature's state —
|
|
116
|
+
// never stamp a gate onto the root's birth-instant skeleton (branch-drift
|
|
117
|
+
// created by this command, not merely left behind by one).
|
|
118
|
+
const owner = assertOwnerTree(featureName, process.cwd());
|
|
119
|
+
if (owner.blocked) {
|
|
120
|
+
for (const { tone, text } of ownerTreeRefusalLines(featureName, owner.tree, owner.branch, `morph-spec advance ${featureName}`)) {
|
|
121
|
+
console.error(chalk[tone](text));
|
|
122
|
+
}
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
114
126
|
const featurePath = join(process.cwd(), '.morph', 'features', featureName);
|
|
115
127
|
const phase = derivePhase(featurePath);
|
|
116
128
|
|
|
@@ -6,6 +6,7 @@ import { join } from 'path';
|
|
|
6
6
|
import { getFeature, setApprovalGate, getApprovalGate, getApprovalHistory, derivePhase } from '../../core/state/state-manager.js';
|
|
7
7
|
import { unmetPrereqs } from '../../core/state/gate-order.js';
|
|
8
8
|
import { evaluatorReportState, COMPOSITE_SCORE_TOKEN } from '../../lib/evaluator-report.js';
|
|
9
|
+
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* The literal token `review-report.md` carries to declare which tree it
|
|
@@ -86,6 +87,14 @@ export async function unapproveCommand(featureName, gateName, options = {}) {
|
|
|
86
87
|
process.exit(1);
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
const owner = assertOwnerTree(featureName, process.cwd());
|
|
91
|
+
if (owner.blocked) {
|
|
92
|
+
for (const { tone, text } of ownerTreeRefusalLines(featureName, owner.tree, owner.branch, `morph-spec unapprove ${featureName} ${gateName}`)) {
|
|
93
|
+
console.error(chalk[tone](text));
|
|
94
|
+
}
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
89
98
|
const currentStatus = getApprovalGate(featureName, gateName);
|
|
90
99
|
if (!currentStatus?.approved) {
|
|
91
100
|
console.log(chalk.yellow(`\n⚠️ Gate "${gateName}" is not currently approved — nothing to revoke\n`));
|
|
@@ -155,6 +164,17 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
155
164
|
process.exit(1);
|
|
156
165
|
}
|
|
157
166
|
|
|
167
|
+
// Owner-tree guard: a live worktree elsewhere owns this feature's state —
|
|
168
|
+
// never stamp a gate onto the root's birth-instant skeleton (branch-drift
|
|
169
|
+
// created by this command, not merely left behind by one).
|
|
170
|
+
const owner = assertOwnerTree(featureName, process.cwd());
|
|
171
|
+
if (owner.blocked) {
|
|
172
|
+
for (const { tone, text } of ownerTreeRefusalLines(featureName, owner.tree, owner.branch, `morph-spec approve ${featureName} ${gateName}`)) {
|
|
173
|
+
console.error(chalk[tone](text));
|
|
174
|
+
}
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
158
178
|
// Check if already approved
|
|
159
179
|
const currentStatus = getApprovalGate(featureName, gateName);
|
|
160
180
|
if (currentStatus?.approved) {
|
|
@@ -215,7 +215,10 @@ export async function createCommand(featureName, options = {}) {
|
|
|
215
215
|
// read. Regenerated here because setup ran before the copy above.
|
|
216
216
|
try {
|
|
217
217
|
const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
|
|
218
|
-
|
|
218
|
+
// Same reason as runWorktreeSetup's own call: `worktree` was just
|
|
219
|
+
// stripped from this branch-local copy a few lines up, so the doc
|
|
220
|
+
// must be told explicitly where it actually lives.
|
|
221
|
+
writeHandoff(featureName, worktreeResult.path, { worktree: { path: worktreeResult.path, branch: worktreeResult.branch } });
|
|
219
222
|
} catch { /* fail-open */ }
|
|
220
223
|
} catch { /* fail-open — the feature already exists in root */ }
|
|
221
224
|
}
|