@polymorphism-tech/morph-spec 8.1.2 → 8.2.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 +15 -0
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +6 -4
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +6 -3
- package/framework/MORPH.md +8 -2
- package/framework/hooks/claude-code/core/worktree-context.js +43 -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 +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/commands/project/fleet.js +54 -0
- package/src/commands/project/worktree.js +56 -7
- package/src/commands/state/advance.js +8 -0
- package/src/commands/state/create.js +6 -0
- package/src/commands/state/handoff.js +49 -0
- package/src/lib/generators/handoff-generator.js +267 -0
- package/src/lib/generators/index.js +1 -3
- package/src/lib/worktree/env-allocator.js +149 -0
- package/src/lib/worktree/fleet-scanner.js +156 -0
- package/src/lib/context/context-builder.js +0 -289
- package/src/lib/generators/context-generator.js +0 -519
- package/src/lib/generators/mandate-generator.js +0 -468
- package/src/lib/generators/recap-generator.js +0 -285
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.2.0
|
|
7
7
|
**Requires:** Node.js 22+, Claude Code
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -394,4 +394,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
|
|
|
394
394
|
|
|
395
395
|
---
|
|
396
396
|
|
|
397
|
-
*morph-spec v8.
|
|
397
|
+
*morph-spec v8.2.0 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/bin/morph-spec.js
CHANGED
|
@@ -24,6 +24,7 @@ import { updateCommand } from '../src/commands/project/update.js';
|
|
|
24
24
|
import { doctorCommand } from '../src/commands/project/doctor.js';
|
|
25
25
|
import { statusCommand } from '../src/commands/project/status.js';
|
|
26
26
|
import { worktreeSetupCommand, worktreeLinkCommand, worktreeFinishCommand, worktreeListCommand } from '../src/commands/project/worktree.js';
|
|
27
|
+
import { fleetCommand } from '../src/commands/project/fleet.js';
|
|
27
28
|
|
|
28
29
|
// === Approval gates (human-only) ===
|
|
29
30
|
import { approveCommand, approvalStatusCommand, unapproveCommand } from '../src/commands/state/approve.js';
|
|
@@ -33,6 +34,7 @@ import { scoreCommand } from '../src/commands/state/score.js';
|
|
|
33
34
|
import { gateDecisionCommand } from '../src/commands/state/gate-decision.js';
|
|
34
35
|
import { createCommand } from '../src/commands/state/create.js';
|
|
35
36
|
import { retypeCommand } from '../src/commands/state/retype.js';
|
|
37
|
+
import { handoffCommand } from '../src/commands/state/handoff.js';
|
|
36
38
|
|
|
37
39
|
// === Tasks ===
|
|
38
40
|
import { dagCommand } from '../src/commands/tasks/dag.js';
|
|
@@ -195,6 +197,12 @@ program
|
|
|
195
197
|
.option('--json', 'Output as JSON')
|
|
196
198
|
.action((feature, newType, options) => retypeCommand(feature, newType, options));
|
|
197
199
|
|
|
200
|
+
program
|
|
201
|
+
.command('handoff <feature>')
|
|
202
|
+
.description('Generate the compressed session-handoff document (.morph/features/{f}/handoff.md) — pointers, not copies')
|
|
203
|
+
.option('--json', 'Output as JSON')
|
|
204
|
+
.action((feature, options) => handoffCommand(feature, options));
|
|
205
|
+
|
|
198
206
|
// ─── Tasks ────────────────────────────────────────────────────────────────
|
|
199
207
|
program
|
|
200
208
|
.command('dag <feature>')
|
|
@@ -321,6 +329,13 @@ worktreeCommand
|
|
|
321
329
|
.option('--json', 'Output as JSON')
|
|
322
330
|
.action((options) => worktreeListCommand(options));
|
|
323
331
|
|
|
332
|
+
// ─── Fleet ────────────────────────────────────────────────────────────────
|
|
333
|
+
program
|
|
334
|
+
.command('fleet')
|
|
335
|
+
.description('Fleet view — every active feature across primary root and worktrees (phase, gates, tasks, ports, handoff, activity)')
|
|
336
|
+
.option('--json', 'Output as JSON')
|
|
337
|
+
.action((options) => fleetCommand(options));
|
|
338
|
+
|
|
324
339
|
// ─── Dashboard ────────────────────────────────────────────────────────────
|
|
325
340
|
program
|
|
326
341
|
.command('dashboard')
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
|
@@ -99,16 +99,18 @@ morph-spec retype {feature} {newType} # change type mid-fligh
|
|
|
99
99
|
|
|
100
100
|
---
|
|
101
101
|
|
|
102
|
-
## CLI — worktrees (parallel development)
|
|
102
|
+
## CLI — worktrees (parallel development / fleet)
|
|
103
103
|
|
|
104
|
-
One feature = one git worktree = one Claude Code instance. Independent features run side by side without stepping on each other's state; a SessionStart hook injects each worktree's feature identity into context.
|
|
104
|
+
One feature = one git worktree = one Claude Code instance. Independent features run side by side without stepping on each other's state; a SessionStart hook injects each worktree's feature identity, allocated ports and handoff pointer into context. Each worktree gets a deterministic port block (`.morph/worktree.env`) and a regenerable handoff document.
|
|
105
105
|
|
|
106
106
|
```bash
|
|
107
107
|
morph-spec create {feature} --worktree # birth the feature already isolated in its own worktree
|
|
108
108
|
morph-spec worktree setup {feature} # create .worktrees/{feature}/ on branch morph/{feature}
|
|
109
109
|
morph-spec worktree link [--all] # relink shared infra (node_modules, .claude, .morph/framework)
|
|
110
|
-
morph-spec worktree finish {feature} # merge --no-ff back to the primary root, clean up (requires Gate 3)
|
|
110
|
+
morph-spec worktree finish {feature} # merge --no-ff back to the primary root, clean up + release ports (requires Gate 3)
|
|
111
111
|
morph-spec worktree list [--json] # active feature worktrees with phase/gate status
|
|
112
|
+
morph-spec handoff {feature} [--json] # regenerate the compressed session-handoff doc (pointers, not copies)
|
|
113
|
+
morph-spec fleet [--json] # fleet view: every active feature (root + worktrees) with phase/gates/ports/handoff/activity
|
|
112
114
|
```
|
|
113
115
|
|
|
114
116
|
---
|
|
@@ -237,4 +239,4 @@ These files are never edited directly. Use CLI commands or `morph-spec update` i
|
|
|
237
239
|
|
|
238
240
|
---
|
|
239
241
|
|
|
240
|
-
*morph-spec v8.
|
|
242
|
+
*morph-spec v8.2.0 by Polymorphism Tech*
|
package/docs/QUICKSTART.md
CHANGED
package/framework/CLAUDE.md
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
1. Leia `.morph/framework/MORPH.md` — o charter NLH com papéis, fases, gates e skills.
|
|
8
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
|
-
3.
|
|
9
|
+
3. Se existir `.morph/features/{feature}/handoff.md` para a feature ativa, **leia-o antes de re-derivar contexto à mão** — é o documento de retomada (comprimido, aponta para os artefatos; regenere com `morph-spec handoff <f>` se parecer velho).
|
|
10
|
+
4. 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
11
|
|
|
11
12
|
Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
12
13
|
|
|
@@ -49,7 +50,9 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
|
49
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
51
|
- `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.
|
|
51
52
|
- `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.
|
|
52
|
-
- `morph-spec worktree setup|link|finish|list` — gerencia o ciclo do worktree por feature (`setup` cria `.worktrees/{f}/` na branch `morph/{f}`; `link` religa infra compartilhada; `finish` faz merge `--no-ff
|
|
53
|
+
- `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).
|
|
54
|
+
- `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`.
|
|
55
|
+
- `morph-spec fleet` — visão da frota: toda feature ativa (raiz + worktrees) com fase, gates pendentes, tasks, portas, handoff e atividade. Use entre sessões para decidir onde atuar.
|
|
53
56
|
|
|
54
57
|
## Skills core (em `.claude/skills/`)
|
|
55
58
|
|
|
@@ -68,7 +71,7 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
|
68
71
|
|
|
69
72
|
Toda feature nasce com um `workType` (`morph-spec create <f> --type <tipo>` ou classificado de `--request "<pedido>"`) 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.
|
|
70
73
|
|
|
71
|
-
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. `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. Detalhes em `.morph/framework/MORPH.md` §2.2.
|
|
74
|
+
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.
|
|
72
75
|
|
|
73
76
|
## Personas (em `.claude/agents/`)
|
|
74
77
|
|
package/framework/MORPH.md
CHANGED
|
@@ -112,14 +112,20 @@ Nem todo trabalho merece o pipeline completo. Cada feature nasce com um **workTy
|
|
|
112
112
|
|
|
113
113
|
**Compat:** uma feature sem `workType` (criada antes do v8) é tratada como `feature`. Mudança de tipo mid-flight: `morph-spec retype <f> <tipo>` (desfaz só carimbos de política, preserva aprovações humanas; bloqueado após o Gate 3).
|
|
114
114
|
|
|
115
|
-
### 2.2 Desenvolvimento paralelo por worktree
|
|
115
|
+
### 2.2 Desenvolvimento paralelo por worktree (frota)
|
|
116
116
|
|
|
117
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:
|
|
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.
|
|
121
121
|
- Infra compartilhada (`node_modules`, `.claude`, `.morph/framework`) é linkada por junction; `memory/` e `logs/` são locais de cada worktree (isolamento correto). Se faltar infra num worktree (aberto com `git worktree add` puro), rode `morph-spec worktree link --all`.
|
|
122
|
-
- **
|
|
122
|
+
- **Ambiente separado (portas):** o setup aloca um **bloco de portas determinístico** por feature e escreve `.morph/worktree.env` no worktree (`PORT`, `MORPH_PORT_RANGE`; `ASPNETCORE_URLS` quando o stack tem dotnet). **Suba dev servers SEMPRE nas portas do bloco** — as portas padrão (3000/5000) pertencem à raiz; duas instâncias no default se matam. O arquivo é local e git-invisível; o hook de SessionStart injeta as portas. Overrides em `config.json → fleet.basePort/blockSize`.
|
|
123
|
+
- **Banco de dados:** worktrees compartilham o mesmo banco por padrão. Para isolamento real com o MCP do Neon disponível, crie um branch de DB `morph/{f}` e anote a connection string no `.morph/worktree.env` (a CLI nunca toca em credenciais); sem Neon, prefixe schema/DB local com o nome da feature. Migration destrutiva num worktree compartilhando banco → integre antes de abrir worktrees irmãos.
|
|
124
|
+
- **Limitação conhecida:** `node_modules` é compartilhado (junction) — mudança de dependência feita num worktree afeta os irmãos. Mudou dependência: conclua e integre (`finish`) antes de seguir com worktrees paralelos.
|
|
125
|
+
- **Handoff entre sessões:** `.morph/features/{f}/handoff.md` é o documento de retomada — comprimido, derivado e regenerável (`morph-spec handoff <f>`), ele **aponta** para os artefatos em vez de copiá-los. É regenerado automaticamente no `worktree setup`, `create --worktree` e `advance`; o hook de SessionStart aponta para ele. **Ao abrir uma sessão numa feature em andamento, leia o handoff antes de re-derivar contexto à mão.** Nunca o edite manualmente (é git-invisível e sobrescrito na próxima regeneração).
|
|
126
|
+
- **Visão da frota:** `morph-spec fleet` agrega raiz + worktrees (fase, gates pendentes, tasks, portas, handoff, último commit, sujeira) — use entre sessões para decidir onde atuar. `morph-spec worktree list` continua sendo a visão enxuta por worktree.
|
|
127
|
+
- **Ownership de arquivos:** não há lock em runtime, por design — a colisão de escrita se resolve **em plan-time** (`morph-spec dag` detecta overlap de `outputs` e recomenda `isolation: "worktree"`) e **por construção** (uma feature = um worktree = um escritor). Entre features, arquivos compartilhados convergem no merge do `finish` — conflito ali é o sinal honesto de escopo sobreposto, não um bug.
|
|
128
|
+
- **Fechamento:** aprove o Gate 3 dentro do worktree, depois `morph-spec worktree finish <f>` (merge `--no-ff` da branch na raiz primária limpa; recusa se worktree ou raiz estiverem sujos; aborta em conflito; libera o bloco de portas) e por fim `morph-spec archive <f>` na raiz.
|
|
123
129
|
|
|
124
130
|
---
|
|
125
131
|
|
|
@@ -97,11 +97,54 @@ function featureFromSingleDir(dir) {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Fleet env line: the worktree's allocated port block, read from the manifest
|
|
102
|
+
* `worktree setup` writes. Fail-open — a missing or malformed manifest just
|
|
103
|
+
* omits the line (the session then uses project defaults, which is exactly
|
|
104
|
+
* the pre-fleet behavior).
|
|
105
|
+
*/
|
|
106
|
+
function buildEnvLine(dir) {
|
|
107
|
+
try {
|
|
108
|
+
const envPath = join(dir, '.morph', 'worktree.env');
|
|
109
|
+
if (!existsSync(envPath)) return null;
|
|
110
|
+
const content = readFileSync(envPath, 'utf8');
|
|
111
|
+
const port = content.match(/^PORT=(\d+)$/m)?.[1];
|
|
112
|
+
if (!port) return null;
|
|
113
|
+
const range = content.match(/^MORPH_PORT_RANGE=([\d-]+)$/m)?.[1];
|
|
114
|
+
return `🔌 Ambiente desta worktree: PORT=${port}${range ? ` (bloco ${range})` : ''}. ` +
|
|
115
|
+
`Suba dev servers SEMPRE nessas portas (fonte: .morph/worktree.env) — as portas padrão pertencem à raiz.`;
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Fleet handoff line: point the fresh session at the compressed handoff doc
|
|
123
|
+
* before it starts re-deriving context by hand. Fail-open.
|
|
124
|
+
*/
|
|
125
|
+
function buildHandoffLine(dir, feature) {
|
|
126
|
+
try {
|
|
127
|
+
const handoffPath = join(dir, '.morph', 'features', feature, 'handoff.md');
|
|
128
|
+
if (!existsSync(handoffPath)) return null;
|
|
129
|
+
const generatedAt = readFileSync(handoffPath, 'utf8')
|
|
130
|
+
.match(/gerado em (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/)?.[1];
|
|
131
|
+
return `📄 Handoff: .morph/features/${feature}/handoff.md${generatedAt ? ` (gerado ${generatedAt})` : ''} — leia antes de continuar.`;
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
100
137
|
function buildWorktreeContext(dir, feature) {
|
|
101
138
|
const phase = getFeaturePhase(feature, dir);
|
|
102
139
|
let text = `🌳 Worktree desta feature: ${feature} (branch morph/${feature}, fase ${phase}). ` +
|
|
103
140
|
`Trabalhe apenas nesta feature nesta sessão; o estado vive no .morph/ deste worktree.`;
|
|
104
141
|
|
|
142
|
+
const envLine = buildEnvLine(dir);
|
|
143
|
+
if (envLine) text += `\n${envLine}`;
|
|
144
|
+
|
|
145
|
+
const handoffLine = buildHandoffLine(dir, feature);
|
|
146
|
+
if (handoffLine) text += `\n${handoffLine}`;
|
|
147
|
+
|
|
105
148
|
const missingJunctions = !existsSync(join(dir, 'node_modules')) || !existsSync(join(dir, '.claude'));
|
|
106
149
|
if (missingJunctions) {
|
|
107
150
|
text += `\n⚠ Infra compartilhada ausente (node_modules/.claude). Rode: morph-spec worktree link --all`;
|
|
@@ -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.2.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.2.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.2.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-apply
|
|
@@ -177,7 +177,7 @@ Depois do batch paralelo, executar o restante em modo sequencial (passo 3a).
|
|
|
177
177
|
|
|
178
178
|
Quando o `morph-spec dag {feature}` reporta `isolation.recommended: "worktree"` (dois fluxos paralelos planejam escrever no mesmo path — colisão de `outputs`), despache cada fluxo com o parâmetro `isolation: "worktree"` do tool Agent. Cada agente trabalha numa working tree própria; build/test rodam lá dentro sem colidir.
|
|
179
179
|
|
|
180
|
-
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). O agente commita na branch do worktree e reporta a branch.
|
|
180
|
+
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.
|
|
181
181
|
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.
|
|
182
182
|
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.
|
|
183
183
|
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.
|
|
@@ -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.2.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.2.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.2.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-hotfix
|
|
@@ -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.2.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.2.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-proposal
|
|
@@ -4,7 +4,7 @@ description: Conduz a fase de revisão e produz o review-report.md para o Gate 3
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar a fase review, após todas as tasks de tasks.json estarem com status `done`
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.2.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.2.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.2.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-standards
|
package/package.json
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `morph-spec fleet [--json]` — the fleet view.
|
|
3
|
+
*
|
|
4
|
+
* One line per active feature across the primary root and every morph/*
|
|
5
|
+
* worktree: phase, pending gates, task progress, port block, handoff
|
|
6
|
+
* freshness, last commit and dirtiness. This is the terminal surface the
|
|
7
|
+
* operator scans between sessions ("what is each instance doing, what is
|
|
8
|
+
* waiting on me") — `worktree list` remains the lean per-worktree view.
|
|
9
|
+
*
|
|
10
|
+
* Read-only, always exits 0 (fail-open — an empty fleet is an answer).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { scanFleet } from '../../lib/worktree/fleet-scanner.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {{ json?: boolean, cwd?: string }} [options]
|
|
18
|
+
*/
|
|
19
|
+
export function fleetCommand(options = {}) {
|
|
20
|
+
const fleet = scanFleet(options.cwd || process.cwd());
|
|
21
|
+
|
|
22
|
+
if (options.json) {
|
|
23
|
+
console.log(JSON.stringify(fleet));
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!fleet.entries.length) {
|
|
28
|
+
console.log(chalk.gray('Frota vazia — nenhuma feature ativa (raiz ou worktrees).'));
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const col = (s, w) => String(s ?? '-').padEnd(w);
|
|
33
|
+
console.log(chalk.white.bold(
|
|
34
|
+
col('FEATURE', 20) + col('TYPE', 9) + col('PHASE', 11) + col('TASKS', 8)
|
|
35
|
+
+ col('GATES PEND.', 20) + col('WHERE', 24) + col('PORTS', 12) + col('HANDOFF', 9) + 'ATIVIDADE',
|
|
36
|
+
));
|
|
37
|
+
for (const e of fleet.entries) {
|
|
38
|
+
const tasks = e.tasks ? `${e.tasks.done}/${e.tasks.total}` : '-';
|
|
39
|
+
const gates = e.pendingGates.length ? e.pendingGates.join(',') : '-';
|
|
40
|
+
const where = e.location === 'worktree' ? e.branch : 'raiz';
|
|
41
|
+
const ports = e.env ? `${e.env.range[0]}-${e.env.range[1]}` : '-';
|
|
42
|
+
const handoff = e.handoff?.exists ? 'sim' : '-';
|
|
43
|
+
const activity = [
|
|
44
|
+
e.lastCommit ? `commit ${e.lastCommit}` : null,
|
|
45
|
+
Number.isInteger(e.dirty) && e.dirty > 0 ? chalk.yellow(`${e.dirty} dirty`) : null,
|
|
46
|
+
].filter(Boolean).join(' · ') || '-';
|
|
47
|
+
console.log(
|
|
48
|
+
col(e.feature, 20) + col(e.workType || '-', 9) + col(e.phase, 11) + col(tasks, 8)
|
|
49
|
+
+ col(gates, 20) + col(where, 24) + col(ports, 12) + col(handoff, 9) + activity,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
console.log(chalk.gray(`\nRaiz primária: ${fleet.root}`));
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import { execSync, execFileSync } from 'child_process';
|
|
38
|
-
import { existsSync, mkdirSync, symlinkSync, readFileSync, appendFileSync, lstatSync, unlinkSync, rmdirSync, readdirSync, cpSync } from 'fs';
|
|
38
|
+
import { existsSync, mkdirSync, symlinkSync, readFileSync, writeFileSync, appendFileSync, lstatSync, unlinkSync, rmdirSync, readdirSync, cpSync } from 'fs';
|
|
39
39
|
import { join, resolve, dirname, relative } from 'path';
|
|
40
40
|
import chalk from 'chalk';
|
|
41
41
|
|
|
@@ -196,7 +196,7 @@ export function setupInfraLinks(worktreePath, mainCwd, opts = {}) {
|
|
|
196
196
|
// already speak for it. `lstat` reports a POSIX symlink and a Windows
|
|
197
197
|
// junction alike as a symbolic link, so the exclude lands identically on
|
|
198
198
|
// both platforms — as does the bug it prevents (see excludeInfraLinks).
|
|
199
|
-
|
|
199
|
+
excludeWorktreePaths(mainAbs, dirLinks.filter((rel) => {
|
|
200
200
|
try {
|
|
201
201
|
return lstatSync(join(wtAbs, rel)).isSymbolicLink();
|
|
202
202
|
} catch {
|
|
@@ -205,11 +205,13 @@ export function setupInfraLinks(worktreePath, mainCwd, opts = {}) {
|
|
|
205
205
|
}));
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
/** Marks the block `
|
|
208
|
+
/** Marks the block `excludeWorktreePaths` owns in `.git/info/exclude`. */
|
|
209
209
|
const EXCLUDE_MARKER = '# morph-spec: infra links inside worktrees (managed — see setupInfraLinks)';
|
|
210
210
|
|
|
211
211
|
/**
|
|
212
|
-
* Teach the repo's git to ignore
|
|
212
|
+
* Teach the repo's git to ignore worktree-carried paths morph itself creates —
|
|
213
|
+
* the infra links from `setupInfraLinks` and the fleet env manifest
|
|
214
|
+
* (`.morph/worktree.env`). Same mechanism, same reasons, for both.
|
|
213
215
|
*
|
|
214
216
|
* CRITICAL — without this, `worktree finish` is unusable on POSIX and
|
|
215
217
|
* destructive at worst, because git never sees a link as a link: it sees what
|
|
@@ -247,9 +249,9 @@ const EXCLUDE_MARKER = '# morph-spec: infra links inside worktrees (managed —
|
|
|
247
249
|
* not write costs a refused finish, never a wrong one.
|
|
248
250
|
*
|
|
249
251
|
* @param {string} mainCwd - Absolute path to the main project root
|
|
250
|
-
* @param {string[]} rels - worktree-relative
|
|
252
|
+
* @param {string[]} rels - worktree-relative paths to exclude
|
|
251
253
|
*/
|
|
252
|
-
function
|
|
254
|
+
function excludeWorktreePaths(mainCwd, rels) {
|
|
253
255
|
if (!rels.length) return;
|
|
254
256
|
try {
|
|
255
257
|
// `--git-common-dir`, never `join(mainCwd, '.git')`: the primary root may
|
|
@@ -416,10 +418,47 @@ export async function runWorktreeSetup(feature, options = {}) {
|
|
|
416
418
|
// Set up infrastructure junctions (node_modules, .claude, .morph/framework).
|
|
417
419
|
setupInfraLinks(worktreePath, cwd);
|
|
418
420
|
|
|
421
|
+
// Fleet env: allocate this feature's deterministic port block and write the
|
|
422
|
+
// worktree-local manifest (.morph/worktree.env), excluded from git via the
|
|
423
|
+
// same common-dir mechanism as the infra links (it must be invisible to the
|
|
424
|
+
// finish clean-checks). Under `.morph/` on purpose: that prefix is already
|
|
425
|
+
// shieldable at finish (backed up, never a removal blocker). Fail-open — a
|
|
426
|
+
// worktree without ports is degraded, never broken.
|
|
427
|
+
let envBlock = null;
|
|
428
|
+
try {
|
|
429
|
+
const { allocateEnvBlock, resolveFleetConfig, buildEnvFileContent } =
|
|
430
|
+
await import('../../lib/worktree/env-allocator.js');
|
|
431
|
+
const common = execSync('git rev-parse --git-common-dir', { cwd, stdio: 'pipe' }).toString().trim();
|
|
432
|
+
if (common) {
|
|
433
|
+
const gitCommonDir = resolve(cwd, common);
|
|
434
|
+
envBlock = allocateEnvBlock(feature, { gitCommonDir, ...resolveFleetConfig(cwd) });
|
|
435
|
+
let stack = '';
|
|
436
|
+
try {
|
|
437
|
+
const cfg = JSON.parse(readFileSync(join(cwd, '.morph', 'config', 'config.json'), 'utf8').replace(/^/, ''));
|
|
438
|
+
stack = cfg?.project?.stack || '';
|
|
439
|
+
} catch { /* fail-open — stackless env still carries PORT */ }
|
|
440
|
+
mkdirSync(join(worktreePath, '.morph'), { recursive: true });
|
|
441
|
+
writeFileSync(join(worktreePath, '.morph', 'worktree.env'), buildEnvFileContent(feature, envBlock, stack), 'utf8');
|
|
442
|
+
excludeWorktreePaths(cwd, [join('.morph', 'worktree.env')]);
|
|
443
|
+
}
|
|
444
|
+
} catch { envBlock = null; /* fail-open */ }
|
|
445
|
+
|
|
419
446
|
// Materialize a local, real state.json in the worktree (self-heal from the
|
|
420
447
|
// committed feature.json files) so hooks work from the very first session.
|
|
421
448
|
const materialized = await materializeState(worktreePath);
|
|
422
449
|
|
|
450
|
+
// Fleet handoff: the fresh instance that opens this worktree reads
|
|
451
|
+
// handoff.md before anything else (SessionStart hook points at it). The doc
|
|
452
|
+
// is derived and git-invisible (see handoff-generator). Fail-open — and a
|
|
453
|
+
// no-op in the create --worktree flow, where the feature dir only lands in
|
|
454
|
+
// the worktree after create's copy (create regenerates it then).
|
|
455
|
+
try {
|
|
456
|
+
if (existsSync(join(worktreePath, '.morph', 'features', feature, 'feature.json'))) {
|
|
457
|
+
const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
|
|
458
|
+
writeHandoff(feature, worktreePath);
|
|
459
|
+
}
|
|
460
|
+
} catch { /* fail-open */ }
|
|
461
|
+
|
|
423
462
|
// Persist in state (non-blocking — feature may not be in state yet).
|
|
424
463
|
// NOTE: imports from core/ (the lib/ path was a latent bug that silently
|
|
425
464
|
// no-op'd this whole block); loadState/saveState operate on the cwd's state.
|
|
@@ -442,6 +481,7 @@ export async function runWorktreeSetup(feature, options = {}) {
|
|
|
442
481
|
branch,
|
|
443
482
|
feature,
|
|
444
483
|
materialized: materialized.materialized,
|
|
484
|
+
...(envBlock ? { env: { base: envBlock.base, range: envBlock.range } } : {}),
|
|
445
485
|
nextStep: `Abra o Claude Code em ${worktreePath} para trabalhar nesta feature`,
|
|
446
486
|
};
|
|
447
487
|
} catch (err) {
|
|
@@ -524,7 +564,7 @@ export function worktreeLinkCommand(options = {}) {
|
|
|
524
564
|
* @param {string} cwd
|
|
525
565
|
* @returns {Array<{ path: string, branch: string|null, head: string|null }>}
|
|
526
566
|
*/
|
|
527
|
-
function parseWorktreeList(cwd) {
|
|
567
|
+
export function parseWorktreeList(cwd) {
|
|
528
568
|
let output;
|
|
529
569
|
try {
|
|
530
570
|
output = execSync('git worktree list --porcelain', { cwd, stdio: 'pipe' }).toString();
|
|
@@ -1084,6 +1124,15 @@ export async function worktreeFinishCommand(feature, options = {}) {
|
|
|
1084
1124
|
try {
|
|
1085
1125
|
execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
|
|
1086
1126
|
} catch { /* fail-open */ }
|
|
1127
|
+
|
|
1128
|
+
// Fleet env: the worktree is gone, so no server can be running on its
|
|
1129
|
+
// block — release it for the next feature. ONLY here: a kept/declined
|
|
1130
|
+
// worktree may still be serving on those ports. Fail-open metadata cleanup.
|
|
1131
|
+
try {
|
|
1132
|
+
const { releaseEnvBlock } = await import('../../lib/worktree/env-allocator.js');
|
|
1133
|
+
const common = execSync('git rev-parse --git-common-dir', { cwd: root, stdio: 'pipe' }).toString().trim();
|
|
1134
|
+
if (common) releaseEnvBlock(feature, { gitCommonDir: resolve(root, common) });
|
|
1135
|
+
} catch { /* fail-open */ }
|
|
1087
1136
|
}
|
|
1088
1137
|
|
|
1089
1138
|
// The feature is now merged into the primary root — clear the `worktree`
|
|
@@ -86,6 +86,14 @@ export async function advanceCommand(featureName, options = {}) {
|
|
|
86
86
|
if (!existsSync(nextPath)) mkdirSync(nextPath, { recursive: true });
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
// Fleet handoff: a phase transition is exactly when a session hands the
|
|
90
|
+
// feature over (to a new instance, or to itself after /compact) — refresh
|
|
91
|
+
// the derived handoff.md so the next reader starts from the new phase.
|
|
92
|
+
try {
|
|
93
|
+
const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
|
|
94
|
+
writeHandoff(featureName, process.cwd());
|
|
95
|
+
} catch { /* fail-open */ }
|
|
96
|
+
|
|
89
97
|
console.log(chalk.green(`\n✅ Advanced "${featureName}" past the ${phase} phase.`));
|
|
90
98
|
if (gate) console.log(chalk.gray(` Gate "${gate}" approved (${approvedBy}).`));
|
|
91
99
|
if (nextDir) console.log(chalk.gray(` Scaffolded ${nextDir}/ — ready for the next phase's first artifact.\n`));
|
|
@@ -155,6 +155,12 @@ export async function createCommand(featureName, options = {}) {
|
|
|
155
155
|
// the copy (no features yet), so its `materialized:false` was stale.
|
|
156
156
|
const remat = await materializeState(worktreeResult.path);
|
|
157
157
|
worktreeResult.materialized = remat.materialized;
|
|
158
|
+
// Same reason the setup path generates one: the new instance's first
|
|
159
|
+
// read. Regenerated here because setup ran before the copy above.
|
|
160
|
+
try {
|
|
161
|
+
const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
|
|
162
|
+
writeHandoff(featureName, worktreeResult.path);
|
|
163
|
+
} catch { /* fail-open */ }
|
|
158
164
|
} catch { /* fail-open — the feature already exists in root */ }
|
|
159
165
|
}
|
|
160
166
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `morph-spec handoff <feature>` — generate the compressed session-handoff
|
|
3
|
+
* document (`.morph/features/{f}/handoff.md`).
|
|
4
|
+
*
|
|
5
|
+
* The fleet workflow hands a feature between Claude Code instances (a new
|
|
6
|
+
* worktree session, a resumed session after a phase transition). The handoff
|
|
7
|
+
* is the sanctioned way to carry context across: derived, regenerable, and
|
|
8
|
+
* pointer-based (see handoff-generator.js). This command is also called
|
|
9
|
+
* in-process by `worktree setup`, `create --worktree` and `advance` — the CLI
|
|
10
|
+
* surface exists for the LLM/user to refresh the document on demand.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { buildHandoff, writeHandoff } from '../../lib/generators/handoff-generator.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} featureName
|
|
18
|
+
* @param {{ json?: boolean }} [options]
|
|
19
|
+
*/
|
|
20
|
+
export function handoffCommand(featureName, options = {}) {
|
|
21
|
+
const result = buildHandoff(featureName, process.cwd());
|
|
22
|
+
if (!result) {
|
|
23
|
+
if (options.json) {
|
|
24
|
+
console.log(JSON.stringify({ error: 'feature-not-found', feature: featureName }));
|
|
25
|
+
} else {
|
|
26
|
+
console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
|
|
27
|
+
}
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const path = writeHandoff(featureName, process.cwd());
|
|
32
|
+
if (!path) {
|
|
33
|
+
if (options.json) {
|
|
34
|
+
console.log(JSON.stringify({ error: 'write-failed', feature: featureName }));
|
|
35
|
+
} else {
|
|
36
|
+
console.error(chalk.red(`\n❌ Could not write handoff.md for ${featureName}\n`));
|
|
37
|
+
}
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (options.json) {
|
|
42
|
+
console.log(JSON.stringify({ path, feature: featureName, ...result.meta }));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(chalk.green(`\n✅ Handoff gerado: ${path}`));
|
|
47
|
+
console.log(chalk.gray(` fase ${result.meta.phase} · tasks ${result.meta.tasksDone}/${result.meta.tasksTotal} · ${result.meta.generatedAt}`));
|
|
48
|
+
console.log(chalk.gray(' A próxima sessão lê este documento antes de continuar (ponteiros, não cópias).\n'));
|
|
49
|
+
}
|