claude-autopm 1.29.0 → 1.30.1
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.md +29 -1
- package/autopm/.claude/scripts/pm/prd-new.js +33 -6
- package/autopm/.claude/scripts/pm/template-list.js +25 -3
- package/autopm/.claude/scripts/pm/template-new.js +25 -3
- package/autopm/lib/README-FILTER-SEARCH.md +285 -0
- package/autopm/lib/analytics-engine.js +689 -0
- package/autopm/lib/batch-processor-integration.js +366 -0
- package/autopm/lib/batch-processor.js +278 -0
- package/autopm/lib/burndown-chart.js +415 -0
- package/autopm/lib/conflict-history.js +316 -0
- package/autopm/lib/conflict-resolver.js +330 -0
- package/autopm/lib/dependency-analyzer.js +466 -0
- package/autopm/lib/filter-engine.js +414 -0
- package/autopm/lib/guide/interactive-guide.js +756 -0
- package/autopm/lib/guide/manager.js +663 -0
- package/autopm/lib/query-parser.js +322 -0
- package/autopm/lib/template-engine.js +347 -0
- package/autopm/lib/visual-diff.js +297 -0
- package/install/install.js +2 -1
- package/lib/ai-providers/base-provider.js +110 -0
- package/lib/conflict-history.js +316 -0
- package/lib/conflict-resolver.js +330 -0
- package/lib/visual-diff.js +297 -0
- package/package.json +1 -1
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BurndownChart - ASCII burndown chart generator
|
|
3
|
+
*
|
|
4
|
+
* Generates visual burndown charts comparing ideal vs actual task completion.
|
|
5
|
+
*
|
|
6
|
+
* @example Basic Usage
|
|
7
|
+
* ```javascript
|
|
8
|
+
* const BurndownChart = require('./lib/burndown-chart');
|
|
9
|
+
* const chart = new BurndownChart();
|
|
10
|
+
*
|
|
11
|
+
* const rendered = await chart.generate('epic-001', {
|
|
12
|
+
* basePath: '.claude'
|
|
13
|
+
* });
|
|
14
|
+
* console.log(rendered);
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* @example Custom Rendering
|
|
18
|
+
* ```javascript
|
|
19
|
+
* const chart = new BurndownChart({ width: 80, height: 20 });
|
|
20
|
+
*
|
|
21
|
+
* const ideal = [10, 8, 6, 4, 2, 0];
|
|
22
|
+
* const actual = [10, 9, 7, 5, 2, 0];
|
|
23
|
+
*
|
|
24
|
+
* const rendered = chart.renderChart(ideal, actual, {
|
|
25
|
+
* epicId: 'epic-001',
|
|
26
|
+
* epicTitle: 'User Authentication',
|
|
27
|
+
* startDate: '2025-10-01',
|
|
28
|
+
* endDate: '2025-10-05'
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @module BurndownChart
|
|
33
|
+
* @version 1.0.0
|
|
34
|
+
* @since v1.29.0
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const FilterEngine = require('./filter-engine');
|
|
38
|
+
const path = require('path');
|
|
39
|
+
|
|
40
|
+
class BurndownChart {
|
|
41
|
+
/**
|
|
42
|
+
* Create BurndownChart instance
|
|
43
|
+
*
|
|
44
|
+
* @param {Object} options - Configuration options
|
|
45
|
+
* @param {number} options.width - Chart width in characters (default: 60)
|
|
46
|
+
* @param {number} options.height - Chart height in lines (default: 15)
|
|
47
|
+
*/
|
|
48
|
+
constructor(options = {}) {
|
|
49
|
+
this.width = options.width || 60;
|
|
50
|
+
this.height = options.height || 15;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generate burndown chart for an epic
|
|
55
|
+
*
|
|
56
|
+
* @param {string} epicId - Epic ID
|
|
57
|
+
* @param {Object} options - Options
|
|
58
|
+
* @param {string} options.basePath - Base path (default: '.claude')
|
|
59
|
+
* @param {string} options.startDate - Start date (YYYY-MM-DD, default: epic created date)
|
|
60
|
+
* @param {number} options.days - Number of days (default: 30)
|
|
61
|
+
* @returns {Promise<string>} - Rendered ASCII chart
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* const chart = await generator.generate('epic-001');
|
|
65
|
+
* console.log(chart);
|
|
66
|
+
*/
|
|
67
|
+
async generate(epicId, options = {}) {
|
|
68
|
+
const basePath = options.basePath || '.claude';
|
|
69
|
+
const filterEngine = new FilterEngine({ basePath });
|
|
70
|
+
|
|
71
|
+
// Load epic and tasks
|
|
72
|
+
const epicDir = path.join(basePath, 'epics', epicId);
|
|
73
|
+
const tasks = await filterEngine.loadFiles(epicDir);
|
|
74
|
+
|
|
75
|
+
const epicFile = tasks.find(t => t.path.endsWith('epic.md'));
|
|
76
|
+
const taskFiles = tasks.filter(t => t.frontmatter.id && t.frontmatter.id !== epicId);
|
|
77
|
+
|
|
78
|
+
if (taskFiles.length === 0) {
|
|
79
|
+
return this._renderEmptyChart(epicId, epicFile);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Determine date range
|
|
83
|
+
let startDate = options.startDate;
|
|
84
|
+
if (!startDate && epicFile) {
|
|
85
|
+
startDate = epicFile.frontmatter.created || this._findEarliestDate(taskFiles);
|
|
86
|
+
}
|
|
87
|
+
if (!startDate) {
|
|
88
|
+
startDate = this._formatDate(new Date());
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const days = options.days || 30;
|
|
92
|
+
|
|
93
|
+
// Calculate burndown data
|
|
94
|
+
const ideal = this.calculateIdealBurndown(taskFiles.length, days);
|
|
95
|
+
const actual = this.calculateActualBurndown(taskFiles, startDate, days);
|
|
96
|
+
|
|
97
|
+
// Calculate velocity
|
|
98
|
+
const velocity = this._calculateVelocity(taskFiles, startDate, days);
|
|
99
|
+
|
|
100
|
+
// Calculate estimated completion
|
|
101
|
+
const estimatedCompletion = this._estimateCompletion(taskFiles, velocity, startDate);
|
|
102
|
+
|
|
103
|
+
// Render chart
|
|
104
|
+
return this.renderChart(ideal, actual, {
|
|
105
|
+
epicId,
|
|
106
|
+
epicTitle: epicFile ? epicFile.frontmatter.title : epicId,
|
|
107
|
+
startDate,
|
|
108
|
+
endDate: this._addDays(startDate, days),
|
|
109
|
+
velocity,
|
|
110
|
+
estimatedCompletion
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Calculate ideal burndown line
|
|
116
|
+
*
|
|
117
|
+
* @param {number} total - Total number of tasks
|
|
118
|
+
* @param {number} days - Number of days
|
|
119
|
+
* @returns {Array<number>} - Ideal burndown values
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* const ideal = chart.calculateIdealBurndown(25, 30);
|
|
123
|
+
* // Returns: [25, 24.17, 23.33, ..., 0]
|
|
124
|
+
*/
|
|
125
|
+
calculateIdealBurndown(total, days) {
|
|
126
|
+
const ideal = [];
|
|
127
|
+
const rate = total / days;
|
|
128
|
+
|
|
129
|
+
for (let i = 0; i <= days; i++) {
|
|
130
|
+
const remaining = Math.max(0, total - (rate * i));
|
|
131
|
+
ideal.push(Math.round(remaining * 100) / 100);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return ideal;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Calculate actual burndown from task completion
|
|
139
|
+
*
|
|
140
|
+
* @param {Array} tasks - Task files with frontmatter
|
|
141
|
+
* @param {string} startDate - Start date (YYYY-MM-DD)
|
|
142
|
+
* @param {number} days - Number of days
|
|
143
|
+
* @returns {Array<number>} - Actual burndown values
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* const actual = chart.calculateActualBurndown(tasks, '2025-10-01', 30);
|
|
147
|
+
* // Returns: [25, 24, 22, ..., 3]
|
|
148
|
+
*/
|
|
149
|
+
calculateActualBurndown(tasks, startDate, days) {
|
|
150
|
+
const actual = [];
|
|
151
|
+
const total = tasks.length;
|
|
152
|
+
|
|
153
|
+
const startDateObj = new Date(startDate);
|
|
154
|
+
|
|
155
|
+
for (let i = 0; i <= days; i++) {
|
|
156
|
+
const currentDate = new Date(startDateObj);
|
|
157
|
+
currentDate.setDate(currentDate.getDate() + i);
|
|
158
|
+
const currentDateStr = this._formatDate(currentDate);
|
|
159
|
+
|
|
160
|
+
// Count tasks completed BEFORE this date (not including current day)
|
|
161
|
+
const completedCount = tasks.filter(t => {
|
|
162
|
+
// Support both frontmatter and direct object formats
|
|
163
|
+
const status = t.frontmatter ? t.frontmatter.status : t.status;
|
|
164
|
+
const completed = t.frontmatter ? t.frontmatter.completed : t.completed;
|
|
165
|
+
|
|
166
|
+
if (status !== 'completed' || !completed) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
return completed < currentDateStr;
|
|
170
|
+
}).length;
|
|
171
|
+
|
|
172
|
+
const remaining = total - completedCount;
|
|
173
|
+
actual.push(remaining);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return actual;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Render ASCII chart
|
|
181
|
+
*
|
|
182
|
+
* @param {Array<number>} ideal - Ideal burndown values
|
|
183
|
+
* @param {Array<number>} actual - Actual burndown values
|
|
184
|
+
* @param {Object} metadata - Chart metadata
|
|
185
|
+
* @returns {string} - Rendered ASCII chart
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* const chart = generator.renderChart(ideal, actual, {
|
|
189
|
+
* epicId: 'epic-001',
|
|
190
|
+
* epicTitle: 'Authentication',
|
|
191
|
+
* startDate: '2025-10-01',
|
|
192
|
+
* endDate: '2025-10-30'
|
|
193
|
+
* });
|
|
194
|
+
*/
|
|
195
|
+
renderChart(ideal, actual, metadata) {
|
|
196
|
+
const lines = [];
|
|
197
|
+
|
|
198
|
+
// Title
|
|
199
|
+
lines.push(`Epic: ${metadata.epicTitle} (${metadata.epicId})`);
|
|
200
|
+
lines.push(`Burndown Chart - ${this._formatDateForDisplay(metadata.startDate)} to ${this._formatDateForDisplay(metadata.endDate)}`);
|
|
201
|
+
lines.push('');
|
|
202
|
+
|
|
203
|
+
// Find max value for scaling
|
|
204
|
+
const maxValue = Math.max(...ideal, ...actual, 1);
|
|
205
|
+
|
|
206
|
+
// Generate chart lines
|
|
207
|
+
const chartHeight = this.height;
|
|
208
|
+
const chartWidth = this.width;
|
|
209
|
+
|
|
210
|
+
for (let row = 0; row < chartHeight; row++) {
|
|
211
|
+
const value = maxValue - (row * maxValue / (chartHeight - 1));
|
|
212
|
+
const valueLabel = String(Math.round(value)).padStart(4, ' ');
|
|
213
|
+
|
|
214
|
+
let line = `${valueLabel} `;
|
|
215
|
+
|
|
216
|
+
if (row === 0) {
|
|
217
|
+
line += '┤';
|
|
218
|
+
} else if (row === chartHeight - 1) {
|
|
219
|
+
line += '└';
|
|
220
|
+
} else {
|
|
221
|
+
line += '│';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Plot points
|
|
225
|
+
const pointsPerCol = Math.max(1, ideal.length / chartWidth);
|
|
226
|
+
|
|
227
|
+
for (let col = 0; col < chartWidth; col++) {
|
|
228
|
+
const dataIndex = Math.floor(col * pointsPerCol);
|
|
229
|
+
|
|
230
|
+
if (dataIndex >= ideal.length) {
|
|
231
|
+
line += ' ';
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const idealValue = ideal[dataIndex];
|
|
236
|
+
const actualValue = actual[dataIndex] !== undefined ? actual[dataIndex] : idealValue;
|
|
237
|
+
|
|
238
|
+
const idealY = Math.round((chartHeight - 1) * (1 - idealValue / maxValue));
|
|
239
|
+
const actualY = Math.round((chartHeight - 1) * (1 - actualValue / maxValue));
|
|
240
|
+
|
|
241
|
+
if (row === idealY && row === actualY) {
|
|
242
|
+
line += '●'; // Both lines at same point
|
|
243
|
+
} else if (row === idealY) {
|
|
244
|
+
line += '━'; // Ideal line
|
|
245
|
+
} else if (row === actualY) {
|
|
246
|
+
line += '╲'; // Actual line
|
|
247
|
+
} else if (row > idealY && row <= actualY && actualValue > idealValue) {
|
|
248
|
+
line += '╲'; // Filling actual line when behind
|
|
249
|
+
} else if (row <= idealY && row > actualY && actualValue < idealValue) {
|
|
250
|
+
line += '╲'; // Filling actual line when ahead
|
|
251
|
+
} else {
|
|
252
|
+
line += ' ';
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
lines.push(line);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// X-axis
|
|
260
|
+
const xAxis = ' ' + '─'.repeat(chartWidth);
|
|
261
|
+
lines.push(xAxis);
|
|
262
|
+
|
|
263
|
+
// Date labels
|
|
264
|
+
const startDateLabel = this._formatDateForDisplay(metadata.startDate);
|
|
265
|
+
const endDateLabel = this._formatDateForDisplay(metadata.endDate);
|
|
266
|
+
const midLabel = '';
|
|
267
|
+
|
|
268
|
+
const dateLabels = ` ${startDateLabel}${' '.repeat(chartWidth - startDateLabel.length - endDateLabel.length)}${endDateLabel}`;
|
|
269
|
+
lines.push(dateLabels);
|
|
270
|
+
lines.push('');
|
|
271
|
+
|
|
272
|
+
// Legend
|
|
273
|
+
lines.push('Legend: ━━━ Ideal ╲╲╲ Actual');
|
|
274
|
+
lines.push('');
|
|
275
|
+
|
|
276
|
+
// Status
|
|
277
|
+
const status = this._calculateStatus(ideal, actual);
|
|
278
|
+
lines.push(`Status: ${status.text}`);
|
|
279
|
+
|
|
280
|
+
if (metadata.velocity) {
|
|
281
|
+
lines.push(`Velocity: ${metadata.velocity} tasks/week`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (metadata.estimatedCompletion) {
|
|
285
|
+
lines.push(`Estimated Completion: ${metadata.estimatedCompletion}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return lines.join('\n');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ============================================================================
|
|
292
|
+
// Private Helper Methods
|
|
293
|
+
// ============================================================================
|
|
294
|
+
|
|
295
|
+
_renderEmptyChart(epicId, epicFile) {
|
|
296
|
+
const lines = [];
|
|
297
|
+
const title = epicFile ? epicFile.frontmatter.title : epicId;
|
|
298
|
+
|
|
299
|
+
lines.push(`Epic: ${title} (${epicId})`);
|
|
300
|
+
lines.push('Burndown Chart');
|
|
301
|
+
lines.push('');
|
|
302
|
+
lines.push('No tasks found for this epic.');
|
|
303
|
+
lines.push('');
|
|
304
|
+
|
|
305
|
+
return lines.join('\n');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
_findEarliestDate(taskFiles) {
|
|
309
|
+
const dates = taskFiles
|
|
310
|
+
.map(t => t.frontmatter.created)
|
|
311
|
+
.filter(d => d)
|
|
312
|
+
.sort();
|
|
313
|
+
|
|
314
|
+
return dates.length > 0 ? dates[0] : this._formatDate(new Date());
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
_calculateVelocity(taskFiles, startDate, days) {
|
|
318
|
+
const completedTasks = taskFiles.filter(t => {
|
|
319
|
+
const status = t.frontmatter ? t.frontmatter.status : t.status;
|
|
320
|
+
const completed = t.frontmatter ? t.frontmatter.completed : t.completed;
|
|
321
|
+
return status === 'completed' && completed;
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const weeks = days / 7;
|
|
325
|
+
return weeks > 0 ? Math.round((completedTasks.length / weeks) * 10) / 10 : 0;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
_estimateCompletion(taskFiles, velocity, startDate) {
|
|
329
|
+
if (velocity === 0) return null;
|
|
330
|
+
|
|
331
|
+
const remainingTasks = taskFiles.filter(t => {
|
|
332
|
+
const status = t.frontmatter ? t.frontmatter.status : t.status;
|
|
333
|
+
return status !== 'completed';
|
|
334
|
+
}).length;
|
|
335
|
+
|
|
336
|
+
const weeksRemaining = remainingTasks / velocity;
|
|
337
|
+
const daysRemaining = Math.ceil(weeksRemaining * 7);
|
|
338
|
+
|
|
339
|
+
const completionDate = new Date(startDate);
|
|
340
|
+
completionDate.setDate(completionDate.getDate() + daysRemaining);
|
|
341
|
+
|
|
342
|
+
return this._formatDate(completionDate);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
_calculateStatus(ideal, actual) {
|
|
346
|
+
if (ideal.length === 0 || actual.length === 0) {
|
|
347
|
+
return { text: 'ON TRACK', ahead: false, behind: false };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Compare at the last non-zero point, or at 75% through the period
|
|
351
|
+
// This gives a better sense of whether we're ahead/behind during execution
|
|
352
|
+
const compareIndex = Math.floor(Math.min(ideal.length, actual.length) * 0.75);
|
|
353
|
+
const idealValue = ideal[compareIndex];
|
|
354
|
+
const actualValue = actual[compareIndex];
|
|
355
|
+
|
|
356
|
+
const difference = idealValue - actualValue;
|
|
357
|
+
|
|
358
|
+
// If there's minimal difference, we're on track
|
|
359
|
+
if (Math.abs(difference) < 0.5) {
|
|
360
|
+
return { text: 'ON TRACK', ahead: false, behind: false };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Calculate percentage difference using initial value as base
|
|
364
|
+
const initialValue = ideal[0] > 0 ? ideal[0] : 10;
|
|
365
|
+
const percentDiff = Math.abs(difference / initialValue) * 100;
|
|
366
|
+
|
|
367
|
+
if (percentDiff < 5) {
|
|
368
|
+
return { text: 'ON TRACK', ahead: false, behind: false };
|
|
369
|
+
} else if (difference > 0) {
|
|
370
|
+
// Actual is lower than ideal = ahead of schedule (burned down more tasks)
|
|
371
|
+
return {
|
|
372
|
+
text: `AHEAD OF SCHEDULE (${Math.round(percentDiff)}% ahead)`,
|
|
373
|
+
ahead: true,
|
|
374
|
+
behind: false
|
|
375
|
+
};
|
|
376
|
+
} else {
|
|
377
|
+
// Actual is higher than ideal = behind schedule (more tasks remaining)
|
|
378
|
+
return {
|
|
379
|
+
text: `BEHIND SCHEDULE (${Math.round(percentDiff)}% behind)`,
|
|
380
|
+
ahead: false,
|
|
381
|
+
behind: true
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
_formatDate(date) {
|
|
387
|
+
if (typeof date === 'string') {
|
|
388
|
+
date = new Date(date);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const year = date.getFullYear();
|
|
392
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
393
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
394
|
+
return `${year}-${month}-${day}`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
_formatDateForDisplay(dateStr) {
|
|
398
|
+
if (!dateStr) return '';
|
|
399
|
+
|
|
400
|
+
const date = new Date(dateStr);
|
|
401
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
402
|
+
const month = months[date.getMonth()];
|
|
403
|
+
const day = date.getDate();
|
|
404
|
+
|
|
405
|
+
return `${month} ${day}`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
_addDays(dateStr, days) {
|
|
409
|
+
const date = new Date(dateStr);
|
|
410
|
+
date.setDate(date.getDate() + days);
|
|
411
|
+
return this._formatDate(date);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
module.exports = BurndownChart;
|