@polymorphism-tech/morph-spec 8.3.0 → 8.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 +11 -8
- package/bin/morph-spec.js +37 -11
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +12 -7
- package/docs/QUICKSTART.md +19 -10
- package/framework/CLAUDE.md +9 -6
- package/framework/MORPH.md +5 -3
- package/framework/commands/morph-archive.md +11 -4
- 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 +17 -4
- 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 +8 -5
- 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 +5 -5
- 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 +4 -2
- 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/project/worktree.js +154 -357
- package/src/commands/state/advance.js +17 -0
- package/src/commands/state/approve.js +13 -0
- package/src/commands/state/archive.js +148 -70
- package/src/commands/state/create.js +67 -3
- package/src/commands/state/delete.js +143 -0
- package/src/commands/state/finish.js +422 -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 +49 -14
- package/src/dashboard/scanner/state-scanner.js +1 -1
- package/src/lib/generators/handoff-generator.js +2 -2
- package/src/lib/git/default-branch.js +73 -0
- package/src/lib/worktree/env-allocator.js +1 -1
- package/src/utils/file-copier.js +2 -2
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.5.0
|
|
7
7
|
**Requires:** Node.js 22+, Claude Code
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -140,7 +140,7 @@ These commands are available inside Claude Code after init.
|
|
|
140
140
|
| `/morph-apply {feature}` | Autonomous execution + review (Gate 3) |
|
|
141
141
|
| `/morph-hotfix {feature}` | Surgical hotfix ADW — scout → fix → human Gate 3 → ship |
|
|
142
142
|
| `/morph-status [feature]` | Status of the active (or specified) feature |
|
|
143
|
-
| `/morph-archive {feature}` | Archive
|
|
143
|
+
| `/morph-archive {feature}` | Archive **without integrating** (legacy feature / cleanup); normal close is `morph-spec finish --pr\|--merge` |
|
|
144
144
|
| `/morph-preflight` | Pre-deploy validation (specs, contracts, tests, infra) |
|
|
145
145
|
| `/morph-troubleshoot [error]` | Root-cause diagnosis for .NET/Next.js errors |
|
|
146
146
|
|
|
@@ -167,9 +167,12 @@ morph-spec status {feature} # Feature status dashboard
|
|
|
167
167
|
morph-spec approve {feature} {gate} # Approve a phase gate (proposal, plan, review)
|
|
168
168
|
morph-spec advance {feature} # Approve the current gate and scaffold the next phase folder
|
|
169
169
|
morph-spec approval-status {feature} # Show approval status for all gates
|
|
170
|
-
morph-spec
|
|
170
|
+
morph-spec finish {feature} --pr|--merge # Close a feature: archive + integrate (PR or local merge) + teardown
|
|
171
|
+
morph-spec archive {feature} # Archive WITHOUT integrating (legacy feature / cleanup)
|
|
171
172
|
```
|
|
172
173
|
|
|
174
|
+
`finish` is the normal close for both flows: it archives (committing the move inside `morph/{feature}`), then integrates — `--merge` merges `--no-ff` into the default branch locally; `--pr` pushes the branch and runs `gh pr create` (needs `origin` + authenticated `gh`) — and tears down the worktree if there is one. Plain `archive` is reserved for legacy features (born before every feature had a `morph/{feature}` branch) and cleanup.
|
|
175
|
+
|
|
173
176
|
Task progress within a feature is tracked with the native `TaskCreate`/`TaskUpdate` tools, persisted to `tasks.json` — there is no separate CLI command for starting or completing a task.
|
|
174
177
|
|
|
175
178
|
### Validation
|
|
@@ -184,8 +187,8 @@ morph-spec verify {feature} [task] # Deterministic build+tests+validators ch
|
|
|
184
187
|
### Work-type routing
|
|
185
188
|
|
|
186
189
|
```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>" #
|
|
190
|
+
morph-spec create {feature} --type feature|bug|chore|hotfix --description "<what/why>" # Explicit birth: type dimensions the pipeline, description is required
|
|
191
|
+
morph-spec create {feature} --request "<free text>" # Detector classifies the type AND seeds the description
|
|
189
192
|
morph-spec retype {feature} {newType} # Change work type mid-flight (blocked after Gate 3)
|
|
190
193
|
morph-spec dag {feature} [--mermaid] # DAG dispatch decision; --mermaid emits a flowchart
|
|
191
194
|
```
|
|
@@ -193,11 +196,11 @@ morph-spec dag {feature} [--mermaid] # DAG dispatch d
|
|
|
193
196
|
### Worktrees (parallel development)
|
|
194
197
|
|
|
195
198
|
```bash
|
|
196
|
-
morph-spec create {feature} --worktree # Birth the feature already isolated in its own worktree
|
|
199
|
+
morph-spec create {feature} --description "<what/why>" --worktree # Birth the feature already isolated in its own worktree
|
|
197
200
|
morph-spec worktree setup {feature} # Create .worktrees/{feature}/ on branch morph/{feature}
|
|
198
201
|
morph-spec worktree link [--all] # Relink shared infra (node_modules, .claude, .morph/framework)
|
|
199
|
-
morph-spec worktree finish {feature} # Merge --no-ff back to the primary root, clean up (requires Gate 3)
|
|
200
202
|
morph-spec worktree list [--json] # List active feature worktrees with phase/gate status
|
|
203
|
+
# Close a worktree feature with `morph-spec finish {feature} --pr|--merge` (see Feature workflow) — it absorbed `worktree finish`.
|
|
201
204
|
```
|
|
202
205
|
|
|
203
206
|
---
|
|
@@ -365,4 +368,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
|
|
|
365
368
|
|
|
366
369
|
---
|
|
367
370
|
|
|
368
|
-
*morph-spec v8.
|
|
371
|
+
*morph-spec v8.5.0 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/bin/morph-spec.js
CHANGED
|
@@ -23,18 +23,21 @@ import { installPluginCommand } from '../src/commands/project/install-plugin-cmd
|
|
|
23
23
|
import { updateCommand } from '../src/commands/project/update.js';
|
|
24
24
|
import { doctorCommand } from '../src/commands/project/doctor.js';
|
|
25
25
|
import { statusCommand } from '../src/commands/project/status.js';
|
|
26
|
-
import { worktreeSetupCommand, worktreeLinkCommand,
|
|
26
|
+
import { worktreeSetupCommand, worktreeLinkCommand, worktreeListCommand } from '../src/commands/project/worktree.js';
|
|
27
27
|
import { fleetCommand } from '../src/commands/project/fleet.js';
|
|
28
28
|
|
|
29
29
|
// === Approval gates (human-only) ===
|
|
30
30
|
import { approveCommand, approvalStatusCommand, unapproveCommand } from '../src/commands/state/approve.js';
|
|
31
31
|
import { advanceCommand } from '../src/commands/state/advance.js';
|
|
32
32
|
import { archiveCommand } from '../src/commands/state/archive.js';
|
|
33
|
+
import { finishCommand } from '../src/commands/state/finish.js';
|
|
33
34
|
import { scoreCommand } from '../src/commands/state/score.js';
|
|
34
35
|
import { gateDecisionCommand } from '../src/commands/state/gate-decision.js';
|
|
35
36
|
import { createCommand } from '../src/commands/state/create.js';
|
|
36
37
|
import { retypeCommand } from '../src/commands/state/retype.js';
|
|
37
38
|
import { handoffCommand } from '../src/commands/state/handoff.js';
|
|
39
|
+
import { listCommand } from '../src/commands/state/list.js';
|
|
40
|
+
import { deleteCommand } from '../src/commands/state/delete.js';
|
|
38
41
|
|
|
39
42
|
// === Tasks ===
|
|
40
43
|
import { dagCommand } from '../src/commands/tasks/dag.js';
|
|
@@ -126,13 +129,22 @@ program
|
|
|
126
129
|
.option('-v, --verbose', 'Show detailed output')
|
|
127
130
|
.action(statusCommand);
|
|
128
131
|
|
|
132
|
+
program
|
|
133
|
+
.command('list')
|
|
134
|
+
.description('List every feature × status × gates (G1/G2/G3) × workType — the audit board')
|
|
135
|
+
.option('--all', 'Include archived features (.morph/archive)')
|
|
136
|
+
.option('--archived', 'Include archived features (alias of --all)')
|
|
137
|
+
.option('--json', 'Output as JSON')
|
|
138
|
+
.action((options) => listCommand(options));
|
|
139
|
+
|
|
129
140
|
// ─── Approval gates (dev-only) ────────────────────────────────────────────
|
|
130
141
|
program
|
|
131
142
|
.command('approve <feature> <gate>')
|
|
132
|
-
.description('Approve a phase gate (proposal, plan, review)')
|
|
143
|
+
.description('Approve a phase gate (proposal, plan, review). Enforces gate order (N requires N-1)')
|
|
133
144
|
.option('--approver <name>', 'Name of approver (default: current user when interactive, "auto" otherwise)')
|
|
134
145
|
.option('--mode <mode>', 'Approval mode: "user" or "auto" (default: detected from TTY)')
|
|
135
146
|
.option('--notes <notes>', 'Approval notes')
|
|
147
|
+
.option('--force', 'Approve out of order (bypass the gate-monotonicity check)')
|
|
136
148
|
.action((feature, gate, options) => approveCommand(feature, gate, options));
|
|
137
149
|
|
|
138
150
|
program
|
|
@@ -142,15 +154,35 @@ program
|
|
|
142
154
|
.option('--mode <mode>', 'Approval mode: "user" or "auto" (default: detected from TTY)')
|
|
143
155
|
.option('--design', 'From proposal, advance into the optional design (uiux) phase instead of plan')
|
|
144
156
|
.option('--notes <notes>', 'Approval notes')
|
|
157
|
+
.option('--force', 'Advance even if a prerequisite gate is unapproved (out-of-order recovery)')
|
|
145
158
|
.action((feature, options) => advanceCommand(feature, options));
|
|
146
159
|
|
|
147
160
|
program
|
|
148
161
|
.command('archive <feature>')
|
|
149
|
-
.description('Archive a completed feature (atomic, git-aware): move folder + update state')
|
|
162
|
+
.description('Archive a completed feature (atomic, git-aware): move folder + update state. Archive WITHOUT integrating (legacy features, cleanup) — the close path is `finish`')
|
|
150
163
|
.option('--no-git', 'Skip git staging of the move')
|
|
151
164
|
.option('--force', 'Archive even if Gate 3 (review) is not approved')
|
|
152
165
|
.action((feature, options) => archiveCommand(feature, options));
|
|
153
166
|
|
|
167
|
+
program
|
|
168
|
+
.command('finish <feature>')
|
|
169
|
+
.description('Close a feature: archive → integrate. --merge merges morph/{f} into the default (local); --pr pushes it + opens a PR. Works with and without a worktree')
|
|
170
|
+
.option('--pr', 'Push morph/{feature} and open a PR against the default branch (no local merge)')
|
|
171
|
+
.option('--merge', 'Merge morph/{feature} into the default branch locally (no push)')
|
|
172
|
+
.option('--keep', 'Keep the worktree/branch after integrating (worktree flow only)')
|
|
173
|
+
.option('--force', 'Skip the Gate-3-approved precondition')
|
|
174
|
+
.option('--discard-ignored', 'Allow removing the worktree even though it holds gitignored files that exist nowhere else (e.g. .env) — they are destroyed')
|
|
175
|
+
.action((feature, options) => finishCommand(feature, options));
|
|
176
|
+
|
|
177
|
+
program
|
|
178
|
+
.command('delete <feature>')
|
|
179
|
+
.description('Delete a feature (hard by default; --archive marks it abandoned). Refuses if a gate is approved unless --force')
|
|
180
|
+
.option('--archive', 'Soft delete: move to .morph/archive with status "abandoned" instead of removing')
|
|
181
|
+
.option('--force', 'Delete even when a gate is approved')
|
|
182
|
+
.option('--no-git', 'Skip git staging of the removal')
|
|
183
|
+
.option('--json', 'Output as JSON')
|
|
184
|
+
.action((feature, options) => deleteCommand(feature, options));
|
|
185
|
+
|
|
154
186
|
program
|
|
155
187
|
.command('approval-status <feature>')
|
|
156
188
|
.description('Show approval status for all gates')
|
|
@@ -178,6 +210,8 @@ program
|
|
|
178
210
|
program
|
|
179
211
|
.command('create <feature>')
|
|
180
212
|
.description('Classify work type (--type/--request), scaffold + stamp policy gates, and create the canonical feature.json (explicit birth — see framework/schemas/feature.schema.json)')
|
|
213
|
+
.option('--name <name>', 'Human-readable feature name (default: the slug)')
|
|
214
|
+
.option('--description <text>', 'What/why of the feature — required at birth unless --request is given (it seeds the description)')
|
|
181
215
|
.option('--status <status>', 'Initial feature status (default: in_progress)')
|
|
182
216
|
.option('--type <type>', 'Work type: feature|bug|chore|hotfix (default: feature) — see src/core/work-types.js')
|
|
183
217
|
.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)')
|
|
@@ -318,14 +352,6 @@ worktreeCommand
|
|
|
318
352
|
.option('--all', 'Link all shared dirs, not just node_modules')
|
|
319
353
|
.action((options) => worktreeLinkCommand(options));
|
|
320
354
|
|
|
321
|
-
worktreeCommand
|
|
322
|
-
.command('finish <feature>')
|
|
323
|
-
.description('Merge the feature branch back into the primary root and clean up the worktree (requires Gate 3 approved)')
|
|
324
|
-
.option('--keep', 'Keep the worktree and branch after merging')
|
|
325
|
-
.option('--force', 'Skip the Gate-3-approved precondition')
|
|
326
|
-
.option('--discard-ignored', 'Allow removing the worktree even though it holds gitignored files that exist nowhere else (e.g. .env) — they are destroyed')
|
|
327
|
-
.action((feature, options) => worktreeFinishCommand(feature, options));
|
|
328
|
-
|
|
329
355
|
worktreeCommand
|
|
330
356
|
.command('list')
|
|
331
357
|
.description('List active feature worktrees with their phase and gate status')
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
|
@@ -46,7 +46,7 @@ proposal → [uiux] → plan → implement → review
|
|
|
46
46
|
| `/morph-hotfix {feature}` | Surgical hotfix ADW (scout → fix → human gate → ship) |
|
|
47
47
|
| `/morph-apply {feature}` | Autonomous execution + review (Gate 3) |
|
|
48
48
|
| `/morph-status [feature]` | Status of the active (or specified) feature |
|
|
49
|
-
| `/morph-archive {feature}` | Archive
|
|
49
|
+
| `/morph-archive {feature}` | Archive **without integrating** (legacy feature / cleanup) — normal close is `morph-spec finish --pr\|--merge` |
|
|
50
50
|
| `/morph-preflight` | Pre-deploy validation (specs, contracts, tests, infra) |
|
|
51
51
|
| `/morph-troubleshoot [error]` | Root-cause diagnosis for .NET/Next.js errors |
|
|
52
52
|
|
|
@@ -59,18 +59,23 @@ morph-spec approve {feature} {gate} [--mode auto] [--approver name] [--notes tex
|
|
|
59
59
|
morph-spec advance {feature} [--design] # approve current gate + scaffold next phase folder in one step
|
|
60
60
|
morph-spec approval-status {feature} [--json]
|
|
61
61
|
morph-spec unapprove {feature} {gate} [--reason text] # recovery only
|
|
62
|
-
morph-spec
|
|
62
|
+
morph-spec finish {feature} --pr|--merge [--keep] [--force] [--discard-ignored] # normal close: archive + integrate + teardown
|
|
63
|
+
morph-spec archive {feature} [--force] [--no-git] # archive WITHOUT integrating (legacy feature / cleanup)
|
|
63
64
|
```
|
|
64
65
|
|
|
66
|
+
`finish` is the normal way to close a feature (both the worktree flow and the common flow): it archives (committing the move inside `morph/{feature}`), then integrates — `--merge` merges `--no-ff` into the default branch locally (no push); `--pr` pushes `morph/{feature}` + `gh pr create` (needs `origin` + authenticated `gh`). It then tears down the worktree and releases the port block. `archive` alone is reserved for legacy features (no `morph/{feature}` branch) and cleanup.
|
|
67
|
+
|
|
65
68
|
---
|
|
66
69
|
|
|
67
70
|
## CLI — feature birth & work-type routing
|
|
68
71
|
|
|
69
72
|
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
73
|
|
|
74
|
+
Every feature is also born with an **identity**: `--description` is required (`create` refuses a nameless scaffold without it), `name` defaults to the slug.
|
|
75
|
+
|
|
71
76
|
```bash
|
|
72
|
-
morph-spec create {feature} --type feature|bug|chore|hotfix
|
|
73
|
-
morph-spec create {feature} --request "<free text>"
|
|
77
|
+
morph-spec create {feature} --type feature|bug|chore|hotfix --description "<what/why>"
|
|
78
|
+
morph-spec create {feature} --request "<free text>" # detector classifies the type AND seeds the description
|
|
74
79
|
morph-spec retype {feature} {newType} # change type mid-flight (blocked after Gate 3)
|
|
75
80
|
```
|
|
76
81
|
|
|
@@ -111,11 +116,11 @@ morph-spec dag {feature} --mermaid # emit the tasks.json DAG as a Mermai
|
|
|
111
116
|
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
117
|
|
|
113
118
|
```bash
|
|
114
|
-
morph-spec create {feature} --worktree
|
|
119
|
+
morph-spec create {feature} --description "<what/why>" --worktree # birth the feature already isolated in its own worktree
|
|
115
120
|
morph-spec worktree setup {feature} # create .worktrees/{feature}/ on branch morph/{feature}
|
|
116
121
|
morph-spec worktree link [--all] # relink shared infra (node_modules[, .claude, .morph/framework])
|
|
117
|
-
morph-spec worktree finish {feature} # merge --no-ff back to the primary root, clean up + release ports (requires Gate 3)
|
|
118
122
|
morph-spec worktree list [--json] # active feature worktrees with phase/gate status
|
|
123
|
+
# Close a worktree feature with `morph-spec finish {feature} --pr|--merge` (see above) — it absorbed `worktree finish`.
|
|
119
124
|
morph-spec handoff {feature} [--json] # regenerate the compressed session-handoff doc (pointers, not copies)
|
|
120
125
|
morph-spec fleet [--json] # every active feature (root + worktrees) with phase/gates/ports/handoff/activity
|
|
121
126
|
```
|
|
@@ -180,4 +185,4 @@ Never edited directly — go through CLI commands or `morph-spec update` instead
|
|
|
180
185
|
|
|
181
186
|
---
|
|
182
187
|
|
|
183
|
-
*morph-spec v8.
|
|
188
|
+
*morph-spec v8.5.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,18 +163,21 @@ 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
|
```
|
|
164
170
|
|
|
165
|
-
Open the new worktree directory in its own Claude Code instance and work normally — a SessionStart hook injects the worktree's feature identity into context automatically. When the feature is done:
|
|
171
|
+
Open the new worktree directory in its own Claude Code instance and work normally — a SessionStart hook injects the worktree's feature identity into context automatically. When the feature is done (Gate 3 approved), close it in one step:
|
|
166
172
|
|
|
167
173
|
```bash
|
|
168
|
-
morph-spec
|
|
169
|
-
|
|
174
|
+
morph-spec finish my-feature --merge # archive + merge --no-ff into the default branch (local), tear down the worktree
|
|
175
|
+
# or, to open a PR instead of merging locally:
|
|
176
|
+
morph-spec finish my-feature --pr # archive + push morph/my-feature + gh pr create (keeps the branch the PR needs)
|
|
170
177
|
```
|
|
171
178
|
|
|
179
|
+
`finish` archives the feature (committing the move inside `morph/my-feature`) *and* integrates *and* tears down the worktree in one step — it replaces the old `worktree finish` + separate `archive`. The same command closes features developed **without** a worktree (they're born on `morph/{feature}` too).
|
|
180
|
+
|
|
172
181
|
List every active worktree with its phase and gate status:
|
|
173
182
|
|
|
174
183
|
```bash
|
|
@@ -240,4 +249,4 @@ morph-spec doctor
|
|
|
240
249
|
|
|
241
250
|
---
|
|
242
251
|
|
|
243
|
-
*morph-spec v8.
|
|
252
|
+
*morph-spec v8.5.0 by Polymorphism Tech*
|
package/framework/CLAUDE.md
CHANGED
|
@@ -36,21 +36,24 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
|
36
36
|
| `/morph-hotfix {feature}` | ADW cirúrgico de hotfix (scout → fix → gate humano → ship) |
|
|
37
37
|
| `/morph-apply {feature}` | Execução autônoma + review (Gate 3) |
|
|
38
38
|
| `/morph-status [feature]` | Status da feature ativa (ou a especificada) |
|
|
39
|
-
| `/morph-archive {feature}` | Arquiva
|
|
39
|
+
| `/morph-archive {feature}` | Arquiva-**sem-integrar** (features legadas/faxina) — wrapper de `morph-spec archive`. Fechamento normal = `morph-spec finish --pr\|--merge` |
|
|
40
40
|
| `/morph-preflight` | Validação pré-deploy (specs, contratos, testes, infra) |
|
|
41
41
|
| `/morph-troubleshoot [erro]` | Diagnóstico de erros .NET/Next.js com root-cause analysis |
|
|
42
42
|
|
|
43
43
|
**Comandos CLI de gate e de estado** (a LLM os usa em vez de editar o estado à mão):
|
|
44
44
|
- `morph-spec approve <feature> <gate> --mode auto` — aprova um gate (`proposal`/`plan`/`review`) em `trust: auto`.
|
|
45
45
|
- `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).
|
|
46
|
-
- `morph-spec archive <feature>` — arquiva
|
|
46
|
+
- `morph-spec archive <feature>` — arquiva-**sem-integrar** (atômico, git-aware, commita o move escopado por pathspec): reservado a features legadas (sem branch `morph/{f}`) e faxina. O fechamento normal (arquiva **e** integra num passo) é `morph-spec finish`. É o que `/morph-archive` chama.
|
|
47
|
+
- `morph-spec finish <feature> --pr|--merge` — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}`, depois integra. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. Flags: `--keep` mantém o worktree, `--force` dispensa o Gate 3, `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados.
|
|
47
48
|
- `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
49
|
- `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
50
|
- `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.
|
|
51
|
+
- `morph-spec create <feature> --description "<o quê/porquê>" --type <feature|bug|chore|hotfix> [--request "<txt>"] [--name "<nome>"] [--worktree]` — cria a estrutura da feature + `feature.json` canônico explicitamente (nascimento explícito no início da proposal; o hook state-sync continua cobrindo o caminho implícito como fallback). **Identidade no nascimento:** `name` (default = slug) e `description` são carimbados; a `--description` é **obrigatória** numa feature nova (ou `--request`, que a semeia) — sem nenhuma das duas o `create` recusa (fim dos scaffolds anônimos). `--type` classifica direto; `--request "<pedido>"` roda o detector determinístico (empate/sem sinal sai com exit 2 e candidatos — a LLM decide e re-roda com `--type` + `--request`/`--description`); `--worktree` já nasce a feature no seu worktree próprio. **Toda feature nasce numa branch `morph/{f}`** (ramificada da default) mesmo sem `--worktree` — por isso o fechamento por `morph-spec finish` é idêntico nos dois fluxos.
|
|
52
|
+
- `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).
|
|
53
|
+
- `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
54
|
- `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
55
|
- `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
|
-
- `morph-spec worktree setup|link|
|
|
56
|
+
- `morph-spec worktree setup|link|list` — gerencia o ciclo do worktree por feature (`setup` cria `.worktrees/{f}/` na branch `morph/{f}`, aloca o bloco de portas e escreve `.morph/worktree.env`; `link` religa infra compartilhada; `list` mostra fase/gate de cada worktree ativo). O fechamento saiu daqui — use `morph-spec finish` (acima).
|
|
54
57
|
- `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
58
|
- `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.
|
|
56
59
|
- `morph-spec telemetry [--json] [--feature <f>] [--limit <n>]` — reconstrói o caminho de execução da sessão a partir de `.morph/logs/events.jsonl` (skills invocadas, personas despachadas + prompts, standards/base de conhecimento lidos, mix de tools, prompts do usuário). Capturado silenciosamente pelo hook `telemetry-log` em todo tool call e todo UserPromptSubmit.
|
|
@@ -70,9 +73,9 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
|
70
73
|
|
|
71
74
|
## Roteamento por tipo de trabalho
|
|
72
75
|
|
|
73
|
-
Toda feature nasce com um `workType` (`morph-spec create <f> --type <tipo>` ou classificado de `--request "<pedido>"
|
|
76
|
+
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
77
|
|
|
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
|
|
78
|
+
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 e rode `morph-spec finish <f> --pr|--merge` (arquiva dentro da branch, integra e desmonta o worktree num passo só). Visão agregada: `morph-spec fleet`. Detalhes em `.morph/framework/MORPH.md` §2.2.
|
|
76
79
|
|
|
77
80
|
## Personas (em `.claude/agents/`)
|
|
78
81
|
|
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
|
|------|----------|-------|
|
|
@@ -112,9 +112,11 @@ 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
|
+
**Nascimento numa branch:** toda feature — **com ou sem `--worktree`** — nasce numa branch `morph/{f}` ramificada da default. O fluxo comum (sem worktree) desenvolve direto em `morph/{f}` na raiz; o fluxo worktree isola em `.worktrees/{f}`. Por construção o fechamento é simétrico: `morph-spec finish <f> --pr|--merge` (§2.2) fecha os dois do mesmo jeito. Features criadas **antes** deste modelo (sem branch `morph/{f}`) fecham por `morph-spec archive <f>` (arquivar-sem-integrar), não por `finish`.
|
|
116
|
+
|
|
115
117
|
### 2.2 Desenvolvimento paralelo por worktree (frota)
|
|
116
118
|
|
|
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:
|
|
119
|
+
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
120
|
|
|
119
121
|
- **Uma feature = um worktree = uma instância.** Nunca abra duas instâncias na mesma feature (o setup recusa com exit 3).
|
|
120
122
|
- 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.
|
|
@@ -125,7 +127,7 @@ Features independentes podem ser desenvolvidas **em paralelo, cada uma no seu gi
|
|
|
125
127
|
- **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
128
|
- **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
129
|
- **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
|
|
130
|
+
- **Fechamento:** aprove o Gate 3, depois **`morph-spec finish <f> --pr|--merge`** — o encerramento único e simétrico (mesmo comando nos dois fluxos, worktree e comum). Ele **arquiva** a feature commitando o move `features/{f} → archive/{f}` **dentro da branch `morph/{f}`** (o estado arquivado viaja na própria integração, não num commit à parte) e então integra: `--merge` faz o merge `--no-ff` **local** na branch default (sem push; recusa se worktree/raiz estiverem sujos; aborta em conflito) e `--pr` faz `push` de `morph/{f}` + `gh pr create` (sem merge local; exige `origin` + `gh` autenticado — as pré-condições rodam **antes** de arquivar, então um `--pr` recusado não deixa meio-estado). Em ambos o worktree é desmontado (PROVE-or-DECLINE) e o bloco de portas liberado; no `--pr` a branch é **preservada** (o PR aberto precisa dela) e a sessão permanece em `morph/{f}`. Não há mais `worktree finish` nem `archive` separado no caminho feliz — `morph-spec archive` fica reservado a arquivar-sem-integrar (features legadas, faxina).
|
|
129
131
|
|
|
130
132
|
---
|
|
131
133
|
|
|
@@ -6,12 +6,18 @@ allowed-tools: Read, Bash
|
|
|
6
6
|
|
|
7
7
|
# Archive MORPH Feature
|
|
8
8
|
|
|
9
|
-
Archive a feature
|
|
10
|
-
|
|
9
|
+
Archive-**without-integrate**: move a feature out of the active set in `.morph/features/` without
|
|
10
|
+
merging or opening a PR. This is the **reserved** path — for **legacy** features (born before the
|
|
11
|
+
"every feature is born on a `morph/{f}` branch" model, so there's no branch to integrate) and for
|
|
12
|
+
**cleanup**. The **normal close** of a feature is `morph-spec finish {feature} --pr|--merge`, which
|
|
13
|
+
archives *and* integrates *and* tears down the worktree in one step (see `MORPH.md` §2.2 and
|
|
14
|
+
`morph-apply` §6). Reach for `/morph-archive` only when integration is not wanted.
|
|
11
15
|
|
|
12
16
|
This is a thin wrapper over the atomic, git-aware CLI command — do **not** move folders or edit state
|
|
13
17
|
by hand. The command moves the folder, stamps the feature `completed` + `archivedAt` in its
|
|
14
|
-
`feature.json`,
|
|
18
|
+
`feature.json`, **commits the move scoped to the feature's own pathspec** (`.morph/features/{f}` +
|
|
19
|
+
`.morph/archive/{f}`, never `commit -a`), and cleans the derivable root index, all-or-nothing. Re-running
|
|
20
|
+
is idempotent: an already-archived feature is reported, not re-moved.
|
|
15
21
|
|
|
16
22
|
## Workflow
|
|
17
23
|
|
|
@@ -28,7 +34,8 @@ by hand. The command moves the folder, stamps the feature `completed` + `archive
|
|
|
28
34
|
```
|
|
29
35
|
The command refuses if Gate 3 isn't approved (override with `--force` only when the user asks).
|
|
30
36
|
It moves `.morph/features/{feature}/` → `.morph/archive/{feature}/` (via `git mv` when possible),
|
|
31
|
-
sets `status: "completed"` + `archivedAt`,
|
|
37
|
+
sets `status: "completed"` + `archivedAt`, commits that move scoped to the feature's pathspec, and
|
|
38
|
+
drops the feature from the thin index.
|
|
32
39
|
|
|
33
40
|
3. **Report** the new path (`.morph/archive/{feature}/`). Do not invent metrics not recorded in
|
|
34
41
|
`feature.json` or `recap.md`.
|
|
@@ -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: {
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"title": "MORPH-SPEC feature.json",
|
|
4
|
-
"description": "Canonical shape of the authoritative, committed per-feature state at .morph/features/{feature}/feature.json (v5 per-feature state model). Born via `morph-spec create` (explicit) or the state-sync hook (implicit fallback). Server-side writers: `morph-spec approve/advance` (approvalGates), `morph-spec score` (taskScores), `morph-spec gate-decision` (gateDecisions), `morph-spec archive` (status/archivedAt). Never hand-edit the CLI-owned fields — manual Edits collide with the state-sync hook. This schema is documentational: additionalProperties stays true so older/newer CLIs interoperate.",
|
|
4
|
+
"description": "Canonical shape of the authoritative, committed per-feature state at .morph/features/{feature}/feature.json (v5 per-feature state model). Born via `morph-spec create` (explicit) or the state-sync hook (implicit fallback). Server-side writers: `morph-spec approve/advance` (approvalGates), `morph-spec score` (taskScores), `morph-spec gate-decision` (gateDecisions), `morph-spec archive`/`finish` (status/archivedAt — `finish` archives internally before integrating). Never hand-edit the CLI-owned fields — manual Edits collide with the state-sync hook. This schema is documentational: additionalProperties stays true so older/newer CLIs interoperate.",
|
|
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\").",
|
|
@@ -187,7 +200,7 @@
|
|
|
187
200
|
},
|
|
188
201
|
"worktree": {
|
|
189
202
|
"type": "object",
|
|
190
|
-
"description": "Bookkeeping do worktree ativo desta feature no ROOT feature.json, gravado por `morph-spec worktree setup` (via `create --worktree` ou standalone). Removido por `worktree finish
|
|
203
|
+
"description": "Bookkeeping do worktree ativo desta feature no ROOT feature.json, gravado por `morph-spec worktree setup` (via `create --worktree` ou standalone). Removido no fechamento por `morph-spec finish` (que absorveu o antigo `worktree finish`).",
|
|
191
204
|
"properties": {
|
|
192
205
|
"path": { "type": "string" },
|
|
193
206
|
"branch": { "type": "string" },
|