wogiflow 1.0.11 → 1.0.13
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/.workflow/specs/architecture.md.template +24 -0
- package/.workflow/specs/stack.md.template +33 -0
- package/.workflow/specs/testing.md.template +36 -0
- package/README.md +90 -1
- package/lib/unified-wizard.js +569 -30
- package/package.json +1 -1
- package/scripts/MEMORY-ARCHITECTURE.md +150 -0
- package/scripts/flow +20 -19
- package/scripts/flow-auto-context.js +97 -3
- package/scripts/flow-conflict-resolver.js +735 -0
- package/scripts/flow-context-gatherer.js +520 -0
- package/scripts/flow-context-monitor.js +148 -19
- package/scripts/flow-damage-control.js +5 -1
- package/scripts/flow-export-profile +168 -1
- package/scripts/flow-import-profile +257 -6
- package/scripts/flow-instruction-richness.js +182 -18
- package/scripts/flow-knowledge-router.js +2 -0
- package/scripts/flow-knowledge-sync.js +2 -0
- package/scripts/{flow-transcript-chunking.js → flow-long-input-chunking.js} +4 -2
- package/scripts/{flow-transcript-parsing.js → flow-long-input-parsing.js} +35 -0
- package/scripts/{flow-transcript-stories.js → flow-long-input-stories.js} +86 -38
- package/scripts/{flow-transcript-digest.js → flow-long-input.js} +231 -15
- package/scripts/flow-memory-db.js +386 -1
- package/scripts/flow-memory-sync.js +2 -0
- package/scripts/flow-model-adapter.js +53 -29
- package/scripts/flow-model-router.js +246 -1
- package/scripts/flow-morning.js +94 -0
- package/scripts/flow-onboard +223 -10
- package/scripts/flow-orchestrate-validation.js +539 -0
- package/scripts/flow-orchestrate.js +16 -507
- package/scripts/flow-pattern-extractor.js +1265 -0
- package/scripts/flow-prompt-composer.js +222 -2
- package/scripts/flow-quality-guard.js +594 -0
- package/scripts/flow-section-index.js +713 -0
- package/scripts/flow-section-resolver.js +484 -0
- package/scripts/flow-session-end.js +188 -2
- package/scripts/flow-skill-create.js +19 -3
- package/scripts/flow-skill-matcher.js +122 -7
- package/scripts/flow-statusline-setup.js +218 -0
- package/scripts/flow-step-review.js +19 -0
- package/scripts/flow-tech-debt.js +734 -0
- package/scripts/flow-utils.js +2 -0
- package/scripts/hooks/core/long-input-gate.js +293 -0
- package/scripts/flow-parallel-detector.js +0 -399
- package/scripts/flow-parallel-dispatch.js +0 -987
- /package/scripts/{flow-transcript-language.js → flow-long-input-language.js} +0 -0
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Technical Debt Manager
|
|
5
|
+
*
|
|
6
|
+
* Tracks, manages, and helps resolve technical debt accumulated across sessions.
|
|
7
|
+
* Issues are captured from session reviews and persisted for tracking over time.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* CLI:
|
|
11
|
+
* flow tech-debt # Show summary
|
|
12
|
+
* flow tech-debt list # List all open items
|
|
13
|
+
* flow tech-debt list --aging # Show only aging items
|
|
14
|
+
* flow tech-debt list --fixable # Show only auto-fixable items
|
|
15
|
+
* flow tech-debt fix # Run auto-fixes (batch)
|
|
16
|
+
* flow tech-debt dismiss <id> # Mark as won't-fix
|
|
17
|
+
* flow tech-debt promote <id> # Create task from debt item
|
|
18
|
+
*
|
|
19
|
+
* Programmatic:
|
|
20
|
+
* const { TechDebtManager } = require('./flow-tech-debt');
|
|
21
|
+
* const manager = new TechDebtManager();
|
|
22
|
+
* manager.addIssues(issues);
|
|
23
|
+
* manager.getStats();
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const crypto = require('crypto');
|
|
29
|
+
|
|
30
|
+
// Import shared utilities from flow-utils
|
|
31
|
+
const {
|
|
32
|
+
colors: c,
|
|
33
|
+
safeJsonParse,
|
|
34
|
+
writeJson,
|
|
35
|
+
fileExists,
|
|
36
|
+
getProjectRoot,
|
|
37
|
+
ensureDir
|
|
38
|
+
} = require('./flow-utils');
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Constants
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
const DEFAULT_AGING_THRESHOLD = 3; // Sessions before item is "aging"
|
|
45
|
+
const AUTO_FIXABLE_TYPES = [
|
|
46
|
+
'console.log',
|
|
47
|
+
'unused-import',
|
|
48
|
+
'debugger',
|
|
49
|
+
'trailing-whitespace',
|
|
50
|
+
'empty-catch'
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Utility Functions
|
|
55
|
+
// ============================================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Generate unique debt item ID (6 bytes for better collision resistance)
|
|
59
|
+
*/
|
|
60
|
+
function generateDebtId() {
|
|
61
|
+
return 'td-' + crypto.randomBytes(6).toString('hex');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get current date in YYYY-MM-DD format
|
|
66
|
+
*/
|
|
67
|
+
function getCurrentDate() {
|
|
68
|
+
return new Date().toISOString().split('T')[0];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Safe JSON write with atomic temp file pattern
|
|
73
|
+
*/
|
|
74
|
+
function safeWriteJson(filePath, data) {
|
|
75
|
+
const dir = path.dirname(filePath);
|
|
76
|
+
ensureDir(dir);
|
|
77
|
+
writeJson(filePath, data);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Tech Debt Manager Class
|
|
82
|
+
// ============================================================================
|
|
83
|
+
|
|
84
|
+
class TechDebtManager {
|
|
85
|
+
constructor(projectRoot = null) {
|
|
86
|
+
this.projectRoot = projectRoot || getProjectRoot();
|
|
87
|
+
this.debtFilePath = path.join(this.projectRoot, '.workflow', 'state', 'tech-debt.json');
|
|
88
|
+
this.configPath = path.join(this.projectRoot, '.workflow', 'config.json');
|
|
89
|
+
this.readyPath = path.join(this.projectRoot, '.workflow', 'state', 'ready.json');
|
|
90
|
+
this.data = this.load();
|
|
91
|
+
this.config = this.loadConfig();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Load tech debt data from file
|
|
96
|
+
*/
|
|
97
|
+
load() {
|
|
98
|
+
const defaultData = {
|
|
99
|
+
version: '1.0.0',
|
|
100
|
+
lastUpdated: new Date().toISOString(),
|
|
101
|
+
issues: [],
|
|
102
|
+
stats: {
|
|
103
|
+
totalOpen: 0,
|
|
104
|
+
bySeverity: { critical: 0, high: 0, medium: 0, low: 0 },
|
|
105
|
+
autoFixable: 0
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const data = safeJsonParse(this.debtFilePath, defaultData);
|
|
110
|
+
return data;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Load config for tech debt settings
|
|
115
|
+
*/
|
|
116
|
+
loadConfig() {
|
|
117
|
+
const config = safeJsonParse(this.configPath, {});
|
|
118
|
+
return config.techDebt || {
|
|
119
|
+
enabled: true,
|
|
120
|
+
promptOnSessionEnd: true,
|
|
121
|
+
showInMorningBriefing: true,
|
|
122
|
+
agingThreshold: DEFAULT_AGING_THRESHOLD,
|
|
123
|
+
autoFix: {
|
|
124
|
+
enabled: true,
|
|
125
|
+
types: AUTO_FIXABLE_TYPES
|
|
126
|
+
},
|
|
127
|
+
debtBudget: {
|
|
128
|
+
enabled: false,
|
|
129
|
+
maxOpenItems: 20
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Save tech debt data to file
|
|
136
|
+
*/
|
|
137
|
+
save() {
|
|
138
|
+
this.data.lastUpdated = new Date().toISOString();
|
|
139
|
+
this.updateStats();
|
|
140
|
+
safeWriteJson(this.debtFilePath, this.data);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Update statistics
|
|
145
|
+
*/
|
|
146
|
+
updateStats() {
|
|
147
|
+
const openIssues = this.data.issues.filter(i => i.status === 'open');
|
|
148
|
+
|
|
149
|
+
this.data.stats = {
|
|
150
|
+
totalOpen: openIssues.length,
|
|
151
|
+
bySeverity: {
|
|
152
|
+
critical: openIssues.filter(i => i.severity === 'critical').length,
|
|
153
|
+
high: openIssues.filter(i => i.severity === 'high').length,
|
|
154
|
+
medium: openIssues.filter(i => i.severity === 'medium').length,
|
|
155
|
+
low: openIssues.filter(i => i.severity === 'low').length
|
|
156
|
+
},
|
|
157
|
+
autoFixable: openIssues.filter(i => i.autoFixable).length
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Create unique key for deduplication
|
|
163
|
+
*/
|
|
164
|
+
createIssueKey(issue) {
|
|
165
|
+
return `${issue.file}:${issue.line}:${issue.description}`.toLowerCase();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Determine if an issue is auto-fixable
|
|
170
|
+
*/
|
|
171
|
+
isAutoFixable(issue) {
|
|
172
|
+
const fixablePatterns = [
|
|
173
|
+
/console\.(log|debug|info|warn)/i,
|
|
174
|
+
/unused\s+(import|variable|parameter)/i,
|
|
175
|
+
/debugger\s+statement/i,
|
|
176
|
+
/trailing\s+whitespace/i,
|
|
177
|
+
/empty\s+catch/i
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
const desc = issue.description.toLowerCase();
|
|
181
|
+
return fixablePatterns.some(pattern => pattern.test(desc));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Add issues from session review
|
|
186
|
+
* Deduplicates and updates session count for existing issues
|
|
187
|
+
*/
|
|
188
|
+
addIssues(issues) {
|
|
189
|
+
const today = getCurrentDate();
|
|
190
|
+
const existingKeys = new Map(
|
|
191
|
+
this.data.issues.map(i => [this.createIssueKey(i), i])
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
let added = 0;
|
|
195
|
+
let updated = 0;
|
|
196
|
+
|
|
197
|
+
for (const issue of issues) {
|
|
198
|
+
const key = this.createIssueKey(issue);
|
|
199
|
+
const existing = existingKeys.get(key);
|
|
200
|
+
|
|
201
|
+
if (existing) {
|
|
202
|
+
// Update existing issue
|
|
203
|
+
existing.sessionsSeen = (existing.sessionsSeen || 1) + 1;
|
|
204
|
+
existing.lastSeen = today;
|
|
205
|
+
updated++;
|
|
206
|
+
} else {
|
|
207
|
+
// Add new issue
|
|
208
|
+
const newIssue = {
|
|
209
|
+
id: generateDebtId(),
|
|
210
|
+
file: issue.file,
|
|
211
|
+
line: issue.line || 0,
|
|
212
|
+
category: issue.category || 'code',
|
|
213
|
+
severity: issue.severity || 'low',
|
|
214
|
+
description: issue.description,
|
|
215
|
+
fix: issue.fix || '',
|
|
216
|
+
firstSeen: today,
|
|
217
|
+
lastSeen: today,
|
|
218
|
+
sessionsSeen: 1,
|
|
219
|
+
status: 'open',
|
|
220
|
+
autoFixable: this.isAutoFixable(issue)
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
this.data.issues.push(newIssue);
|
|
224
|
+
existingKeys.set(key, newIssue);
|
|
225
|
+
added++;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this.save();
|
|
230
|
+
return { added, updated };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get all open issues
|
|
235
|
+
*/
|
|
236
|
+
getOpenIssues() {
|
|
237
|
+
return this.data.issues.filter(i => i.status === 'open');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get aging issues (seen >= threshold sessions)
|
|
242
|
+
*/
|
|
243
|
+
getAgingIssues() {
|
|
244
|
+
const threshold = this.config.agingThreshold || DEFAULT_AGING_THRESHOLD;
|
|
245
|
+
return this.getOpenIssues().filter(i => i.sessionsSeen >= threshold);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get auto-fixable issues
|
|
250
|
+
*/
|
|
251
|
+
getAutoFixable() {
|
|
252
|
+
return this.getOpenIssues().filter(i => i.autoFixable);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get issues by severity
|
|
257
|
+
*/
|
|
258
|
+
getBySeverity(severity) {
|
|
259
|
+
return this.getOpenIssues().filter(i => i.severity === severity);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Mark issue as fixed
|
|
264
|
+
*/
|
|
265
|
+
markFixed(id) {
|
|
266
|
+
const issue = this.data.issues.find(i => i.id === id);
|
|
267
|
+
if (issue) {
|
|
268
|
+
issue.status = 'fixed';
|
|
269
|
+
issue.fixedAt = new Date().toISOString();
|
|
270
|
+
this.save();
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Mark issue as dismissed (won't fix)
|
|
278
|
+
*/
|
|
279
|
+
dismiss(id, reason = '') {
|
|
280
|
+
const issue = this.data.issues.find(i => i.id === id);
|
|
281
|
+
if (issue) {
|
|
282
|
+
issue.status = 'dismissed';
|
|
283
|
+
issue.dismissedAt = new Date().toISOString();
|
|
284
|
+
issue.dismissReason = reason;
|
|
285
|
+
this.save();
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Promote debt item to task in ready.json
|
|
293
|
+
*/
|
|
294
|
+
promoteToTask(id) {
|
|
295
|
+
const issue = this.data.issues.find(i => i.id === id && i.status === 'open');
|
|
296
|
+
if (!issue) {
|
|
297
|
+
return { success: false, error: 'Issue not found or not open' };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const ready = safeJsonParse(this.readyPath, { ready: [], inProgress: [] });
|
|
301
|
+
|
|
302
|
+
// Check if already promoted
|
|
303
|
+
const existingTask = ready.ready.find(t => t.debtItemId === id);
|
|
304
|
+
if (existingTask) {
|
|
305
|
+
return { success: false, error: 'Already promoted to task' };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Create task
|
|
309
|
+
const task = {
|
|
310
|
+
id: `wf-debt-${crypto.randomBytes(4).toString('hex')}`,
|
|
311
|
+
title: `Fix tech debt: ${issue.description.slice(0, 50)}`,
|
|
312
|
+
type: 'refactor',
|
|
313
|
+
priority: issue.severity === 'critical' ? 'high' : (issue.severity === 'high' ? 'medium' : 'low'),
|
|
314
|
+
source: 'tech-debt',
|
|
315
|
+
debtItemId: issue.id,
|
|
316
|
+
file: issue.file,
|
|
317
|
+
line: issue.line,
|
|
318
|
+
created: new Date().toISOString()
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
ready.ready.push(task);
|
|
322
|
+
safeWriteJson(this.readyPath, ready);
|
|
323
|
+
|
|
324
|
+
issue.promotedToTask = task.id;
|
|
325
|
+
this.save();
|
|
326
|
+
|
|
327
|
+
return { success: true, taskId: task.id };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Get statistics
|
|
332
|
+
*/
|
|
333
|
+
getStats() {
|
|
334
|
+
this.updateStats();
|
|
335
|
+
return {
|
|
336
|
+
...this.data.stats,
|
|
337
|
+
agingCount: this.getAgingIssues().length
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Run auto-fixes for all auto-fixable issues
|
|
343
|
+
* Returns list of files modified
|
|
344
|
+
*/
|
|
345
|
+
runAutoFix() {
|
|
346
|
+
const fixableIssues = this.getAutoFixable();
|
|
347
|
+
const fixedFiles = new Map();
|
|
348
|
+
const fixed = [];
|
|
349
|
+
const failed = [];
|
|
350
|
+
|
|
351
|
+
// Group by file
|
|
352
|
+
const byFile = new Map();
|
|
353
|
+
for (const issue of fixableIssues) {
|
|
354
|
+
if (!byFile.has(issue.file)) {
|
|
355
|
+
byFile.set(issue.file, []);
|
|
356
|
+
}
|
|
357
|
+
byFile.get(issue.file).push(issue);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Process each file
|
|
361
|
+
for (const [filePath, issues] of byFile) {
|
|
362
|
+
const fullPath = path.join(this.projectRoot, filePath);
|
|
363
|
+
|
|
364
|
+
if (!fs.existsSync(fullPath)) {
|
|
365
|
+
for (const issue of issues) {
|
|
366
|
+
failed.push({ issue, reason: 'File not found' });
|
|
367
|
+
}
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
let content = fs.readFileSync(fullPath, 'utf-8');
|
|
373
|
+
let lines = content.split('\n');
|
|
374
|
+
const linesToRemove = new Set();
|
|
375
|
+
|
|
376
|
+
// Collect lines to fix
|
|
377
|
+
for (const issue of issues) {
|
|
378
|
+
const desc = issue.description.toLowerCase();
|
|
379
|
+
|
|
380
|
+
// console.log - remove line
|
|
381
|
+
if (/console\.(log|debug|info|warn)/i.test(desc)) {
|
|
382
|
+
if (issue.line > 0 && issue.line <= lines.length) {
|
|
383
|
+
const line = lines[issue.line - 1];
|
|
384
|
+
if (/console\.(log|debug|info|warn)\s*\(/.test(line)) {
|
|
385
|
+
linesToRemove.add(issue.line - 1);
|
|
386
|
+
fixed.push(issue);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// debugger - remove line
|
|
393
|
+
if (/debugger\s+statement/i.test(desc)) {
|
|
394
|
+
if (issue.line > 0 && issue.line <= lines.length) {
|
|
395
|
+
const line = lines[issue.line - 1];
|
|
396
|
+
if (/^\s*debugger\s*;?\s*$/.test(line)) {
|
|
397
|
+
linesToRemove.add(issue.line - 1);
|
|
398
|
+
fixed.push(issue);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// empty catch - add comment
|
|
405
|
+
if (/empty\s+catch/i.test(desc)) {
|
|
406
|
+
if (issue.line > 0 && issue.line <= lines.length) {
|
|
407
|
+
const line = lines[issue.line - 1];
|
|
408
|
+
if (/catch\s*(\([^)]*\))?\s*\{\s*\}/.test(line)) {
|
|
409
|
+
lines[issue.line - 1] = line.replace(
|
|
410
|
+
/catch\s*(\([^)]*\))?\s*\{\s*\}/,
|
|
411
|
+
(match, param) => `catch ${param || '(err)'} { /* intentionally empty */ }`
|
|
412
|
+
);
|
|
413
|
+
fixed.push(issue);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// If we couldn't fix it
|
|
420
|
+
failed.push({ issue, reason: 'Could not apply fix' });
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Remove lines (in reverse order to preserve line numbers)
|
|
424
|
+
const sortedLinesToRemove = Array.from(linesToRemove).sort((a, b) => b - a);
|
|
425
|
+
for (const lineIndex of sortedLinesToRemove) {
|
|
426
|
+
lines.splice(lineIndex, 1);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Write back
|
|
430
|
+
if (linesToRemove.size > 0 || fixed.some(i => i.file === filePath)) {
|
|
431
|
+
fs.writeFileSync(fullPath, lines.join('\n'), 'utf-8');
|
|
432
|
+
fixedFiles.set(filePath, issues.filter(i => fixed.includes(i)).length);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
} catch (err) {
|
|
436
|
+
for (const issue of issues) {
|
|
437
|
+
failed.push({ issue, reason: err.message });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Mark fixed issues
|
|
443
|
+
for (const issue of fixed) {
|
|
444
|
+
this.markFixed(issue.id);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
fixed: fixed.length,
|
|
449
|
+
failed: failed.length,
|
|
450
|
+
files: Array.from(fixedFiles.keys()),
|
|
451
|
+
details: { fixed, failed }
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Create tasks for all aging issues
|
|
457
|
+
*/
|
|
458
|
+
promoteAgingToTasks() {
|
|
459
|
+
const aging = this.getAgingIssues().filter(i => !i.promotedToTask);
|
|
460
|
+
const promoted = [];
|
|
461
|
+
|
|
462
|
+
for (const issue of aging) {
|
|
463
|
+
const result = this.promoteToTask(issue.id);
|
|
464
|
+
if (result.success) {
|
|
465
|
+
promoted.push({ issue, taskId: result.taskId });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return promoted;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ============================================================================
|
|
474
|
+
// CLI Interface
|
|
475
|
+
// ============================================================================
|
|
476
|
+
|
|
477
|
+
function printHeader(text) {
|
|
478
|
+
const width = 60;
|
|
479
|
+
console.log(`${c.cyan}╔${'═'.repeat(width - 2)}╗${c.reset}`);
|
|
480
|
+
console.log(`${c.cyan}║${c.reset} ${c.bold}${text.padEnd(width - 4)}${c.reset}${c.cyan}║${c.reset}`);
|
|
481
|
+
console.log(`${c.cyan}╚${'═'.repeat(width - 2)}╝${c.reset}`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function printSection(text) {
|
|
485
|
+
console.log(`\n${c.yellow}━━━ ${text} ━━━${c.reset}`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function formatIssue(issue, showDetails = true) {
|
|
489
|
+
const severityColors = {
|
|
490
|
+
critical: c.red,
|
|
491
|
+
high: c.yellow,
|
|
492
|
+
medium: c.blue,
|
|
493
|
+
low: c.dim
|
|
494
|
+
};
|
|
495
|
+
const color = severityColors[issue.severity] || c.dim;
|
|
496
|
+
|
|
497
|
+
let line = ` ${c.dim}[${issue.id}]${c.reset} ${issue.file}`;
|
|
498
|
+
if (issue.line) line += `:${issue.line}`;
|
|
499
|
+
line += ` ${color}(${issue.severity})${c.reset}`;
|
|
500
|
+
|
|
501
|
+
if (issue.sessionsSeen > 1) {
|
|
502
|
+
line += ` ${c.yellow}⚠ ${issue.sessionsSeen} sessions${c.reset}`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
console.log(line);
|
|
506
|
+
|
|
507
|
+
if (showDetails) {
|
|
508
|
+
console.log(` ${c.dim}${issue.description}${c.reset}`);
|
|
509
|
+
if (issue.fix) {
|
|
510
|
+
console.log(` ${c.green}Fix: ${issue.fix}${c.reset}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function showSummary(manager) {
|
|
516
|
+
const stats = manager.getStats();
|
|
517
|
+
|
|
518
|
+
printHeader('Technical Debt Dashboard');
|
|
519
|
+
console.log('');
|
|
520
|
+
|
|
521
|
+
console.log(`${c.bold}Summary:${c.reset} ${stats.totalOpen} open items`);
|
|
522
|
+
console.log(` ${c.red}Critical: ${stats.bySeverity.critical}${c.reset} ` +
|
|
523
|
+
`${c.yellow}High: ${stats.bySeverity.high}${c.reset} ` +
|
|
524
|
+
`${c.blue}Medium: ${stats.bySeverity.medium}${c.reset} ` +
|
|
525
|
+
`${c.dim}Low: ${stats.bySeverity.low}${c.reset}`);
|
|
526
|
+
|
|
527
|
+
if (stats.agingCount > 0) {
|
|
528
|
+
console.log(`\n${c.yellow}⚠ ${stats.agingCount} items aging (3+ sessions)${c.reset}`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (stats.autoFixable > 0) {
|
|
532
|
+
console.log(`${c.green}✓ ${stats.autoFixable} auto-fixable items available${c.reset}`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Show quick commands
|
|
536
|
+
console.log(`\n${c.dim}Commands:${c.reset}`);
|
|
537
|
+
console.log(` ${c.cyan}flow tech-debt list${c.reset} List all items`);
|
|
538
|
+
console.log(` ${c.cyan}flow tech-debt list --aging${c.reset} Show aging items`);
|
|
539
|
+
console.log(` ${c.cyan}flow tech-debt fix${c.reset} Run auto-fixes`);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function showList(manager, options = {}) {
|
|
543
|
+
let issues;
|
|
544
|
+
let title;
|
|
545
|
+
|
|
546
|
+
if (options.aging) {
|
|
547
|
+
issues = manager.getAgingIssues();
|
|
548
|
+
title = 'Aging Issues (3+ sessions)';
|
|
549
|
+
} else if (options.fixable) {
|
|
550
|
+
issues = manager.getAutoFixable();
|
|
551
|
+
title = 'Auto-Fixable Issues';
|
|
552
|
+
} else if (options.severity) {
|
|
553
|
+
issues = manager.getBySeverity(options.severity);
|
|
554
|
+
title = `${options.severity.charAt(0).toUpperCase() + options.severity.slice(1)} Issues`;
|
|
555
|
+
} else {
|
|
556
|
+
issues = manager.getOpenIssues();
|
|
557
|
+
title = 'All Open Issues';
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
printSection(title);
|
|
561
|
+
|
|
562
|
+
if (issues.length === 0) {
|
|
563
|
+
console.log(` ${c.dim}No issues found${c.reset}`);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
for (const issue of issues) {
|
|
568
|
+
formatIssue(issue, options.verbose);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
console.log(`\n${c.dim}Total: ${issues.length} items${c.reset}`);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function runFix(manager) {
|
|
575
|
+
const fixable = manager.getAutoFixable();
|
|
576
|
+
|
|
577
|
+
if (fixable.length === 0) {
|
|
578
|
+
console.log(`${c.yellow}No auto-fixable issues found.${c.reset}`);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
console.log(`${c.cyan}Running auto-fix on ${fixable.length} items...${c.reset}\n`);
|
|
583
|
+
|
|
584
|
+
const result = manager.runAutoFix();
|
|
585
|
+
|
|
586
|
+
if (result.fixed > 0) {
|
|
587
|
+
console.log(`${c.green}✓ Fixed ${result.fixed} issues${c.reset}`);
|
|
588
|
+
for (const file of result.files) {
|
|
589
|
+
console.log(` ${c.dim}${file}${c.reset}`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (result.failed > 0) {
|
|
594
|
+
console.log(`\n${c.yellow}⚠ Could not fix ${result.failed} issues${c.reset}`);
|
|
595
|
+
for (const { issue, reason } of result.details.failed) {
|
|
596
|
+
console.log(` ${c.dim}[${issue.id}] ${reason}${c.reset}`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function dismissIssue(manager, id, reason) {
|
|
602
|
+
if (manager.dismiss(id, reason)) {
|
|
603
|
+
console.log(`${c.green}✓ Dismissed: ${id}${c.reset}`);
|
|
604
|
+
} else {
|
|
605
|
+
console.log(`${c.red}✗ Issue not found: ${id}${c.reset}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function promoteIssue(manager, id) {
|
|
610
|
+
const result = manager.promoteToTask(id);
|
|
611
|
+
if (result.success) {
|
|
612
|
+
console.log(`${c.green}✓ Created task: ${result.taskId}${c.reset}`);
|
|
613
|
+
} else {
|
|
614
|
+
console.log(`${c.red}✗ ${result.error}${c.reset}`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function showHelp() {
|
|
619
|
+
console.log(`
|
|
620
|
+
${c.bold}Technical Debt Manager${c.reset}
|
|
621
|
+
|
|
622
|
+
${c.cyan}Usage:${c.reset}
|
|
623
|
+
flow tech-debt Show summary dashboard
|
|
624
|
+
flow tech-debt list List all open items
|
|
625
|
+
flow tech-debt list --aging Show items seen 3+ sessions
|
|
626
|
+
flow tech-debt list --fixable Show auto-fixable items
|
|
627
|
+
flow tech-debt list --severity X Filter by severity (critical/high/medium/low)
|
|
628
|
+
flow tech-debt fix Run auto-fixes (batch)
|
|
629
|
+
flow tech-debt dismiss <id> Mark as won't-fix
|
|
630
|
+
flow tech-debt promote <id> Create task from debt item
|
|
631
|
+
flow tech-debt promote-aging Create tasks for all aging items
|
|
632
|
+
|
|
633
|
+
${c.cyan}Options:${c.reset}
|
|
634
|
+
-v, --verbose Show detailed descriptions
|
|
635
|
+
--help Show this help
|
|
636
|
+
|
|
637
|
+
${c.cyan}Examples:${c.reset}
|
|
638
|
+
flow tech-debt list --aging -v Show aging items with details
|
|
639
|
+
flow tech-debt fix Auto-fix all safe items
|
|
640
|
+
flow tech-debt dismiss td-abc123 Dismiss a specific item
|
|
641
|
+
`);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function main() {
|
|
645
|
+
const args = process.argv.slice(2);
|
|
646
|
+
const command = args[0] || 'summary';
|
|
647
|
+
|
|
648
|
+
const manager = new TechDebtManager();
|
|
649
|
+
|
|
650
|
+
// Parse flags
|
|
651
|
+
const flags = {
|
|
652
|
+
aging: args.includes('--aging'),
|
|
653
|
+
fixable: args.includes('--fixable'),
|
|
654
|
+
verbose: args.includes('-v') || args.includes('--verbose'),
|
|
655
|
+
severity: null
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
const severityIndex = args.indexOf('--severity');
|
|
659
|
+
if (severityIndex !== -1 && args[severityIndex + 1]) {
|
|
660
|
+
flags.severity = args[severityIndex + 1];
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
switch (command) {
|
|
664
|
+
case 'summary':
|
|
665
|
+
case 'status':
|
|
666
|
+
showSummary(manager);
|
|
667
|
+
break;
|
|
668
|
+
|
|
669
|
+
case 'list':
|
|
670
|
+
showList(manager, flags);
|
|
671
|
+
break;
|
|
672
|
+
|
|
673
|
+
case 'fix':
|
|
674
|
+
runFix(manager);
|
|
675
|
+
break;
|
|
676
|
+
|
|
677
|
+
case 'dismiss':
|
|
678
|
+
const dismissId = args[1];
|
|
679
|
+
const reason = args.slice(2).join(' ');
|
|
680
|
+
if (!dismissId) {
|
|
681
|
+
console.log(`${c.red}Usage: flow tech-debt dismiss <id> [reason]${c.reset}`);
|
|
682
|
+
process.exit(1);
|
|
683
|
+
}
|
|
684
|
+
dismissIssue(manager, dismissId, reason);
|
|
685
|
+
break;
|
|
686
|
+
|
|
687
|
+
case 'promote':
|
|
688
|
+
const promoteId = args[1];
|
|
689
|
+
if (!promoteId) {
|
|
690
|
+
console.log(`${c.red}Usage: flow tech-debt promote <id>${c.reset}`);
|
|
691
|
+
process.exit(1);
|
|
692
|
+
}
|
|
693
|
+
promoteIssue(manager, promoteId);
|
|
694
|
+
break;
|
|
695
|
+
|
|
696
|
+
case 'promote-aging':
|
|
697
|
+
const promoted = manager.promoteAgingToTasks();
|
|
698
|
+
if (promoted.length > 0) {
|
|
699
|
+
console.log(`${c.green}✓ Created ${promoted.length} tasks from aging items:${c.reset}`);
|
|
700
|
+
for (const { issue, taskId } of promoted) {
|
|
701
|
+
console.log(` ${taskId}: ${issue.description.slice(0, 50)}`);
|
|
702
|
+
}
|
|
703
|
+
} else {
|
|
704
|
+
console.log(`${c.dim}No aging items to promote${c.reset}`);
|
|
705
|
+
}
|
|
706
|
+
break;
|
|
707
|
+
|
|
708
|
+
case '--help':
|
|
709
|
+
case 'help':
|
|
710
|
+
showHelp();
|
|
711
|
+
break;
|
|
712
|
+
|
|
713
|
+
default:
|
|
714
|
+
console.log(`${c.red}Unknown command: ${command}${c.reset}`);
|
|
715
|
+
showHelp();
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// ============================================================================
|
|
721
|
+
// Exports
|
|
722
|
+
// ============================================================================
|
|
723
|
+
|
|
724
|
+
module.exports = {
|
|
725
|
+
TechDebtManager,
|
|
726
|
+
generateDebtId,
|
|
727
|
+
AUTO_FIXABLE_TYPES,
|
|
728
|
+
DEFAULT_AGING_THRESHOLD
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
// Run CLI if executed directly
|
|
732
|
+
if (require.main === module) {
|
|
733
|
+
main();
|
|
734
|
+
}
|