@polymorphism-tech/morph-spec 7.2.0 → 7.3.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/README.md +2 -2
  2. package/claude-plugin.json +1 -1
  3. package/docs/CHEATSHEET.md +1 -1
  4. package/docs/QUICKSTART.md +1 -1
  5. package/framework/CLAUDE.md +1 -1
  6. package/framework/commands/morph-apply.md +1 -1
  7. package/framework/commands/morph-proposal.md +1 -1
  8. package/framework/hooks/README.md +1 -1
  9. package/framework/hooks/claude-code/core/validate-completion.js +136 -136
  10. package/framework/phases.json +1 -1
  11. package/framework/skills/README.md +1 -1
  12. package/framework/skills/core/morph-apply/SKILL.md +1 -1
  13. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  14. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  15. package/framework/skills/core/morph-implement/SKILL.md +1 -1
  16. package/framework/skills/core/morph-plan/SKILL.md +1 -1
  17. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  18. package/framework/skills/core/morph-review/SKILL.md +1 -1
  19. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  20. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  21. package/framework/templates/README.md +708 -709
  22. package/framework/templates/REGISTRY.json +1 -20
  23. package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
  24. package/framework/templates/docs/proposal.md +171 -171
  25. package/framework/templates/feature/recap.md +145 -145
  26. package/package.json +1 -1
  27. package/src/commands/project/doctor.js +941 -946
  28. package/src/commands/project/status.js +373 -376
  29. package/src/commands/validation/validate-feature.js +315 -287
  30. package/src/core/state/state-manager.js +900 -934
  31. package/src/dashboard/app/App.jsx +29 -31
  32. package/src/dashboard/app/components/Sidebar.jsx +44 -45
  33. package/src/dashboard/scanner/index.js +20 -21
  34. package/src/lib/context/context-builder.js +3 -3
  35. package/src/lib/generators/context-generator.js +2 -3
  36. package/src/lib/generators/mandate-generator.js +48 -92
  37. package/src/lib/generators/metadata-extractor.js +385 -386
  38. package/src/lib/generators/recap-generator.js +285 -285
  39. package/src/lib/validators/content/content-validator.js +89 -286
  40. package/src/lib/validators/content/index.js +0 -1
  41. package/framework/templates/feature/tasks.md +0 -199
  42. package/framework/workflows/README.md +0 -1041
  43. package/framework/workflows/configs/fast-track.json +0 -46
  44. package/framework/workflows/configs/full.json +0 -64
  45. package/framework/workflows/configs/quick.json +0 -32
  46. package/framework/workflows/configs/standard.json +0 -57
  47. package/framework/workflows/docs/STORY-DRIVEN-DEVELOPMENT.md +0 -392
  48. package/framework/workflows/docs/design-impl.md +0 -37
  49. package/framework/workflows/docs/enforcement-pipeline.md +0 -668
  50. package/framework/workflows/docs/fast-track.md +0 -29
  51. package/framework/workflows/docs/full-morph.md +0 -69
  52. package/framework/workflows/docs/standard.md +0 -49
  53. package/framework/workflows/docs/ui-refresh.md +0 -39
  54. package/src/commands/state/index.js +0 -7
  55. package/src/core/workflows/workflow-detector.js +0 -586
  56. package/src/core/workflows/workflow-router.js +0 -204
  57. package/src/dashboard/app/pages/Workflows.jsx +0 -346
  58. package/src/dashboard/scanner/workflows-scanner.js +0 -18
  59. package/src/lib/phase-chain/phase-validator.js +0 -359
  60. package/src/lib/validators/content/cross-phase-validator.js +0 -281
  61. package/src/lib/validators/spec-validator.js +0 -312
@@ -1,376 +1,373 @@
1
- /**
2
- * MORPH-SPEC Feature Status Command
3
- *
4
- * Displays a comprehensive dashboard showing feature progress, phase status,
5
- * outputs, tasks, approvals, and next steps.
6
- *
7
- * Usage:
8
- * morph-spec status <feature-name>
9
- * morph-spec status <feature-name> --json
10
- *
11
- * Example:
12
- * morph-spec status user-portal
13
- */
14
-
15
- import chalk from 'chalk';
16
- import { loadState, getFeature, derivePhase, deriveOutputs } from '../../core/state/state-manager.js';
17
- import { PHASES } from '../../lib/phase-chain/phase-validator.js';
18
-
19
- /**
20
- * Phase symbols for visual representation
21
- */
22
- const PHASE_SYMBOLS = {
23
- completed: '✅',
24
- current: '⏸️',
25
- pending: '⬜',
26
- skipped: '⊘'
27
- };
28
-
29
- /**
30
- * Phase definitions with order
31
- */
32
- const PHASE_ORDER = [
33
- { key: 'proposal', name: 'FASE 0: PROPOSAL', order: 0 },
34
- { key: 'uiux', name: 'FASE 1: UI/UX', order: 1, optional: true },
35
- { key: 'plan', name: 'FASE 2: PLAN', order: 2 },
36
- { key: 'implement', name: 'FASE 3: IMPLEMENT', order: 3 },
37
- { key: 'review', name: 'FASE 4: REVIEW', order: 4 }
38
- ];
39
-
40
- /**
41
- * Calculate phase status (completed, current, pending, skipped)
42
- */
43
- function getPhaseStatus(phaseKey, currentPhase, feature) {
44
- const phase = PHASE_ORDER.find(p => p.key === phaseKey);
45
- const current = PHASE_ORDER.find(p => p.key === currentPhase);
46
-
47
- if (!phase || !current) return 'pending';
48
-
49
- // Check if phase was skipped (optional phases)
50
- if (phase.optional && phase.order < current.order) {
51
- // Check if any outputs were created for this phase
52
- const hasOutputs = checkPhaseOutputs(phaseKey, feature);
53
- if (!hasOutputs) return 'skipped';
54
- }
55
-
56
- if (phase.order < current.order) return 'completed';
57
- if (phase.order === current.order) return 'current';
58
- return 'pending';
59
- }
60
-
61
- /**
62
- * Check if phase has any outputs created
63
- */
64
- function checkPhaseOutputs(phaseKey, feature) {
65
- const phaseOutputMap = {
66
- proposal: ['proposal', 'stackScan', 'migrationPlan'],
67
- uiux: ['uiDesignSystem', 'uiMockups', 'uiComponents', 'uiFlows'],
68
- plan: ['spec', 'mandate', 'tasks', 'decisions'],
69
- implement: ['recap'],
70
- review: ['reviewReport']
71
- };
72
-
73
- const outputs = phaseOutputMap[phaseKey] || [];
74
- return outputs.some(output => feature.outputs[output]?.created);
75
- }
76
-
77
- /**
78
- * Format phase display with symbol and status
79
- */
80
- function formatPhase(phaseKey, currentPhase, feature, options = {}) {
81
- const phase = PHASE_ORDER.find(p => p.key === phaseKey);
82
- const status = getPhaseStatus(phaseKey, currentPhase, feature);
83
- const symbol = PHASE_SYMBOLS[status];
84
-
85
- let display = `${symbol} ${phase.name}`;
86
-
87
- if (phase.optional) {
88
- display += ` ${chalk.gray('(optional)')}`;
89
- }
90
-
91
- if (status === 'current') {
92
- display = chalk.cyan(display) + chalk.cyan(' ← CURRENT');
93
- } else if (status === 'completed') {
94
- display = chalk.green(display);
95
- } else if (status === 'skipped') {
96
- display = chalk.gray(display + ' (skipped)');
97
- } else {
98
- display = chalk.gray(display);
99
- }
100
-
101
- // Add output files if verbose
102
- if (options.verbose && status !== 'pending' && status !== 'skipped') {
103
- const phaseOutputMap = {
104
- proposal: ['proposal', 'stackScan', 'migrationPlan'],
105
- uiux: ['uiDesignSystem', 'uiMockups', 'uiComponents', 'uiFlows'],
106
- plan: ['spec', 'mandate', 'tasks', 'decisions'],
107
- implement: ['recap'],
108
- review: ['reviewReport']
109
- };
110
-
111
- const outputs = phaseOutputMap[phaseKey] || [];
112
- const created = outputs.filter(o => feature.outputs[o]?.created);
113
- if (created.length > 0) {
114
- display += chalk.gray(`\n Files: ${created.length}/${outputs.length}`);
115
- }
116
- }
117
-
118
- return display;
119
- }
120
-
121
- /**
122
- * Calculate completion percentage
123
- */
124
- function calculateCompletionPercentage(feature) {
125
- const totalPhases = PHASE_ORDER.filter(p => !p.optional).length; // 6 required phases
126
- const completedPhases = PHASE_ORDER.filter(p => {
127
- if (p.optional) return false;
128
- const status = getPhaseStatus(p.key, feature.phase, feature);
129
- return status === 'completed';
130
- }).length;
131
-
132
- return Math.round((completedPhases / totalPhases) * 100);
133
- }
134
-
135
- /**
136
- * Get next steps based on current phase
137
- */
138
- function getNextSteps(feature) {
139
- const steps = [];
140
-
141
- // Check for pending approvals relevant to current phase
142
- const phaseApprovalMap = {
143
- proposal: 'proposal',
144
- uiux: 'uiux',
145
- plan: 'plan',
146
- implement: 'plan',
147
- review: 'review'
148
- };
149
-
150
- const relevantGate = phaseApprovalMap[feature.phase];
151
- const approvalGates = feature.approvalGates || {};
152
-
153
- if (relevantGate && approvalGates[relevantGate] && !approvalGates[relevantGate].approved) {
154
- steps.push({
155
- type: 'approval',
156
- message: `Approve ${relevantGate} to advance`,
157
- command: `morph-spec approve ${feature.name || 'feature'} ${relevantGate}`
158
- });
159
- }
160
-
161
- // Check for missing outputs in current phase
162
- const phaseOutputMap = {
163
- proposal: ['proposal'],
164
- plan: ['spec', 'mandate', 'tasks'],
165
- implement: ['recap'],
166
- review: ['reviewReport']
167
- };
168
-
169
- const requiredOutputs = phaseOutputMap[feature.phase] || [];
170
- const missingOutputs = requiredOutputs.filter(o => !feature.outputs[o]?.created);
171
-
172
- if (missingOutputs.length > 0) {
173
- steps.push({
174
- type: 'output',
175
- message: `Create missing outputs: ${missingOutputs.join(', ')}`,
176
- command: null
177
- });
178
- }
179
-
180
- // Suggest next phase if current is complete
181
- const hasApprovalPending = relevantGate && approvalGates[relevantGate] && !approvalGates[relevantGate].approved;
182
- if (missingOutputs.length === 0 && !hasApprovalPending) {
183
- const currentPhaseIndex = PHASE_ORDER.findIndex(p => p.key === feature.phase);
184
- if (currentPhaseIndex >= 0 && currentPhaseIndex < PHASE_ORDER.length - 1) {
185
- const nextPhase = PHASE_ORDER[currentPhaseIndex + 1];
186
- steps.push({
187
- type: 'advance',
188
- message: `Advance to ${nextPhase.name}`,
189
- command: `morph-spec phase advance ${feature.name || 'feature'}`
190
- });
191
- } else if (currentPhaseIndex === PHASE_ORDER.length - 1) {
192
- steps.push({
193
- type: 'complete',
194
- message: 'Feature complete! Consider archiving.',
195
- command: null
196
- });
197
- }
198
- }
199
-
200
- return steps;
201
- }
202
-
203
- /**
204
- * Estimate remaining time based on tasks
205
- */
206
- function estimateRemainingTime(feature) {
207
- if (!feature.tasks || feature.tasks.total === 0) {
208
- return 'Not estimated';
209
- }
210
-
211
- const remaining = feature.tasks.total - feature.tasks.completed;
212
- const avgHoursPerTask = 2; // Configurable estimate
213
- const hours = remaining * avgHoursPerTask;
214
-
215
- if (hours < 1) return '< 1 hour';
216
- if (hours === 1) return '1 hour';
217
- if (hours < 8) return `${hours} hours`;
218
- const days = Math.ceil(hours / 8);
219
- return `~${days} day${days > 1 ? 's' : ''} (${hours}h)`;
220
- }
221
-
222
- /**
223
- * Main status command
224
- */
225
- export async function statusCommand(featureName, options = {}) {
226
- if (!featureName) {
227
- console.error(chalk.red('✗ Error: Feature name required'));
228
- console.error(chalk.gray('Usage: morph-spec status <feature-name>'));
229
- process.exit(1);
230
- }
231
-
232
- let feature;
233
- try {
234
- feature = getFeature(featureName);
235
- } catch (err) {
236
- console.error(chalk.red(`✗ Error: ${err.message}`));
237
- console.error(chalk.gray('Run \'morph-spec doctor\' to diagnose or fix your state.json'));
238
- process.exit(1);
239
- }
240
- if (!feature) {
241
- console.error(chalk.red(`✗ Error: Feature '${featureName}' not found`));
242
- console.error(chalk.gray(`Run 'morph-spec status' to list known features`));
243
- process.exit(1);
244
- }
245
-
246
- // Derive phase and outputs from filesystem if not stored in state
247
- const { join } = await import('path');
248
- const featurePath = join(process.cwd(), '.morph', 'features', featureName);
249
- if (!feature.phase) {
250
- feature.phase = derivePhase(featurePath);
251
- }
252
- if (!feature.outputs) {
253
- feature.outputs = deriveOutputs(featureName, process.cwd());
254
- }
255
-
256
- // JSON output mode
257
- if (options.json) {
258
- const status = {
259
- feature: featureName,
260
- phase: feature.phase,
261
- status: feature.status,
262
- workflow: feature.workflow,
263
- completion: calculateCompletionPercentage(feature),
264
- phases: PHASE_ORDER.map(p => ({
265
- key: p.key,
266
- name: p.name,
267
- status: getPhaseStatus(p.key, feature.phase, feature),
268
- optional: p.optional || false
269
- })),
270
- outputs: Object.entries(feature.outputs).map(([key, output]) => ({
271
- type: key,
272
- created: output.created,
273
- path: output.path
274
- })),
275
- tasks: feature.tasks || { total: 0, completed: 0, inProgress: 0, pending: 0 },
276
- approvals: feature.approvalGates || {},
277
- nextSteps: getNextSteps(feature),
278
- estimatedTimeRemaining: estimateRemainingTime(feature)
279
- };
280
-
281
- console.log(JSON.stringify(status, null, 2));
282
- return;
283
- }
284
-
285
- // Visual dashboard output
286
- console.log(chalk.cyan('\n┌────────────────────────────────────────────────────────────┐'));
287
- console.log(chalk.cyan('│ MORPH-SPEC STATUS │'));
288
- console.log(chalk.cyan('└────────────────────────────────────────────────────────────┘\n'));
289
-
290
- // Feature info
291
- console.log(chalk.white.bold(`Feature: ${featureName}`));
292
- console.log(chalk.gray(`Status: ${feature.status || 'unknown'}`));
293
- console.log(chalk.gray(`Workflow: ${feature.workflow || 'auto'}`));
294
- console.log(chalk.gray(`Completion: ${calculateCompletionPercentage(feature)}%`));
295
- console.log('');
296
-
297
- // Phase progression
298
- console.log(chalk.white.bold('Phases:'));
299
- console.log(chalk.cyan('├─────────────────────────────────────────────────────────────┤'));
300
- PHASE_ORDER.forEach(phase => {
301
- console.log(`│ ${formatPhase(phase.key, feature.phase, feature, options)}`);
302
- });
303
- console.log(chalk.cyan('└─────────────────────────────────────────────────────────────┘'));
304
- console.log('');
305
-
306
- // Outputs summary
307
- const totalOutputs = Object.keys(feature.outputs).length;
308
- const createdOutputs = Object.values(feature.outputs).filter(o => o.created).length;
309
- console.log(chalk.white.bold(`Outputs: ${createdOutputs}/${totalOutputs} complete`));
310
-
311
- if (options.verbose) {
312
- console.log(chalk.gray('Output files:'));
313
- Object.entries(feature.outputs).forEach(([key, output]) => {
314
- const symbol = output.created ? '✅' : '⬜';
315
- const display = output.created ? chalk.green(key) : chalk.gray(key);
316
- console.log(` ${symbol} ${display}`);
317
- });
318
- }
319
- console.log('');
320
-
321
- // Tasks progress
322
- if (feature.tasks && feature.tasks.total > 0) {
323
- console.log(chalk.white.bold('Tasks Progress:'));
324
- const { total, completed, inProgress, pending } = feature.tasks;
325
- const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
326
-
327
- console.log(` Total: ${total}`);
328
- console.log(chalk.green(` ✓ Completed: ${completed} (${percentage}%)`));
329
- if (inProgress > 0) console.log(chalk.cyan(` ▶ In Progress: ${inProgress}`));
330
- if (pending > 0) console.log(chalk.gray(` ⬜ Pending: ${pending}`));
331
- console.log(` Estimated time remaining: ${estimateRemainingTime(feature)}`);
332
- console.log('');
333
- }
334
-
335
- // Approval gates
336
- if (feature.approvalGates) {
337
- const gates = Object.entries(feature.approvalGates);
338
- const approved = gates.filter(([_, g]) => g.approved).length;
339
- console.log(chalk.white.bold(`Approval Gates: ${approved}/${gates.length} approved`));
340
-
341
- if (options.verbose) {
342
- gates.forEach(([gate, info]) => {
343
- const symbol = info.approved ? '✅' : '⬜';
344
- const display = info.approved ? chalk.green(gate) : chalk.gray(gate);
345
- let extra = '';
346
- if (info.approved && info.approvedBy) {
347
- extra = chalk.gray(` (by ${info.approvedBy})`);
348
- }
349
- console.log(` ${symbol} ${display}${extra}`);
350
- });
351
- }
352
- console.log('');
353
- }
354
-
355
- // Next steps
356
- const nextSteps = getNextSteps(feature);
357
- if (nextSteps.length > 0) {
358
- console.log(chalk.white.bold('Next Steps:'));
359
- nextSteps.forEach((step, index) => {
360
- console.log(chalk.yellow(` ${index + 1}. ${step.message}`));
361
- if (step.command) {
362
- console.log(chalk.gray(` → ${step.command}`));
363
- }
364
- });
365
- console.log('');
366
- } else {
367
- console.log(chalk.green('✓ All tasks complete!\n'));
368
- }
369
-
370
- // Footer
371
- console.log(chalk.gray('─'.repeat(60)));
372
- console.log(chalk.gray('Run with --verbose for detailed output'));
373
- console.log(chalk.gray('Run with --json for JSON output\n'));
374
- }
375
-
376
- export default statusCommand;
1
+ /**
2
+ * MORPH-SPEC Feature Status Command
3
+ *
4
+ * Displays a comprehensive dashboard showing feature progress, phase status,
5
+ * outputs, tasks, approvals, and next steps.
6
+ *
7
+ * Usage:
8
+ * morph-spec status <feature-name>
9
+ * morph-spec status <feature-name> --json
10
+ *
11
+ * Example:
12
+ * morph-spec status user-portal
13
+ */
14
+
15
+ import chalk from 'chalk';
16
+ import { loadState, getFeature, derivePhase, deriveOutputs } from '../../core/state/state-manager.js';
17
+
18
+ /**
19
+ * Phase symbols for visual representation
20
+ */
21
+ const PHASE_SYMBOLS = {
22
+ completed: '✅',
23
+ current: '⏸️',
24
+ pending: '⬜',
25
+ skipped: '⊘'
26
+ };
27
+
28
+ /**
29
+ * Phase definitions with order
30
+ */
31
+ const PHASE_ORDER = [
32
+ { key: 'proposal', name: 'FASE 0: PROPOSAL', order: 0 },
33
+ { key: 'uiux', name: 'FASE 1: UI/UX', order: 1, optional: true },
34
+ { key: 'plan', name: 'FASE 2: PLAN', order: 2 },
35
+ { key: 'implement', name: 'FASE 3: IMPLEMENT', order: 3 },
36
+ { key: 'review', name: 'FASE 4: REVIEW', order: 4 }
37
+ ];
38
+
39
+ /**
40
+ * Calculate phase status (completed, current, pending, skipped)
41
+ */
42
+ function getPhaseStatus(phaseKey, currentPhase, feature) {
43
+ const phase = PHASE_ORDER.find(p => p.key === phaseKey);
44
+ const current = PHASE_ORDER.find(p => p.key === currentPhase);
45
+
46
+ if (!phase || !current) return 'pending';
47
+
48
+ // Check if phase was skipped (optional phases)
49
+ if (phase.optional && phase.order < current.order) {
50
+ // Check if any outputs were created for this phase
51
+ const hasOutputs = checkPhaseOutputs(phaseKey, feature);
52
+ if (!hasOutputs) return 'skipped';
53
+ }
54
+
55
+ if (phase.order < current.order) return 'completed';
56
+ if (phase.order === current.order) return 'current';
57
+ return 'pending';
58
+ }
59
+
60
+ /**
61
+ * Check if phase has any outputs created
62
+ */
63
+ function checkPhaseOutputs(phaseKey, feature) {
64
+ const phaseOutputMap = {
65
+ proposal: ['proposal', 'stackScan', 'migrationPlan'],
66
+ uiux: ['uiDesignSystem', 'uiMockups', 'uiComponents', 'uiFlows'],
67
+ plan: ['spec', 'mandate', 'tasks', 'decisions'],
68
+ implement: ['recap'],
69
+ review: ['reviewReport']
70
+ };
71
+
72
+ const outputs = phaseOutputMap[phaseKey] || [];
73
+ return outputs.some(output => feature.outputs[output]?.created);
74
+ }
75
+
76
+ /**
77
+ * Format phase display with symbol and status
78
+ */
79
+ function formatPhase(phaseKey, currentPhase, feature, options = {}) {
80
+ const phase = PHASE_ORDER.find(p => p.key === phaseKey);
81
+ const status = getPhaseStatus(phaseKey, currentPhase, feature);
82
+ const symbol = PHASE_SYMBOLS[status];
83
+
84
+ let display = `${symbol} ${phase.name}`;
85
+
86
+ if (phase.optional) {
87
+ display += ` ${chalk.gray('(optional)')}`;
88
+ }
89
+
90
+ if (status === 'current') {
91
+ display = chalk.cyan(display) + chalk.cyan(' ← CURRENT');
92
+ } else if (status === 'completed') {
93
+ display = chalk.green(display);
94
+ } else if (status === 'skipped') {
95
+ display = chalk.gray(display + ' (skipped)');
96
+ } else {
97
+ display = chalk.gray(display);
98
+ }
99
+
100
+ // Add output files if verbose
101
+ if (options.verbose && status !== 'pending' && status !== 'skipped') {
102
+ const phaseOutputMap = {
103
+ proposal: ['proposal', 'stackScan', 'migrationPlan'],
104
+ uiux: ['uiDesignSystem', 'uiMockups', 'uiComponents', 'uiFlows'],
105
+ plan: ['spec', 'mandate', 'tasks', 'decisions'],
106
+ implement: ['recap'],
107
+ review: ['reviewReport']
108
+ };
109
+
110
+ const outputs = phaseOutputMap[phaseKey] || [];
111
+ const created = outputs.filter(o => feature.outputs[o]?.created);
112
+ if (created.length > 0) {
113
+ display += chalk.gray(`\n Files: ${created.length}/${outputs.length}`);
114
+ }
115
+ }
116
+
117
+ return display;
118
+ }
119
+
120
+ /**
121
+ * Calculate completion percentage
122
+ */
123
+ function calculateCompletionPercentage(feature) {
124
+ const totalPhases = PHASE_ORDER.filter(p => !p.optional).length; // 6 required phases
125
+ const completedPhases = PHASE_ORDER.filter(p => {
126
+ if (p.optional) return false;
127
+ const status = getPhaseStatus(p.key, feature.phase, feature);
128
+ return status === 'completed';
129
+ }).length;
130
+
131
+ return Math.round((completedPhases / totalPhases) * 100);
132
+ }
133
+
134
+ /**
135
+ * Get next steps based on current phase
136
+ */
137
+ function getNextSteps(feature) {
138
+ const steps = [];
139
+
140
+ // Check for pending approvals relevant to current phase
141
+ const phaseApprovalMap = {
142
+ proposal: 'proposal',
143
+ uiux: 'uiux',
144
+ plan: 'plan',
145
+ implement: 'plan',
146
+ review: 'review'
147
+ };
148
+
149
+ const relevantGate = phaseApprovalMap[feature.phase];
150
+ const approvalGates = feature.approvalGates || {};
151
+
152
+ if (relevantGate && approvalGates[relevantGate] && !approvalGates[relevantGate].approved) {
153
+ steps.push({
154
+ type: 'approval',
155
+ message: `Approve ${relevantGate} to advance`,
156
+ command: `morph-spec approve ${feature.name || 'feature'} ${relevantGate}`
157
+ });
158
+ }
159
+
160
+ // Check for missing outputs in current phase
161
+ const phaseOutputMap = {
162
+ proposal: ['proposal'],
163
+ plan: ['spec', 'mandate', 'tasks'],
164
+ implement: ['recap'],
165
+ review: ['reviewReport']
166
+ };
167
+
168
+ const requiredOutputs = phaseOutputMap[feature.phase] || [];
169
+ const missingOutputs = requiredOutputs.filter(o => !feature.outputs[o]?.created);
170
+
171
+ if (missingOutputs.length > 0) {
172
+ steps.push({
173
+ type: 'output',
174
+ message: `Create missing outputs: ${missingOutputs.join(', ')}`,
175
+ command: null
176
+ });
177
+ }
178
+
179
+ // Suggest next phase if current is complete
180
+ const hasApprovalPending = relevantGate && approvalGates[relevantGate] && !approvalGates[relevantGate].approved;
181
+ if (missingOutputs.length === 0 && !hasApprovalPending) {
182
+ const currentPhaseIndex = PHASE_ORDER.findIndex(p => p.key === feature.phase);
183
+ if (currentPhaseIndex >= 0 && currentPhaseIndex < PHASE_ORDER.length - 1) {
184
+ const nextPhase = PHASE_ORDER[currentPhaseIndex + 1];
185
+ steps.push({
186
+ type: 'advance',
187
+ message: `Advance to ${nextPhase.name}`,
188
+ command: `morph-spec phase advance ${feature.name || 'feature'}`
189
+ });
190
+ } else if (currentPhaseIndex === PHASE_ORDER.length - 1) {
191
+ steps.push({
192
+ type: 'complete',
193
+ message: 'Feature complete! Consider archiving.',
194
+ command: null
195
+ });
196
+ }
197
+ }
198
+
199
+ return steps;
200
+ }
201
+
202
+ /**
203
+ * Estimate remaining time based on tasks
204
+ */
205
+ function estimateRemainingTime(feature) {
206
+ if (!feature.tasks || feature.tasks.total === 0) {
207
+ return 'Not estimated';
208
+ }
209
+
210
+ const remaining = feature.tasks.total - feature.tasks.completed;
211
+ const avgHoursPerTask = 2; // Configurable estimate
212
+ const hours = remaining * avgHoursPerTask;
213
+
214
+ if (hours < 1) return '< 1 hour';
215
+ if (hours === 1) return '1 hour';
216
+ if (hours < 8) return `${hours} hours`;
217
+ const days = Math.ceil(hours / 8);
218
+ return `~${days} day${days > 1 ? 's' : ''} (${hours}h)`;
219
+ }
220
+
221
+ /**
222
+ * Main status command
223
+ */
224
+ export async function statusCommand(featureName, options = {}) {
225
+ if (!featureName) {
226
+ console.error(chalk.red('✗ Error: Feature name required'));
227
+ console.error(chalk.gray('Usage: morph-spec status <feature-name>'));
228
+ process.exit(1);
229
+ }
230
+
231
+ let feature;
232
+ try {
233
+ feature = getFeature(featureName);
234
+ } catch (err) {
235
+ console.error(chalk.red(`✗ Error: ${err.message}`));
236
+ console.error(chalk.gray('Run \'morph-spec doctor\' to diagnose or fix your state.json'));
237
+ process.exit(1);
238
+ }
239
+ if (!feature) {
240
+ console.error(chalk.red(`✗ Error: Feature '${featureName}' not found`));
241
+ console.error(chalk.gray(`Run 'morph-spec status' to list known features`));
242
+ process.exit(1);
243
+ }
244
+
245
+ // Derive phase and outputs from filesystem if not stored in state
246
+ const { join } = await import('path');
247
+ const featurePath = join(process.cwd(), '.morph', 'features', featureName);
248
+ if (!feature.phase) {
249
+ feature.phase = derivePhase(featurePath);
250
+ }
251
+ if (!feature.outputs) {
252
+ feature.outputs = deriveOutputs(featureName, process.cwd());
253
+ }
254
+
255
+ // JSON output mode
256
+ if (options.json) {
257
+ const status = {
258
+ feature: featureName,
259
+ phase: feature.phase,
260
+ status: feature.status,
261
+ completion: calculateCompletionPercentage(feature),
262
+ phases: PHASE_ORDER.map(p => ({
263
+ key: p.key,
264
+ name: p.name,
265
+ status: getPhaseStatus(p.key, feature.phase, feature),
266
+ optional: p.optional || false
267
+ })),
268
+ outputs: Object.entries(feature.outputs).map(([key, output]) => ({
269
+ type: key,
270
+ created: output.created,
271
+ path: output.path
272
+ })),
273
+ tasks: feature.tasks || { total: 0, completed: 0, inProgress: 0, pending: 0 },
274
+ approvals: feature.approvalGates || {},
275
+ nextSteps: getNextSteps(feature),
276
+ estimatedTimeRemaining: estimateRemainingTime(feature)
277
+ };
278
+
279
+ console.log(JSON.stringify(status, null, 2));
280
+ return;
281
+ }
282
+
283
+ // Visual dashboard output
284
+ console.log(chalk.cyan('\n┌────────────────────────────────────────────────────────────┐'));
285
+ console.log(chalk.cyan('│ MORPH-SPEC STATUS │'));
286
+ console.log(chalk.cyan('└────────────────────────────────────────────────────────────┘\n'));
287
+
288
+ // Feature info
289
+ console.log(chalk.white.bold(`Feature: ${featureName}`));
290
+ console.log(chalk.gray(`Status: ${feature.status || 'unknown'}`));
291
+ console.log(chalk.gray(`Completion: ${calculateCompletionPercentage(feature)}%`));
292
+ console.log('');
293
+
294
+ // Phase progression
295
+ console.log(chalk.white.bold('Phases:'));
296
+ console.log(chalk.cyan('├─────────────────────────────────────────────────────────────┤'));
297
+ PHASE_ORDER.forEach(phase => {
298
+ console.log(`│ ${formatPhase(phase.key, feature.phase, feature, options)}`);
299
+ });
300
+ console.log(chalk.cyan('└─────────────────────────────────────────────────────────────┘'));
301
+ console.log('');
302
+
303
+ // Outputs summary
304
+ const totalOutputs = Object.keys(feature.outputs).length;
305
+ const createdOutputs = Object.values(feature.outputs).filter(o => o.created).length;
306
+ console.log(chalk.white.bold(`Outputs: ${createdOutputs}/${totalOutputs} complete`));
307
+
308
+ if (options.verbose) {
309
+ console.log(chalk.gray('Output files:'));
310
+ Object.entries(feature.outputs).forEach(([key, output]) => {
311
+ const symbol = output.created ? '✅' : '⬜';
312
+ const display = output.created ? chalk.green(key) : chalk.gray(key);
313
+ console.log(` ${symbol} ${display}`);
314
+ });
315
+ }
316
+ console.log('');
317
+
318
+ // Tasks progress
319
+ if (feature.tasks && feature.tasks.total > 0) {
320
+ console.log(chalk.white.bold('Tasks Progress:'));
321
+ const { total, completed, inProgress, pending } = feature.tasks;
322
+ const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
323
+
324
+ console.log(` Total: ${total}`);
325
+ console.log(chalk.green(` ✓ Completed: ${completed} (${percentage}%)`));
326
+ if (inProgress > 0) console.log(chalk.cyan(` ▶ In Progress: ${inProgress}`));
327
+ if (pending > 0) console.log(chalk.gray(` ⬜ Pending: ${pending}`));
328
+ console.log(` Estimated time remaining: ${estimateRemainingTime(feature)}`);
329
+ console.log('');
330
+ }
331
+
332
+ // Approval gates
333
+ if (feature.approvalGates) {
334
+ const gates = Object.entries(feature.approvalGates);
335
+ const approved = gates.filter(([_, g]) => g.approved).length;
336
+ console.log(chalk.white.bold(`Approval Gates: ${approved}/${gates.length} approved`));
337
+
338
+ if (options.verbose) {
339
+ gates.forEach(([gate, info]) => {
340
+ const symbol = info.approved ? '✅' : '⬜';
341
+ const display = info.approved ? chalk.green(gate) : chalk.gray(gate);
342
+ let extra = '';
343
+ if (info.approved && info.approvedBy) {
344
+ extra = chalk.gray(` (by ${info.approvedBy})`);
345
+ }
346
+ console.log(` ${symbol} ${display}${extra}`);
347
+ });
348
+ }
349
+ console.log('');
350
+ }
351
+
352
+ // Next steps
353
+ const nextSteps = getNextSteps(feature);
354
+ if (nextSteps.length > 0) {
355
+ console.log(chalk.white.bold('Next Steps:'));
356
+ nextSteps.forEach((step, index) => {
357
+ console.log(chalk.yellow(` ${index + 1}. ${step.message}`));
358
+ if (step.command) {
359
+ console.log(chalk.gray(` → ${step.command}`));
360
+ }
361
+ });
362
+ console.log('');
363
+ } else {
364
+ console.log(chalk.green('✓ All tasks complete!\n'));
365
+ }
366
+
367
+ // Footer
368
+ console.log(chalk.gray('─'.repeat(60)));
369
+ console.log(chalk.gray('Run with --verbose for detailed output'));
370
+ console.log(chalk.gray('Run with --json for JSON output\n'));
371
+ }
372
+
373
+ export default statusCommand;