@polymorphism-tech/morph-spec 8.22.0 → 8.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/morph-spec.js +27 -1
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +1 -1
- package/framework/MORPH.md +2 -2
- package/framework/agents.json +1 -0
- package/framework/docs/CLI.md +5 -3
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +118 -0
- package/framework/hooks/shared/reparse-scan.js +100 -0
- 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 +2 -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 +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +14 -2
- package/framework/skills/core/morph-plan/SKILL.md +6 -4
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +29 -3
- 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/framework/standards/STANDARDS.json +28 -0
- package/framework/standards/backend/dotnet/fluent-validation-vsa.md +5 -2
- package/framework/standards/backend/dotnet/testing.md +127 -0
- package/package.json +1 -1
- package/src/commands/project/worktree.js +250 -86
- package/src/commands/state/approve.js +11 -0
- package/src/commands/state/archive.js +12 -3
- package/src/commands/state/finish.js +30 -16
- package/src/commands/state/handoff.js +3 -2
- package/src/commands/state/usecase.js +77 -0
- package/src/commands/validation/verify.js +6 -0
- package/src/lib/generators/handoff-generator.js +2 -2
- package/src/lib/specs/usecase-promoter.js +135 -19
- package/src/lib/specs/usecase-registry.js +237 -0
- package/src/lib/validators/traceability-check.js +198 -0
- package/src/lib/validators/verify-runner.js +14 -0
- package/src/lib/worktree/worktree-audit.js +6 -2
- package/src/utils/hooks-installer.js +12 -8
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.24.1
|
|
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.24.1 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/bin/morph-spec.js
CHANGED
|
@@ -23,7 +23,7 @@ import { installPluginCommand } from '../src/commands/project/install-plugin-cmd
|
|
|
23
23
|
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
|
-
import { worktreeSetupCommand, worktreeLinkCommand, worktreeListCommand } from '../src/commands/project/worktree.js';
|
|
26
|
+
import { worktreeSetupCommand, worktreeLinkCommand, worktreeListCommand, worktreeRemoveCommand } from '../src/commands/project/worktree.js';
|
|
27
27
|
import { fleetCommand } from '../src/commands/project/fleet.js';
|
|
28
28
|
import { claimCommand } from '../src/commands/project/claim.js';
|
|
29
29
|
import { standardsCommand } from '../src/commands/project/standards.js';
|
|
@@ -42,6 +42,7 @@ import { retypeCommand } from '../src/commands/state/retype.js';
|
|
|
42
42
|
import { handoffCommand } from '../src/commands/state/handoff.js';
|
|
43
43
|
import { listCommand } from '../src/commands/state/list.js';
|
|
44
44
|
import { deleteCommand } from '../src/commands/state/delete.js';
|
|
45
|
+
import { usecaseReserveCommand } from '../src/commands/state/usecase.js';
|
|
45
46
|
|
|
46
47
|
// === Tasks ===
|
|
47
48
|
import { dagCommand } from '../src/commands/tasks/dag.js';
|
|
@@ -258,6 +259,19 @@ program
|
|
|
258
259
|
.option('--json', 'Output as JSON')
|
|
259
260
|
.action((feature, options) => handoffCommand(feature, options));
|
|
260
261
|
|
|
262
|
+
// ─── Use cases ────────────────────────────────────────────────────────────
|
|
263
|
+
const usecaseCommand = program
|
|
264
|
+
.command('usecase')
|
|
265
|
+
.description('Project-wide UC-{nn} numbering (acervo + every worktree of this repo)');
|
|
266
|
+
|
|
267
|
+
usecaseCommand
|
|
268
|
+
.command('reserve')
|
|
269
|
+
.description('Atomically reserve the next N UC-{nn} numbers, shared across every worktree of this repo — replaces the old max+1 disk scan, which collides across worktrees')
|
|
270
|
+
.option('--count <n>', 'How many numbers to reserve', '1')
|
|
271
|
+
.option('--feature <name>', 'Feature to record as the reservation owner (defaults to the sole active feature, if unambiguous) — also what the promotion collision guard compares against')
|
|
272
|
+
.option('--json', 'Output as JSON')
|
|
273
|
+
.action((options) => usecaseReserveCommand(options));
|
|
274
|
+
|
|
261
275
|
// ─── Tasks ────────────────────────────────────────────────────────────────
|
|
262
276
|
program
|
|
263
277
|
.command('dag <feature>')
|
|
@@ -397,6 +411,18 @@ worktreeCommand
|
|
|
397
411
|
.option('--all', 'Link all shared dirs, not just node_modules')
|
|
398
412
|
.action((options) => worktreeLinkCommand(options));
|
|
399
413
|
|
|
414
|
+
// The SAFE removal — it drops the infra junctions before letting git near the
|
|
415
|
+
// directory. Raw `git worktree remove` follows them out and empties the primary
|
|
416
|
+
// root's .claude/ and .morph/framework/, so nothing in this repo (message, doc,
|
|
417
|
+
// or skill) may point at it; they all point here.
|
|
418
|
+
worktreeCommand
|
|
419
|
+
.command('remove [feature]')
|
|
420
|
+
.description('Safely tear down a worktree (drops infra junctions first, PROVE-or-DECLINE) — does NOT close the feature; that is `finish`')
|
|
421
|
+
.option('--target <path>', 'Remove the worktree at this path (for worktrees with no feature registration)')
|
|
422
|
+
.option('--discard-ignored', 'Allow removing even though it holds gitignored files that exist nowhere else (e.g. .env) — they are destroyed')
|
|
423
|
+
.option('--delete-branch', 'Also delete the branch (default: kept — unintegrated commits live there)')
|
|
424
|
+
.action((feature, options) => worktreeRemoveCommand(feature, options));
|
|
425
|
+
|
|
400
426
|
worktreeCommand
|
|
401
427
|
.command('list')
|
|
402
428
|
.description('List active feature worktrees with their phase and gate status')
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
package/framework/CLAUDE.md
CHANGED
|
@@ -60,7 +60,7 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
|
60
60
|
| `retype <f> <tipo>` | Muda o workType mid-flight; bloqueado após o Gate 3 |
|
|
61
61
|
| `finish <f> --pr\|--merge` | Encerramento único: arquiva, promove use cases, integra, desmonta worktree |
|
|
62
62
|
| `archive <f>` | Arquiva sem integrar (legadas/faxina) |
|
|
63
|
-
| `worktree setup\|link\|list` | Ciclo do worktree
|
|
63
|
+
| `worktree setup\|link\|remove\|list` | Ciclo do worktree; `remove` é a única remoção segura |
|
|
64
64
|
| `handoff <f>` | Regenera o documento de retomada da feature |
|
|
65
65
|
| `fleet` | Visão agregada de toda feature ativa (raiz + worktrees) |
|
|
66
66
|
| `claim [f] [--release] [--list]` | Posse advisory — confirme antes de `finish`/`archive`/`delete` se outra sessão tiver claim vivo |
|
package/framework/MORPH.md
CHANGED
|
@@ -118,11 +118,11 @@ Features independentes podem ser desenvolvidas **em paralelo, cada uma no seu gi
|
|
|
118
118
|
- **Uma feature = um worktree = uma instância.** Nunca abra duas instâncias na mesma feature (setup recusa, exit 3).
|
|
119
119
|
- `feature.json` viaja **commitado na branch** `morph/{f}`. `state.json` local do worktree é índice derivável — materializado por 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
122
|
- **Ambiente separado (portas):** o setup aloca um **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 portas padrão (3000/5000) pertencem à raiz. Arquivo local, git-invisível; SessionStart injeta as portas. Overrides em `config.json → fleet.basePort/blockSize`.
|
|
123
123
|
- **Banco de dados:** compartilhado por padrão. Isolamento real (Neon MCP): branch de DB `morph/{f}` + connection string no `.morph/worktree.env`; sem Neon, prefixe schema/DB local pelo nome da feature. Migration destrutiva compartilhando banco → integre antes de abrir worktrees irmãos.
|
|
124
124
|
- **Limitação conhecida:** `node_modules` compartilhado (junction) — mudança de dependência afeta os irmãos. Mudou dependência: conclua e integre (`finish`) antes de seguir em paralelo.
|
|
125
|
-
- **Handoff entre sessões:** `.morph/features/{f}/handoff.md` — documento de retomada comprimido, regenerável (`morph-spec handoff <f>`), **aponta** para os artefatos em vez de copiá-los. Regenerado em `worktree setup`/`create --worktree`/`advance`. **Leia-o antes de re-derivar contexto à mão.** Nunca edite manualmente.
|
|
125
|
+
- **Handoff entre sessões:** `.morph/features/{f}/handoff.md` — documento de retomada comprimido, regenerável (`morph-spec handoff <f>`), **aponta** para os artefatos em vez de copiá-los. Regenerado em `worktree setup`/`create --worktree`/`advance`/`approve`. **Leia-o antes de re-derivar contexto à mão.** Nunca edite manualmente.
|
|
126
126
|
- **Visão da frota:** `morph-spec fleet` agrega raiz + worktrees (fase, gates, tasks, portas, handoff, último commit, sujeira). `morph-spec worktree list` é a visão enxuta por worktree.
|
|
127
127
|
- **Ownership de arquivos:** sem lock em runtime — colisão se resolve **em plan-time** (`morph-spec 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).
|
package/framework/agents.json
CHANGED
package/framework/docs/CLI.md
CHANGED
|
@@ -21,10 +21,12 @@
|
|
|
21
21
|
|
|
22
22
|
## Worktree / Fleet
|
|
23
23
|
|
|
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. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. Flags: `--keep` mantém o worktree, `--force` dispensa o Gate 3, `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados.
|
|
24
|
+
- **`morph-spec 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, `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados.
|
|
25
|
+
- **`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.
|
|
25
26
|
- **`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.
|
|
26
|
-
- **`morph-spec worktree setup|link|list`** — gerencia o ciclo do worktree por feature (`setup` cria `worktrees/{f}/` na branch `morph/{f}`, aloca o bloco de portas e escreve `.morph/worktree.env`; `link` religa infra compartilhada; `list` mostra fase/gate de cada worktree ativo).
|
|
27
|
-
- **`morph-spec
|
|
27
|
+
- **`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
|
+
- **`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>`** — regenera o documento de retomada (`.morph/features/{f}/handoff.md`): comprimido, derivado, aponta para os artefatos em vez de copiá-los. Gerado automaticamente em `setup`/`create --worktree`/`advance`/`approve`.
|
|
28
30
|
- **`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.
|
|
29
31
|
- **`morph-spec claim [feature] [--release] [--list] [--json]`** — posse **advisory** de feature. Cada sessão reclama sua feature ativa automaticamente (hook SessionStart) e renova o heartbeat a cada tool call; o registro vive em `<git-common-dir>/morph-spec/claims.json`, compartilhado por todos os worktrees. **`morph-spec claim <feature>` (sem flags) ADQUIRE** — se esta árvore já tinha claim vivo em outra feature, ele é liberado no mesmo passo (o CLI reporta "movido de X para Y"); é o jeito de trocar de feature no meio da tarefa sem a dança de dois passos `--release` + reclamar. `--list` força a leitura sem adquirir mesmo passando uma feature. **Claims avisam, nunca bloqueiam** — mas se o SessionStart avisar que outra sessão tem claim vivo, **confirme antes de `finish`/`archive`/`delete`**: duas sessões na mesma feature já custaram um recap.md inteiro e um sub-agent (o worktree sumiu debaixo dele). Claim órfão de sessão morta: `morph-spec claim <f> --release`.
|
|
30
32
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { resolve, isAbsolute } from 'path';
|
|
4
|
+
import { blockResult, runStandalone } from '../../shared/hook-response.js';
|
|
5
|
+
import { stateExists } from '../../shared/state-reader.js';
|
|
6
|
+
import { scanForBlockers } from '../../shared/reparse-scan.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* block-destructive-worktree-removal — the only defense that does not depend on
|
|
10
|
+
* someone reading documentation.
|
|
11
|
+
*
|
|
12
|
+
* A morph worktree carries JUNCTIONS to the primary root's shared infra
|
|
13
|
+
* (`node_modules`, `.claude`, `.morph/framework`). Every generic recursive
|
|
14
|
+
* removal — `git worktree remove` (plain AND `--force`), `rm -rf`,
|
|
15
|
+
* `Remove-Item -Recurse`, `rmdir /s` — FOLLOWS those links out of the worktree
|
|
16
|
+
* and deletes what they point at, leaving the primary root's `.claude/` and
|
|
17
|
+
* `.morph/framework/` as empty directories. That is the incident this hook
|
|
18
|
+
* exists for, and it is not hypothetical: it happened, and the command was
|
|
19
|
+
* copied from a message the CLI itself printed.
|
|
20
|
+
*
|
|
21
|
+
* `morph-spec worktree remove` is safe because it drops the junctions BEFORE
|
|
22
|
+
* letting git near the directory. This hook is what stops the unsafe path from
|
|
23
|
+
* being reachable by accident.
|
|
24
|
+
*
|
|
25
|
+
* DELIBERATELY NARROW — an inert or over-eager guardrail is worse than none:
|
|
26
|
+
* - only recursive-removal shapes, only when a target path can be extracted;
|
|
27
|
+
* - only when that path exists AND the scan finds reparse points under it
|
|
28
|
+
* (an already-unlinked worktree, or one that never had junctions, passes);
|
|
29
|
+
* - anything unparseable passes.
|
|
30
|
+
* The scan is the authority, not the path shape: a worktree kept outside
|
|
31
|
+
* `worktrees/` is protected just the same, and a `worktrees/` directory with
|
|
32
|
+
* nothing to traverse is not blocked for looking dangerous.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** Split a shell-ish command into tokens, honoring single and double quotes. */
|
|
36
|
+
function tokenize(command) {
|
|
37
|
+
const tokens = [];
|
|
38
|
+
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
39
|
+
let m;
|
|
40
|
+
while ((m = re.exec(command)) !== null) tokens.push(m[1] ?? m[2] ?? m[3]);
|
|
41
|
+
return tokens;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The path a recursive-removal command would destroy, or null when this is not
|
|
46
|
+
* such a command (or the target cannot be read off it).
|
|
47
|
+
*
|
|
48
|
+
* @param {string} command
|
|
49
|
+
* @returns {string|null}
|
|
50
|
+
*/
|
|
51
|
+
export function extractRemovalTarget(command) {
|
|
52
|
+
const tokens = tokenize(command);
|
|
53
|
+
if (!tokens.length) return null;
|
|
54
|
+
const lower = tokens.map((t) => t.toLowerCase());
|
|
55
|
+
|
|
56
|
+
// `git worktree remove [--force] <path>` — the path is always the last
|
|
57
|
+
// non-flag token; git has no form of this command without one.
|
|
58
|
+
const gi = lower.indexOf('git');
|
|
59
|
+
if (gi !== -1 && lower[gi + 1] === 'worktree' && lower[gi + 2] === 'remove') {
|
|
60
|
+
const rest = tokens.slice(gi + 3).filter((t) => !t.startsWith('-'));
|
|
61
|
+
return rest.length ? rest[rest.length - 1] : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Generic recursive deletes. Each needs BOTH its command word and a recursive
|
|
65
|
+
// flag — a plain `rm file` or `Remove-Item file` cannot traverse a junction's
|
|
66
|
+
// contents and is none of this hook's business.
|
|
67
|
+
const recursive = tokens.some((t) => /^(-[a-z]*r[a-z]*|--recursive|\/s)$/i.test(t));
|
|
68
|
+
const removalCmd = lower.find((t) => /^(rm|remove-item|ri|rd|rmdir|del)$/.test(t));
|
|
69
|
+
if (!removalCmd || !recursive) return null;
|
|
70
|
+
|
|
71
|
+
const idx = lower.indexOf(removalCmd);
|
|
72
|
+
// A cmd.exe switch is a slash plus a letter or two (`rmdir /s /q`). Anything
|
|
73
|
+
// longer that starts with `/` is a POSIX ABSOLUTE PATH — filtering on the
|
|
74
|
+
// bare `/` made every Linux target look like a flag, so `rm -rf /tmp/wt`
|
|
75
|
+
// extracted nothing and the hook passed exactly where it had to block.
|
|
76
|
+
const isCmdSwitch = (t) => /^\/[a-z]{1,2}$/i.test(t);
|
|
77
|
+
const operands = tokens.slice(idx + 1).filter((t) => !t.startsWith('-') && !isCmdSwitch(t));
|
|
78
|
+
// Named-parameter form: `Remove-Item -Path <p> -Recurse`.
|
|
79
|
+
const pathFlag = lower.findIndex((t) => t === '-path' || t === '-literalpath');
|
|
80
|
+
if (pathFlag !== -1 && tokens[pathFlag + 1]) return tokens[pathFlag + 1];
|
|
81
|
+
return operands.length ? operands[0] : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
|
|
86
|
+
* @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
|
|
87
|
+
*/
|
|
88
|
+
export async function run(payload) {
|
|
89
|
+
try {
|
|
90
|
+
if (!stateExists()) return null;
|
|
91
|
+
|
|
92
|
+
const command = payload?.tool_input?.command || '';
|
|
93
|
+
if (!command) return null;
|
|
94
|
+
|
|
95
|
+
const raw = extractRemovalTarget(command);
|
|
96
|
+
if (!raw) return null;
|
|
97
|
+
|
|
98
|
+
const cwd = payload?.cwd || process.cwd();
|
|
99
|
+
const target = isAbsolute(raw) ? resolve(raw) : resolve(cwd, raw);
|
|
100
|
+
if (!existsSync(target)) return null;
|
|
101
|
+
|
|
102
|
+
const { safe, blockers } = scanForBlockers(target);
|
|
103
|
+
if (safe) return null;
|
|
104
|
+
|
|
105
|
+
const shown = blockers.slice(0, 5).join(', ');
|
|
106
|
+
const more = blockers.length > 5 ? `, +${blockers.length - 5}` : '';
|
|
107
|
+
return blockResult(
|
|
108
|
+
`Remoção bloqueada: "${target}" contém link(s) que este comando SEGUIRIA para fora, apagando o alvo real na raiz do projeto (${shown}${more}).\n\n` +
|
|
109
|
+
`Isto é o que esvazia o \`.claude/\` e o \`.morph/framework/\` da raiz.\n\n` +
|
|
110
|
+
`Use: \`morph-spec worktree remove --target "${target}"\` — ele desmonta as junctions ANTES de remover, e recusa se não conseguir provar que é seguro.\n` +
|
|
111
|
+
`Para fechar a feature de vez: \`morph-spec finish <feature> --pr|--merge\`.`
|
|
112
|
+
);
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await runStandalone(run);
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reparse-scan.js — exhaustive, fail-closed reparse-point (symlink/junction)
|
|
3
|
+
* scan of a directory tree.
|
|
4
|
+
*
|
|
5
|
+
* This lives in `framework/hooks/shared/` rather than `src/` because BOTH sides
|
|
6
|
+
* need it and the dependency direction in this repo only runs one way: `src/`
|
|
7
|
+
* imports from `framework/hooks/shared/` (see `finish.js`'s `claim-registry`
|
|
8
|
+
* and `e2e-check` imports), never the reverse — a hook shipped into a target
|
|
9
|
+
* project has the package, not this repo's `src/` layout.
|
|
10
|
+
*
|
|
11
|
+
* Two callers, one question:
|
|
12
|
+
* - `teardownWorktree` (src/commands/project/worktree.js) — may this worktree
|
|
13
|
+
* be handed to `git worktree remove`?
|
|
14
|
+
* - the `block-destructive-worktree-removal` PreToolUse hook — is the command
|
|
15
|
+
* about to run going to traverse a link out of the worktree?
|
|
16
|
+
*
|
|
17
|
+
* WHY IT MUST BE EXHAUSTIVE — both `git worktree remove` and its `--force`
|
|
18
|
+
* fallback FOLLOW reparse points out of the worktree and delete the PRIMARY
|
|
19
|
+
* ROOT's real files. The plain removal is not the safe one: it refuses only
|
|
20
|
+
* when it sees modified/untracked *tracked-scope* content, so a GITIGNORED
|
|
21
|
+
* junction (`node_modules`, `.claude`) leaves `git status` empty, the removal
|
|
22
|
+
* proceeds, exits 0 — and the target is gone.
|
|
23
|
+
*
|
|
24
|
+
* So the check cannot live in the `catch` of the plain removal (unreachable on
|
|
25
|
+
* exactly the destructive path), and it cannot be a fixed list of the links
|
|
26
|
+
* `setup` creates: a nested link inside a real `node_modules` (npm link, npm
|
|
27
|
+
* workspaces) is invisible to any top-level inventory. We scan the whole tree
|
|
28
|
+
* and NEVER recurse INTO a reparse point — descending one would walk the target
|
|
29
|
+
* we are trying to protect (and could not terminate on a cycle).
|
|
30
|
+
*
|
|
31
|
+
* FAIL-CLOSED, deliberately: an empty result is NOT the same as a safe one. A
|
|
32
|
+
* directory we could not read or an entry we could not lstat might BE (or
|
|
33
|
+
* contain) a reparse point. Those answer `unprovable`, and every caller must
|
|
34
|
+
* treat that exactly like a found link — decline. "I saw nothing" and "I proved
|
|
35
|
+
* there is nothing" are different claims, and only the second may authorize a
|
|
36
|
+
* removal. This is the one module in the shared/ directory that must NOT be
|
|
37
|
+
* fail-open; the whole point is that uncertainty blocks.
|
|
38
|
+
*
|
|
39
|
+
* `lstat` reports a POSIX symlink and a Windows junction alike as a symbolic
|
|
40
|
+
* link, so the scan behaves identically on both platforms — as does the
|
|
41
|
+
* destruction it prevents.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { readdirSync, lstatSync } from 'fs';
|
|
45
|
+
import { join, relative } from 'path';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Every reparse point anywhere under `dir`, as relative paths.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} dir
|
|
51
|
+
* @param {string} [base] - root of the scan, for relative reporting
|
|
52
|
+
* @param {{points: string[], unprovable: string[]}} [acc]
|
|
53
|
+
* @returns {{points: string[], unprovable: string[]}} both empty = provably clean
|
|
54
|
+
*/
|
|
55
|
+
export function findReparsePoints(dir, base = dir, acc = { points: [], unprovable: [] }) {
|
|
56
|
+
let entries;
|
|
57
|
+
try {
|
|
58
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
59
|
+
} catch {
|
|
60
|
+
// Unreadable (ACL, lock, EMFILE): we cannot prove what is in here.
|
|
61
|
+
acc.unprovable.push((relative(base, dir) || '.').replace(/\\/g, '/'));
|
|
62
|
+
return acc;
|
|
63
|
+
}
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
const p = join(dir, entry.name);
|
|
66
|
+
let st;
|
|
67
|
+
try {
|
|
68
|
+
st = lstatSync(p);
|
|
69
|
+
} catch {
|
|
70
|
+
acc.unprovable.push(relative(base, p).replace(/\\/g, '/'));
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (st.isSymbolicLink()) {
|
|
74
|
+
// Forward slashes: every other path this command reports is normalized,
|
|
75
|
+
// and these end up in JSON the caller may compare or display.
|
|
76
|
+
acc.points.push(relative(base, p).replace(/\\/g, '/'));
|
|
77
|
+
continue; // do NOT descend — that is the target, not the worktree
|
|
78
|
+
}
|
|
79
|
+
if (st.isDirectory() && entry.name !== '.git') findReparsePoints(p, base, acc);
|
|
80
|
+
}
|
|
81
|
+
return acc;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The single question both callers actually ask: may this tree be handed to a
|
|
86
|
+
* command that follows links?
|
|
87
|
+
*
|
|
88
|
+
* Collapses `points` and `unprovable` into one list precisely because callers
|
|
89
|
+
* must not weigh them differently — `teardownWorktree` already concatenated
|
|
90
|
+
* them, and a hook that treated `unprovable` as "probably fine" would reopen
|
|
91
|
+
* the hole this module exists to close.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} dir
|
|
94
|
+
* @returns {{ safe: boolean, blockers: string[] }}
|
|
95
|
+
*/
|
|
96
|
+
export function scanForBlockers(dir) {
|
|
97
|
+
const { points, unprovable } = findReparsePoints(dir);
|
|
98
|
+
const blockers = [...points, ...unprovable];
|
|
99
|
+
return { safe: blockers.length === 0, blockers };
|
|
100
|
+
}
|
|
@@ -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.24.1"
|
|
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.24.1"
|
|
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.24.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-apply
|
|
@@ -196,7 +196,7 @@ Quando o `morph-spec dag {feature}` reporta `isolation.recommended: "worktree"`
|
|
|
196
196
|
1. **Dispatch:** mesmo `subagent_type: "morph-{id}"` e standards de §3b, mais `isolation: "worktree"`. Se a stack for Node e faltar `node_modules` no worktree, o agente roda `morph-spec worktree link` (fail-open) antes de testar; se ainda impossível, reporta "verificação: não executada" (o verify pós-merge cobre). Se o agente precisar subir um dev server e existir `.morph/worktree.env` no seu cwd, usa as portas de lá (nunca as padrão — MORPH.md §2.2). O agente commita na branch do worktree e reporta a branch.
|
|
197
197
|
2. **Integração (orquestrador, após todos os relatórios):** faça o merge de cada branch reportada, uma por vez, na ordem de chegada — `git merge --no-ff <branch>`. Conflito → `git merge --abort`, marque a task de volta a `pending` com nota "merge conflict — reexecutar sequencial", siga para a próxima branch. Os paths em conflito são exatamente os de `isolation.collisions` do dag.
|
|
198
198
|
3. **Pós-merge:** rode `morph-spec verify {feature} --json` UMA vez (o merge é o novo ponto de integração — verificação compartilhada obrigatória), depois `morph-eval` + `morph-spec score` por relatório, como em §3b.
|
|
199
|
-
4. **Cleanup:** `git worktree remove` + `git branch -d` das branches integradas + `git worktree prune` (fail-open). Branches não integradas ficam e são registradas no recap com o motivo.
|
|
199
|
+
4. **Cleanup:** `morph-spec worktree remove --target <path>` por worktree (NUNCA `git worktree remove` cru — o passo 1 pode ter criado junction de `node_modules`, e a remoção crua a atravessa e apaga o `node_modules` da raiz) + `git branch -d` das branches integradas + `git worktree prune` (fail-open). Branches não integradas ficam e são registradas no recap com o motivo.
|
|
200
200
|
5. **Agente morto no meio:** se commitou, a branch sobrevive (retomável); senão, a task volta a `pending` na lane sequencial. Registre a falha de protocolo no recap.
|
|
201
201
|
|
|
202
202
|
> Anti-padrões: não misture unidades isoladas e não-isoladas no mesmo batch; o subagent nunca faz merge (integração é exclusiva do orquestrador).
|
|
@@ -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.24.1"
|
|
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.24.1"
|
|
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.24.1"
|
|
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.24.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-implement
|
|
@@ -129,9 +129,21 @@ Ao iniciar cada task de `tasks.json` — antes de escrever qualquer código. Nor
|
|
|
129
129
|
- **`outputs` contém apenas paths realmente produzidos.** Deleções, remoções
|
|
130
130
|
e anotações (ex.: `"pasta/ (removido: a, b)"`) se registram em `notes`,
|
|
131
131
|
nunca em `outputs`. E `notes` é **uma string única** — não um array.
|
|
132
|
+
- **`outputs` DEVE listar TODO arquivo criado ou modificado pela task — sem exceção.**
|
|
133
|
+
Isso inclui artefato auxiliar que não estava no plano original (ex.: uma
|
|
134
|
+
factory de design-time, um seed, um arquivo de config gerado a reboque de
|
|
135
|
+
outro output) — se a task o produziu, ele entra em `outputs` junto com o
|
|
136
|
+
resto. Arquivo shipped sem dono em `outputs` de task alguma é falha de
|
|
137
|
+
protocolo: o Gate 3 (avaliador independente da `morph-review`) aponta como
|
|
138
|
+
achado de rastreabilidade, mesmo que o arquivo em si esteja correto — quem
|
|
139
|
+
lê `tasks.json`/`recap.md` depois precisa achar todo arquivo em produção
|
|
140
|
+
citado em algum lugar, e é essa a lacuna que este parágrafo fecha.
|
|
132
141
|
- **Se os `outputs` reais divergirem dos planejados**, registre o porquê em
|
|
133
142
|
`notes` na mesma edição — o hook exige a note quando há divergência (e a
|
|
134
|
-
mensagem de violação aponta o campo/valor exato do problema).
|
|
143
|
+
mensagem de violação aponta o campo/valor exato do problema). Isto é o
|
|
144
|
+
caso mais comum em que a regra acima se aplica: todo arquivo extra na lista
|
|
145
|
+
de "realmente produzidos" acima do planejado é, por definição, uma
|
|
146
|
+
divergência real vs. planejado — a note explica o porquê.
|
|
135
147
|
- `tasks.json` é protegido após o Gate 2: o hook `protect-spec-files` faz diff
|
|
136
148
|
semântico e libera Write ou Edit desde que nenhum campo de contrato do plano
|
|
137
149
|
(`title`, `description`, `dependencies`, `effort`, `doneCriteria`, `group`)
|
|
@@ -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.24.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-plan
|
|
@@ -56,7 +56,7 @@ Ao iniciar a fase plan — após o `proposal.md` ser aprovado e antes de qualque
|
|
|
56
56
|
|
|
57
57
|
**A regra que sustenta a camada inteira: pós-condição precisa ser observável em teste.** "O sistema fica consistente" não passa. "A resposta contém todos os veterinários não arquivados" passa. Antes de escrever cada pós-condição, pergunte: *que asserção eu escreveria para verificar isto?* Se não há resposta, a pós-condição está mal formulada — reescreva ou descarte. Sem esse filtro a camada vira prosa e não chega ao `doneCriteria`.
|
|
58
58
|
|
|
59
|
-
**Identificador sequencial
|
|
59
|
+
**Identificador sequencial, reservado — nunca leia o disco à mão.** Rode `morph-spec usecase reserve --count {n} --feature {feature}` (`n` = quantos UCs esta feature vai criar; pode reservar de novo se descobrir mais no meio do plano). Um scan manual do maior `UC-{nn}` só enxerga a ÁRVORE CORRENTE — cada worktree tem a própria cópia de `.morph/features/`, então dois worktrees planejando ao mesmo tempo calculam o mesmo maior número e mintam o MESMO `UC-{nn}` sem saber um do outro; na promoção do `finish`, o segundo a fechar sobrescreve por ID e **apaga o use case do primeiro** — já causou perda de dados em produção. `usecase reserve` aloca atomicamente a partir de um contador compartilhado em `<git-common-dir>/morph-spec/uc-registry.json` (visível a todo worktree do repo, mesmo mecanismo do `claim`/fleet), semeado do maior número já visto no acervo + nas features da árvore corrente + no próprio contador — nunca regride. Fora de um repositório git, cai para o scan local antigo e avisa que o paralelismo entre worktrees não está protegido.
|
|
60
60
|
|
|
61
61
|
**Seções obrigatórias:** `Pré-condições` (o que já é verdade antes), `Cenário principal` (o caminho feliz, numerado, **curto** — não é o lugar para exaustão de casos) e `Pós-condições` (**P1**, **P2**… — o que passa a ser verdade, cada uma testável; é a seção que viaja **literal** ao `doneCriteria`, ver abaixo).
|
|
62
62
|
|
|
@@ -67,12 +67,14 @@ Ao iniciar a fase plan — após o `proposal.md` ser aprovado e antes de qualque
|
|
|
67
67
|
| Tipo | Use case |
|
|
68
68
|
|---|---|
|
|
69
69
|
| `feature` | Obrigatório quando há comportamento observável. Feature puramente técnica (refactor, infra, migração) dispensa — **registre a dispensa em `decisions.md`**. |
|
|
70
|
-
| `bug` | Não cria novo: **copie** o `UC-{nn}-*.md` do acervo para `2-plan/usecases/` e **corrija ou adicione a pós-condição** lá — o bug é a pós-condição que faltava. |
|
|
70
|
+
| `bug` | Não cria novo, não reserva número novo: **copie** o `UC-{nn}-*.md` do acervo para `2-plan/usecases/` **mantendo o nome do arquivo publicado** (mesmo slug) e **corrija ou adicione a pós-condição** lá — o bug é a pós-condição que faltava, não a identidade do UC. É esse mesmo nome, inalterado, que faz o `finish` reconhecer a correção como o MESMO UC (abaixo) em vez de uma colisão. |
|
|
71
71
|
| `chore` | Nenhum (já pula a fase plan por política). |
|
|
72
72
|
| `hotfix` | Não bloqueia. Referencia se existir; o pós-mortem pode virar pós-condição numa feature posterior. |
|
|
73
73
|
|
|
74
74
|
**Nunca edite `.morph/specs/usecases/` à mão.** A única via de escrita no acervo é a promoção do `finish`, que lê **só** de `2-plan/usecases/`: é ela que carimba `status: implemented`, sobrescreve por ID (mesmo com slug novo) e leva o arquivo no commit de arquivamento. Editar o acervo direto pula os três e cria uma segunda verdade fora do mecanismo. Mudar um UC existente = copiar para o `2-plan/usecases/` da feature, corrigir lá, e deixar o `finish` sobrescrever.
|
|
75
75
|
|
|
76
|
+
**Backstop de colisão na promoção.** Se, apesar da reserva, duas features chegarem à promoção com o MESMO `UC-{nn}` e slugs diferentes — registro de reserva aponta para uma feature diferente da que já está no acervo —, o `finish`/`archive` **recusa promover esse UC específico** (nunca recusa fechar a feature: fail-open, só aquele use case fica de fora) e imprime onde o arquivo ficou e como recuperar. Se acontecer: renumere com `usecase reserve` e promova manualmente, ou — se for mesmo o caso `bug` — confira se o slug bate com o publicado.
|
|
77
|
+
|
|
76
78
|
O use case fica **editável depois do Gate 2**, por design — é o que permite mudar o comportamento e re-rodar `implement`. A trilha de auditoria fica no git.
|
|
77
79
|
|
|
78
80
|
3. Produza `spec.md` em `.morph/features/{feature}/2-plan/`:
|
|
@@ -223,7 +225,7 @@ Ao registrar a aprovação do Gate 2, atualize o frontmatter de cada use case de
|
|
|
223
225
|
- `morph-spec standards --find "<query>"` — para identificar standards relevantes ao mandate (o índice inteiro só como navegação exploratória; ver `morph-standards`)
|
|
224
226
|
- `.morph/framework/agents.json` — personas por domínio; lido UMA VEZ no passo 5a para carimbar `agent`/`standards` por task
|
|
225
227
|
- `.morph/framework/schemas/tasks.schema.json` — schema obrigatório de tasks.json
|
|
226
|
-
- `.morph/specs/usecases/` — acervo permanente de use cases do projeto (
|
|
228
|
+
- `.morph/specs/usecases/` — acervo permanente de use cases do projeto (`usecase reserve` já lê antes de numerar; em `bug`, é onde mora o UC a corrigir)
|
|
227
229
|
- `.morph/features/{feature}/0-proposal/proposal.md` — escopo da feature
|
|
228
230
|
- `.morph/features/{feature}/0-proposal/stack-scan.md` — contexto técnico detectado
|
|
229
231
|
- `.morph/framework/schemas/tasks.schema.json` — schema e enums de tasks.json
|
|
@@ -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.24.1"
|
|
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.24.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-review
|
|
@@ -42,7 +42,23 @@ e o `recap.md` estar atualizado. Normalmente invocada pela skill orquestradora `
|
|
|
42
42
|
confirme que usam duas constantes de prefixo nomeadas distintas, nenhuma reusada para o
|
|
43
43
|
outro propósito. Falha aqui é bloqueante — quebra silenciosa em runtime, não em build.
|
|
44
44
|
|
|
45
|
-
4. **
|
|
45
|
+
4. **Antes de despachar, verifique se já existe um relatório íntegro** — inclusive em sessão
|
|
46
|
+
retomada. **Esta verificação NÃO é pulada porque a sessão foi retomada** — é exatamente esse o
|
|
47
|
+
cenário que já mordeu em produção: 4 sessões retomadas re-despacharam o avaliador do Gate 3 sem
|
|
48
|
+
nunca ler o `evaluator-report.md` que já existia, íntegro, desde o 1º dispatch (`APPROVED`, score
|
|
49
|
+
9.3) — e a 4ª sessão chegou a afirmar, falsamente, que "o avaliador nunca rodou". `SessionStart:
|
|
50
|
+
resume`/`init` não reseta este passo: ele roda sempre, a cada retomada de controle, antes de
|
|
51
|
+
cogitar um dispatch.
|
|
52
|
+
- Rode `morph-spec report-check {feature} --json` (sem `task`, modo feature inteira) e leia o
|
|
53
|
+
campo `evaluator` da resposta — ou, equivalente, leia `4-review/evaluator-report.md` direto e
|
|
54
|
+
confira: scores por dimensão presentes + a linha `**Composite-Score:**` presente = **íntegro**.
|
|
55
|
+
- **Se íntegro** (`evaluator.exists && evaluator.complete`): **NÃO despache de novo.** Pule direto
|
|
56
|
+
para "**Leia primeiro**" logo abaixo — leia o arquivo e siga para o passo 5 (síntese do
|
|
57
|
+
review-report a partir dele). Re-despachar aqui joga fora um veredito de Gate 3 já válido.
|
|
58
|
+
- **Se ausente, oco, ou sem scores por dimensão:** este é exatamente o caso de falha de protocolo
|
|
59
|
+
que este passo trata — prossiga com o dispatch abaixo.
|
|
60
|
+
|
|
61
|
+
**Despache o avaliador independente** — o score do Gate 3 nunca é auto-atribuído por quem implementou:
|
|
46
62
|
- Rode `morph-spec verify {feature} --json` (escopo feature) e capture o JSON — é a saída real de build+testes+validadores+e2e que você anexa ao prompt do avaliador independente (`morph-evaluator`).
|
|
47
63
|
- **Confira o nó `e2e` do verify antes de despachar.** Ele carrega o estado do stack:
|
|
48
64
|
- `pass` → a aplicação subiu e está saudável; o avaliador pode navegá-la. **Passe a URL no prompt**
|
|
@@ -53,6 +69,9 @@ e o `recap.md` estar atualizado. Normalmente invocada pela skill orquestradora `
|
|
|
53
69
|
legítimo; "docker indisponível no ambiente" numa feature de UI **não** é — registre você mesmo
|
|
54
70
|
`morph-spec gate-decision {feature} review --pause true --reason "e2e não verificado: {razão}"`.
|
|
55
71
|
O disco não distingue dispensa legítima de verificação que faltou; esse julgamento é seu.
|
|
72
|
+
- **Confira também `traceability.warnings`** (advisory, nunca faz `verify` falhar): cada entrada é
|
|
73
|
+
um arquivo citado no `recap.md` sem task dona em `outputs` — repasse ao avaliador como sinal de
|
|
74
|
+
rastreabilidade para a dimensão de qualidade, não como bloqueio automático do gate.
|
|
56
75
|
- Gere o **diff da implementação** (`git diff` do range da feature, ou a lista de arquivos do recap.md com seus conteúdos atuais).
|
|
57
76
|
- Despache o subagent registrado `morph-evaluator` (gerado de `agents.json` — a persona do avaliador é o system prompt dele, com Read/Glob/Grep + Bash somente para verificação + Playwright para navegar, sem Write/Edit), com contexto fresco:
|
|
58
77
|
```
|
|
@@ -61,7 +80,14 @@ e o `recap.md` estar atualizado. Normalmente invocada pela skill orquestradora `
|
|
|
61
80
|
+ nó e2e do verify (stack no ar? em qual URL?)
|
|
62
81
|
instrução: |
|
|
63
82
|
Você NÃO participou da implementação — avalie o que está no diff,
|
|
64
|
-
contra o mandate.
|
|
83
|
+
contra o mandate. NÃO leia `4-review/evaluator-report.md` neste
|
|
84
|
+
caminho antes de escrever o seu: se esse arquivo já existir quando
|
|
85
|
+
você começar, é resíduo de uma tentativa anterior — NÃO é insumo,
|
|
86
|
+
é o SEU caminho de escrita, não de leitura. Sua nota nasce do
|
|
87
|
+
mandate + spec + diff + testes, nunca do veredito de outro
|
|
88
|
+
avaliador; ler o relatório de um antecessor antes de pontuar é
|
|
89
|
+
ancoragem — exatamente o que "contexto fresco" existe para evitar.
|
|
90
|
+
Você tem Bash: use `morph-spec verify`, testes/build
|
|
65
91
|
do stack e `git log/diff/show` para conferir por conta própria o que
|
|
66
92
|
foi reportado, em vez de confiar cegamente. Aplique as rubricas em
|
|
67
93
|
.morph/framework/evals/rubrics/ (architecture 30%, contracts 25%,
|
|
@@ -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.24.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-stack-scan
|