@girardmedia/bootspring 2.5.0 → 2.5.2
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 +9 -403
- package/bin/bootspring.js +1 -96
- package/dist/cli/index.js +65134 -0
- package/dist/cli-launcher.js +92 -0
- package/dist/core/index.d.ts +2110 -5582
- package/dist/core/index.js +2 -0
- package/dist/core.js +21123 -5413
- package/dist/mcp/index.d.ts +357 -1
- package/dist/mcp/index.js +2 -0
- package/dist/mcp-server.js +51948 -1976
- package/package.json +27 -63
- package/scripts/postinstall.cjs +144 -0
- package/LICENSE +0 -29
- package/dist/cli/index.cjs +0 -20776
- package/generators/api-docs.js +0 -827
- package/generators/decisions.js +0 -655
- package/generators/generate.js +0 -595
- package/generators/health.js +0 -942
- package/generators/index.ts +0 -82
- package/generators/presets/full.js +0 -28
- package/generators/presets/index.js +0 -12
- package/generators/presets/minimal.js +0 -29
- package/generators/presets/standard.js +0 -28
- package/generators/questionnaire.js +0 -414
- package/generators/sections/advanced.js +0 -136
- package/generators/sections/ai.js +0 -106
- package/generators/sections/auth.js +0 -89
- package/generators/sections/backend.js +0 -146
- package/generators/sections/business.js +0 -118
- package/generators/sections/content.js +0 -300
- package/generators/sections/deployment.js +0 -139
- package/generators/sections/features.js +0 -122
- package/generators/sections/frontend.js +0 -118
- package/generators/sections/identity.js +0 -76
- package/generators/sections/index.js +0 -40
- package/generators/sections/instructions.js +0 -146
- package/generators/sections/payments.js +0 -104
- package/generators/sections/plugins.js +0 -142
- package/generators/sections/pre-build.js +0 -130
- package/generators/sections/security.js +0 -127
- package/generators/sections/technical.js +0 -171
- package/generators/sections/testing.js +0 -125
- package/generators/sections/workflow.js +0 -104
- package/generators/sprint.js +0 -675
- package/generators/templates/agents.template.js +0 -199
- package/generators/templates/assistant-context.template.js +0 -83
- package/generators/templates/build-planning.template.js +0 -708
- package/generators/templates/claude.template.js +0 -379
- package/generators/templates/content.template.js +0 -819
- package/generators/templates/index.js +0 -16
- package/generators/templates/planning.template.js +0 -515
- package/generators/templates/seed.template.js +0 -109
- package/generators/visual-doc-generator.js +0 -910
- package/scripts/postinstall.js +0 -197
- /package/{claude-commands → assets/claude-commands}/agent.md +0 -0
- /package/{claude-commands → assets/claude-commands}/bs.md +0 -0
- /package/{claude-commands → assets/claude-commands}/build.md +0 -0
- /package/{claude-commands → assets/claude-commands}/skill.md +0 -0
- /package/{claude-commands → assets/claude-commands}/todo.md +0 -0
package/generators/sprint.js
DELETED
|
@@ -1,675 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bootspring SPRINT.md Generator
|
|
3
|
-
*
|
|
4
|
-
* Generates sprint documentation with goals, tasks, blockers,
|
|
5
|
-
* dependencies, progress tracking, and burndown metrics.
|
|
6
|
-
*
|
|
7
|
-
* @package bootspring
|
|
8
|
-
* @module generators/sprint
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const path = require('path');
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Sprint status values
|
|
16
|
-
*/
|
|
17
|
-
const SPRINT_STATUS = {
|
|
18
|
-
PLANNING: 'planning',
|
|
19
|
-
ACTIVE: 'active',
|
|
20
|
-
REVIEW: 'review',
|
|
21
|
-
COMPLETED: 'completed'
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Task status values
|
|
26
|
-
*/
|
|
27
|
-
const TASK_STATUS = {
|
|
28
|
-
TODO: 'todo',
|
|
29
|
-
IN_PROGRESS: 'in_progress',
|
|
30
|
-
BLOCKED: 'blocked',
|
|
31
|
-
IN_REVIEW: 'in_review',
|
|
32
|
-
DONE: 'done'
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Priority levels
|
|
37
|
-
*/
|
|
38
|
-
const PRIORITY = {
|
|
39
|
-
CRITICAL: { value: 1, label: 'Critical', emoji: '🔴' },
|
|
40
|
-
HIGH: { value: 2, label: 'High', emoji: '🟠' },
|
|
41
|
-
MEDIUM: { value: 3, label: 'Medium', emoji: '🟡' },
|
|
42
|
-
LOW: { value: 4, label: 'Low', emoji: '🟢' }
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Default sprint configuration
|
|
47
|
-
*/
|
|
48
|
-
const DEFAULT_CONFIG = {
|
|
49
|
-
sprintDuration: 14, // days
|
|
50
|
-
workingHoursPerDay: 8,
|
|
51
|
-
velocityBuffer: 0.8, // 80% of capacity
|
|
52
|
-
includeWeekends: false
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Sprint Document Generator
|
|
57
|
-
*/
|
|
58
|
-
class SprintGenerator {
|
|
59
|
-
constructor(options = {}) {
|
|
60
|
-
this.projectRoot = options.projectRoot || process.cwd();
|
|
61
|
-
this.config = { ...DEFAULT_CONFIG, ...options };
|
|
62
|
-
this.sprintDataPath = path.join(this.projectRoot, '.bootspring', 'sprint.json');
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Load current sprint data
|
|
67
|
-
*/
|
|
68
|
-
loadSprintData() {
|
|
69
|
-
try {
|
|
70
|
-
if (fs.existsSync(this.sprintDataPath)) {
|
|
71
|
-
return JSON.parse(fs.readFileSync(this.sprintDataPath, 'utf-8'));
|
|
72
|
-
}
|
|
73
|
-
} catch (_err) {
|
|
74
|
-
// Return default
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return this.createDefaultSprint();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Save sprint data
|
|
82
|
-
*/
|
|
83
|
-
saveSprintData(data) {
|
|
84
|
-
const dir = path.dirname(this.sprintDataPath);
|
|
85
|
-
if (!fs.existsSync(dir)) {
|
|
86
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
87
|
-
}
|
|
88
|
-
fs.writeFileSync(this.sprintDataPath, JSON.stringify(data, null, 2));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Create default sprint structure
|
|
93
|
-
*/
|
|
94
|
-
createDefaultSprint() {
|
|
95
|
-
const now = new Date();
|
|
96
|
-
const endDate = new Date(now);
|
|
97
|
-
endDate.setDate(endDate.getDate() + this.config.sprintDuration);
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
id: `sprint-${now.getTime()}`,
|
|
101
|
-
name: `Sprint ${this.getSprintNumber()}`,
|
|
102
|
-
status: SPRINT_STATUS.PLANNING,
|
|
103
|
-
startDate: now.toISOString().split('T')[0],
|
|
104
|
-
endDate: endDate.toISOString().split('T')[0],
|
|
105
|
-
goals: [],
|
|
106
|
-
tasks: [],
|
|
107
|
-
blockers: [],
|
|
108
|
-
metrics: {
|
|
109
|
-
totalPoints: 0,
|
|
110
|
-
completedPoints: 0,
|
|
111
|
-
velocity: 0
|
|
112
|
-
},
|
|
113
|
-
dailyProgress: [],
|
|
114
|
-
team: [],
|
|
115
|
-
notes: ''
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Get sprint number based on history
|
|
121
|
-
*/
|
|
122
|
-
getSprintNumber() {
|
|
123
|
-
const historyPath = path.join(this.projectRoot, '.bootspring', 'sprint-history.json');
|
|
124
|
-
try {
|
|
125
|
-
if (fs.existsSync(historyPath)) {
|
|
126
|
-
const history = JSON.parse(fs.readFileSync(historyPath, 'utf-8'));
|
|
127
|
-
return (history.sprints?.length || 0) + 1;
|
|
128
|
-
}
|
|
129
|
-
} catch (_err) {
|
|
130
|
-
// Return default
|
|
131
|
-
}
|
|
132
|
-
return 1;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Add a goal to the sprint
|
|
137
|
-
*/
|
|
138
|
-
addGoal(sprint, goal) {
|
|
139
|
-
sprint.goals.push({
|
|
140
|
-
id: `goal-${Date.now()}`,
|
|
141
|
-
description: goal.description,
|
|
142
|
-
success_criteria: goal.success_criteria || [],
|
|
143
|
-
status: 'pending',
|
|
144
|
-
created: new Date().toISOString()
|
|
145
|
-
});
|
|
146
|
-
return sprint;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Add a task to the sprint
|
|
151
|
-
*/
|
|
152
|
-
addTask(sprint, task) {
|
|
153
|
-
sprint.tasks.push({
|
|
154
|
-
id: `task-${Date.now()}`,
|
|
155
|
-
title: task.title,
|
|
156
|
-
description: task.description || '',
|
|
157
|
-
assignee: task.assignee || null,
|
|
158
|
-
estimate: task.estimate || 0, // story points or hours
|
|
159
|
-
status: TASK_STATUS.TODO,
|
|
160
|
-
priority: task.priority || 'medium',
|
|
161
|
-
dependencies: task.dependencies || [],
|
|
162
|
-
blockedBy: task.blockedBy || [],
|
|
163
|
-
labels: task.labels || [],
|
|
164
|
-
created: new Date().toISOString(),
|
|
165
|
-
started: null,
|
|
166
|
-
completed: null
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// Update metrics
|
|
170
|
-
sprint.metrics.totalPoints += task.estimate || 0;
|
|
171
|
-
|
|
172
|
-
return sprint;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Add a blocker
|
|
177
|
-
*/
|
|
178
|
-
addBlocker(sprint, blocker) {
|
|
179
|
-
sprint.blockers.push({
|
|
180
|
-
id: `blocker-${Date.now()}`,
|
|
181
|
-
description: blocker.description,
|
|
182
|
-
affectedTasks: blocker.affectedTasks || [],
|
|
183
|
-
severity: blocker.severity || 'medium',
|
|
184
|
-
status: 'open',
|
|
185
|
-
owner: blocker.owner || null,
|
|
186
|
-
created: new Date().toISOString(),
|
|
187
|
-
resolved: null
|
|
188
|
-
});
|
|
189
|
-
return sprint;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Update task status
|
|
194
|
-
*/
|
|
195
|
-
updateTaskStatus(sprint, taskId, newStatus) {
|
|
196
|
-
const task = sprint.tasks.find(t => t.id === taskId);
|
|
197
|
-
if (task) {
|
|
198
|
-
const oldStatus = task.status;
|
|
199
|
-
task.status = newStatus;
|
|
200
|
-
|
|
201
|
-
if (newStatus === TASK_STATUS.IN_PROGRESS && !task.started) {
|
|
202
|
-
task.started = new Date().toISOString();
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (newStatus === TASK_STATUS.DONE && !task.completed) {
|
|
206
|
-
task.completed = new Date().toISOString();
|
|
207
|
-
sprint.metrics.completedPoints += task.estimate || 0;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// If reverting from done, adjust points
|
|
211
|
-
if (oldStatus === TASK_STATUS.DONE && newStatus !== TASK_STATUS.DONE) {
|
|
212
|
-
sprint.metrics.completedPoints -= task.estimate || 0;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return sprint;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Record daily progress
|
|
220
|
-
*/
|
|
221
|
-
recordDailyProgress(sprint) {
|
|
222
|
-
const today = new Date().toISOString().split('T')[0];
|
|
223
|
-
|
|
224
|
-
// Check if already recorded today
|
|
225
|
-
const existingIndex = sprint.dailyProgress.findIndex(p => p.date === today);
|
|
226
|
-
|
|
227
|
-
const progress = {
|
|
228
|
-
date: today,
|
|
229
|
-
completedPoints: sprint.metrics.completedPoints,
|
|
230
|
-
remainingPoints: sprint.metrics.totalPoints - sprint.metrics.completedPoints,
|
|
231
|
-
tasksDone: sprint.tasks.filter(t => t.status === TASK_STATUS.DONE).length,
|
|
232
|
-
tasksInProgress: sprint.tasks.filter(t => t.status === TASK_STATUS.IN_PROGRESS).length,
|
|
233
|
-
blockerCount: sprint.blockers.filter(b => b.status === 'open').length
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
if (existingIndex >= 0) {
|
|
237
|
-
sprint.dailyProgress[existingIndex] = progress;
|
|
238
|
-
} else {
|
|
239
|
-
sprint.dailyProgress.push(progress);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return sprint;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Calculate burndown data
|
|
247
|
-
*/
|
|
248
|
-
calculateBurndown(sprint) {
|
|
249
|
-
const startDate = new Date(sprint.startDate);
|
|
250
|
-
const endDate = new Date(sprint.endDate);
|
|
251
|
-
const totalDays = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24));
|
|
252
|
-
const idealPointsPerDay = sprint.metrics.totalPoints / totalDays;
|
|
253
|
-
|
|
254
|
-
const burndown = [];
|
|
255
|
-
const currentDate = new Date(startDate);
|
|
256
|
-
let idealRemaining = sprint.metrics.totalPoints;
|
|
257
|
-
|
|
258
|
-
while (currentDate <= endDate) {
|
|
259
|
-
const dateStr = currentDate.toISOString().split('T')[0];
|
|
260
|
-
const actualProgress = sprint.dailyProgress.find(p => p.date === dateStr);
|
|
261
|
-
|
|
262
|
-
burndown.push({
|
|
263
|
-
date: dateStr,
|
|
264
|
-
ideal: Math.max(0, Math.round(idealRemaining * 10) / 10),
|
|
265
|
-
actual: actualProgress ? actualProgress.remainingPoints : null
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
idealRemaining -= idealPointsPerDay;
|
|
269
|
-
currentDate.setDate(currentDate.getDate() + 1);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return burndown;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Calculate sprint metrics
|
|
277
|
-
*/
|
|
278
|
-
calculateMetrics(sprint) {
|
|
279
|
-
const tasks = sprint.tasks;
|
|
280
|
-
const now = new Date();
|
|
281
|
-
const startDate = new Date(sprint.startDate);
|
|
282
|
-
const endDate = new Date(sprint.endDate);
|
|
283
|
-
|
|
284
|
-
const totalDays = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24));
|
|
285
|
-
const elapsedDays = Math.ceil((now - startDate) / (1000 * 60 * 60 * 24));
|
|
286
|
-
const remainingDays = Math.max(0, totalDays - elapsedDays);
|
|
287
|
-
|
|
288
|
-
const completedTasks = tasks.filter(t => t.status === TASK_STATUS.DONE);
|
|
289
|
-
const inProgressTasks = tasks.filter(t => t.status === TASK_STATUS.IN_PROGRESS);
|
|
290
|
-
const blockedTasks = tasks.filter(t => t.status === TASK_STATUS.BLOCKED);
|
|
291
|
-
const todoTasks = tasks.filter(t => t.status === TASK_STATUS.TODO);
|
|
292
|
-
|
|
293
|
-
const completedPoints = completedTasks.reduce((sum, t) => sum + (t.estimate || 0), 0);
|
|
294
|
-
const totalPoints = tasks.reduce((sum, t) => sum + (t.estimate || 0), 0);
|
|
295
|
-
const remainingPoints = totalPoints - completedPoints;
|
|
296
|
-
|
|
297
|
-
const percentComplete = totalPoints > 0 ? Math.round((completedPoints / totalPoints) * 100) : 0;
|
|
298
|
-
const velocity = elapsedDays > 0 ? Math.round((completedPoints / elapsedDays) * 10) / 10 : 0;
|
|
299
|
-
const projectedCompletion = velocity > 0 ? Math.ceil(remainingPoints / velocity) : null;
|
|
300
|
-
|
|
301
|
-
return {
|
|
302
|
-
totalTasks: tasks.length,
|
|
303
|
-
completedTasks: completedTasks.length,
|
|
304
|
-
inProgressTasks: inProgressTasks.length,
|
|
305
|
-
blockedTasks: blockedTasks.length,
|
|
306
|
-
todoTasks: todoTasks.length,
|
|
307
|
-
totalPoints,
|
|
308
|
-
completedPoints,
|
|
309
|
-
remainingPoints,
|
|
310
|
-
percentComplete,
|
|
311
|
-
velocity,
|
|
312
|
-
elapsedDays,
|
|
313
|
-
remainingDays,
|
|
314
|
-
projectedCompletion,
|
|
315
|
-
onTrack: projectedCompletion !== null && projectedCompletion <= remainingDays,
|
|
316
|
-
openBlockers: sprint.blockers.filter(b => b.status === 'open').length
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Generate SPRINT.md content
|
|
322
|
-
*/
|
|
323
|
-
generate(sprint = null) {
|
|
324
|
-
if (!sprint) {
|
|
325
|
-
sprint = this.loadSprintData();
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const metrics = this.calculateMetrics(sprint);
|
|
329
|
-
const burndown = this.calculateBurndown(sprint);
|
|
330
|
-
const now = new Date().toISOString().split('T')[0];
|
|
331
|
-
|
|
332
|
-
const sections = [];
|
|
333
|
-
|
|
334
|
-
// Header
|
|
335
|
-
sections.push(`# ${sprint.name}
|
|
336
|
-
|
|
337
|
-
**Status:** ${this.formatStatus(sprint.status)}
|
|
338
|
-
**Period:** ${sprint.startDate} → ${sprint.endDate}
|
|
339
|
-
**Last Updated:** ${now}
|
|
340
|
-
|
|
341
|
-
---`);
|
|
342
|
-
|
|
343
|
-
// Progress Overview
|
|
344
|
-
const progressBar = this.generateProgressBar(metrics.percentComplete);
|
|
345
|
-
sections.push(`## Progress Overview
|
|
346
|
-
|
|
347
|
-
${progressBar} **${metrics.percentComplete}%** Complete
|
|
348
|
-
|
|
349
|
-
| Metric | Value |
|
|
350
|
-
|--------|-------|
|
|
351
|
-
| Total Tasks | ${metrics.totalTasks} |
|
|
352
|
-
| Completed | ${metrics.completedTasks} |
|
|
353
|
-
| In Progress | ${metrics.inProgressTasks} |
|
|
354
|
-
| Blocked | ${metrics.blockedTasks} |
|
|
355
|
-
| Story Points | ${metrics.completedPoints}/${metrics.totalPoints} |
|
|
356
|
-
| Velocity | ${metrics.velocity} pts/day |
|
|
357
|
-
| Days Remaining | ${metrics.remainingDays} |
|
|
358
|
-
| Status | ${metrics.onTrack ? '✅ On Track' : '⚠️ At Risk'} |
|
|
359
|
-
|
|
360
|
-
---`);
|
|
361
|
-
|
|
362
|
-
// Sprint Goals
|
|
363
|
-
if (sprint.goals.length > 0) {
|
|
364
|
-
sections.push(`## Sprint Goals
|
|
365
|
-
|
|
366
|
-
${sprint.goals.map((goal, i) => {
|
|
367
|
-
const statusIcon = goal.status === 'completed' ? '✅' : goal.status === 'in_progress' ? '🔄' : '⬜';
|
|
368
|
-
return `${i + 1}. ${statusIcon} **${goal.description}**
|
|
369
|
-
${goal.success_criteria.length > 0 ? goal.success_criteria.map(c => ` - ${c}`).join('\n') : ''}`;
|
|
370
|
-
}).join('\n\n')}
|
|
371
|
-
|
|
372
|
-
---`);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Tasks by Status
|
|
376
|
-
sections.push(`## Tasks
|
|
377
|
-
|
|
378
|
-
### 🔄 In Progress (${metrics.inProgressTasks})
|
|
379
|
-
|
|
380
|
-
${this.formatTaskList(sprint.tasks.filter(t => t.status === TASK_STATUS.IN_PROGRESS))}
|
|
381
|
-
|
|
382
|
-
### 🚫 Blocked (${metrics.blockedTasks})
|
|
383
|
-
|
|
384
|
-
${this.formatTaskList(sprint.tasks.filter(t => t.status === TASK_STATUS.BLOCKED))}
|
|
385
|
-
|
|
386
|
-
### 📋 To Do (${metrics.todoTasks})
|
|
387
|
-
|
|
388
|
-
${this.formatTaskList(sprint.tasks.filter(t => t.status === TASK_STATUS.TODO))}
|
|
389
|
-
|
|
390
|
-
### ✅ Done (${metrics.completedTasks})
|
|
391
|
-
|
|
392
|
-
${this.formatTaskList(sprint.tasks.filter(t => t.status === TASK_STATUS.DONE), true)}
|
|
393
|
-
|
|
394
|
-
---`);
|
|
395
|
-
|
|
396
|
-
// Blockers
|
|
397
|
-
if (sprint.blockers.length > 0) {
|
|
398
|
-
const openBlockers = sprint.blockers.filter(b => b.status === 'open');
|
|
399
|
-
const resolvedBlockers = sprint.blockers.filter(b => b.status === 'resolved');
|
|
400
|
-
|
|
401
|
-
sections.push(`## Blockers
|
|
402
|
-
|
|
403
|
-
### 🚨 Open (${openBlockers.length})
|
|
404
|
-
|
|
405
|
-
${openBlockers.length > 0 ? openBlockers.map(b => `- **${b.description}**
|
|
406
|
-
- Severity: ${b.severity}
|
|
407
|
-
- Owner: ${b.owner || 'Unassigned'}
|
|
408
|
-
- Affects: ${b.affectedTasks.length} task(s)`).join('\n\n') : '_No open blockers_'}
|
|
409
|
-
|
|
410
|
-
### ✅ Resolved (${resolvedBlockers.length})
|
|
411
|
-
|
|
412
|
-
${resolvedBlockers.length > 0 ? resolvedBlockers.map(b => `- ~~${b.description}~~ (resolved ${b.resolved || 'unknown'})`).join('\n') : '_No resolved blockers_'}
|
|
413
|
-
|
|
414
|
-
---`);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Burndown Chart (ASCII)
|
|
418
|
-
sections.push(`## Burndown
|
|
419
|
-
|
|
420
|
-
\`\`\`
|
|
421
|
-
${this.generateAsciiBurndown(burndown, metrics.totalPoints)}
|
|
422
|
-
\`\`\`
|
|
423
|
-
|
|
424
|
-
---`);
|
|
425
|
-
|
|
426
|
-
// Daily Log
|
|
427
|
-
if (sprint.dailyProgress.length > 0) {
|
|
428
|
-
const recentProgress = sprint.dailyProgress.slice(-7);
|
|
429
|
-
sections.push(`## Daily Progress (Last 7 Days)
|
|
430
|
-
|
|
431
|
-
| Date | Points Done | Remaining | Tasks Done | Blockers |
|
|
432
|
-
|------|-------------|-----------|------------|----------|
|
|
433
|
-
${recentProgress.map(p => `| ${p.date} | ${p.completedPoints} | ${p.remainingPoints} | ${p.tasksDone} | ${p.blockerCount} |`).join('\n')}
|
|
434
|
-
|
|
435
|
-
---`);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Team
|
|
439
|
-
if (sprint.team.length > 0) {
|
|
440
|
-
sections.push(`## Team
|
|
441
|
-
|
|
442
|
-
${sprint.team.map(member => `- **${member.name}** - ${member.role || 'Team Member'}
|
|
443
|
-
- Tasks: ${sprint.tasks.filter(t => t.assignee === member.name).length}
|
|
444
|
-
- Points: ${sprint.tasks.filter(t => t.assignee === member.name).reduce((s, t) => s + (t.estimate || 0), 0)}`).join('\n\n')}
|
|
445
|
-
|
|
446
|
-
---`);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Notes
|
|
450
|
-
if (sprint.notes) {
|
|
451
|
-
sections.push(`## Notes
|
|
452
|
-
|
|
453
|
-
${sprint.notes}
|
|
454
|
-
|
|
455
|
-
---`);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Footer
|
|
459
|
-
sections.push(`---
|
|
460
|
-
|
|
461
|
-
*Generated by [Bootspring](https://bootspring.com) Sprint Generator*
|
|
462
|
-
*Sprint ID: ${sprint.id}*
|
|
463
|
-
`);
|
|
464
|
-
|
|
465
|
-
return sections.join('\n\n');
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* Format status with emoji
|
|
470
|
-
*/
|
|
471
|
-
formatStatus(status) {
|
|
472
|
-
const statusMap = {
|
|
473
|
-
[SPRINT_STATUS.PLANNING]: '📝 Planning',
|
|
474
|
-
[SPRINT_STATUS.ACTIVE]: '🚀 Active',
|
|
475
|
-
[SPRINT_STATUS.REVIEW]: '🔍 Review',
|
|
476
|
-
[SPRINT_STATUS.COMPLETED]: '✅ Completed'
|
|
477
|
-
};
|
|
478
|
-
return statusMap[status] || status;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Generate progress bar
|
|
483
|
-
*/
|
|
484
|
-
generateProgressBar(percent, width = 20) {
|
|
485
|
-
const filled = Math.round((percent / 100) * width);
|
|
486
|
-
const empty = width - filled;
|
|
487
|
-
return `[${'█'.repeat(filled)}${'░'.repeat(empty)}]`;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Format task list
|
|
492
|
-
*/
|
|
493
|
-
formatTaskList(tasks, collapsed = false) {
|
|
494
|
-
if (tasks.length === 0) {
|
|
495
|
-
return '_No tasks_';
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
return tasks.map(task => {
|
|
499
|
-
const priority = PRIORITY[task.priority?.toUpperCase()] || PRIORITY.MEDIUM;
|
|
500
|
-
const estimate = task.estimate ? `(${task.estimate} pts)` : '';
|
|
501
|
-
const assignee = task.assignee ? `@${task.assignee}` : '';
|
|
502
|
-
const labels = task.labels.length > 0 ? task.labels.map(l => `\`${l}\``).join(' ') : '';
|
|
503
|
-
|
|
504
|
-
if (collapsed) {
|
|
505
|
-
return `- [x] ${task.title} ${estimate}`;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
return `- [ ] ${priority.emoji} **${task.title}** ${estimate}
|
|
509
|
-
${task.description ? task.description + '\n ' : ''}${assignee} ${labels}`.trim();
|
|
510
|
-
}).join('\n\n');
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* Generate ASCII burndown chart
|
|
515
|
-
*/
|
|
516
|
-
generateAsciiBurndown(burndown, maxPoints) {
|
|
517
|
-
const height = 10;
|
|
518
|
-
const width = Math.min(burndown.length, 30);
|
|
519
|
-
const pointsPerRow = maxPoints / height;
|
|
520
|
-
|
|
521
|
-
const chart = [];
|
|
522
|
-
|
|
523
|
-
// Y-axis labels
|
|
524
|
-
for (let row = 0; row <= height; row++) {
|
|
525
|
-
const points = Math.round(maxPoints - (row * pointsPerRow));
|
|
526
|
-
const label = points.toString().padStart(4);
|
|
527
|
-
let line = `${label} |`;
|
|
528
|
-
|
|
529
|
-
// Plot points
|
|
530
|
-
for (let col = 0; col < width && col < burndown.length; col++) {
|
|
531
|
-
const data = burndown[col];
|
|
532
|
-
const idealRow = Math.round((maxPoints - data.ideal) / pointsPerRow);
|
|
533
|
-
const actualRow = data.actual !== null ? Math.round((maxPoints - data.actual) / pointsPerRow) : null;
|
|
534
|
-
|
|
535
|
-
if (row === idealRow && actualRow === row) {
|
|
536
|
-
line += '◆'; // Both ideal and actual
|
|
537
|
-
} else if (row === idealRow) {
|
|
538
|
-
line += '─'; // Ideal line
|
|
539
|
-
} else if (row === actualRow) {
|
|
540
|
-
line += '●'; // Actual point
|
|
541
|
-
} else {
|
|
542
|
-
line += ' ';
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
chart.push(line);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// X-axis
|
|
550
|
-
chart.push(' +' + '─'.repeat(width));
|
|
551
|
-
chart.push(' ' + burndown.slice(0, width).map((_, i) => (i % 5 === 0 ? '|' : ' ')).join(''));
|
|
552
|
-
|
|
553
|
-
// Legend
|
|
554
|
-
chart.push('');
|
|
555
|
-
chart.push('Legend: ─ Ideal ● Actual ◆ Both');
|
|
556
|
-
|
|
557
|
-
return chart.join('\n');
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* Import tasks from todo.md
|
|
562
|
-
*/
|
|
563
|
-
importFromTodo(sprint, todoContent) {
|
|
564
|
-
const lines = todoContent.split('\n');
|
|
565
|
-
const tasks = [];
|
|
566
|
-
|
|
567
|
-
for (const line of lines) {
|
|
568
|
-
const match = line.match(/^[-*]\s*\[([ x])\]\s*(.+)$/);
|
|
569
|
-
if (match) {
|
|
570
|
-
const done = match[1] === 'x';
|
|
571
|
-
const title = match[2].trim();
|
|
572
|
-
|
|
573
|
-
tasks.push({
|
|
574
|
-
title,
|
|
575
|
-
estimate: this.estimateFromTitle(title),
|
|
576
|
-
status: done ? TASK_STATUS.DONE : TASK_STATUS.TODO,
|
|
577
|
-
priority: this.inferPriority(title)
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
for (const task of tasks) {
|
|
583
|
-
this.addTask(sprint, task);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
return sprint;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* Estimate points from task title (heuristic)
|
|
591
|
-
*/
|
|
592
|
-
estimateFromTitle(title) {
|
|
593
|
-
const lower = title.toLowerCase();
|
|
594
|
-
|
|
595
|
-
if (lower.includes('fix') || lower.includes('typo') || lower.includes('update')) {
|
|
596
|
-
return 1;
|
|
597
|
-
}
|
|
598
|
-
if (lower.includes('add') || lower.includes('implement') || lower.includes('create')) {
|
|
599
|
-
return 3;
|
|
600
|
-
}
|
|
601
|
-
if (lower.includes('refactor') || lower.includes('redesign') || lower.includes('migrate')) {
|
|
602
|
-
return 5;
|
|
603
|
-
}
|
|
604
|
-
if (lower.includes('integrate') || lower.includes('authentication') || lower.includes('payment')) {
|
|
605
|
-
return 8;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
return 2; // Default
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
/**
|
|
612
|
-
* Infer priority from title
|
|
613
|
-
*/
|
|
614
|
-
inferPriority(title) {
|
|
615
|
-
const lower = title.toLowerCase();
|
|
616
|
-
|
|
617
|
-
if (lower.includes('critical') || lower.includes('urgent') || lower.includes('security')) {
|
|
618
|
-
return 'critical';
|
|
619
|
-
}
|
|
620
|
-
if (lower.includes('important') || lower.includes('high')) {
|
|
621
|
-
return 'high';
|
|
622
|
-
}
|
|
623
|
-
if (lower.includes('low') || lower.includes('nice to have')) {
|
|
624
|
-
return 'low';
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
return 'medium';
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* Generate SPRINT.md from project state
|
|
633
|
-
*/
|
|
634
|
-
function generate(options = {}) {
|
|
635
|
-
const generator = new SprintGenerator(options);
|
|
636
|
-
const sprint = generator.loadSprintData();
|
|
637
|
-
return generator.generate(sprint);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
/**
|
|
641
|
-
* Create a new sprint
|
|
642
|
-
*/
|
|
643
|
-
function createSprint(options = {}) {
|
|
644
|
-
const generator = new SprintGenerator(options);
|
|
645
|
-
const sprint = generator.createDefaultSprint();
|
|
646
|
-
generator.saveSprintData(sprint);
|
|
647
|
-
return sprint;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
/**
|
|
651
|
-
* Load current sprint
|
|
652
|
-
*/
|
|
653
|
-
function loadSprint(options = {}) {
|
|
654
|
-
const generator = new SprintGenerator(options);
|
|
655
|
-
return generator.loadSprintData();
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
/**
|
|
659
|
-
* Save sprint data
|
|
660
|
-
*/
|
|
661
|
-
function saveSprint(sprint, options = {}) {
|
|
662
|
-
const generator = new SprintGenerator(options);
|
|
663
|
-
generator.saveSprintData(sprint);
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
module.exports = {
|
|
667
|
-
SprintGenerator,
|
|
668
|
-
generate,
|
|
669
|
-
createSprint,
|
|
670
|
-
loadSprint,
|
|
671
|
-
saveSprint,
|
|
672
|
-
SPRINT_STATUS,
|
|
673
|
-
TASK_STATUS,
|
|
674
|
-
PRIORITY
|
|
675
|
-
};
|