@polymorphism-tech/morph-spec 7.2.0 → 7.4.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 (74) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +4 -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 +2 -2
  8. package/framework/MORPH.md +29 -12
  9. package/framework/agents.json +132 -132
  10. package/framework/commands/morph-apply.md +1 -1
  11. package/framework/commands/morph-proposal.md +1 -1
  12. package/framework/evals/rubrics/README.md +9 -8
  13. package/framework/hooks/README.md +3 -1
  14. package/framework/hooks/claude-code/core/gate-guard.js +160 -0
  15. package/framework/hooks/claude-code/core/state-sync.js +15 -3
  16. package/framework/hooks/claude-code/core/validate-completion.js +241 -136
  17. package/framework/hooks/shared/state-reader.js +236 -221
  18. package/framework/phases.json +1 -1
  19. package/framework/skills/README.md +2 -2
  20. package/framework/skills/core/morph-apply/SKILL.md +63 -21
  21. package/framework/skills/core/morph-brainstorming/SKILL.md +5 -1
  22. package/framework/skills/core/morph-eval/SKILL.md +4 -5
  23. package/framework/skills/core/morph-implement/SKILL.md +47 -19
  24. package/framework/skills/core/morph-plan/SKILL.md +8 -2
  25. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  26. package/framework/skills/core/morph-review/SKILL.md +13 -6
  27. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  28. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  29. package/framework/standards/ai-agents/setup.md +258 -244
  30. package/framework/templates/README.md +708 -709
  31. package/framework/templates/REGISTRY.json +1 -20
  32. package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
  33. package/framework/templates/docs/proposal.md +171 -171
  34. package/framework/templates/feature/recap.md +145 -145
  35. package/package.json +1 -1
  36. package/src/commands/project/doctor.js +941 -946
  37. package/src/commands/project/init.js +517 -515
  38. package/src/commands/project/status.js +373 -376
  39. package/src/commands/state/approve.js +251 -213
  40. package/src/commands/validation/validate-feature.js +356 -287
  41. package/src/core/state/state-manager.js +910 -934
  42. package/src/dashboard/app/App.jsx +29 -31
  43. package/src/dashboard/app/components/Sidebar.jsx +44 -45
  44. package/src/dashboard/scanner/index.js +20 -21
  45. package/src/lib/context/context-builder.js +3 -3
  46. package/src/lib/generators/context-generator.js +2 -3
  47. package/src/lib/generators/mandate-generator.js +48 -92
  48. package/src/lib/generators/metadata-extractor.js +385 -386
  49. package/src/lib/generators/recap-generator.js +285 -285
  50. package/src/lib/risk-detector.js +2 -2
  51. package/src/lib/validators/content/content-validator.js +89 -286
  52. package/src/lib/validators/content/index.js +0 -1
  53. package/src/utils/hooks-installer.js +527 -516
  54. package/framework/templates/feature/tasks.md +0 -199
  55. package/framework/workflows/README.md +0 -1041
  56. package/framework/workflows/configs/fast-track.json +0 -46
  57. package/framework/workflows/configs/full.json +0 -64
  58. package/framework/workflows/configs/quick.json +0 -32
  59. package/framework/workflows/configs/standard.json +0 -57
  60. package/framework/workflows/docs/STORY-DRIVEN-DEVELOPMENT.md +0 -392
  61. package/framework/workflows/docs/design-impl.md +0 -37
  62. package/framework/workflows/docs/enforcement-pipeline.md +0 -668
  63. package/framework/workflows/docs/fast-track.md +0 -29
  64. package/framework/workflows/docs/full-morph.md +0 -69
  65. package/framework/workflows/docs/standard.md +0 -49
  66. package/framework/workflows/docs/ui-refresh.md +0 -39
  67. package/src/commands/state/index.js +0 -7
  68. package/src/core/workflows/workflow-detector.js +0 -586
  69. package/src/core/workflows/workflow-router.js +0 -204
  70. package/src/dashboard/app/pages/Workflows.jsx +0 -346
  71. package/src/dashboard/scanner/workflows-scanner.js +0 -18
  72. package/src/lib/phase-chain/phase-validator.js +0 -359
  73. package/src/lib/validators/content/cross-phase-validator.js +0 -281
  74. 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;