@polymorphism-tech/morph-spec 7.3.0 → 7.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +22 -2
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +10 -5
  8. package/framework/MORPH.md +31 -14
  9. package/framework/agents.json +132 -132
  10. package/framework/commands/create-umbraco-lp.md +35 -0
  11. package/framework/commands/migrate-lp-to-umbraco.md +33 -0
  12. package/framework/commands/morph-archive.md +17 -27
  13. package/framework/evals/rubrics/README.md +9 -8
  14. package/framework/hooks/README.md +2 -0
  15. package/framework/hooks/claude-code/core/enforce-phase-writes.js +115 -90
  16. package/framework/hooks/claude-code/core/gate-guard.js +160 -0
  17. package/framework/hooks/claude-code/core/protect-spec-files.js +32 -30
  18. package/framework/hooks/claude-code/core/state-sync.js +16 -36
  19. package/framework/hooks/claude-code/core/validate-completion.js +105 -0
  20. package/framework/hooks/shared/state-reader.js +302 -221
  21. package/framework/hooks/shared/state-writer.js +124 -0
  22. package/framework/rules/morph-task-tracking.md +69 -49
  23. package/framework/rules/morph-workflow.md +192 -191
  24. package/framework/skills/README.md +1 -1
  25. package/framework/skills/core/create-umbraco-lp/SKILL.md +252 -0
  26. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +452 -0
  27. package/framework/skills/core/morph-apply/SKILL.md +69 -27
  28. package/framework/skills/core/morph-brainstorming/SKILL.md +6 -2
  29. package/framework/skills/core/morph-eval/SKILL.md +4 -5
  30. package/framework/skills/core/morph-implement/SKILL.md +51 -22
  31. package/framework/skills/core/morph-plan/SKILL.md +10 -4
  32. package/framework/skills/core/morph-proposal/SKILL.md +8 -7
  33. package/framework/skills/core/morph-review/SKILL.md +15 -8
  34. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  35. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  36. package/framework/standards/ai-agents/setup.md +258 -244
  37. package/framework/templates/REGISTRY.json +1 -3
  38. package/framework/templates/context/CONTEXT-FEATURE.md +10 -17
  39. package/framework/templates/context/CONTEXT.md +181 -180
  40. package/framework/templates/docs/onboarding.md +122 -118
  41. package/framework/templates/docs/proposal.md +10 -0
  42. package/framework/templates/docs/spec.md +228 -228
  43. package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +80 -80
  44. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +90 -89
  45. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +2 -1
  46. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +44 -43
  47. package/framework/templates/meta-prompts/validators/checkpoint-validator.md +107 -107
  48. package/package.json +1 -1
  49. package/src/commands/project/init.js +517 -515
  50. package/src/commands/project/update.js +415 -368
  51. package/src/commands/project/worktree.js +212 -210
  52. package/src/commands/state/advance.js +82 -0
  53. package/src/commands/state/approve.js +251 -213
  54. package/src/commands/state/archive.js +111 -0
  55. package/src/commands/validation/validate-feature.js +244 -79
  56. package/src/core/state/state-manager.js +256 -74
  57. package/src/dashboard/scanner/state-scanner.js +35 -8
  58. package/src/lib/generators/context-generator.js +12 -8
  59. package/src/lib/risk-detector.js +2 -2
  60. package/src/utils/file-copier.js +7 -0
  61. package/src/utils/hooks-installer.js +527 -516
@@ -1,213 +1,251 @@
1
- import { Command } from 'commander';
2
- import chalk from 'chalk';
3
- import { getFeature, setApprovalGate, getApprovalGate, getApprovalHistory } from '../../core/state/state-manager.js';
4
-
5
- /**
6
- * Unapprove action function - Revoke a previously approved gate
7
- */
8
- export async function unapproveCommand(featureName, gateName, options = {}) {
9
- try {
10
- if (!VALID_GATES.includes(gateName)) {
11
- console.error(chalk.red(`\n❌ Invalid gate: ${gateName}`));
12
- console.log(chalk.gray(`Valid gates: ${VALID_GATES.join(', ')}\n`));
13
- process.exit(1);
14
- }
15
-
16
- const feature = getFeature(featureName);
17
- if (!feature) {
18
- console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
19
- process.exit(1);
20
- }
21
-
22
- const currentStatus = getApprovalGate(featureName, gateName);
23
- if (!currentStatus?.approved) {
24
- console.log(chalk.yellow(`\n⚠️ Gate "${gateName}" is not currently approved — nothing to revoke\n`));
25
- return;
26
- }
27
-
28
- await setApprovalGate(featureName, gateName, false, {
29
- revokedBy: options.revoker || process.env.USER || process.env.USERNAME || 'user',
30
- revokedAt: new Date().toISOString(),
31
- reason: options.reason
32
- });
33
-
34
- console.log(chalk.yellow(`\n↩️ Gate "${gateName}" approval revoked for feature: ${featureName}`));
35
- console.log(chalk.gray(`Workflow is now blocked at the ${gateName} gate\n`));
36
-
37
- } catch (error) {
38
- console.error(chalk.red(`\n❌ Error revoking gate: ${error.message}\n`));
39
- process.exit(1);
40
- }
41
- }
42
-
43
- /**
44
- * Approval Commands - Manage approval gates for MORPH workflow
45
- *
46
- * Commands:
47
- * - approve: Mark a gate as approved
48
- * - unapprove: Revoke a gate approval
49
- * - approval-status: Show approval status for all gates
50
- */
51
-
52
- const program = new Command();
53
-
54
- /**
55
- * Valid approval gate names
56
- */
57
- const VALID_GATES = ['proposal', 'uiux', 'plan', 'review'];
58
-
59
- /**
60
- * Approve action function - Mark a gate as approved
61
- */
62
- export async function approveCommand(featureName, gateName, options = {}) {
63
- try {
64
- // Validate gate name
65
- if (!VALID_GATES.includes(gateName)) {
66
- console.error(chalk.red(`\n❌ Invalid gate: ${gateName}`));
67
- console.log(chalk.gray(`Valid gates: ${VALID_GATES.join(', ')}\n`));
68
- process.exit(1);
69
- }
70
-
71
- // Get feature state
72
- const feature = getFeature(featureName);
73
- if (!feature) {
74
- console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
75
- process.exit(1);
76
- }
77
-
78
- // Check if already approved
79
- const currentStatus = getApprovalGate(featureName, gateName);
80
- if (currentStatus?.approved) {
81
- console.log(chalk.yellow(`\n⚠️ Gate "${gateName}" is already approved`));
82
- console.log(chalk.gray(`Approved at: ${currentStatus.timestamp}`));
83
- console.log(chalk.gray(`Approved by: ${currentStatus.approvedBy || 'Unknown'}\n`));
84
- return;
85
- }
86
-
87
- // Set approval
88
- await setApprovalGate(featureName, gateName, true, {
89
- approvedBy: options.approver || process.env.USER || process.env.USERNAME || 'user',
90
- approvedAt: new Date().toISOString(),
91
- notes: options.notes
92
- });
93
-
94
- console.log(chalk.green(`\n✅ Gate "${gateName}" approved for feature: ${featureName}`));
95
- console.log(chalk.gray(`Workflow can now proceed past ${gateName} phase\n`));
96
-
97
- } catch (error) {
98
- console.error(chalk.red(`\n❌ Error approving gate: ${error.message}\n`));
99
- process.exit(1);
100
- }
101
- }
102
-
103
- /**
104
- * Approve command - Mark a gate as approved
105
- */
106
- program
107
- .command('approve <feature> <gate>')
108
- .description('Approve a phase gate to allow workflow progression')
109
- .action(approveCommand);
110
-
111
- /**
112
- * Approval status action function - Show all gates and their status
113
- */
114
- export async function approvalStatusCommand(featureName, options = {}) {
115
- try {
116
- // Get feature state
117
- const feature = getFeature(featureName);
118
- if (!feature) {
119
- console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
120
- process.exit(1);
121
- }
122
-
123
- // JSON output
124
- if (options.json) {
125
- const status = {
126
- feature: featureName,
127
- phase: feature.phase,
128
- status: feature.status,
129
- gates: {}
130
- };
131
-
132
- for (const gateName of VALID_GATES) {
133
- status.gates[gateName] = getApprovalGate(featureName, gateName) || { approved: false };
134
- }
135
-
136
- console.log(JSON.stringify(status, null, 2));
137
- return;
138
- }
139
-
140
- console.log(chalk.bold(`\n📋 Approval Status for: ${featureName}`));
141
- console.log('━'.repeat(60));
142
-
143
- // Show current phase
144
- console.log(chalk.cyan(`\nCurrent Phase: ${feature.phase || 'unknown'}`));
145
- console.log(chalk.gray(`Feature Status: ${feature.status || 'unknown'}\n`));
146
-
147
- // Show each gate
148
- console.log(chalk.bold('Approval Gates:'));
149
-
150
- for (const gateName of VALID_GATES) {
151
- const gateStatus = getApprovalGate(featureName, gateName);
152
-
153
- if (!gateStatus) {
154
- console.log(` ${chalk.gray('○')} ${gateName}: ${chalk.gray('Not yet evaluated')}`);
155
- continue;
156
- }
157
-
158
- if (gateStatus.approved) {
159
- console.log(` ${chalk.green('✓')} ${gateName}: ${chalk.green('Approved')}`);
160
- if (gateStatus.timestamp) {
161
- console.log(chalk.gray(` Approved at: ${gateStatus.timestamp}`));
162
- }
163
- if (gateStatus.approvedBy) {
164
- console.log(chalk.gray(` Approved by: ${gateStatus.approvedBy}`));
165
- }
166
- } else {
167
- console.log(` ${chalk.red('✗')} ${gateName}: ${chalk.red('Rejected / Not Approved')}`);
168
- if (gateStatus.reason) {
169
- console.log(chalk.gray(` Reason: ${gateStatus.reason}`));
170
- }
171
- if (gateStatus.timestamp) {
172
- console.log(chalk.gray(` Rejected at: ${gateStatus.timestamp}`));
173
- }
174
- }
175
- }
176
-
177
- // Show approval history if available
178
- const history = getApprovalHistory(featureName);
179
- if (history && history.length > 0) {
180
- console.log(chalk.bold('\nApproval History:'));
181
- history.slice(-5).forEach(entry => {
182
- const status = entry.approved ? chalk.green('✓ Approved') : chalk.red('✗ Rejected');
183
- console.log(` ${status} ${entry.gate} - ${entry.timestamp}`);
184
- });
185
- }
186
-
187
- console.log('\n' + '━'.repeat(60) + '\n');
188
-
189
- } catch (error) {
190
- console.error(chalk.red(`\n❌ Error showing approval status: ${error.message}\n`));
191
- process.exit(1);
192
- }
193
- }
194
-
195
- /**
196
- * Approval status command - Show all gates and their status
197
- */
198
- program
199
- .command('approval-status <feature>')
200
- .alias('status')
201
- .description('Show approval status for all gates')
202
- .action(approvalStatusCommand);
203
-
204
- // Only parse if run directly (not imported as module)
205
- if (import.meta.url === `file://${process.argv[1]}`) {
206
- if (process.argv.length > 2) {
207
- program.parse(process.argv);
208
- } else {
209
- program.help();
210
- }
211
- }
212
-
213
- export default program;
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { getFeature, setApprovalGate, getApprovalGate, getApprovalHistory } from '../../core/state/state-manager.js';
4
+
5
+ /**
6
+ * Unapprove action function - Revoke a previously approved gate
7
+ */
8
+ export async function unapproveCommand(featureName, gateName, options = {}) {
9
+ try {
10
+ if (!VALID_GATES.includes(gateName)) {
11
+ console.error(chalk.red(`\n❌ Invalid gate: ${gateName}`));
12
+ console.log(chalk.gray(`Valid gates: ${VALID_GATES.join(', ')}\n`));
13
+ process.exit(1);
14
+ }
15
+
16
+ const feature = getFeature(featureName);
17
+ if (!feature) {
18
+ console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
19
+ process.exit(1);
20
+ }
21
+
22
+ const currentStatus = getApprovalGate(featureName, gateName);
23
+ if (!currentStatus?.approved) {
24
+ console.log(chalk.yellow(`\n⚠️ Gate "${gateName}" is not currently approved — nothing to revoke\n`));
25
+ return;
26
+ }
27
+
28
+ // Same honesty rule as approveCommand: don't pretend a human revoked
29
+ // the gate when there's no interactive shell behind the invocation.
30
+ let revokedBy;
31
+ if (options.revoker) {
32
+ revokedBy = options.revoker;
33
+ } else if (options.mode === 'auto') {
34
+ revokedBy = 'auto';
35
+ } else if (process.stdin.isTTY) {
36
+ revokedBy = process.env.USER || process.env.USERNAME || 'user';
37
+ } else {
38
+ revokedBy = 'auto';
39
+ }
40
+
41
+ await setApprovalGate(featureName, gateName, false, {
42
+ revokedBy,
43
+ revokedAt: new Date().toISOString(),
44
+ reason: options.reason
45
+ });
46
+
47
+ console.log(chalk.yellow(`\n↩️ Gate "${gateName}" approval revoked for feature: ${featureName}`));
48
+ console.log(chalk.gray(`Workflow is now blocked at the ${gateName} gate\n`));
49
+
50
+ } catch (error) {
51
+ console.error(chalk.red(`\n❌ Error revoking gate: ${error.message}\n`));
52
+ process.exit(1);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Approval Commands - Manage approval gates for MORPH workflow
58
+ *
59
+ * Commands:
60
+ * - approve: Mark a gate as approved
61
+ * - unapprove: Revoke a gate approval
62
+ * - approval-status: Show approval status for all gates
63
+ */
64
+
65
+ const program = new Command();
66
+
67
+ /**
68
+ * Valid approval gate names
69
+ */
70
+ const VALID_GATES = ['proposal', 'uiux', 'plan', 'review'];
71
+
72
+ /**
73
+ * Approve action function - Mark a gate as approved
74
+ */
75
+ export async function approveCommand(featureName, gateName, options = {}) {
76
+ try {
77
+ // Validate gate name
78
+ if (!VALID_GATES.includes(gateName)) {
79
+ console.error(chalk.red(`\n❌ Invalid gate: ${gateName}`));
80
+ console.log(chalk.gray(`Valid gates: ${VALID_GATES.join(', ')}\n`));
81
+ process.exit(1);
82
+ }
83
+
84
+ // Get feature state
85
+ const feature = getFeature(featureName);
86
+ if (!feature) {
87
+ console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
88
+ process.exit(1);
89
+ }
90
+
91
+ // Check if already approved
92
+ const currentStatus = getApprovalGate(featureName, gateName);
93
+ if (currentStatus?.approved) {
94
+ console.log(chalk.yellow(`\n⚠️ Gate "${gateName}" is already approved`));
95
+ console.log(chalk.gray(`Approved at: ${currentStatus.timestamp}`));
96
+ console.log(chalk.gray(`Approved by: ${currentStatus.approvedBy || 'Unknown'}\n`));
97
+ return;
98
+ }
99
+
100
+ // Set approval.
101
+ //
102
+ // approvedBy resolution (most explicit wins):
103
+ // 1. --approver flag (CLI caller signalled who approved)
104
+ // 2. --mode auto → "auto" (intentionally non-human signature)
105
+ // 3. interactive shell (TTY on stdin) → $USER / $USERNAME (a real
106
+ // human is at the terminal; their name is the honest answer)
107
+ // 4. non-interactive (no TTY) → "auto"
108
+ //
109
+ // The TTY check exists because morph-spec is increasingly invoked
110
+ // from headless contexts (claude -p subprocesses, CI, the
111
+ // morph-e2e-tester). Stamping the host's $USERNAME on a gate that
112
+ // no human actually approved is misleading — it lies to the audit
113
+ // trail. "auto" reads as what it is: a deterministic policy gate,
114
+ // not a human signing off.
115
+ let approvedBy;
116
+ if (options.approver) {
117
+ approvedBy = options.approver;
118
+ } else if (options.mode === 'auto') {
119
+ approvedBy = 'auto';
120
+ } else if (process.stdin.isTTY) {
121
+ approvedBy = process.env.USER || process.env.USERNAME || 'user';
122
+ } else {
123
+ approvedBy = 'auto';
124
+ }
125
+
126
+ await setApprovalGate(featureName, gateName, true, {
127
+ approvedBy,
128
+ approvedAt: new Date().toISOString(),
129
+ notes: options.notes
130
+ });
131
+
132
+ console.log(chalk.green(`\n✅ Gate "${gateName}" approved for feature: ${featureName}`));
133
+ console.log(chalk.gray(`Workflow can now proceed past ${gateName} phase\n`));
134
+
135
+ } catch (error) {
136
+ console.error(chalk.red(`\n❌ Error approving gate: ${error.message}\n`));
137
+ process.exit(1);
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Approve command - Mark a gate as approved
143
+ */
144
+ program
145
+ .command('approve <feature> <gate>')
146
+ .description('Approve a phase gate to allow workflow progression')
147
+ .action(approveCommand);
148
+
149
+ /**
150
+ * Approval status action function - Show all gates and their status
151
+ */
152
+ export async function approvalStatusCommand(featureName, options = {}) {
153
+ try {
154
+ // Get feature state
155
+ const feature = getFeature(featureName);
156
+ if (!feature) {
157
+ console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
158
+ process.exit(1);
159
+ }
160
+
161
+ // JSON output
162
+ if (options.json) {
163
+ const status = {
164
+ feature: featureName,
165
+ phase: feature.phase,
166
+ status: feature.status,
167
+ gates: {}
168
+ };
169
+
170
+ for (const gateName of VALID_GATES) {
171
+ status.gates[gateName] = getApprovalGate(featureName, gateName) || { approved: false };
172
+ }
173
+
174
+ console.log(JSON.stringify(status, null, 2));
175
+ return;
176
+ }
177
+
178
+ console.log(chalk.bold(`\n📋 Approval Status for: ${featureName}`));
179
+ console.log('━'.repeat(60));
180
+
181
+ // Show current phase
182
+ console.log(chalk.cyan(`\nCurrent Phase: ${feature.phase || 'unknown'}`));
183
+ console.log(chalk.gray(`Feature Status: ${feature.status || 'unknown'}\n`));
184
+
185
+ // Show each gate
186
+ console.log(chalk.bold('Approval Gates:'));
187
+
188
+ for (const gateName of VALID_GATES) {
189
+ const gateStatus = getApprovalGate(featureName, gateName);
190
+
191
+ if (!gateStatus) {
192
+ console.log(` ${chalk.gray('○')} ${gateName}: ${chalk.gray('Not yet evaluated')}`);
193
+ continue;
194
+ }
195
+
196
+ if (gateStatus.approved) {
197
+ console.log(` ${chalk.green('✓')} ${gateName}: ${chalk.green('Approved')}`);
198
+ if (gateStatus.timestamp) {
199
+ console.log(chalk.gray(` Approved at: ${gateStatus.timestamp}`));
200
+ }
201
+ if (gateStatus.approvedBy) {
202
+ console.log(chalk.gray(` Approved by: ${gateStatus.approvedBy}`));
203
+ }
204
+ } else {
205
+ console.log(` ${chalk.red('✗')} ${gateName}: ${chalk.red('Rejected / Not Approved')}`);
206
+ if (gateStatus.reason) {
207
+ console.log(chalk.gray(` Reason: ${gateStatus.reason}`));
208
+ }
209
+ if (gateStatus.timestamp) {
210
+ console.log(chalk.gray(` Rejected at: ${gateStatus.timestamp}`));
211
+ }
212
+ }
213
+ }
214
+
215
+ // Show approval history if available
216
+ const history = getApprovalHistory(featureName);
217
+ if (history && history.length > 0) {
218
+ console.log(chalk.bold('\nApproval History:'));
219
+ history.slice(-5).forEach(entry => {
220
+ const status = entry.approved ? chalk.green('✓ Approved') : chalk.red('✗ Rejected');
221
+ console.log(` ${status} ${entry.gate} - ${entry.timestamp}`);
222
+ });
223
+ }
224
+
225
+ console.log('\n' + '━'.repeat(60) + '\n');
226
+
227
+ } catch (error) {
228
+ console.error(chalk.red(`\n❌ Error showing approval status: ${error.message}\n`));
229
+ process.exit(1);
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Approval status command - Show all gates and their status
235
+ */
236
+ program
237
+ .command('approval-status <feature>')
238
+ .alias('status')
239
+ .description('Show approval status for all gates')
240
+ .action(approvalStatusCommand);
241
+
242
+ // Only parse if run directly (not imported as module)
243
+ if (import.meta.url === `file://${process.argv[1]}`) {
244
+ if (process.argv.length > 2) {
245
+ program.parse(process.argv);
246
+ } else {
247
+ program.help();
248
+ }
249
+ }
250
+
251
+ export default program;
@@ -0,0 +1,111 @@
1
+ import chalk from 'chalk';
2
+ import { existsSync, renameSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { execSync } from 'child_process';
5
+ import { getFeature, getApprovalGate } from '../../core/state/state-manager.js';
6
+
7
+ /**
8
+ * `morph-spec archive <feature>` — atomic, git-aware archive of a completed
9
+ * feature. Replaces the manual `/morph-archive` mv: it stamps the feature
10
+ * completed, moves `.morph/features/{name}/` → `.morph/archive/{name}/`
11
+ * (staging the move in git when possible), and cleans the thin root index.
12
+ *
13
+ * Taxonomy: archived = `status: "completed"` + `archivedAt` (per the project
14
+ * decision — no separate "archived" status value).
15
+ */
16
+
17
+ function isGitRepo(cwd) {
18
+ try {
19
+ execSync('git rev-parse --is-inside-work-tree', { cwd, stdio: 'pipe' });
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ export async function archiveCommand(featureName, options = {}) {
27
+ try {
28
+ const feature = getFeature(featureName);
29
+ if (!feature) {
30
+ console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
31
+ process.exit(1);
32
+ }
33
+
34
+ // Precondition: Gate 3 (review) approved — archiving is for completed work.
35
+ const reviewGate = getApprovalGate(featureName, 'review');
36
+ if (!reviewGate?.approved && !options.force) {
37
+ console.error(chalk.red(`\n❌ Gate 3 (review) is not approved for "${featureName}".`));
38
+ console.log(chalk.gray(`Approve it first (morph-spec approve ${featureName} review) or pass --force.\n`));
39
+ process.exit(1);
40
+ }
41
+
42
+ const cwd = process.cwd();
43
+ const featuresPath = join(cwd, '.morph', 'features', featureName);
44
+ if (!existsSync(featuresPath)) {
45
+ console.error(chalk.red(`\n❌ Feature folder not found: .morph/features/${featureName}\n`));
46
+ process.exit(1);
47
+ }
48
+ const archiveDir = join(cwd, '.morph', 'archive');
49
+ const archivePath = join(archiveDir, featureName);
50
+ if (existsSync(archivePath)) {
51
+ console.error(chalk.red(`\n❌ Already archived: .morph/archive/${featureName}\n`));
52
+ process.exit(1);
53
+ }
54
+
55
+ // 1) Stamp the authoritative feature.json (in place) as completed + archived.
56
+ const fjPath = join(featuresPath, 'feature.json');
57
+ if (existsSync(fjPath)) {
58
+ const fj = JSON.parse(readFileSync(fjPath, 'utf8'));
59
+ fj.status = 'completed';
60
+ fj.archivedAt = new Date().toISOString();
61
+ fj.updatedAt = fj.archivedAt;
62
+ writeFileSync(fjPath, JSON.stringify(fj, null, 2), 'utf8');
63
+ }
64
+
65
+ // 2) Move the folder. Prefer `git mv` (keeps history + stages in one step);
66
+ // fall back to a filesystem rename (+ git add) when the folder isn't
67
+ // tracked yet or git mv fails.
68
+ mkdirSync(archiveDir, { recursive: true });
69
+ const useGit = isGitRepo(cwd) && options.git !== false;
70
+ let staged = false;
71
+ if (useGit) {
72
+ try {
73
+ execSync(`git mv "${featuresPath}" "${archivePath}"`, { cwd, stdio: 'pipe' });
74
+ staged = true;
75
+ } catch {
76
+ renameSync(featuresPath, archivePath);
77
+ try {
78
+ execSync(`git add -A "${featuresPath}" "${archivePath}"`, { cwd, stdio: 'pipe' });
79
+ staged = true;
80
+ } catch { /* not fatal — the move succeeded */ }
81
+ }
82
+ } else {
83
+ renameSync(featuresPath, archivePath);
84
+ }
85
+
86
+ // 3) Drop the feature from the thin root index. We edit the index file
87
+ // directly rather than calling saveState — saveState would re-create
88
+ // .morph/features/{name}/feature.json and undo the archive.
89
+ const rootPath = join(cwd, '.morph', 'state.json');
90
+ if (existsSync(rootPath)) {
91
+ try {
92
+ const root = JSON.parse(readFileSync(rootPath, 'utf8'));
93
+ if (root.features && root.features[featureName]) {
94
+ delete root.features[featureName];
95
+ root.metadata = root.metadata || {};
96
+ root.metadata.totalFeatures = Object.keys(root.features).length;
97
+ writeFileSync(rootPath, JSON.stringify(root, null, 2), 'utf8');
98
+ }
99
+ } catch { /* index is derivable — non-fatal */ }
100
+ }
101
+
102
+ console.log(chalk.green(`\n📦 Archived "${featureName}" → .morph/archive/${featureName}/`));
103
+ console.log(chalk.gray(` status: completed, archivedAt stamped in feature.json.`));
104
+ if (staged) console.log(chalk.gray(` Move staged in git.\n`));
105
+ else if (useGit) console.log(chalk.gray(` Move complete; stage it manually if needed.\n`));
106
+ else console.log('');
107
+ } catch (error) {
108
+ console.error(chalk.red(`\n❌ Error archiving feature: ${error.message}\n`));
109
+ process.exit(1);
110
+ }
111
+ }