musubi-sdd 2.0.7 → 2.1.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.ja.md CHANGED
@@ -4,13 +4,20 @@
4
4
 
5
5
  MUSUBIは、6つの主要フレームワークのベスト機能を統合した包括的なSDD(仕様駆動開発)フレームワークであり、複数のAIコーディングエージェントに対応した本番環境対応ツールです。
6
6
 
7
- ## 🚀 v2.0.0 の新機能
7
+ ## 🚀 v2.1.0 の新機能
8
+
9
+ - 🔄 **ワークフローエンジン** - ステージ管理とメトリクス収集の新CLI `musubi-workflow`
10
+ - 📊 **メトリクス収集** - ステージごとの所要時間、イテレーション回数、フィードバックループを追跡
11
+ - 🔬 **Spike/PoCステージ** - 要件定義前の調査・プロトタイピング用ステージ0
12
+ - 👀 **コードレビューステージ** - 実装とテストの間のステージ5.5
13
+ - 🔄 **振り返りステージ** - 継続的改善のためのステージ9
14
+ - ✅ **ステージ検証ガイド** - ステージ遷移の検証チェックリスト
15
+
16
+ ### 以前のバージョン (v2.0.0)
8
17
 
9
18
  - 🔌 **CodeGraphMCPServer統合** - 14のMCPツールによる高度なコード分析
10
19
  - 🧠 **GraphRAG駆動検索** - Louvainコミュニティ検出によるセマンティックコード理解
11
20
  - 🔍 **11エージェント強化** - 主要エージェントがMCPツールを活用して深いコード分析を実現
12
- - 📊 **依存関係分析** - `find_dependencies`, `find_callers`, `analyze_module_structure`
13
- - 🎯 **スマートコードナビゲーション** - `local_search`, `global_search`, `query_codebase`
14
21
 
15
22
  ## 特徴
16
23
 
package/README.md CHANGED
@@ -8,13 +8,20 @@
8
8
 
9
9
  MUSUBI is a comprehensive SDD (Specification Driven Development) framework that synthesizes the best features from 6 leading frameworks into a production-ready tool for multiple AI coding agents.
10
10
 
11
- ## 🚀 What's New in v2.0.0
11
+ ## 🚀 What's New in v2.1.0
12
+
13
+ - 🔄 **Workflow Engine** - New `musubi-workflow` CLI for stage management and metrics
14
+ - 📊 **Metrics Collection** - Track time per stage, iteration counts, feedback loops
15
+ - 🔬 **Spike/PoC Stage** - Stage 0 for research and prototyping before requirements
16
+ - 👀 **Code Review Stage** - Stage 5.5 between implementation and testing
17
+ - 🔄 **Retrospective Stage** - Stage 9 for continuous improvement
18
+ - ✅ **Stage Validation Guide** - Checklists for stage transition validation
19
+
20
+ ### Previous (v2.0.0)
12
21
 
13
22
  - 🔌 **CodeGraphMCPServer Integration** - 14 MCP tools for enhanced code analysis
14
23
  - 🧠 **GraphRAG-Powered Search** - Semantic code understanding with Louvain community detection
15
24
  - 🔍 **11 Agents Enhanced** - Key agents now leverage MCP tools for deeper code analysis
16
- - 📊 **Dependency Analysis** - `find_dependencies`, `find_callers`, `analyze_module_structure`
17
- - 🎯 **Smart Code Navigation** - `local_search`, `global_search`, `query_codebase`
18
25
 
19
26
  ## Features
20
27
 
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MUSUBI Workflow CLI
5
+ *
6
+ * Manage workflow state, transitions, and metrics.
7
+ *
8
+ * Commands:
9
+ * init <feature> - Initialize workflow for a feature
10
+ * status - Show current workflow status
11
+ * next [stage] - Transition to next stage
12
+ * complete - Complete the workflow
13
+ * metrics - Show workflow metrics summary
14
+ * history - Show workflow history
15
+ */
16
+
17
+ const { program } = require('commander');
18
+ const { WorkflowEngine, WORKFLOW_STAGES } = require('../src/managers/workflow');
19
+ const chalk = require('chalk');
20
+
21
+ const engine = new WorkflowEngine();
22
+
23
+ // Stage icons for visual feedback
24
+ const STAGE_ICONS = {
25
+ spike: '🔬',
26
+ research: '📚',
27
+ requirements: '📋',
28
+ design: '📐',
29
+ tasks: '📝',
30
+ implementation: '💻',
31
+ review: '👀',
32
+ testing: '🧪',
33
+ deployment: '🚀',
34
+ monitoring: '📊',
35
+ retrospective: '🔄'
36
+ };
37
+
38
+ /**
39
+ * Format stage name with icon
40
+ */
41
+ function formatStage(stage) {
42
+ const icon = STAGE_ICONS[stage] || '📌';
43
+ return `${icon} ${stage}`;
44
+ }
45
+
46
+ /**
47
+ * Display workflow status
48
+ */
49
+ async function showStatus() {
50
+ const state = await engine.getState();
51
+
52
+ if (!state) {
53
+ console.log(chalk.yellow('\n⚠️ No active workflow. Use "musubi-workflow init <feature>" to start.'));
54
+ return;
55
+ }
56
+
57
+ console.log(chalk.bold('\n📊 Workflow Status\n'));
58
+ console.log(chalk.white(`Feature: ${chalk.cyan(state.feature)}`));
59
+ console.log(chalk.white(`Current Stage: ${formatStage(state.currentStage)}`));
60
+ console.log(chalk.white(`Started: ${new Date(state.startedAt).toLocaleString()}`));
61
+
62
+ // Show stage progress
63
+ console.log(chalk.bold('\n📈 Stage Progress:\n'));
64
+
65
+ const allStages = Object.keys(WORKFLOW_STAGES);
66
+ const currentIndex = allStages.indexOf(state.currentStage);
67
+
68
+ allStages.forEach((stage, index) => {
69
+ const data = state.stages[stage];
70
+ let status = '';
71
+ let color = chalk.gray;
72
+
73
+ if (data?.status === 'completed') {
74
+ status = `✅ Completed (${data.duration})`;
75
+ color = chalk.green;
76
+ } else if (stage === state.currentStage) {
77
+ status = '🔄 In Progress';
78
+ color = chalk.blue;
79
+ } else if (index < currentIndex) {
80
+ status = '⏭️ Skipped';
81
+ color = chalk.gray;
82
+ } else {
83
+ status = '⏳ Pending';
84
+ color = chalk.gray;
85
+ }
86
+
87
+ console.log(color(` ${formatStage(stage).padEnd(25)} ${status}`));
88
+ });
89
+
90
+ // Show valid transitions
91
+ const validNext = await engine.getValidTransitions();
92
+ if (validNext.length > 0) {
93
+ console.log(chalk.bold('\n🔀 Valid Transitions:'));
94
+ validNext.forEach(stage => {
95
+ console.log(chalk.cyan(` → ${formatStage(stage)}`));
96
+ });
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Display workflow history
102
+ */
103
+ async function showHistory() {
104
+ const state = await engine.getState();
105
+
106
+ if (!state || !state.history) {
107
+ console.log(chalk.yellow('\n⚠️ No workflow history available.'));
108
+ return;
109
+ }
110
+
111
+ console.log(chalk.bold('\n📜 Workflow History\n'));
112
+
113
+ state.history.forEach(event => {
114
+ const time = new Date(event.timestamp).toLocaleString();
115
+ let desc = '';
116
+
117
+ switch (event.action) {
118
+ case 'workflow-started':
119
+ desc = `Started workflow for "${event.feature}" at ${formatStage(event.stage)}`;
120
+ break;
121
+ case 'stage-transition':
122
+ desc = `${formatStage(event.from)} → ${formatStage(event.to)}`;
123
+ if (event.notes) desc += ` (${event.notes})`;
124
+ break;
125
+ case 'feedback-loop':
126
+ desc = `🔄 Feedback: ${formatStage(event.from)} → ${formatStage(event.to)} - ${event.reason}`;
127
+ break;
128
+ case 'workflow-completed':
129
+ desc = `✅ Workflow completed`;
130
+ if (event.notes) desc += ` (${event.notes})`;
131
+ break;
132
+ default:
133
+ desc = event.action;
134
+ }
135
+
136
+ console.log(chalk.white(` ${chalk.gray(time)} ${desc}`));
137
+ });
138
+ }
139
+
140
+ /**
141
+ * Display metrics summary
142
+ */
143
+ async function showMetrics() {
144
+ const summary = await engine.getMetricsSummary();
145
+
146
+ console.log(chalk.bold('\n📊 Workflow Metrics Summary\n'));
147
+
148
+ if (summary.message) {
149
+ console.log(chalk.yellow(` ${summary.message}`));
150
+ return;
151
+ }
152
+
153
+ console.log(chalk.white(` Total Workflows: ${summary.totalWorkflows}`));
154
+ console.log(chalk.white(` Completed: ${summary.completedWorkflows}`));
155
+ console.log(chalk.white(` Stage Transitions: ${summary.stageTransitions}`));
156
+ console.log(chalk.white(` Feedback Loops: ${summary.feedbackLoops}`));
157
+
158
+ if (summary.averageDuration) {
159
+ console.log(chalk.white(` Average Duration: ${summary.averageDuration}`));
160
+ }
161
+
162
+ if (Object.keys(summary.stageStats).length > 0) {
163
+ console.log(chalk.bold('\n📈 Stage Visit Counts:\n'));
164
+ Object.entries(summary.stageStats)
165
+ .sort((a, b) => b[1].visits - a[1].visits)
166
+ .forEach(([stage, data]) => {
167
+ console.log(chalk.white(` ${formatStage(stage).padEnd(25)} ${data.visits} visits`));
168
+ });
169
+ }
170
+ }
171
+
172
+ // CLI Commands
173
+ program
174
+ .name('musubi-workflow')
175
+ .description('MUSUBI Workflow Engine - Manage SDD workflow state and metrics')
176
+ .version('2.0.7');
177
+
178
+ program
179
+ .command('init <feature>')
180
+ .description('Initialize a new workflow for a feature')
181
+ .option('-s, --stage <stage>', 'Starting stage', 'requirements')
182
+ .action(async (feature, options) => {
183
+ try {
184
+ const state = await engine.initWorkflow(feature, { startStage: options.stage });
185
+ console.log(chalk.green(`\n✅ Workflow initialized for "${feature}"`));
186
+ console.log(chalk.cyan(` Starting at: ${formatStage(state.currentStage)}`));
187
+ } catch (error) {
188
+ console.error(chalk.red(`\n❌ Error: ${error.message}`));
189
+ process.exit(1);
190
+ }
191
+ });
192
+
193
+ program
194
+ .command('status')
195
+ .description('Show current workflow status')
196
+ .action(showStatus);
197
+
198
+ program
199
+ .command('next [stage]')
200
+ .description('Transition to the next stage')
201
+ .option('-n, --notes <notes>', 'Transition notes')
202
+ .action(async (stage, options) => {
203
+ try {
204
+ const state = await engine.getState();
205
+ if (!state) {
206
+ console.log(chalk.yellow('\n⚠️ No active workflow.'));
207
+ return;
208
+ }
209
+
210
+ // If no stage specified, show valid options
211
+ if (!stage) {
212
+ const validNext = await engine.getValidTransitions();
213
+ console.log(chalk.bold('\n🔀 Valid next stages:'));
214
+ validNext.forEach(s => console.log(chalk.cyan(` → ${formatStage(s)}`)));
215
+ console.log(chalk.white('\nUse: musubi-workflow next <stage>'));
216
+ return;
217
+ }
218
+
219
+ await engine.transitionTo(stage, options.notes);
220
+ console.log(chalk.green(`\n✅ Transitioned to ${formatStage(stage)}`));
221
+ } catch (error) {
222
+ console.error(chalk.red(`\n❌ Error: ${error.message}`));
223
+ process.exit(1);
224
+ }
225
+ });
226
+
227
+ program
228
+ .command('feedback <from> <to>')
229
+ .description('Record a feedback loop')
230
+ .requiredOption('-r, --reason <reason>', 'Reason for feedback loop')
231
+ .action(async (from, to, options) => {
232
+ try {
233
+ await engine.recordFeedbackLoop(from, to, options.reason);
234
+ await engine.transitionTo(to, `Feedback: ${options.reason}`);
235
+ console.log(chalk.yellow(`\n🔄 Feedback loop recorded: ${formatStage(from)} → ${formatStage(to)}`));
236
+ console.log(chalk.gray(` Reason: ${options.reason}`));
237
+ } catch (error) {
238
+ console.error(chalk.red(`\n❌ Error: ${error.message}`));
239
+ process.exit(1);
240
+ }
241
+ });
242
+
243
+ program
244
+ .command('complete')
245
+ .description('Complete the current workflow')
246
+ .option('-n, --notes <notes>', 'Completion notes')
247
+ .action(async (options) => {
248
+ try {
249
+ const summary = await engine.completeWorkflow(options.notes);
250
+
251
+ console.log(chalk.green('\n✅ Workflow Completed!\n'));
252
+ console.log(chalk.bold('📊 Summary:'));
253
+ console.log(chalk.white(` Feature: ${summary.feature}`));
254
+ console.log(chalk.white(` Total Duration: ${summary.totalDuration}`));
255
+ console.log(chalk.white(` Stages: ${summary.stages.length}`));
256
+ console.log(chalk.white(` Feedback Loops: ${summary.feedbackLoops}`));
257
+
258
+ if (summary.stages.length > 0) {
259
+ console.log(chalk.bold('\n📈 Stage Breakdown:'));
260
+ summary.stages.forEach(s => {
261
+ console.log(chalk.white(` ${formatStage(s.name).padEnd(25)} ${s.duration} (${s.attempts} attempt${s.attempts > 1 ? 's' : ''})`));
262
+ });
263
+ }
264
+ } catch (error) {
265
+ console.error(chalk.red(`\n❌ Error: ${error.message}`));
266
+ process.exit(1);
267
+ }
268
+ });
269
+
270
+ program
271
+ .command('history')
272
+ .description('Show workflow history')
273
+ .action(showHistory);
274
+
275
+ program
276
+ .command('metrics')
277
+ .description('Show workflow metrics summary')
278
+ .action(showMetrics);
279
+
280
+ program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musubi-sdd",
3
- "version": "2.0.7",
3
+ "version": "2.1.0",
4
4
  "description": "Ultimate Specification Driven Development Tool with 25 Agents for 7 AI Coding Platforms + MCP Integration (Claude Code, GitHub Copilot, Cursor, Gemini CLI, Windsurf, Codex, Qwen Code)",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -17,7 +17,8 @@
17
17
  "musubi-tasks": "bin/musubi-tasks.js",
18
18
  "musubi-trace": "bin/musubi-trace.js",
19
19
  "musubi-gaps": "bin/musubi-gaps.js",
20
- "musubi-change": "bin/musubi-change.js"
20
+ "musubi-change": "bin/musubi-change.js",
21
+ "musubi-workflow": "bin/musubi-workflow.js"
21
22
  },
22
23
  "scripts": {
23
24
  "test": "jest",
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Workflow Engine for MUSUBI SDD
3
+ *
4
+ * Manages workflow state, stage transitions, and metrics collection.
5
+ */
6
+
7
+ const fs = require('fs-extra');
8
+ const path = require('path');
9
+ const yaml = require('js-yaml');
10
+
11
+ /**
12
+ * Workflow stages with their valid transitions
13
+ */
14
+ const WORKFLOW_STAGES = {
15
+ spike: { next: ['requirements'], optional: true },
16
+ research: { next: ['requirements'], optional: true },
17
+ requirements: { next: ['design'] },
18
+ design: { next: ['tasks'] },
19
+ tasks: { next: ['implementation'] },
20
+ implementation: { next: ['review'] },
21
+ review: { next: ['testing', 'implementation'] }, // Can go back to implementation
22
+ testing: { next: ['deployment', 'implementation', 'requirements'] }, // Feedback loops
23
+ deployment: { next: ['monitoring'] },
24
+ monitoring: { next: ['retrospective'] },
25
+ retrospective: { next: ['requirements'] } // New iteration
26
+ };
27
+
28
+ /**
29
+ * Workflow state file path
30
+ */
31
+ const WORKFLOW_STATE_FILE = 'storage/workflow-state.yml';
32
+
33
+ /**
34
+ * Metrics file path
35
+ */
36
+ const METRICS_FILE = 'storage/workflow-metrics.yml';
37
+
38
+ class WorkflowEngine {
39
+ constructor(projectRoot = process.cwd()) {
40
+ this.projectRoot = projectRoot;
41
+ this.stateFile = path.join(projectRoot, WORKFLOW_STATE_FILE);
42
+ this.metricsFile = path.join(projectRoot, METRICS_FILE);
43
+ }
44
+
45
+ /**
46
+ * Initialize workflow for a new feature/iteration
47
+ */
48
+ async initWorkflow(featureName, options = {}) {
49
+ const state = {
50
+ feature: featureName,
51
+ currentStage: options.startStage || 'requirements',
52
+ startedAt: new Date().toISOString(),
53
+ stages: {},
54
+ history: []
55
+ };
56
+
57
+ // Record initial stage
58
+ state.stages[state.currentStage] = {
59
+ enteredAt: new Date().toISOString(),
60
+ status: 'in-progress'
61
+ };
62
+
63
+ state.history.push({
64
+ timestamp: new Date().toISOString(),
65
+ action: 'workflow-started',
66
+ stage: state.currentStage,
67
+ feature: featureName
68
+ });
69
+
70
+ await this.saveState(state);
71
+ await this.recordMetric('workflow_started', { feature: featureName });
72
+
73
+ return state;
74
+ }
75
+
76
+ /**
77
+ * Get current workflow state
78
+ */
79
+ async getState() {
80
+ if (!await fs.pathExists(this.stateFile)) {
81
+ return null;
82
+ }
83
+ const content = await fs.readFile(this.stateFile, 'utf8');
84
+ return yaml.load(content);
85
+ }
86
+
87
+ /**
88
+ * Save workflow state
89
+ */
90
+ async saveState(state) {
91
+ await fs.ensureDir(path.dirname(this.stateFile));
92
+ await fs.writeFile(this.stateFile, yaml.dump(state, { indent: 2 }));
93
+ }
94
+
95
+ /**
96
+ * Transition to the next stage
97
+ */
98
+ async transitionTo(targetStage, notes = '') {
99
+ const state = await this.getState();
100
+ if (!state) {
101
+ throw new Error('No active workflow. Run initWorkflow first.');
102
+ }
103
+
104
+ const currentStage = state.currentStage;
105
+ const validTransitions = WORKFLOW_STAGES[currentStage]?.next || [];
106
+
107
+ if (!validTransitions.includes(targetStage)) {
108
+ throw new Error(
109
+ `Invalid transition: ${currentStage} → ${targetStage}. ` +
110
+ `Valid transitions: ${validTransitions.join(', ')}`
111
+ );
112
+ }
113
+
114
+ // Complete current stage
115
+ if (state.stages[currentStage]) {
116
+ state.stages[currentStage].completedAt = new Date().toISOString();
117
+ state.stages[currentStage].status = 'completed';
118
+ state.stages[currentStage].duration = this.calculateDuration(
119
+ state.stages[currentStage].enteredAt,
120
+ state.stages[currentStage].completedAt
121
+ );
122
+ }
123
+
124
+ // Enter new stage
125
+ state.stages[targetStage] = state.stages[targetStage] || {};
126
+ state.stages[targetStage].enteredAt = new Date().toISOString();
127
+ state.stages[targetStage].status = 'in-progress';
128
+ state.stages[targetStage].attempts = (state.stages[targetStage].attempts || 0) + 1;
129
+
130
+ state.currentStage = targetStage;
131
+
132
+ // Record history
133
+ state.history.push({
134
+ timestamp: new Date().toISOString(),
135
+ action: 'stage-transition',
136
+ from: currentStage,
137
+ to: targetStage,
138
+ notes
139
+ });
140
+
141
+ await this.saveState(state);
142
+ await this.recordMetric('stage_transition', {
143
+ from: currentStage,
144
+ to: targetStage,
145
+ feature: state.feature
146
+ });
147
+
148
+ return state;
149
+ }
150
+
151
+ /**
152
+ * Record a feedback loop (going back to a previous stage)
153
+ */
154
+ async recordFeedbackLoop(fromStage, toStage, reason) {
155
+ const state = await this.getState();
156
+ if (!state) return;
157
+
158
+ state.history.push({
159
+ timestamp: new Date().toISOString(),
160
+ action: 'feedback-loop',
161
+ from: fromStage,
162
+ to: toStage,
163
+ reason
164
+ });
165
+
166
+ await this.saveState(state);
167
+ await this.recordMetric('feedback_loop', {
168
+ from: fromStage,
169
+ to: toStage,
170
+ reason,
171
+ feature: state.feature
172
+ });
173
+ }
174
+
175
+ /**
176
+ * Complete the workflow
177
+ */
178
+ async completeWorkflow(notes = '') {
179
+ const state = await this.getState();
180
+ if (!state) {
181
+ throw new Error('No active workflow.');
182
+ }
183
+
184
+ state.completedAt = new Date().toISOString();
185
+ state.totalDuration = this.calculateDuration(state.startedAt, state.completedAt);
186
+ state.status = 'completed';
187
+
188
+ // Complete current stage
189
+ if (state.stages[state.currentStage]) {
190
+ state.stages[state.currentStage].completedAt = new Date().toISOString();
191
+ state.stages[state.currentStage].status = 'completed';
192
+ }
193
+
194
+ state.history.push({
195
+ timestamp: new Date().toISOString(),
196
+ action: 'workflow-completed',
197
+ notes
198
+ });
199
+
200
+ await this.saveState(state);
201
+ await this.recordMetric('workflow_completed', {
202
+ feature: state.feature,
203
+ totalDuration: state.totalDuration,
204
+ stageCount: Object.keys(state.stages).length
205
+ });
206
+
207
+ // Generate summary
208
+ return this.generateSummary(state);
209
+ }
210
+
211
+ /**
212
+ * Record a metric
213
+ */
214
+ async recordMetric(name, data) {
215
+ let metrics = [];
216
+ if (await fs.pathExists(this.metricsFile)) {
217
+ const content = await fs.readFile(this.metricsFile, 'utf8');
218
+ metrics = yaml.load(content) || [];
219
+ }
220
+
221
+ metrics.push({
222
+ timestamp: new Date().toISOString(),
223
+ name,
224
+ data
225
+ });
226
+
227
+ await fs.ensureDir(path.dirname(this.metricsFile));
228
+ await fs.writeFile(this.metricsFile, yaml.dump(metrics, { indent: 2 }));
229
+ }
230
+
231
+ /**
232
+ * Get workflow metrics summary
233
+ */
234
+ async getMetricsSummary() {
235
+ if (!await fs.pathExists(this.metricsFile)) {
236
+ return { message: 'No metrics recorded yet.' };
237
+ }
238
+
239
+ const content = await fs.readFile(this.metricsFile, 'utf8');
240
+ const metrics = yaml.load(content) || [];
241
+
242
+ const summary = {
243
+ totalWorkflows: 0,
244
+ completedWorkflows: 0,
245
+ feedbackLoops: 0,
246
+ stageTransitions: 0,
247
+ averageDuration: null,
248
+ stageStats: {}
249
+ };
250
+
251
+ const durations = [];
252
+
253
+ metrics.forEach(m => {
254
+ switch (m.name) {
255
+ case 'workflow_started':
256
+ summary.totalWorkflows++;
257
+ break;
258
+ case 'workflow_completed':
259
+ summary.completedWorkflows++;
260
+ if (m.data.totalDuration) {
261
+ durations.push(this.parseDuration(m.data.totalDuration));
262
+ }
263
+ break;
264
+ case 'feedback_loop':
265
+ summary.feedbackLoops++;
266
+ break;
267
+ case 'stage_transition': {
268
+ summary.stageTransitions++;
269
+ const to = m.data.to;
270
+ summary.stageStats[to] = summary.stageStats[to] || { visits: 0 };
271
+ summary.stageStats[to].visits++;
272
+ break;
273
+ }
274
+ }
275
+ });
276
+
277
+ if (durations.length > 0) {
278
+ const avgMs = durations.reduce((a, b) => a + b, 0) / durations.length;
279
+ summary.averageDuration = this.formatDuration(avgMs);
280
+ }
281
+
282
+ return summary;
283
+ }
284
+
285
+ /**
286
+ * Generate workflow summary
287
+ */
288
+ generateSummary(state) {
289
+ const stages = Object.entries(state.stages).map(([name, data]) => ({
290
+ name,
291
+ duration: data.duration || 'N/A',
292
+ attempts: data.attempts || 1
293
+ }));
294
+
295
+ const feedbackLoops = state.history.filter(h => h.action === 'feedback-loop');
296
+
297
+ return {
298
+ feature: state.feature,
299
+ totalDuration: state.totalDuration,
300
+ stages,
301
+ feedbackLoops: feedbackLoops.length,
302
+ feedbackDetails: feedbackLoops
303
+ };
304
+ }
305
+
306
+ /**
307
+ * Calculate duration between two ISO timestamps
308
+ */
309
+ calculateDuration(start, end) {
310
+ const ms = new Date(end) - new Date(start);
311
+ return this.formatDuration(ms);
312
+ }
313
+
314
+ /**
315
+ * Format milliseconds to human readable duration
316
+ */
317
+ formatDuration(ms) {
318
+ const seconds = Math.floor(ms / 1000);
319
+ const minutes = Math.floor(seconds / 60);
320
+ const hours = Math.floor(minutes / 60);
321
+ const days = Math.floor(hours / 24);
322
+
323
+ if (days > 0) return `${days}d ${hours % 24}h`;
324
+ if (hours > 0) return `${hours}h ${minutes % 60}m`;
325
+ if (minutes > 0) return `${minutes}m`;
326
+ return `${seconds}s`;
327
+ }
328
+
329
+ /**
330
+ * Parse duration string to milliseconds
331
+ */
332
+ parseDuration(duration) {
333
+ const match = duration.match(/(\d+)([dhms])/g);
334
+ if (!match) return 0;
335
+
336
+ let ms = 0;
337
+ match.forEach(part => {
338
+ const value = parseInt(part);
339
+ const unit = part.slice(-1);
340
+ switch (unit) {
341
+ case 'd': ms += value * 24 * 60 * 60 * 1000; break;
342
+ case 'h': ms += value * 60 * 60 * 1000; break;
343
+ case 'm': ms += value * 60 * 1000; break;
344
+ case 's': ms += value * 1000; break;
345
+ }
346
+ });
347
+ return ms;
348
+ }
349
+
350
+ /**
351
+ * Get valid next stages from current state
352
+ */
353
+ async getValidTransitions() {
354
+ const state = await this.getState();
355
+ if (!state) return [];
356
+ return WORKFLOW_STAGES[state.currentStage]?.next || [];
357
+ }
358
+ }
359
+
360
+ module.exports = { WorkflowEngine, WORKFLOW_STAGES };