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 +10 -3
- package/README.md +10 -3
- package/bin/musubi-workflow.js +280 -0
- package/package.json +3 -2
- package/src/managers/workflow.js +360 -0
package/README.ja.md
CHANGED
|
@@ -4,13 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
MUSUBIは、6つの主要フレームワークのベスト機能を統合した包括的なSDD(仕様駆動開発)フレームワークであり、複数のAIコーディングエージェントに対応した本番環境対応ツールです。
|
|
6
6
|
|
|
7
|
-
## 🚀 v2.
|
|
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.
|
|
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
|
|
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 };
|