@polymorphism-tech/morph-spec 8.3.0 → 8.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/bin/morph-spec.js +24 -1
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +6 -4
- package/docs/QUICKSTART.md +13 -7
- package/framework/CLAUDE.md +5 -3
- package/framework/MORPH.md +2 -2
- package/framework/commands/morph-hotfix.md +1 -1
- package/framework/hooks/claude-code/core/state-sync.js +41 -9
- package/framework/hooks/shared/state-writer.js +14 -3
- package/framework/schemas/feature.schema.json +15 -2
- 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 +3 -3
- 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 +4 -4
- 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/package.json +1 -1
- package/src/commands/project/doctor.js +28 -0
- package/src/commands/state/advance.js +17 -0
- package/src/commands/state/approve.js +13 -0
- package/src/commands/state/create.js +21 -1
- package/src/commands/state/delete.js +143 -0
- package/src/commands/state/list.js +110 -0
- package/src/core/state/feature-identity.js +36 -0
- package/src/core/state/gate-order.js +51 -0
- package/src/core/state/lifecycle-audit.js +79 -0
- package/src/core/state/state-manager.js +47 -12
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.4.0
|
|
7
7
|
**Requires:** Node.js 22+, Claude Code
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -184,8 +184,8 @@ morph-spec verify {feature} [task] # Deterministic build+tests+validators ch
|
|
|
184
184
|
### Work-type routing
|
|
185
185
|
|
|
186
186
|
```bash
|
|
187
|
-
morph-spec create {feature} --type feature|bug|chore|hotfix # Explicit birth: type dimensions the pipeline
|
|
188
|
-
morph-spec create {feature} --request "<free text>" #
|
|
187
|
+
morph-spec create {feature} --type feature|bug|chore|hotfix --description "<what/why>" # Explicit birth: type dimensions the pipeline, description is required
|
|
188
|
+
morph-spec create {feature} --request "<free text>" # Detector classifies the type AND seeds the description
|
|
189
189
|
morph-spec retype {feature} {newType} # Change work type mid-flight (blocked after Gate 3)
|
|
190
190
|
morph-spec dag {feature} [--mermaid] # DAG dispatch decision; --mermaid emits a flowchart
|
|
191
191
|
```
|
|
@@ -193,7 +193,7 @@ morph-spec dag {feature} [--mermaid] # DAG dispatch d
|
|
|
193
193
|
### Worktrees (parallel development)
|
|
194
194
|
|
|
195
195
|
```bash
|
|
196
|
-
morph-spec create {feature} --worktree # Birth the feature already isolated in its own worktree
|
|
196
|
+
morph-spec create {feature} --description "<what/why>" --worktree # Birth the feature already isolated in its own worktree
|
|
197
197
|
morph-spec worktree setup {feature} # Create .worktrees/{feature}/ on branch morph/{feature}
|
|
198
198
|
morph-spec worktree link [--all] # Relink shared infra (node_modules, .claude, .morph/framework)
|
|
199
199
|
morph-spec worktree finish {feature} # Merge --no-ff back to the primary root, clean up (requires Gate 3)
|
|
@@ -365,4 +365,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
|
|
|
365
365
|
|
|
366
366
|
---
|
|
367
367
|
|
|
368
|
-
*morph-spec v8.
|
|
368
|
+
*morph-spec v8.4.0 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/bin/morph-spec.js
CHANGED
|
@@ -35,6 +35,8 @@ import { gateDecisionCommand } from '../src/commands/state/gate-decision.js';
|
|
|
35
35
|
import { createCommand } from '../src/commands/state/create.js';
|
|
36
36
|
import { retypeCommand } from '../src/commands/state/retype.js';
|
|
37
37
|
import { handoffCommand } from '../src/commands/state/handoff.js';
|
|
38
|
+
import { listCommand } from '../src/commands/state/list.js';
|
|
39
|
+
import { deleteCommand } from '../src/commands/state/delete.js';
|
|
38
40
|
|
|
39
41
|
// === Tasks ===
|
|
40
42
|
import { dagCommand } from '../src/commands/tasks/dag.js';
|
|
@@ -126,13 +128,22 @@ program
|
|
|
126
128
|
.option('-v, --verbose', 'Show detailed output')
|
|
127
129
|
.action(statusCommand);
|
|
128
130
|
|
|
131
|
+
program
|
|
132
|
+
.command('list')
|
|
133
|
+
.description('List every feature × status × gates (G1/G2/G3) × workType — the audit board')
|
|
134
|
+
.option('--all', 'Include archived features (.morph/archive)')
|
|
135
|
+
.option('--archived', 'Include archived features (alias of --all)')
|
|
136
|
+
.option('--json', 'Output as JSON')
|
|
137
|
+
.action((options) => listCommand(options));
|
|
138
|
+
|
|
129
139
|
// ─── Approval gates (dev-only) ────────────────────────────────────────────
|
|
130
140
|
program
|
|
131
141
|
.command('approve <feature> <gate>')
|
|
132
|
-
.description('Approve a phase gate (proposal, plan, review)')
|
|
142
|
+
.description('Approve a phase gate (proposal, plan, review). Enforces gate order (N requires N-1)')
|
|
133
143
|
.option('--approver <name>', 'Name of approver (default: current user when interactive, "auto" otherwise)')
|
|
134
144
|
.option('--mode <mode>', 'Approval mode: "user" or "auto" (default: detected from TTY)')
|
|
135
145
|
.option('--notes <notes>', 'Approval notes')
|
|
146
|
+
.option('--force', 'Approve out of order (bypass the gate-monotonicity check)')
|
|
136
147
|
.action((feature, gate, options) => approveCommand(feature, gate, options));
|
|
137
148
|
|
|
138
149
|
program
|
|
@@ -142,6 +153,7 @@ program
|
|
|
142
153
|
.option('--mode <mode>', 'Approval mode: "user" or "auto" (default: detected from TTY)')
|
|
143
154
|
.option('--design', 'From proposal, advance into the optional design (uiux) phase instead of plan')
|
|
144
155
|
.option('--notes <notes>', 'Approval notes')
|
|
156
|
+
.option('--force', 'Advance even if a prerequisite gate is unapproved (out-of-order recovery)')
|
|
145
157
|
.action((feature, options) => advanceCommand(feature, options));
|
|
146
158
|
|
|
147
159
|
program
|
|
@@ -151,6 +163,15 @@ program
|
|
|
151
163
|
.option('--force', 'Archive even if Gate 3 (review) is not approved')
|
|
152
164
|
.action((feature, options) => archiveCommand(feature, options));
|
|
153
165
|
|
|
166
|
+
program
|
|
167
|
+
.command('delete <feature>')
|
|
168
|
+
.description('Delete a feature (hard by default; --archive marks it abandoned). Refuses if a gate is approved unless --force')
|
|
169
|
+
.option('--archive', 'Soft delete: move to .morph/archive with status "abandoned" instead of removing')
|
|
170
|
+
.option('--force', 'Delete even when a gate is approved')
|
|
171
|
+
.option('--no-git', 'Skip git staging of the removal')
|
|
172
|
+
.option('--json', 'Output as JSON')
|
|
173
|
+
.action((feature, options) => deleteCommand(feature, options));
|
|
174
|
+
|
|
154
175
|
program
|
|
155
176
|
.command('approval-status <feature>')
|
|
156
177
|
.description('Show approval status for all gates')
|
|
@@ -178,6 +199,8 @@ program
|
|
|
178
199
|
program
|
|
179
200
|
.command('create <feature>')
|
|
180
201
|
.description('Classify work type (--type/--request), scaffold + stamp policy gates, and create the canonical feature.json (explicit birth — see framework/schemas/feature.schema.json)')
|
|
202
|
+
.option('--name <name>', 'Human-readable feature name (default: the slug)')
|
|
203
|
+
.option('--description <text>', 'What/why of the feature — required at birth unless --request is given (it seeds the description)')
|
|
181
204
|
.option('--status <status>', 'Initial feature status (default: in_progress)')
|
|
182
205
|
.option('--type <type>', 'Work type: feature|bug|chore|hotfix (default: feature) — see src/core/work-types.js')
|
|
183
206
|
.option('--request <text>', 'Free-text request classified by the deterministic detector; tie/none exits 2 with candidates for an LLM/user decision (feature NOT created)')
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
|
@@ -68,9 +68,11 @@ morph-spec archive {feature} [--force] [--no-git]
|
|
|
68
68
|
|
|
69
69
|
Every feature is born with a **work type** that dimensions its pipeline: `feature` (full proposal → [uiux] → plan → implement → review), `bug` (lean), `chore` (fast-track straight to implement), `hotfix` (scout → surgical fix → human Gate 3 always, even under `trust: auto`).
|
|
70
70
|
|
|
71
|
+
Every feature is also born with an **identity**: `--description` is required (`create` refuses a nameless scaffold without it), `name` defaults to the slug.
|
|
72
|
+
|
|
71
73
|
```bash
|
|
72
|
-
morph-spec create {feature} --type feature|bug|chore|hotfix
|
|
73
|
-
morph-spec create {feature} --request "<free text>"
|
|
74
|
+
morph-spec create {feature} --type feature|bug|chore|hotfix --description "<what/why>"
|
|
75
|
+
morph-spec create {feature} --request "<free text>" # detector classifies the type AND seeds the description
|
|
74
76
|
morph-spec retype {feature} {newType} # change type mid-flight (blocked after Gate 3)
|
|
75
77
|
```
|
|
76
78
|
|
|
@@ -111,7 +113,7 @@ morph-spec dag {feature} --mermaid # emit the tasks.json DAG as a Mermai
|
|
|
111
113
|
One feature = one git worktree = one Claude Code instance. Each worktree gets its own deterministic port block (`.morph/worktree.env`) and a regenerable handoff document.
|
|
112
114
|
|
|
113
115
|
```bash
|
|
114
|
-
morph-spec create {feature} --worktree
|
|
116
|
+
morph-spec create {feature} --description "<what/why>" --worktree # birth the feature already isolated in its own worktree
|
|
115
117
|
morph-spec worktree setup {feature} # create .worktrees/{feature}/ on branch morph/{feature}
|
|
116
118
|
morph-spec worktree link [--all] # relink shared infra (node_modules[, .claude, .morph/framework])
|
|
117
119
|
morph-spec worktree finish {feature} # merge --no-ff back to the primary root, clean up + release ports (requires Gate 3)
|
|
@@ -180,4 +182,4 @@ Never edited directly — go through CLI commands or `morph-spec update` instead
|
|
|
180
182
|
|
|
181
183
|
---
|
|
182
184
|
|
|
183
|
-
*morph-spec v8.
|
|
185
|
+
*morph-spec v8.4.0 by Polymorphism Tech*
|
package/docs/QUICKSTART.md
CHANGED
|
@@ -129,14 +129,20 @@ morph-spec verify my-feature T001 # scoped to one task
|
|
|
129
129
|
|
|
130
130
|
Not every change needs the full pipeline walked above. Every feature is born with a **work type** that dimensions its pipeline:
|
|
131
131
|
|
|
132
|
+
Every feature is also born with an **identity** — a `--description` (the one field a human must supply; `create` refuses a nameless scaffold without it) and a `name` (defaults to the slug):
|
|
133
|
+
|
|
132
134
|
```bash
|
|
133
|
-
|
|
134
|
-
morph-spec create my-
|
|
135
|
-
|
|
136
|
-
morph-spec create my-
|
|
135
|
+
# full: proposal → [uiux] → plan → implement → review
|
|
136
|
+
morph-spec create my-feature --type feature --description "what & why"
|
|
137
|
+
# bug: lean proposal/plan
|
|
138
|
+
morph-spec create my-fix --type bug --description "what broke"
|
|
139
|
+
# chore: skips straight to implement (Gates 1/2 stamped by policy)
|
|
140
|
+
morph-spec create my-chore --type chore --description "the tidy-up"
|
|
141
|
+
# hotfix: scout → surgical fix → test loop; Gate 3 always pauses for a human
|
|
142
|
+
morph-spec create my-hotfix --type hotfix --description "the incident"
|
|
137
143
|
```
|
|
138
144
|
|
|
139
|
-
You can also let the deterministic detector classify a free-text request instead of picking a type by hand:
|
|
145
|
+
You can also let the deterministic detector classify a free-text request instead of picking a type by hand (`--request` doubles as the description):
|
|
140
146
|
|
|
141
147
|
```bash
|
|
142
148
|
morph-spec create my-feature --request "produção caiu, corrigir urgente"
|
|
@@ -157,7 +163,7 @@ Production incidents run through the dedicated `/morph-hotfix my-feature` slash
|
|
|
157
163
|
Independent features can be developed in parallel, each isolated in its own git worktree — one feature, one worktree, one Claude Code instance:
|
|
158
164
|
|
|
159
165
|
```bash
|
|
160
|
-
morph-spec create my-feature --worktree
|
|
166
|
+
morph-spec create my-feature --description "what & why" --worktree # birth the feature already isolated
|
|
161
167
|
# or, for an existing feature:
|
|
162
168
|
morph-spec worktree setup my-feature # creates .worktrees/my-feature/ on branch morph/my-feature
|
|
163
169
|
```
|
|
@@ -240,4 +246,4 @@ morph-spec doctor
|
|
|
240
246
|
|
|
241
247
|
---
|
|
242
248
|
|
|
243
|
-
*morph-spec v8.
|
|
249
|
+
*morph-spec v8.4.0 by Polymorphism Tech*
|
package/framework/CLAUDE.md
CHANGED
|
@@ -47,7 +47,9 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
|
47
47
|
- `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).
|
|
48
48
|
- `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.
|
|
49
49
|
- `morph-spec gate-decision <feature> <gate> --pause <bool> [--reason <txt>] [--signals <json>]` — grava `gateDecisions` no feature.json com timestamp server-side (shape em `framework/schemas/feature.schema.json`); **nunca edite gateDecisions à mão** — o Edit manual colide com o hook state-sync.
|
|
50
|
-
- `morph-spec create <feature> --type <feature|bug|chore|hotfix> [--request "<txt>"] [--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). `--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`); `--worktree` já nasce a feature no seu worktree próprio.
|
|
50
|
+
- `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.
|
|
51
|
+
- `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).
|
|
52
|
+
- `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).
|
|
51
53
|
- `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.
|
|
52
54
|
- `morph-spec verify <feature> [task] [--json]` — cadeia determinística build+testes+validadores (código antes do juiz LLM); escopo de task quando informado, senão a feature inteira.
|
|
53
55
|
- `morph-spec worktree setup|link|finish|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; `finish` faz merge `--no-ff`, limpa e libera o bloco; `list` mostra fase/gate de cada worktree ativo).
|
|
@@ -70,9 +72,9 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
|
70
72
|
|
|
71
73
|
## Roteamento por tipo de trabalho
|
|
72
74
|
|
|
73
|
-
Toda feature nasce com um `workType` (`morph-spec create <f> --type <tipo>` ou classificado de `--request "<pedido>"
|
|
75
|
+
Toda feature nasce com um `workType` (`morph-spec create <f> --description "<o quê/porquê>" --type <tipo>` ou classificado de `--request "<pedido>"`, que também semeia a descrição obrigatória) que dimensiona o pipeline: `feature` roda o fluxo completo (proposal → [uiux] → plan → implement → review); `bug` roda uma versão enxuta (proposal/plan lean); `chore` pula direto para implement (Gates 1/2 carimbados por política no nascimento); `hotfix` roda scout → fix cirúrgico → test loop → ship. O Gate 3 do `hotfix` **pausa sempre para um humano**, mesmo em `trust: auto` — o CLI recusa `morph-spec approve <f> review --mode auto` nesse tipo. Detalhes em `.morph/framework/MORPH.md` §2.1 e §5.
|
|
74
76
|
|
|
75
|
-
Features independentes podem rodar em paralelo, cada uma isolada num git worktree (`morph-spec create <f> --worktree` ou `morph-spec worktree setup <f>`) — uma feature por worktree, uma instância de Claude Code por worktree. Cada worktree tem seu **bloco de portas** próprio (`.morph/worktree.env` — suba dev servers SEMPRE nelas, nunca nas portas padrão) e seu **handoff.md** de retomada. `feature.json` viaja commitado na branch `morph/{f}`; ao fechar, aprove o Gate 3 dentro do worktree e rode `morph-spec worktree finish <f>` seguido de `morph-spec archive <f>` na raiz. Visão agregada: `morph-spec fleet`. Detalhes em `.morph/framework/MORPH.md` §2.2.
|
|
77
|
+
Features independentes podem rodar em paralelo, cada uma isolada num git worktree (`morph-spec create <f> --description "<o quê/porquê>" --worktree` ou `morph-spec worktree setup <f>`) — uma feature por worktree, uma instância de Claude Code por worktree. Cada worktree tem seu **bloco de portas** próprio (`.morph/worktree.env` — suba dev servers SEMPRE nelas, nunca nas portas padrão) e seu **handoff.md** de retomada. `feature.json` viaja commitado na branch `morph/{f}`; ao fechar, aprove o Gate 3 dentro do worktree e rode `morph-spec worktree finish <f>` seguido de `morph-spec archive <f>` na raiz. Visão agregada: `morph-spec fleet`. Detalhes em `.morph/framework/MORPH.md` §2.2.
|
|
76
78
|
|
|
77
79
|
## Personas (em `.claude/agents/`)
|
|
78
80
|
|
package/framework/MORPH.md
CHANGED
|
@@ -97,7 +97,7 @@ Não avance de fase sem o gate correspondente ter sido confirmado.
|
|
|
97
97
|
|
|
98
98
|
### 2.1 Roteamento por tipo de trabalho
|
|
99
99
|
|
|
100
|
-
Nem todo trabalho merece o pipeline completo. Cada feature nasce com um **workType** (`morph-spec create <f> --type <tipo
|
|
100
|
+
Nem todo trabalho merece o pipeline completo. Cada feature nasce com **identidade** (`name` = slug por padrão + `description` obrigatória) e um **workType** (`morph-spec create <f> --description "<o quê/porquê>" --type <tipo>`, ou classificado de `--request "<pedido>"` — que também semeia a descrição), e o tipo dimensiona o pipeline:
|
|
101
101
|
|
|
102
102
|
| Tipo | Pipeline | Gates |
|
|
103
103
|
|------|----------|-------|
|
|
@@ -114,7 +114,7 @@ Nem todo trabalho merece o pipeline completo. Cada feature nasce com um **workTy
|
|
|
114
114
|
|
|
115
115
|
### 2.2 Desenvolvimento paralelo por worktree (frota)
|
|
116
116
|
|
|
117
|
-
Features independentes podem ser desenvolvidas **em paralelo, cada uma no seu git worktree**, com uma instância de Claude Code por worktree. `morph-spec create <f> --worktree` (ou `morph-spec worktree setup <f>`) cria `.worktrees/{f}/` na branch `morph/{f}`. Contrato:
|
|
117
|
+
Features independentes podem ser desenvolvidas **em paralelo, cada uma no seu git worktree**, com uma instância de Claude Code por worktree. `morph-spec create <f> --description "<o quê/porquê>" --worktree` (ou `morph-spec worktree setup <f>`) cria `.worktrees/{f}/` na branch `morph/{f}`. Contrato:
|
|
118
118
|
|
|
119
119
|
- **Uma feature = um worktree = uma instância.** Nunca abra duas instâncias na mesma feature (o setup recusa com exit 3).
|
|
120
120
|
- O estado autoritativo (`feature.json`) viaja **commitado na branch** `morph/{f}`. O `state.json` local do worktree é um índice derivável — materializado por self-heal, nunca sincronizado à mão.
|
|
@@ -16,7 +16,7 @@ Resolve uma queda ou bug crítico de produção com prioridade máxima em veloci
|
|
|
16
16
|
|
|
17
17
|
## Fluxo
|
|
18
18
|
|
|
19
|
-
1. **Nascimento:** `morph-spec create {feature-name} --type hotfix` — carimba os gates `proposal`/`plan` por política e cria `0-proposal/`, `2-plan/`, `3-implement/` num passo só.
|
|
19
|
+
1. **Nascimento:** `morph-spec create {feature-name} --type hotfix --description "<incidente>"` — carimba `name`+`description` (obrigatória) e os gates `proposal`/`plan` por política e cria `0-proposal/`, `2-plan/`, `3-implement/` num passo só.
|
|
20
20
|
2. **Scout obrigatório:** despacha `subagent_type: "morph-scout"` (fallback `general-purpose` + persona colada) para localizar root cause e blast radius antes de qualquer fix.
|
|
21
21
|
3. **Hotfix brief:** escreve `0-proposal/proposal.md` curto (sintoma/root cause/blast radius/plano do fix) e `2-plan/tasks.json` mínimo (T1 fix, T2 verify/regressão).
|
|
22
22
|
4. **Fix cirúrgico:** implementa a mudança mínima que resolve o sintoma — sem refatoração, sem hardening fora de escopo.
|
|
@@ -66,8 +66,9 @@ function registerFeatureIfMissing(state, featureName) {
|
|
|
66
66
|
if (!state.features) state.features = {};
|
|
67
67
|
if (state.features[featureName]) return false;
|
|
68
68
|
|
|
69
|
-
// Canonical object-form gates (incl. optional uiux)
|
|
70
|
-
|
|
69
|
+
// Canonical object-form gates (incl. optional uiux) + stamped identity
|
|
70
|
+
// (name=slug, workType, createdAt) — no anonymous shell, no boolean drift.
|
|
71
|
+
state.features[featureName] = createDefaultFeature(featureName);
|
|
71
72
|
return true;
|
|
72
73
|
}
|
|
73
74
|
|
|
@@ -114,6 +115,15 @@ function gateFromQuestion(question) {
|
|
|
114
115
|
return null;
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
// Gate monotonicity — mirror of src/core/state/gate-order.js (hooks are
|
|
119
|
+
// installed standalone and cannot import from src/). Keep in sync:
|
|
120
|
+
// proposal → plan → review; uiux requires proposal.
|
|
121
|
+
const GATE_PREREQ = { proposal: [], uiux: ['proposal'], plan: ['proposal'], review: ['plan'] };
|
|
122
|
+
function unmetGatePrereqs(gate, gates) {
|
|
123
|
+
const isApproved = (g) => gates?.[g]?.approved === true || gates?.[g] === true;
|
|
124
|
+
return (GATE_PREREQ[gate] || []).filter((p) => !isApproved(p));
|
|
125
|
+
}
|
|
126
|
+
|
|
117
127
|
function handleWriteEdit(input, cwd) {
|
|
118
128
|
const filePath = input.tool_input?.file_path;
|
|
119
129
|
const featureName = extractFeatureName(filePath);
|
|
@@ -139,23 +149,38 @@ function handleWriteEdit(input, cwd) {
|
|
|
139
149
|
|
|
140
150
|
function handleAskUserQuestion(input, cwd) {
|
|
141
151
|
const answers = input.tool_response?.answers ?? input.tool_response?.responses ?? {};
|
|
142
|
-
if (typeof answers !== 'object' || answers === null) return
|
|
152
|
+
if (typeof answers !== 'object' || answers === null) return null;
|
|
143
153
|
|
|
144
154
|
const state = loadState(cwd);
|
|
145
|
-
if (!state) return
|
|
155
|
+
if (!state) return null;
|
|
146
156
|
|
|
147
157
|
const featureName = findActiveFeature(state);
|
|
148
|
-
if (!featureName) return
|
|
158
|
+
if (!featureName) return null;
|
|
149
159
|
|
|
160
|
+
const warnings = [];
|
|
150
161
|
for (const [question, answer] of Object.entries(answers)) {
|
|
151
162
|
if (!isApprovalAnswer(answer)) continue;
|
|
152
163
|
const gate = gateFromQuestion(question);
|
|
153
164
|
if (!gate) continue;
|
|
154
165
|
|
|
155
|
-
const
|
|
166
|
+
const gates = state.features[featureName].approvalGates;
|
|
167
|
+
const existing = gates?.[gate];
|
|
156
168
|
const alreadyApproved = existing === true || existing?.approved === true;
|
|
157
169
|
if (alreadyApproved) continue;
|
|
158
170
|
|
|
171
|
+
// Gate monotonicity: fail-open. The human said "aprovado", so we DO approve
|
|
172
|
+
// (never block a human in the loop), but if a predecessor gate is still
|
|
173
|
+
// pending we surface a non-blocking warning — the CLI `morph-spec approve`
|
|
174
|
+
// hard-refuses the same shape; here we alert so the operator can reconcile.
|
|
175
|
+
const unmet = unmetGatePrereqs(gate, gates);
|
|
176
|
+
if (unmet.length) {
|
|
177
|
+
warnings.push(
|
|
178
|
+
`⚠️ [morph-spec] GATE FORA DE ORDEM — o Gate "${gate}" de "${featureName}" foi aprovado, ` +
|
|
179
|
+
`mas o(s) gate(s) anterior(es) ${unmet.join(', ')} não estão aprovados (ordem: ${unmet.join(' → ')} → ${gate}). ` +
|
|
180
|
+
`Aprove os gates anteriores ou reveja com 'morph-spec doctor'.`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
159
184
|
// An "aprovado" answer to AskUserQuestion is a human approving in the loop.
|
|
160
185
|
// Route through the shared canonical gate writer so the hook and the CLI
|
|
161
186
|
// (`morph-spec approve`) produce one consistent shape
|
|
@@ -163,7 +188,7 @@ function handleAskUserQuestion(input, cwd) {
|
|
|
163
188
|
// and a manual approval stays distinguishable from a policy auto-approval.
|
|
164
189
|
setGateApproved(featureName, gate, { approvedBy: 'manual' }, cwd);
|
|
165
190
|
}
|
|
166
|
-
return
|
|
191
|
+
return warnings.length ? warnings.join('\n') : null;
|
|
167
192
|
}
|
|
168
193
|
|
|
169
194
|
async function main() {
|
|
@@ -176,17 +201,24 @@ async function main() {
|
|
|
176
201
|
const input = readStdin();
|
|
177
202
|
const toolName = input.tool_name;
|
|
178
203
|
|
|
204
|
+
let warning = null;
|
|
179
205
|
try {
|
|
180
206
|
if (toolName === 'Write' || toolName === 'Edit') {
|
|
181
207
|
handleWriteEdit(input, cwd);
|
|
182
208
|
} else if (toolName === 'AskUserQuestion') {
|
|
183
|
-
handleAskUserQuestion(input, cwd);
|
|
209
|
+
warning = handleAskUserQuestion(input, cwd);
|
|
184
210
|
}
|
|
185
211
|
} catch {
|
|
186
212
|
// fail-open
|
|
187
213
|
}
|
|
188
214
|
|
|
189
|
-
|
|
215
|
+
// A non-blocking order-violation warning is surfaced as injected context
|
|
216
|
+
// (hookSpecificOutput.additionalContext); everything else exits silently.
|
|
217
|
+
if (warning) {
|
|
218
|
+
exit({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: warning } });
|
|
219
|
+
} else {
|
|
220
|
+
exit();
|
|
221
|
+
}
|
|
190
222
|
}
|
|
191
223
|
|
|
192
224
|
main().catch(() => exit());
|
|
@@ -91,7 +91,7 @@ export function persistFeature(featureName, feature, projectPath) {
|
|
|
91
91
|
*/
|
|
92
92
|
export function setGateApproved(featureName, gate, meta = {}, projectPath) {
|
|
93
93
|
try {
|
|
94
|
-
const feature = readFeatureFile(featureName, projectPath) || createDefaultFeature();
|
|
94
|
+
const feature = readFeatureFile(featureName, projectPath) || createDefaultFeature(featureName);
|
|
95
95
|
const now = new Date().toISOString();
|
|
96
96
|
feature.approvalGates = feature.approvalGates || {};
|
|
97
97
|
feature.approvalGates[gate] = {
|
|
@@ -106,11 +106,22 @@ export function setGateApproved(featureName, gate, meta = {}, projectPath) {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/**
|
|
110
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Default feature shape (canonical object-form gates, incl. optional uiux).
|
|
111
|
+
* Stamps identity — name (the slug), workType, createdAt — so the implicit
|
|
112
|
+
* birth path (state-sync registering a feature on the first Write) never
|
|
113
|
+
* produces an anonymous {status,updatedAt} shell. `description` stays absent
|
|
114
|
+
* (a hook can't invent it); `list`/`doctor` flag its absence. Mirrors the CLI
|
|
115
|
+
* createFeature's stamping.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} [featureName] - the feature slug, stamped as `name`
|
|
118
|
+
*/
|
|
119
|
+
export function createDefaultFeature(featureName) {
|
|
111
120
|
const now = new Date().toISOString();
|
|
112
121
|
return {
|
|
122
|
+
...(featureName ? { name: featureName } : {}),
|
|
113
123
|
status: 'in_progress',
|
|
124
|
+
workType: 'feature',
|
|
114
125
|
createdAt: now,
|
|
115
126
|
updatedAt: now,
|
|
116
127
|
approvalGates: {
|
|
@@ -5,10 +5,18 @@
|
|
|
5
5
|
"type": "object",
|
|
6
6
|
"required": ["status", "approvalGates"],
|
|
7
7
|
"properties": {
|
|
8
|
+
"name": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"description": "Human-readable feature name, stamped at birth by `morph-spec create` (defaults to the slug) and by the state-sync hook's implicit registration. Kept out of the schema's `required` array — adding it there would invalidate every feature.json that predates identity stamping; instead `create` enforces it at birth and `morph-spec doctor`/`list` flag its absence leniently."
|
|
11
|
+
},
|
|
12
|
+
"description": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "What/why of the feature. The one identity field a human must supply — `morph-spec create` refuses a new feature without it (--description, or --request which seeds it). Absent on features born via the implicit hook path; surfaced as a warning by `doctor`/`list`, never a hard read-time rejection (see name)."
|
|
15
|
+
},
|
|
8
16
|
"status": {
|
|
9
17
|
"type": "string",
|
|
10
|
-
"enum": ["draft", "in_progress", "completed"],
|
|
11
|
-
"description": "Feature lifecycle status. `completed` is stamped by `morph-spec archive` together with archivedAt."
|
|
18
|
+
"enum": ["draft", "in_progress", "completed", "abandoned"],
|
|
19
|
+
"description": "Feature lifecycle status. `completed` is stamped by `morph-spec archive` together with archivedAt. `abandoned` is stamped by `morph-spec delete --archive` together with abandonedAt (dead scaffold / killed draft, kept in .morph/archive)."
|
|
12
20
|
},
|
|
13
21
|
"createdAt": { "type": "string", "format": "date-time" },
|
|
14
22
|
"updatedAt": {
|
|
@@ -21,6 +29,11 @@
|
|
|
21
29
|
"format": "date-time",
|
|
22
30
|
"description": "Stamped by `morph-spec archive`."
|
|
23
31
|
},
|
|
32
|
+
"abandonedAt": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"format": "date-time",
|
|
35
|
+
"description": "Stamped by `morph-spec delete --archive` when a dead/abandoned feature is moved to .morph/archive."
|
|
36
|
+
},
|
|
24
37
|
"approvalGates": {
|
|
25
38
|
"type": "object",
|
|
26
39
|
"description": "One entry per gate. Written ONLY by `morph-spec approve`/`advance` (approvedBy: \"auto\") or the state-sync hook on a human \"aprovado\" answer (approvedBy: \"manual\").",
|
|
@@ -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.4.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# create-umbraco-lp
|
|
@@ -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.4.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# migrate-lp-to-umbraco
|
|
@@ -4,7 +4,7 @@ description: Orquestra a execução autônoma de tasks de uma feature morph-spec
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: após Gate 2 aprovado, ao receber instrução "use /morph-apply" ou "implementar feature"
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.4.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.4.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.4.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.4.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-hotfix
|
|
@@ -28,10 +28,10 @@ Resolver uma queda ou bug crítico de produção o mais rápido possível — pr
|
|
|
28
28
|
### 1. Nascimento da feature
|
|
29
29
|
|
|
30
30
|
```
|
|
31
|
-
morph-spec create {feature} --type hotfix
|
|
31
|
+
morph-spec create {feature} --type hotfix --description "<o incidente: o que quebrou / sintoma>"
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
Carimba
|
|
34
|
+
Carimba `name` (o slug) + `description` (obrigatória no nascimento — descreva o incidente) e os gates `proposal`/`plan` como `approvedBy: "policy:hotfix"`, e cria as pastas `0-proposal/`, `2-plan/`, `3-implement/` num passo só — a feature já nasce em `implement` (fase derivada é gate-aware). Se a feature já existe, siga adiante sem recriar.
|
|
35
35
|
|
|
36
36
|
### 2. Scout — mapa do território (obrigatório)
|
|
37
37
|
|
|
@@ -4,7 +4,7 @@ description: Guia a produção dos três artefatos de planejamento de uma featur
|
|
|
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.4.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.4.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-proposal
|
|
@@ -38,10 +38,10 @@ Antes de qualquer trabalho de negócio, classifique o pedido. Se `{feature-name}
|
|
|
38
38
|
morph-spec create {feature-name} --request "<pedido do usuário>"
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
(ou `--type <tipo
|
|
41
|
+
(ou `--type <tipo> --description "<o quê/porquê>"` diretamente quando o tipo é óbvio pelo pedido — ex.: o usuário já diz "isso é um hotfix"). O `--request` **serve de semente da descrição**: toda feature nasce com `name` (o slug) e `description` (obrigatória) carimbados. Sem `--request` nem `--description`, o `create` recusa — nada de scaffolds anônimos.
|
|
42
42
|
|
|
43
43
|
- **Classificação `high` confidence** — a feature nasce com o `workType` decidido pelo detector determinístico (keywords: `hotfix` — "produção caiu/urgente/incident/p0"; `bug` — "bug/corrigir/exception/regressão"; `chore` — "typo/rename/bump/config/docs/cleanup"). Prossiga para o branch correspondente abaixo.
|
|
44
|
-
- **Exit 2** (empate `tie` ou nenhum sinal `none`) — o comando sai sem criar a feature e imprime `{ needsDecision, candidates, matched }`. **Você é o roteador**: decida o tipo entre os candidatos e re-rode com `morph-spec create {feature-name} --type <tipo
|
|
44
|
+
- **Exit 2** (empate `tie` ou nenhum sinal `none`) — o comando sai sem criar a feature e imprime `{ needsDecision, candidates, matched }`. **Você é o roteador**: decida o tipo entre os candidatos e re-rode com `morph-spec create {feature-name} --type <tipo> --request "<pedido do usuário>"` (o `--request` re-passado carimba a descrição). Só use `AskUserQuestion` para o usuário quando o pedido for genuinamente indeterminado — a maioria dos empates o roteador resolve sozinho pelo contexto da conversa.
|
|
45
45
|
|
|
46
46
|
**Branches por tipo** (dimensionamento completo em `MORPH.md` §2.1):
|
|
47
47
|
|
|
@@ -55,7 +55,7 @@ morph-spec create {feature-name} --request "<pedido do usuário>"
|
|
|
55
55
|
- Se argumento ausente: perguntar via `AskUserQuestion` (1 pergunta, header "Feature").
|
|
56
56
|
- Verificar `.morph/features/{feature-name}/`:
|
|
57
57
|
- Se `0-proposal/proposal.md` existe e tem conteúdo: pular para passo 3.
|
|
58
|
-
- Caso contrário: rode `morph-spec create {feature-name}` — cria `.morph/features/{feature-name}/0-proposal/` e o `feature.json` canônico (shape em `.morph/framework/schemas/feature.schema.json`) num passo só, server-side. Não crie a estrutura à mão nem dependa do nascimento implícito via hook no primeiro Write. Depois siga o passo 2.
|
|
58
|
+
- Caso contrário: rode `morph-spec create {feature-name} --description "<resumo do pedido do usuário>"` — cria `.morph/features/{feature-name}/0-proposal/` e o `feature.json` canônico (shape em `.morph/framework/schemas/feature.schema.json`) num passo só, server-side, com `name`+`description` carimbados. A `--description` é **obrigatória** no nascimento (ou `--request`, que a semeia). Não crie a estrutura à mão nem dependa do nascimento implícito via hook no primeiro Write. Depois siga o passo 2.
|
|
59
59
|
|
|
60
60
|
### 2. Invocar `Skill(morph-brainstorming)`
|
|
61
61
|
|
|
@@ -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.4.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.4.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-stack-scan
|
|
@@ -4,7 +4,7 @@ description: Localiza e carrega um standard específico do STANDARDS.json sob de
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: quando precisar consultar um standard específico durante a implementação
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.4.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-standards
|