wogiflow 2.1.3 → 2.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.
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Progress Tracker
5
+ *
6
+ * Manages progress state for long-running tasks (reviews, audits, multi-criteria).
7
+ * Writes to .workflow/state/task-progress.json for hook/status line consumption.
8
+ * Optionally updates task title in ready.json with progress prefix for status line.
9
+ *
10
+ * Usage:
11
+ * flow-progress-tracker.js update <json> Update progress state
12
+ * flow-progress-tracker.js get Get current progress
13
+ * flow-progress-tracker.js clear Clear progress state
14
+ * flow-progress-tracker.js format <json> Format a progress bar string
15
+ *
16
+ * The AI calls this at natural checkpoints during execution.
17
+ */
18
+
19
+ const fs = require('node:fs');
20
+ const path = require('node:path');
21
+ const { PATHS, safeJsonParse, safeJsonParseString, getReadyData, saveReadyData } = require('./flow-utils');
22
+
23
+ const PROGRESS_PATH = path.join(PATHS.state, 'task-progress.json');
24
+
25
+ // ============================================================
26
+ // Progress State Management
27
+ // ============================================================
28
+
29
+ /**
30
+ * Update the progress state file.
31
+ *
32
+ * @param {Object} progress
33
+ * @param {string} progress.taskId - Current task ID
34
+ * @param {string} progress.command - Command running (e.g., "/wogi-review")
35
+ * @param {string} progress.phase - Current phase name
36
+ * @param {number} progress.phaseNum - Current phase number (1-based)
37
+ * @param {number} progress.totalPhases - Total phases
38
+ * @param {string} [progress.step] - Current sub-step description
39
+ * @param {number} [progress.stepNum] - Current sub-step number
40
+ * @param {number} [progress.totalSteps] - Total sub-steps in this phase
41
+ * @param {boolean} [progress.updateTitle] - Update task title in ready.json
42
+ * @returns {{ saved: boolean }}
43
+ */
44
+ function updateProgress(progress) {
45
+ const state = {
46
+ taskId: progress.taskId,
47
+ command: progress.command,
48
+ phase: progress.phase,
49
+ phaseNum: progress.phaseNum || 0,
50
+ totalPhases: progress.totalPhases || 0,
51
+ step: progress.step || null,
52
+ stepNum: progress.stepNum || 0,
53
+ totalSteps: progress.totalSteps || 0,
54
+ percentage: calculatePercentage(progress),
55
+ startedAt: getExistingStartTime() || new Date().toISOString(),
56
+ lastUpdate: new Date().toISOString()
57
+ };
58
+
59
+ try {
60
+ fs.mkdirSync(path.dirname(PROGRESS_PATH), { recursive: true });
61
+ fs.writeFileSync(PROGRESS_PATH, JSON.stringify(state, null, 2));
62
+ } catch (err) {
63
+ return { saved: false, reason: err.message };
64
+ }
65
+
66
+ // Update task title in ready.json for status line visibility (opt-in)
67
+ if (progress.updateTitle === true && state.taskId) {
68
+ updateTaskTitle(state);
69
+ }
70
+
71
+ return { saved: true, state };
72
+ }
73
+
74
+ /**
75
+ * Calculate progress percentage from phase/step numbers.
76
+ */
77
+ function calculatePercentage(progress) {
78
+ const { phaseNum, totalPhases, stepNum, totalSteps } = progress;
79
+ if (!totalPhases || !phaseNum) return 0;
80
+
81
+ // Phase-level progress
82
+ const phaseProgress = ((phaseNum - 1) / totalPhases) * 100;
83
+
84
+ // Step-level progress within the current phase
85
+ const phaseWeight = 100 / totalPhases;
86
+ const stepProgress = (totalSteps && stepNum)
87
+ ? (stepNum / totalSteps) * phaseWeight
88
+ : 0;
89
+
90
+ return Math.min(100, Math.round(phaseProgress + stepProgress));
91
+ }
92
+
93
+ /**
94
+ * Get existing start time to preserve across updates.
95
+ */
96
+ function getExistingStartTime() {
97
+ try {
98
+ const existing = safeJsonParse(PROGRESS_PATH, null);
99
+ return existing?.startedAt || null;
100
+ } catch (err) {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Update task title in ready.json with progress prefix.
107
+ * Format: "[2/5] Original title"
108
+ */
109
+ function updateTaskTitle(state) {
110
+ try {
111
+ const data = getReadyData();
112
+ const task = data.inProgress.find(t => t.id === state.taskId);
113
+ if (!task) return;
114
+
115
+ // Strip any existing progress prefix
116
+ const cleanTitle = task.title.replace(/^\[\d+\/\d+\]\s*/, '');
117
+
118
+ // Add new prefix
119
+ task.title = `[${state.phaseNum}/${state.totalPhases}] ${cleanTitle}`;
120
+ saveReadyData(data);
121
+ } catch (err) {
122
+ // Non-fatal — title update is cosmetic
123
+ if (process.env.DEBUG) {
124
+ console.error(`[progress-tracker] Title update failed: ${err.message}`);
125
+ }
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Get current progress state.
131
+ * @returns {Object|null}
132
+ */
133
+ function getProgress() {
134
+ return safeJsonParse(PROGRESS_PATH, null);
135
+ }
136
+
137
+ /**
138
+ * Clear progress state (called on task completion).
139
+ *
140
+ * NOTE: Title restoration (stripping [N/M] prefix) is handled inside the
141
+ * task-completed hook's withLock() callback to avoid race conditions on ready.json.
142
+ * This function only deletes the progress state file.
143
+ */
144
+ function clearProgress() {
145
+ try {
146
+ fs.unlinkSync(PROGRESS_PATH);
147
+ return { cleared: true };
148
+ } catch (err) {
149
+ if (err.code === 'ENOENT') return { cleared: true };
150
+ return { cleared: false, reason: err.message };
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Format a progress bar string for conversation output.
156
+ *
157
+ * @param {Object} opts
158
+ * @param {number} opts.current - Current step
159
+ * @param {number} opts.total - Total steps
160
+ * @param {string} opts.label - Label text
161
+ * @param {number} [opts.width=20] - Bar width
162
+ * @returns {string} Formatted progress line
163
+ */
164
+ function formatProgressBar(opts) {
165
+ const { current, total, label, width = 20 } = opts;
166
+ const pct = total > 0 ? Math.round((current / total) * 100) : 0;
167
+ const filled = total > 0 ? Math.round((current / total) * width) : 0;
168
+ const empty = width - filled;
169
+
170
+ const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(empty);
171
+ return `[${bar}] ${pct}% ${label} (${current}/${total})`;
172
+ }
173
+
174
+ /**
175
+ * Format a multi-level progress display for conversation output.
176
+ *
177
+ * @param {Object} opts
178
+ * @param {string} opts.command - Command name
179
+ * @param {string} opts.phase - Current phase
180
+ * @param {number} opts.phaseNum
181
+ * @param {number} opts.totalPhases
182
+ * @param {string} [opts.step] - Current step within phase
183
+ * @param {number} [opts.stepNum]
184
+ * @param {number} [opts.totalSteps]
185
+ * @returns {string} Multi-line progress display
186
+ */
187
+ function formatProgress(opts) {
188
+ const lines = [];
189
+ const pct = calculatePercentage(opts);
190
+
191
+ // Phase-level bar
192
+ lines.push(formatProgressBar({
193
+ current: opts.phaseNum,
194
+ total: opts.totalPhases,
195
+ label: opts.phase
196
+ }));
197
+
198
+ // Step-level detail (if applicable)
199
+ if (opts.step && opts.totalSteps) {
200
+ lines.push(` ${opts.step} (${opts.stepNum}/${opts.totalSteps})`);
201
+ }
202
+
203
+ return lines.join('\n');
204
+ }
205
+
206
+ // ============================================================
207
+ // CLI Interface
208
+ // ============================================================
209
+
210
+ function main() {
211
+ const command = process.argv[2];
212
+ const arg = process.argv[3];
213
+
214
+ switch (command) {
215
+ case 'update': {
216
+ if (!arg) {
217
+ console.error('Usage: flow-progress-tracker.js update \'{"taskId":"...","phase":"...","phaseNum":1,"totalPhases":5}\'');
218
+ process.exit(1);
219
+ }
220
+ const progress = safeJsonParseString(arg, null);
221
+ if (!progress) {
222
+ console.error('Invalid JSON argument');
223
+ process.exit(1);
224
+ }
225
+ const result = updateProgress(progress);
226
+ console.log(JSON.stringify(result, null, 2));
227
+ break;
228
+ }
229
+
230
+ case 'get': {
231
+ const state = getProgress();
232
+ if (state) {
233
+ console.log(JSON.stringify(state, null, 2));
234
+ } else {
235
+ console.log(JSON.stringify({ active: false }));
236
+ }
237
+ break;
238
+ }
239
+
240
+ case 'clear': {
241
+ const result = clearProgress();
242
+ console.log(JSON.stringify(result, null, 2));
243
+ break;
244
+ }
245
+
246
+ case 'format': {
247
+ if (!arg) {
248
+ console.error('Usage: flow-progress-tracker.js format \'{"current":2,"total":5,"label":"Phase"}\'');
249
+ process.exit(1);
250
+ }
251
+ const opts = safeJsonParseString(arg, null);
252
+ if (!opts) {
253
+ console.error('Invalid JSON argument');
254
+ process.exit(1);
255
+ }
256
+ console.log(formatProgressBar(opts));
257
+ break;
258
+ }
259
+
260
+ default:
261
+ console.log(`
262
+ Wogi Flow - Progress Tracker
263
+
264
+ Usage: flow-progress-tracker.js <command> [args]
265
+
266
+ Commands:
267
+ update <json> Update progress state + task title
268
+ get Get current progress state
269
+ clear Clear progress state
270
+ format <json> Format a progress bar string
271
+
272
+ Update format:
273
+ {"taskId":"wf-xxx","command":"/wogi-review","phase":"AI Review","phaseNum":2,"totalPhases":5}
274
+ `);
275
+ }
276
+ }
277
+
278
+ module.exports = {
279
+ updateProgress,
280
+ getProgress,
281
+ clearProgress,
282
+ formatProgressBar,
283
+ formatProgress,
284
+ calculatePercentage
285
+ };
286
+
287
+ if (require.main === module) {
288
+ main();
289
+ }