@polymorphism-tech/morph-spec 7.4.0 → 7.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/morph-spec.js +18 -0
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +9 -4
- package/framework/MORPH.md +4 -4
- package/framework/commands/create-umbraco-lp.md +35 -0
- package/framework/commands/migrate-lp-to-umbraco.md +33 -0
- package/framework/commands/morph-archive.md +17 -27
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +115 -90
- package/framework/hooks/claude-code/core/protect-spec-files.js +32 -30
- package/framework/hooks/claude-code/core/state-sync.js +16 -48
- package/framework/hooks/shared/state-reader.js +74 -8
- package/framework/hooks/shared/state-writer.js +124 -0
- package/framework/rules/morph-task-tracking.md +69 -49
- package/framework/rules/morph-workflow.md +192 -191
- package/framework/skills/core/create-umbraco-lp/SKILL.md +252 -0
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +452 -0
- package/framework/skills/core/morph-apply/SKILL.md +15 -15
- package/framework/skills/core/morph-brainstorming/SKILL.md +5 -5
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +5 -4
- package/framework/skills/core/morph-plan/SKILL.md +6 -6
- package/framework/skills/core/morph-proposal/SKILL.md +8 -7
- package/framework/skills/core/morph-review/SKILL.md +3 -3
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/templates/REGISTRY.json +1 -3
- package/framework/templates/context/CONTEXT-FEATURE.md +10 -17
- package/framework/templates/context/CONTEXT.md +181 -180
- package/framework/templates/docs/onboarding.md +122 -118
- package/framework/templates/docs/proposal.md +10 -0
- package/framework/templates/docs/spec.md +228 -228
- package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +80 -80
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +90 -89
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +2 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +44 -43
- package/framework/templates/meta-prompts/validators/checkpoint-validator.md +107 -107
- package/package.json +1 -1
- package/src/commands/project/update.js +415 -368
- package/src/commands/project/worktree.js +212 -210
- package/src/commands/state/advance.js +82 -0
- package/src/commands/state/archive.js +111 -0
- package/src/commands/validation/validate-feature.js +229 -105
- package/src/core/state/state-manager.js +238 -66
- package/src/dashboard/scanner/state-scanner.js +35 -8
- package/src/lib/generators/context-generator.js +12 -8
- package/src/utils/file-copier.js +7 -0
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:** 7.
|
|
6
|
+
**Version:** 7.5.0
|
|
7
7
|
**Requires:** Node.js 22+, Claude Code
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -365,4 +365,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
|
|
|
365
365
|
|
|
366
366
|
---
|
|
367
367
|
|
|
368
|
-
*morph-spec v7.
|
|
368
|
+
*morph-spec v7.5.0 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/bin/morph-spec.js
CHANGED
|
@@ -26,6 +26,8 @@ import { worktreeSetupCommand } from '../src/commands/project/worktree.js';
|
|
|
26
26
|
|
|
27
27
|
// === Approval gates (human-only) ===
|
|
28
28
|
import { approveCommand, approvalStatusCommand, unapproveCommand } from '../src/commands/state/approve.js';
|
|
29
|
+
import { advanceCommand } from '../src/commands/state/advance.js';
|
|
30
|
+
import { archiveCommand } from '../src/commands/state/archive.js';
|
|
29
31
|
|
|
30
32
|
// === Validation ===
|
|
31
33
|
import { validateCommand } from './validate.js';
|
|
@@ -117,6 +119,22 @@ program
|
|
|
117
119
|
.option('--notes <notes>', 'Approval notes')
|
|
118
120
|
.action((feature, gate, options) => approveCommand(feature, gate, options));
|
|
119
121
|
|
|
122
|
+
program
|
|
123
|
+
.command('advance <feature>')
|
|
124
|
+
.description('Approve the current phase gate and scaffold the next phase folder (one step)')
|
|
125
|
+
.option('--approver <name>', 'Name of approver (default: current user when interactive, "auto" otherwise)')
|
|
126
|
+
.option('--mode <mode>', 'Approval mode: "user" or "auto" (default: detected from TTY)')
|
|
127
|
+
.option('--design', 'From proposal, advance into the optional design (uiux) phase instead of plan')
|
|
128
|
+
.option('--notes <notes>', 'Approval notes')
|
|
129
|
+
.action((feature, options) => advanceCommand(feature, options));
|
|
130
|
+
|
|
131
|
+
program
|
|
132
|
+
.command('archive <feature>')
|
|
133
|
+
.description('Archive a completed feature (atomic, git-aware): move folder + update state')
|
|
134
|
+
.option('--no-git', 'Skip git staging of the move')
|
|
135
|
+
.option('--force', 'Archive even if Gate 3 (review) is not approved')
|
|
136
|
+
.action((feature, options) => archiveCommand(feature, options));
|
|
137
|
+
|
|
120
138
|
program
|
|
121
139
|
.command('approval-status <feature>')
|
|
122
140
|
.description('Show approval status for all gates')
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
package/framework/CLAUDE.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
## Como começar cada sessão
|
|
6
6
|
|
|
7
7
|
1. Leia `.morph/framework/MORPH.md` — o charter NLH com papéis, fases, gates e skills.
|
|
8
|
-
2. Leia `.morph/state.json` —
|
|
8
|
+
2. Leia `.morph/state.json` — índice fino e gitignored (só status + updatedAt por feature) — para identificar a feature `in_progress` e a fase atual. **Não o edite à mão:** é reconstruído a partir dos `feature.json` a cada load. O estado autoritativo e commitado de cada feature vive em `.morph/features/{feature}/feature.json`.
|
|
9
9
|
3. Assuma o papel correspondente (Orchestrator/Specialist/Evaluator) e continue o trabalho. Não aguarde instrução do usuário se há trabalho pendente entre gates.
|
|
10
10
|
|
|
11
11
|
Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
@@ -15,13 +15,13 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
|
15
15
|
**NUNCA:**
|
|
16
16
|
- Pular para código sem proposal aprovado (Gate 1) e plano aprovado (Gate 2).
|
|
17
17
|
- Modificar o contrato do plano depois do Gate 2: `spec.md`, `mandate.md`, e os campos de plano de `tasks.json` (`title`, `description`, `dependencies`, `effort`, `doneCriteria`, `group`) estão congelados.
|
|
18
|
-
-
|
|
18
|
+
- Tocar os campos de contrato do plano em `tasks.json` após o Gate 2 (`title`, `description`, `dependencies`, `effort`, `doneCriteria`, `group`) — o hook `protect-spec-files` faz diff semântico e bloqueia mudanças neles, seja via `Write` ou `Edit`. (Os campos mutáveis `status`/`outputs`/`notes` continuam graváveis por `Write` ou `Edit` — ver SEMPRE.)
|
|
19
19
|
- Listar perguntas em texto simples — sempre use a tool `AskUserQuestion` (1-4 perguntas por chamada).
|
|
20
20
|
- Usar `Guid.NewGuid()`, classes não-sealed sem justificativa, exceptions como fluxo de controle, camada de Service/Manager entre handler e dados, ou SQL cru sem justificativa.
|
|
21
21
|
|
|
22
22
|
**SEMPRE:**
|
|
23
23
|
- Auto-advance entre tarefas durante a fase implement — não pause sem ambiguidade bloqueante.
|
|
24
|
-
- Atualizar o progresso em `tasks.json`
|
|
24
|
+
- Atualizar o progresso em `tasks.json` ao concluir cada task gravando apenas os campos mutáveis `status`, `outputs` e `notes` — via `Write` OU `Edit` (o hook `protect-spec-files` faz diff semântico e permite qualquer um dos dois desde que o contrato do plano permaneça congelado).
|
|
25
25
|
- Apresentar gates com o template fixo definido em MORPH.md / morph-plan.
|
|
26
26
|
- Registrar decisões não triviais em `decisions.md`.
|
|
27
27
|
- Aplicar `morph-eval` após cada tarefa; silêncio em score ≥ 9 é o sinal correto. Score < 9 dispara o loop de correção autônomo (re-implementa, sem pausa), não escala ao humano entre tarefas.
|
|
@@ -33,10 +33,15 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
|
33
33
|
| `/morph-proposal {feature}` | Entendimento de negócio + plano (Gates 1 e 2) |
|
|
34
34
|
| `/morph-apply {feature}` | Execução autônoma + review (Gate 3) |
|
|
35
35
|
| `/morph-status [feature]` | Status da feature ativa (ou a especificada) |
|
|
36
|
-
| `/morph-archive {feature}` | Arquiva feature concluída (após Gate 3) |
|
|
36
|
+
| `/morph-archive {feature}` | Arquiva feature concluída (após Gate 3) — wrapper de `morph-spec archive` (atômico, git-aware) |
|
|
37
37
|
| `/morph-preflight` | Validação pré-deploy (specs, contratos, testes, infra) |
|
|
38
38
|
| `/morph-troubleshoot [erro]` | Diagnóstico de erros .NET/Next.js com root-cause analysis |
|
|
39
39
|
|
|
40
|
+
**Comandos CLI de gate** (a LLM os usa em vez de editar o estado à mão para aprovar/avançar):
|
|
41
|
+
- `morph-spec approve <feature> <gate> --mode auto` — aprova um gate (`proposal`/`plan`/`review`) em `trust: auto`.
|
|
42
|
+
- `morph-spec advance <feature>` — aprova o gate da fase corrente **e** cria a pasta da próxima fase num passo só (`--design` toma o ramo opcional de uiux a partir da proposta).
|
|
43
|
+
- `morph-spec archive <feature>` — arquiva a feature (atômico, git-aware); é o que `/morph-archive` chama.
|
|
44
|
+
|
|
40
45
|
## Skills core (em `.claude/skills/`)
|
|
41
46
|
|
|
42
47
|
| Skill | Quando usar |
|
package/framework/MORPH.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
> Ele define papéis, fases, outputs, standards, gates e skills do morph-spec.
|
|
5
5
|
> Não procure instruções em hooks — o harness vive aqui.
|
|
6
6
|
>
|
|
7
|
-
> **Depois de ler:** leia `.morph/state.json` para identificar a feature ativa e a fase atual.
|
|
7
|
+
> **Depois de ler:** leia `.morph/state.json` para identificar a feature ativa e a fase atual. `state.json` é um **índice fino e gitignored** (só status + updatedAt por feature, reconstruído a partir dos `feature.json` a cada load) — use-o apenas para descobrir a feature ativa, **nunca o edite à mão** (a edição é perdida no próximo load). O estado autoritativo e commitado de cada feature vive em `.morph/features/{feature}/feature.json`. Se houver uma feature `in_progress`, assuma o papel correspondente à fase e continue o trabalho sem esperar instrução. Se não houver feature ativa, pergunte ao usuário qual feature trabalhar ou se é uma feature nova.
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -77,7 +77,7 @@ Os papéis não são exclusivos — numa sessão longa você transita entre Orch
|
|
|
77
77
|
|
|
78
78
|
## 2. Fases
|
|
79
79
|
|
|
80
|
-
O fluxo padrão de uma feature tem quatro fases. Cada fase tem uma pasta dedicada em `.morph/features/{feature}/`.
|
|
80
|
+
O fluxo padrão de uma feature tem quatro fases. Cada fase tem uma pasta dedicada em `.morph/features/{feature}/`. O estado autoritativo e commitado da feature mora em `.morph/features/{feature}/feature.json` (status, approvalGates, outputs, tasks, gateDecisions, taskScores, archivedAt); a raiz `.morph/state.json` é só um índice fino e gitignored derivado dele, e a identidade do projeto fica no `.morph/config/config.json` (commitado).
|
|
81
81
|
|
|
82
82
|
| Fase | Pasta | Gate | O que termina a fase |
|
|
83
83
|
|-------------|----------------|-----------------------|-------------------------------------------------|
|
|
@@ -186,7 +186,7 @@ Para alterar o modo: editar `.morph/config/config.json → trust` (`"auto"` ou `
|
|
|
186
186
|
|
|
187
187
|
**Comportamento padrão fora de gate:** trabalhe com autonomia total. Gates só existem nestes três pontos. Não pause para confirmação no meio de implementação a menos que encontre uma ambiguidade de escopo que o mandate não cobre — nesse caso, registre a ambiguidade no decisions.md, resolva da forma mais conservadora possível, e sinalize na entrega da task.
|
|
188
188
|
|
|
189
|
-
**O hook state-sync.js**
|
|
189
|
+
**A aprovação de gate é unificada nos comandos da CLI — a LLM nunca edita o estado à mão para aprovar um gate.** Em `trust: "auto"` (sem `AskUserQuestion`), a skill aprova o gate via `morph-spec approve <feature> <gate> --mode auto`; para aprovar o gate da fase corrente **e** já criar a pasta da próxima fase num passo só, use `morph-spec advance <feature>` (adicione `--design` para tomar o ramo opcional de uiux a partir da proposta) — isso substitui o Edit manual de state.json + `mkdir` da pasta de fase. **O hook state-sync.js** cobre o caminho manual: quando uma `AskUserQuestion` é respondida com label começando em "aprovado", ele registra o gate correspondente no `feature.json` autoritativo da feature com `approvedBy: "manual"` (um humano aprovou no loop), na forma canônica. Nos dois caminhos o gate acaba gravado em `.morph/features/{feature}/feature.json` — nunca via Edit à mão de `state.json`.
|
|
190
190
|
|
|
191
191
|
**Registre quem aprovou cada gate (`approvedBy`):** o gate é um objeto `{ approved, timestamp, approvedBy }`.
|
|
192
192
|
- `approvedBy: "auto"` — gate avançado por política `trust: auto` sem decisão humana.
|
|
@@ -194,7 +194,7 @@ Para alterar o modo: editar `.morph/config/config.json → trust` (`"auto"` ou `
|
|
|
194
194
|
|
|
195
195
|
Isso mantém o audit trail honesto justamente nos contextos headless (CI, `claude -p`) onde ele mais importa.
|
|
196
196
|
|
|
197
|
-
**O hook gate-guard.js** confere, sem bloquear, se a ação tomada num gate (pausar via `AskUserQuestion` vs auto-aprovar) bateu com a decisão de trust que a skill computou via `shouldPauseGate`. Para isso, a skill **persiste** a decisão
|
|
197
|
+
**O hook gate-guard.js** confere, sem bloquear, se a ação tomada num gate (pausar via `AskUserQuestion` vs auto-aprovar) bateu com a decisão de trust que a skill computou via `shouldPauseGate`. Para isso, a skill **persiste** a decisão no `feature.json` autoritativo da feature antes de agir: `gateDecisions[gate] = { pause, reason, signals, trust, decidedAt }` (escrito com a tool `Edit` — a allowlist permite `Edit(.morph/features/**)`). Se a ação observada divergir do registro (pausou um gate auto-aprovável, ou auto-aprovou um gate que exigia pausa), o guard injeta um aviso no contexto — a LLM corrige. É um guardrail silencioso, nunca um orquestrador: coerente, sem registro, ou erro → silêncio.
|
|
198
198
|
|
|
199
199
|
---
|
|
200
200
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Cria uma nova landing page ponta-a-ponta a partir do template MORPH Umbraco genérico: coleta decisões, scaffolda com new-lp, sobe e valida, customiza branding e módulos.
|
|
3
|
+
argument-hint: [nome-do-projeto]
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, Skill
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# /create-umbraco-lp — Nova LP Umbraco
|
|
8
|
+
|
|
9
|
+
Cria um projeto de landing page novo a partir do template canônico `MORPH_Umbraco_LP_Template`.
|
|
10
|
+
|
|
11
|
+
**Implementação:** este slash command é um atalho. O agente deve invocar `Skill(create-umbraco-lp)` com o nome do projeto (se fornecido). A skill coleta decisões do usuário uma pergunta por vez, scaffolda via `new-lp.ps1` / `new-lp.sh`, valida o boot (Delivery API + Ping de leads) e apresenta o checklist de entrega.
|
|
12
|
+
|
|
13
|
+
## Argumentos
|
|
14
|
+
|
|
15
|
+
`[nome-do-projeto]` — nome do projeto/LP (opcional; se não fornecido, a skill pergunta).
|
|
16
|
+
|
|
17
|
+
## Pré-requisitos
|
|
18
|
+
|
|
19
|
+
- `gh` autenticado (ou credencial git) com acesso ao repositório **privado** `polymorphism-tech/MORPH_Umbraco_LP_Template` (branch `master`).
|
|
20
|
+
- .NET 10 SDK instalado (`dotnet --version` deve retornar `10.x`).
|
|
21
|
+
- Node.js 20+ e npm instalados.
|
|
22
|
+
- PowerShell 7+ (Windows) ou bash (Linux/macOS) para os scripts.
|
|
23
|
+
|
|
24
|
+
## Anti-padrões
|
|
25
|
+
|
|
26
|
+
- Não editar `backend/uSync/v17/` manualmente durante o setup — deixar o uSync importar no primeiro boot.
|
|
27
|
+
- Não commitar `.env` ou `frontend/.env.local` — contêm credenciais e secrets.
|
|
28
|
+
- Não reutilizar `UMBRACO_GLOBAL_ID` de outro projeto — o script gera um UUID único por execução.
|
|
29
|
+
|
|
30
|
+
## Referências
|
|
31
|
+
|
|
32
|
+
- `framework/skills/core/create-umbraco-lp/SKILL.md`
|
|
33
|
+
- `docs/umbraco-lp-patterns.md` (em `main`)
|
|
34
|
+
|
|
35
|
+
**Projeto:** $ARGUMENTS
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Migra uma LP existente (React/Vite, Next.js estático ou HTML) para o template Umbraco-headless MORPH entregando a LP EXATAMENTE igual à origem (mesmo CSS, imagens, fontes e comportamento) + Umbraco — obtenção automática do template, lift-and-rewire verbatim de cada seção para módulo editável, conexão do formulário ao backend de leads e gate de paridade determinístico. Repo-agnóstica.
|
|
3
|
+
argument-hint: <projeto>
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, Agent, AskUserQuestion, Skill
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# /migrate-lp-to-umbraco — Migração de LP para Umbraco Headless
|
|
8
|
+
|
|
9
|
+
Migra a LP de origem para o template `MORPH_Umbraco_LP_Template` de forma reproduzível, entregando a LP **exatamente igual** à origem com o Umbraco por cima.
|
|
10
|
+
|
|
11
|
+
**Implementação:** este slash command é um atalho. O agente deve invocar `Skill(migrate-lp-to-umbraco)` com o nome do projeto como argumento. A skill é **repo-agnóstica** — obtém o template sozinha e funciona mesmo sem template ou origem pré-baixados.
|
|
12
|
+
|
|
13
|
+
## Argumentos
|
|
14
|
+
|
|
15
|
+
`{projeto}` — nome do projeto em kebab-case (ex.: `reserva-das-flores`, `lp-imobiliaria`).
|
|
16
|
+
|
|
17
|
+
## Fluxo resumido
|
|
18
|
+
|
|
19
|
+
0. **Obter o template** — clone/cache de `polymorphism-tech/MORPH_Umbraco_LP_Template` (privado, branch `master`) via `gh repo clone` (fallback git HTTPS/SSH). Sempre garantido antes de qualquer passo.
|
|
20
|
+
1. **Analisar a origem (capturar para um lift)** — inventariar seções, markup/classes exatos, CSS/tema verbatim, assets byte-a-byte (com `sha256`), fontes e comportamentos. Origem por código-fonte local ou só URL (scrape). Produzir `docs/origem-inventario.md`.
|
|
21
|
+
2. **Mapear para módulos** — cada seção vira um módulo via **lift verbatim** (default) — reuso direto do template só quando o render já bate com a origem. Produzir `docs/mapeamento-modulos.md`.
|
|
22
|
+
3. **Scaffold via new-lp** — cópia fresca do template para o diretório do projeto, `git init`, `scripts/new-lp.sh`/`.ps1` com `--lead-integration`, smoke-boot com `docker compose up --build`.
|
|
23
|
+
4. **Lift verbatim** — copiar markup/CSS/classes/assets de cada seção verbatim para o componente do módulo, trocando só os literais por leitura do backoffice; uSync `.config`, `SeedContentHandler` idempotente, `globals.css` com o tema portado, registro em `moduleRegistry.ts`.
|
|
24
|
+
5. **Conectar formulário** — selecionar sink (`ghl`/`prospectpro`/`webhook`) no backoffice via nó `leadsConfig`; nunca colocar token em código ou env.
|
|
25
|
+
6. **Gate de paridade** — checks determinísticos (DOM+classes, estilo computado, checksum de assets, comportamento) como bloqueio + screenshot diff como corroboração secundária. A skill scaffolda `parity.spec.ts` e `compare-sections.mjs`.
|
|
26
|
+
|
|
27
|
+
## Referências
|
|
28
|
+
|
|
29
|
+
- `framework/skills/core/migrate-lp-to-umbraco/SKILL.md` — playbook completo (passos 0-6, gate de paridade)
|
|
30
|
+
- `polymorphism-tech/MORPH_Umbraco_LP_Template` — template canônico (privado, branch `master`), obtido no passo 0
|
|
31
|
+
- `MORPH_Umbraco_LP_Template/scripts/new-lp.sh` / `new-lp.ps1` — scaffold
|
|
32
|
+
|
|
33
|
+
**Projeto:** $ARGUMENTS
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Archive a completed MORPH-SPEC feature after Gate 3 approval
|
|
3
3
|
argument-hint: <feature-name>
|
|
4
|
-
allowed-tools: Read,
|
|
4
|
+
allowed-tools: Read, Bash
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Archive MORPH Feature
|
|
@@ -9,39 +9,29 @@ allowed-tools: Read, Write, Edit, Bash
|
|
|
9
9
|
Archive a feature that has cleared Gate 3 (review). Use when the work is shipped and you want the
|
|
10
10
|
feature out of the active set in `.morph/features/`.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- [ ] Feature folder exists at `.morph/features/{feature}/`
|
|
16
|
-
- [ ] `4-review/review-report.md` exists and Gate 3 is approved in `state.json`
|
|
17
|
-
- [ ] `3-implement/recap.md` is filled in
|
|
18
|
-
- [ ] Code is merged or otherwise shipped
|
|
19
|
-
|
|
20
|
-
If any precondition fails, stop and report what is missing instead of archiving.
|
|
12
|
+
This is a thin wrapper over the atomic, git-aware CLI command — do **not** move folders or edit state
|
|
13
|
+
by hand. The command moves the folder, stamps the feature `completed` + `archivedAt` in its
|
|
14
|
+
`feature.json`, stages the move in git, and cleans the derivable root index, all-or-nothing.
|
|
21
15
|
|
|
22
16
|
## Workflow
|
|
23
17
|
|
|
24
|
-
1. **
|
|
25
|
-
- `features[name].phase === 'review'`
|
|
26
|
-
- `features[name].approvalGates.review.approved === true`
|
|
27
|
-
|
|
28
|
-
2. **Move the folder** from active to archive:
|
|
18
|
+
1. **Verify** the feature cleared Gate 3:
|
|
29
19
|
```
|
|
30
|
-
|
|
20
|
+
morph-spec approval-status {feature}
|
|
31
21
|
```
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
3. **Update state.json**:
|
|
36
|
-
- Set `features[name].status = 'completed'`
|
|
37
|
-
- Set `features[name].archivedAt = <ISO timestamp>`
|
|
22
|
+
Gate `review` must be `approved`. If not, stop and report what's missing (don't force it without
|
|
23
|
+
the user's say-so).
|
|
38
24
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
25
|
+
2. **Archive** atomically:
|
|
26
|
+
```
|
|
27
|
+
morph-spec archive {feature}
|
|
28
|
+
```
|
|
29
|
+
The command refuses if Gate 3 isn't approved (override with `--force` only when the user asks).
|
|
30
|
+
It moves `.morph/features/{feature}/` → `.morph/archive/{feature}/` (via `git mv` when possible),
|
|
31
|
+
sets `status: "completed"` + `archivedAt`, and drops the feature from the thin index.
|
|
42
32
|
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
3. **Report** the new path (`.morph/archive/{feature}/`). Do not invent metrics not recorded in
|
|
34
|
+
`feature.json` or `recap.md`.
|
|
45
35
|
|
|
46
36
|
---
|
|
47
37
|
|
|
@@ -1,90 +1,115 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* PreToolUse Hook: Enforce Phase-Appropriate File Writes
|
|
5
|
-
*
|
|
6
|
-
* Event: PreToolUse | Matcher: Write|Edit
|
|
7
|
-
*
|
|
8
|
-
* Ensures writes to .morph/features/{feature}/ go to the current phase's subfolder:
|
|
9
|
-
* proposal → only 0-proposal/
|
|
10
|
-
* uiux → only 1-design/
|
|
11
|
-
* plan → only 2-plan/
|
|
12
|
-
* implement → 3-implement/ + any source code (unrestricted)
|
|
13
|
-
* review → only 4-review/
|
|
14
|
-
*
|
|
15
|
-
* Files outside .morph/features/ are always allowed.
|
|
16
|
-
*
|
|
17
|
-
* Fail-open: exits 0 on any error.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { existsSync } from 'fs';
|
|
21
|
-
import { resolve } from 'path';
|
|
22
|
-
import { readStdin } from '../../shared/stdin-reader.js';
|
|
23
|
-
import { stateExists, getFeaturePhase } from '../../shared/state-reader.js';
|
|
24
|
-
import {
|
|
25
|
-
isFeaturePath,
|
|
26
|
-
extractFeatureName,
|
|
27
|
-
extractPhaseDir,
|
|
28
|
-
PHASE_DIRS
|
|
29
|
-
} from '../../shared/phase-utils.js';
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PreToolUse Hook: Enforce Phase-Appropriate File Writes
|
|
5
|
+
*
|
|
6
|
+
* Event: PreToolUse | Matcher: Write|Edit
|
|
7
|
+
*
|
|
8
|
+
* Ensures writes to .morph/features/{feature}/ go to the current phase's subfolder:
|
|
9
|
+
* proposal → only 0-proposal/
|
|
10
|
+
* uiux → only 1-design/
|
|
11
|
+
* plan → only 2-plan/
|
|
12
|
+
* implement → 3-implement/ + any source code (unrestricted)
|
|
13
|
+
* review → only 4-review/
|
|
14
|
+
*
|
|
15
|
+
* Files outside .morph/features/ are always allowed.
|
|
16
|
+
*
|
|
17
|
+
* Fail-open: exits 0 on any error.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { existsSync } from 'fs';
|
|
21
|
+
import { resolve } from 'path';
|
|
22
|
+
import { readStdin } from '../../shared/stdin-reader.js';
|
|
23
|
+
import { stateExists, getFeaturePhase, isGateApproved } from '../../shared/state-reader.js';
|
|
24
|
+
import {
|
|
25
|
+
isFeaturePath,
|
|
26
|
+
extractFeatureName,
|
|
27
|
+
extractPhaseDir,
|
|
28
|
+
PHASE_DIRS
|
|
29
|
+
} from '../../shared/phase-utils.js';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The gate that authorizes writing a phase folder's FIRST artifact. Once that
|
|
33
|
+
* gate is approved, the next phase's canonical file may be created even though
|
|
34
|
+
* the phase only advances (via derivation) once that file exists — this is the
|
|
35
|
+
* fix for the forward-transition chicken-and-egg. proposal-approval unlocks both
|
|
36
|
+
* the optional `1-design/` and `2-plan/` (the uiux fork).
|
|
37
|
+
*/
|
|
38
|
+
const PHASE_DIR_ENTRY_GATE = {
|
|
39
|
+
'1-design': 'proposal',
|
|
40
|
+
'2-plan': 'proposal',
|
|
41
|
+
'3-implement': 'plan',
|
|
42
|
+
'4-review': 'plan',
|
|
43
|
+
};
|
|
44
|
+
import { block, pass } from '../../shared/hook-response.js';
|
|
45
|
+
import { logHookActivity } from '../../shared/activity-logger.js';
|
|
46
|
+
import { getFilePath } from '../../shared/payload-utils.js';
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
// Amend mode: bypass phase write enforcement for legitimate corrections
|
|
50
|
+
if (process.env.MORPH_AMEND_PHASE) {
|
|
51
|
+
console.error(`[morph-spec] AMEND MODE: bypassing phase protection for phase '${process.env.MORPH_AMEND_PHASE}'`);
|
|
52
|
+
pass();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!stateExists()) pass();
|
|
56
|
+
|
|
57
|
+
const payload = await readStdin();
|
|
58
|
+
if (!payload) pass();
|
|
59
|
+
|
|
60
|
+
const filePath = getFilePath(payload);
|
|
61
|
+
if (!filePath) pass();
|
|
62
|
+
|
|
63
|
+
// Only check files inside .morph/features/
|
|
64
|
+
if (!isFeaturePath(filePath)) pass();
|
|
65
|
+
|
|
66
|
+
const featureName = extractFeatureName(filePath);
|
|
67
|
+
if (!featureName) pass();
|
|
68
|
+
|
|
69
|
+
const phase = getFeaturePhase(featureName);
|
|
70
|
+
if (!phase) pass();
|
|
71
|
+
|
|
72
|
+
// During implement phase, all writes are allowed (source code is unrestricted)
|
|
73
|
+
if (phase === 'implement' || phase === 'sync') pass();
|
|
74
|
+
|
|
75
|
+
const targetDir = extractPhaseDir(filePath);
|
|
76
|
+
if (!targetDir) pass(); // File directly in feature root — allow
|
|
77
|
+
|
|
78
|
+
const allowedDir = PHASE_DIRS[phase];
|
|
79
|
+
if (!allowedDir) pass(); // Unknown phase — allow
|
|
80
|
+
|
|
81
|
+
if (targetDir !== allowedDir) {
|
|
82
|
+
// Allow editing already-existing files from previous phases
|
|
83
|
+
const absolutePath = resolve(process.cwd(), filePath);
|
|
84
|
+
if (existsSync(absolutePath)) {
|
|
85
|
+
logHookActivity('enforce-phase-writes', 'PreToolUse', 'ok');
|
|
86
|
+
pass(); // editing existing file — allow
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Gate-aware forward transition: allow creating the NEXT phase's first
|
|
90
|
+
// artifact as soon as the gate authorizing that phase is approved. Without
|
|
91
|
+
// this, the first file of a new phase can never be written (the phase only
|
|
92
|
+
// advances once that file exists) — the deadlock this hook used to create.
|
|
93
|
+
const entryGate = PHASE_DIR_ENTRY_GATE[targetDir];
|
|
94
|
+
if (entryGate && isGateApproved(featureName, entryGate)) {
|
|
95
|
+
logHookActivity('enforce-phase-writes', 'PreToolUse', 'ok');
|
|
96
|
+
pass();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const phaseLabel = phase.toUpperCase();
|
|
100
|
+
logHookActivity('enforce-phase-writes', 'PreToolUse', 'blocked');
|
|
101
|
+
block(
|
|
102
|
+
`MORPH-SPEC: Writing to '${targetDir}/' is not allowed during ${phaseLabel} phase.\n` +
|
|
103
|
+
`Current phase '${phase}' only permits writes to '${allowedDir}/'.\n\n` +
|
|
104
|
+
`To move forward, approve the current gate and scaffold the next phase in\n` +
|
|
105
|
+
`one step: morph-spec advance ${featureName}\n` +
|
|
106
|
+
`(this approves the gate that unlocks the next phase folder).`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
logHookActivity('enforce-phase-writes', 'PreToolUse', 'ok');
|
|
111
|
+
pass();
|
|
112
|
+
} catch {
|
|
113
|
+
// Fail-open
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
@@ -11,11 +11,14 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Semantic exception for files in MUTABLE_SPEC_FIELDS (e.g. tasks.json):
|
|
13
13
|
* the plan contract is frozen, but per-task progress fields (status, outputs,
|
|
14
|
-
* notes) may still change during the implement phase. For those files a
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
* notes) may still change during the implement phase. For those files BOTH a
|
|
15
|
+
* full Write and a targeted Edit are allowed — but only when the semantic diff
|
|
16
|
+
* touches solely the mutable fields. The Edit is simulated (old_string →
|
|
17
|
+
* new_string applied to the current file) and diffed exactly like a Write, so a
|
|
18
|
+
* one-field status flip via Edit is permitted while any change to the frozen
|
|
19
|
+
* plan contract (titles, dependencies, doneCriteria, the task set) is blocked.
|
|
20
|
+
* This is the sanctioned progress path — the agent never has to rewrite the
|
|
21
|
+
* whole file nor reach for a Node script to record task status.
|
|
19
22
|
*
|
|
20
23
|
* Fail-open: exits 0 on any error.
|
|
21
24
|
*/
|
|
@@ -129,40 +132,39 @@ try {
|
|
|
129
132
|
// that touches only those fields. Anything else stays blocked.
|
|
130
133
|
if (mutableFields) {
|
|
131
134
|
const toolName = getToolName(payload);
|
|
132
|
-
const newContent = payload?.tool_input?.content;
|
|
133
|
-
|
|
134
|
-
// Edit is never the legitimate progress path — progress is written as a
|
|
135
|
-
// full file via writeTasksJson(). Block Edit; fall through to the message.
|
|
136
|
-
if (toolName !== 'Write' || typeof newContent !== 'string') {
|
|
137
|
-
logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
|
|
138
|
-
block(
|
|
139
|
-
`MORPH-SPEC: '${filename}' is locked — the '${requiredGate}' gate has been approved.\n` +
|
|
140
|
-
`Progress updates (${mutableFields.join(', ')}) must be written as a full-file\n` +
|
|
141
|
-
`Write, not an Edit. The plan contract (titles, dependencies, doneCriteria…)\n` +
|
|
142
|
-
`is frozen. To change the plan itself, ask the user to revoke the\n` +
|
|
143
|
-
`'${requiredGate}' gate in .morph/state.json, then re-present it for approval.`
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
135
|
|
|
147
|
-
//
|
|
148
|
-
//
|
|
136
|
+
// Build the proposed document from either a full Write or a targeted Edit,
|
|
137
|
+
// then semantic-diff it against the current file on disk. Any failure to
|
|
138
|
+
// read / parse / apply → fail-open (pass), never a false block.
|
|
139
|
+
const absPath = isAbsolute(filePath) ? filePath : join(process.cwd(), filePath);
|
|
149
140
|
let oldDoc, newDoc;
|
|
150
141
|
try {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
142
|
+
const original = readFileSync(absPath, 'utf-8');
|
|
143
|
+
oldDoc = JSON.parse(original);
|
|
144
|
+
|
|
145
|
+
if (toolName === 'Edit') {
|
|
146
|
+
// Simulate the edit: apply old_string → new_string to the current file.
|
|
147
|
+
const oldStr = payload?.tool_input?.old_string;
|
|
148
|
+
const newStr = payload?.tool_input?.new_string;
|
|
149
|
+
if (typeof oldStr !== 'string' || typeof newStr !== 'string') throw new Error('no edit strings');
|
|
150
|
+
const edited = payload?.tool_input?.replace_all
|
|
151
|
+
? original.split(oldStr).join(newStr)
|
|
152
|
+
: original.replace(oldStr, newStr);
|
|
153
|
+
newDoc = JSON.parse(edited);
|
|
154
|
+
} else {
|
|
155
|
+
const newContent = payload?.tool_input?.content;
|
|
156
|
+
if (typeof newContent !== 'string') throw new Error('no content');
|
|
157
|
+
newDoc = JSON.parse(newContent);
|
|
158
|
+
}
|
|
156
159
|
} catch {
|
|
157
|
-
// Cannot compare — fail-open rather than block a possibly-valid write.
|
|
158
160
|
logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
|
|
159
161
|
pass();
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
const violations = diffPlanContract(oldDoc, newDoc, mutableFields);
|
|
163
165
|
if (violations.length === 0) {
|
|
164
|
-
// Only mutable progress fields changed —
|
|
165
|
-
//
|
|
166
|
+
// Only mutable progress fields changed — a legitimate implement-phase
|
|
167
|
+
// update via Write OR Edit.
|
|
166
168
|
logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
|
|
167
169
|
pass();
|
|
168
170
|
}
|
|
@@ -170,7 +172,7 @@ try {
|
|
|
170
172
|
logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
|
|
171
173
|
block(
|
|
172
174
|
`MORPH-SPEC: '${filename}' is locked — the '${requiredGate}' gate has been approved.\n` +
|
|
173
|
-
`This
|
|
175
|
+
`This ${toolName} changes the frozen plan contract:\n` +
|
|
174
176
|
violations.map(v => ` • ${v}`).join('\n') + '\n\n' +
|
|
175
177
|
`During the implement phase only these fields may change: ${mutableFields.join(', ')}.\n` +
|
|
176
178
|
`To change the plan itself, ask the user to revoke the '${requiredGate}' gate\n` +
|