@sandrinio/vbounce 1.9.0 → 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.md +303 -19
- package/bin/vbounce.mjs +44 -0
- package/brains/AGENTS.md +51 -120
- package/brains/CHANGELOG.md +135 -0
- package/brains/CLAUDE.md +58 -133
- package/brains/GEMINI.md +68 -149
- package/brains/claude-agents/developer.md +6 -4
- package/brains/copilot/copilot-instructions.md +5 -0
- package/brains/cursor-rules/vbounce-process.mdc +3 -0
- package/brains/windsurf/.windsurfrules +5 -0
- package/package.json +1 -1
- package/scripts/close_sprint.mjs +41 -1
- package/scripts/complete_story.mjs +8 -0
- package/scripts/init_sprint.mjs +8 -0
- package/scripts/post_sprint_improve.mjs +486 -0
- package/scripts/product_graph.mjs +387 -0
- package/scripts/product_impact.mjs +167 -0
- package/scripts/suggest_improvements.mjs +206 -43
- package/skills/agent-team/SKILL.md +63 -28
- package/skills/agent-team/references/discovery.md +97 -0
- package/skills/agent-team/references/mid-sprint-triage.md +40 -26
- package/skills/doc-manager/SKILL.md +172 -19
- package/skills/improve/SKILL.md +151 -60
- package/skills/lesson/SKILL.md +14 -0
- package/skills/product-graph/SKILL.md +102 -0
- package/templates/bug.md +90 -0
- package/templates/change_request.md +105 -0
- package/templates/epic.md +19 -16
- package/templates/spike.md +143 -0
- package/templates/sprint.md +51 -17
- package/templates/sprint_report.md +6 -4
- package/templates/story.md +23 -8
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* post_sprint_improve.mjs
|
|
5
|
+
* Post-sprint self-improvement analyzer.
|
|
6
|
+
*
|
|
7
|
+
* Parses sprint report §5 Framework Self-Assessment tables, cross-references
|
|
8
|
+
* LESSONS.md for automation candidates, and checks archived sprint reports
|
|
9
|
+
* for recurring patterns. Outputs a structured improvement manifest.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* ./scripts/post_sprint_improve.mjs S-05
|
|
13
|
+
*
|
|
14
|
+
* Output: .bounce/improvement-manifest.json
|
|
15
|
+
* (consumed by suggest_improvements.mjs and the /improve skill)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import { fileURLToPath } from 'url';
|
|
21
|
+
|
|
22
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
24
|
+
|
|
25
|
+
const sprintId = process.argv[2];
|
|
26
|
+
if (!sprintId || !/^S-\d{2}$/.test(sprintId)) {
|
|
27
|
+
console.error('Usage: post_sprint_improve.mjs S-XX');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Impact Levels
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// P0 Critical — Blocks agent work or causes incorrect output. Fix before next sprint.
|
|
35
|
+
// P1 High — Causes rework (bounces, wasted tokens, repeated manual steps). Fix this cycle.
|
|
36
|
+
// P2 Medium — Friction that slows agents but doesn't block. Fix within 2 sprints.
|
|
37
|
+
// P3 Low — Nice-to-have polish. Batch with other improvements.
|
|
38
|
+
|
|
39
|
+
const IMPACT = {
|
|
40
|
+
P0: { level: 'P0', label: 'Critical', description: 'Blocks agent work or causes incorrect output' },
|
|
41
|
+
P1: { level: 'P1', label: 'High', description: 'Causes rework — bounces, wasted tokens, repeated manual steps' },
|
|
42
|
+
P2: { level: 'P2', label: 'Medium', description: 'Friction that slows agents but does not block' },
|
|
43
|
+
P3: { level: 'P3', label: 'Low', description: 'Polish — nice-to-have, batch with other improvements' },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// 1. Parse Sprint Report §5 Framework Self-Assessment
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Extract §5 findings from a sprint report file.
|
|
52
|
+
* Returns array of { area, finding, sourceAgent, severity, suggestedFix, sprintId }
|
|
53
|
+
*/
|
|
54
|
+
function parseRetroFindings(reportPath, reportSprintId) {
|
|
55
|
+
if (!fs.existsSync(reportPath)) return [];
|
|
56
|
+
|
|
57
|
+
const content = fs.readFileSync(reportPath, 'utf8');
|
|
58
|
+
const findings = [];
|
|
59
|
+
|
|
60
|
+
// Match §5 section (or "## 5. Retrospective" / "## 5. Framework Self-Assessment")
|
|
61
|
+
const section5Match = content.match(/## 5\.\s+(Retrospective|Framework Self-Assessment)[\s\S]*?(?=\n## 6\.|$)/);
|
|
62
|
+
if (!section5Match) return findings;
|
|
63
|
+
|
|
64
|
+
const section5 = section5Match[0];
|
|
65
|
+
|
|
66
|
+
// Extract subsection areas
|
|
67
|
+
const areas = ['Templates', 'Agent Handoffs', 'RAG Pipeline', 'Skills', 'Process Flow', 'Tooling & Scripts'];
|
|
68
|
+
|
|
69
|
+
for (const area of areas) {
|
|
70
|
+
// Find the area's table within §5
|
|
71
|
+
const areaRegex = new RegExp(`####?\\s+${area.replace('&', '&')}[\\s\\S]*?(?=\\n####?\\s|\\n## |\\n---\\s*$|$)`);
|
|
72
|
+
const areaMatch = section5.match(areaRegex);
|
|
73
|
+
if (!areaMatch) continue;
|
|
74
|
+
|
|
75
|
+
const areaContent = areaMatch[0];
|
|
76
|
+
|
|
77
|
+
// Parse table rows: | Finding | Source Agent | Severity | Suggested Fix |
|
|
78
|
+
const rows = areaContent.split('\n').filter(line =>
|
|
79
|
+
line.startsWith('|') &&
|
|
80
|
+
!line.includes('Finding') &&
|
|
81
|
+
!line.includes('---') &&
|
|
82
|
+
line.split('|').length >= 5
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
const cells = row.split('|').map(c => c.trim()).filter(Boolean);
|
|
87
|
+
if (cells.length >= 4) {
|
|
88
|
+
// Skip template placeholder rows
|
|
89
|
+
if (cells[0].startsWith('{') || cells[0].startsWith('e.g.')) continue;
|
|
90
|
+
|
|
91
|
+
findings.push({
|
|
92
|
+
area,
|
|
93
|
+
finding: cells[0],
|
|
94
|
+
sourceAgent: cells[1],
|
|
95
|
+
severity: cells[2],
|
|
96
|
+
suggestedFix: cells[3],
|
|
97
|
+
sprintId: reportSprintId,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return findings;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// 2. Parse LESSONS.md for automation candidates
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse LESSONS.md and classify each lesson by automation potential.
|
|
112
|
+
* Returns array of { date, title, whatHappened, rule, age, automationType, impact }
|
|
113
|
+
*/
|
|
114
|
+
function parseLessons(lessonsPath) {
|
|
115
|
+
if (!fs.existsSync(lessonsPath)) return [];
|
|
116
|
+
|
|
117
|
+
const content = fs.readFileSync(lessonsPath, 'utf8');
|
|
118
|
+
const lessons = [];
|
|
119
|
+
const today = new Date();
|
|
120
|
+
|
|
121
|
+
// Match lesson entries: ### [YYYY-MM-DD] Title
|
|
122
|
+
const entryRegex = /### \[(\d{4}-\d{2}-\d{2})\]\s+(.+?)(?=\n### \[|\n## |$)/gs;
|
|
123
|
+
let match;
|
|
124
|
+
|
|
125
|
+
while ((match = entryRegex.exec(content)) !== null) {
|
|
126
|
+
const date = match[1];
|
|
127
|
+
const title = match[2].trim();
|
|
128
|
+
const body = match[0];
|
|
129
|
+
|
|
130
|
+
const whatHappenedMatch = body.match(/\*\*What happened:\*\*\s*(.+)/);
|
|
131
|
+
const ruleMatch = body.match(/\*\*Rule:\*\*\s*(.+)/);
|
|
132
|
+
|
|
133
|
+
const lessonDate = new Date(date);
|
|
134
|
+
const ageInDays = Math.floor((today - lessonDate) / (1000 * 60 * 60 * 24));
|
|
135
|
+
const ageInSprints = Math.ceil(ageInDays / 14); // approximate 2-week sprints
|
|
136
|
+
|
|
137
|
+
const rule = ruleMatch ? ruleMatch[1].trim() : '';
|
|
138
|
+
|
|
139
|
+
// Classify automation potential based on rule keywords
|
|
140
|
+
const automationType = classifyLessonAutomation(rule);
|
|
141
|
+
|
|
142
|
+
lessons.push({
|
|
143
|
+
date,
|
|
144
|
+
title,
|
|
145
|
+
whatHappened: whatHappenedMatch ? whatHappenedMatch[1].trim() : '',
|
|
146
|
+
rule,
|
|
147
|
+
ageDays: ageInDays,
|
|
148
|
+
ageSprints: ageInSprints,
|
|
149
|
+
automationType,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return lessons;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Classify what type of automation a lesson rule could become.
|
|
158
|
+
*/
|
|
159
|
+
function classifyLessonAutomation(rule) {
|
|
160
|
+
const lower = rule.toLowerCase();
|
|
161
|
+
|
|
162
|
+
// Gate check patterns: "Always check...", "Never use...", "Must have..."
|
|
163
|
+
if (/always (check|verify|ensure|validate|confirm|test|run)/i.test(lower)) return 'gate_check';
|
|
164
|
+
if (/never (use|import|add|create|modify|delete|skip)/i.test(lower)) return 'gate_check';
|
|
165
|
+
if (/must (have|include|contain|use|be)/i.test(lower)) return 'gate_check';
|
|
166
|
+
if (/do not|don't|avoid/i.test(lower)) return 'gate_check';
|
|
167
|
+
|
|
168
|
+
// Script patterns: "Run X before Y", "Use X instead of Y"
|
|
169
|
+
if (/run .+ before/i.test(lower)) return 'script';
|
|
170
|
+
if (/use .+ instead of/i.test(lower)) return 'script';
|
|
171
|
+
|
|
172
|
+
// Template patterns: "Include X in...", "Add X to..."
|
|
173
|
+
if (/include .+ in/i.test(lower)) return 'template_field';
|
|
174
|
+
if (/add .+ to (the )?(story|epic|sprint|report|template)/i.test(lower)) return 'template_field';
|
|
175
|
+
|
|
176
|
+
// Agent config patterns: general rules about behavior
|
|
177
|
+
if (/always|never|before|after/i.test(lower)) return 'agent_config';
|
|
178
|
+
|
|
179
|
+
return 'agent_config'; // default: graduate to agent brain
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// 3. Cross-reference archived sprint reports for recurring patterns
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Find findings that recur across multiple sprint reports.
|
|
188
|
+
* Returns map of finding → { count, sprints, latestSeverity }
|
|
189
|
+
*/
|
|
190
|
+
function findRecurringPatterns(archiveDir, currentFindings) {
|
|
191
|
+
const allFindings = [...currentFindings];
|
|
192
|
+
|
|
193
|
+
// Read archived sprint reports
|
|
194
|
+
if (fs.existsSync(archiveDir)) {
|
|
195
|
+
const sprintDirs = fs.readdirSync(archiveDir).filter(d => /^S-\d{2}$/.test(d));
|
|
196
|
+
for (const dir of sprintDirs) {
|
|
197
|
+
const reportPath = path.join(archiveDir, dir, `sprint-report-${dir}.md`);
|
|
198
|
+
const archived = parseRetroFindings(reportPath, dir);
|
|
199
|
+
allFindings.push(...archived);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Group by normalized finding text (lowercase, trimmed)
|
|
204
|
+
const patterns = {};
|
|
205
|
+
for (const f of allFindings) {
|
|
206
|
+
// Normalize: lowercase, remove quotes, collapse whitespace
|
|
207
|
+
const key = f.finding.toLowerCase().replace(/["']/g, '').replace(/\s+/g, ' ').trim();
|
|
208
|
+
if (!patterns[key]) {
|
|
209
|
+
patterns[key] = {
|
|
210
|
+
finding: f.finding,
|
|
211
|
+
area: f.area,
|
|
212
|
+
count: 0,
|
|
213
|
+
sprints: [],
|
|
214
|
+
severities: [],
|
|
215
|
+
suggestedFixes: [],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
patterns[key].count++;
|
|
219
|
+
if (!patterns[key].sprints.includes(f.sprintId)) {
|
|
220
|
+
patterns[key].sprints.push(f.sprintId);
|
|
221
|
+
}
|
|
222
|
+
patterns[key].severities.push(f.severity);
|
|
223
|
+
if (f.suggestedFix && !patterns[key].suggestedFixes.includes(f.suggestedFix)) {
|
|
224
|
+
patterns[key].suggestedFixes.push(f.suggestedFix);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return patterns;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// 4. Check previous improvement effectiveness
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Read improvement-log.md and check if applied improvements resolved their findings.
|
|
237
|
+
*/
|
|
238
|
+
function checkImprovementEffectiveness(logPath, currentFindings) {
|
|
239
|
+
if (!fs.existsSync(logPath)) return [];
|
|
240
|
+
|
|
241
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
242
|
+
const unresolved = [];
|
|
243
|
+
|
|
244
|
+
// Extract applied items
|
|
245
|
+
const appliedMatch = content.match(/## Applied\n[\s\S]*?(?=\n## |$)/);
|
|
246
|
+
if (!appliedMatch) return [];
|
|
247
|
+
|
|
248
|
+
const rows = appliedMatch[0].split('\n')
|
|
249
|
+
.filter(l => l.startsWith('|') && !l.startsWith('| Sprint') && !l.includes('---'));
|
|
250
|
+
|
|
251
|
+
for (const row of rows) {
|
|
252
|
+
const cells = row.split('|').map(c => c.trim()).filter(Boolean);
|
|
253
|
+
if (cells.length >= 3) {
|
|
254
|
+
const appliedTitle = cells[1];
|
|
255
|
+
// Check if any current finding matches the applied improvement
|
|
256
|
+
const stillPresent = currentFindings.some(f =>
|
|
257
|
+
f.finding.toLowerCase().includes(appliedTitle.toLowerCase()) ||
|
|
258
|
+
appliedTitle.toLowerCase().includes(f.finding.toLowerCase().substring(0, 30))
|
|
259
|
+
);
|
|
260
|
+
if (stillPresent) {
|
|
261
|
+
unresolved.push({
|
|
262
|
+
title: appliedTitle,
|
|
263
|
+
appliedInSprint: cells[0],
|
|
264
|
+
status: 'UNRESOLVED — finding persists after improvement was applied',
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return unresolved;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// 5. Generate improvement proposals
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
function generateProposals(currentFindings, lessons, patterns, unresolvedImprovements) {
|
|
278
|
+
const proposals = [];
|
|
279
|
+
let id = 1;
|
|
280
|
+
|
|
281
|
+
// --- From §5 findings ---
|
|
282
|
+
for (const finding of currentFindings) {
|
|
283
|
+
const patternKey = finding.finding.toLowerCase().replace(/["']/g, '').replace(/\s+/g, ' ').trim();
|
|
284
|
+
const pattern = patterns[patternKey];
|
|
285
|
+
const isRecurring = pattern && pattern.sprints.length > 1;
|
|
286
|
+
|
|
287
|
+
// Determine impact
|
|
288
|
+
let impact;
|
|
289
|
+
if (finding.severity === 'Blocker' && isRecurring) {
|
|
290
|
+
impact = IMPACT.P0;
|
|
291
|
+
} else if (finding.severity === 'Blocker') {
|
|
292
|
+
impact = IMPACT.P1;
|
|
293
|
+
} else if (isRecurring) {
|
|
294
|
+
impact = IMPACT.P1;
|
|
295
|
+
} else {
|
|
296
|
+
impact = IMPACT.P2;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
proposals.push({
|
|
300
|
+
id: id++,
|
|
301
|
+
source: 'retro',
|
|
302
|
+
type: mapAreaToType(finding.area),
|
|
303
|
+
title: finding.finding,
|
|
304
|
+
area: finding.area,
|
|
305
|
+
sourceAgent: finding.sourceAgent,
|
|
306
|
+
severity: finding.severity,
|
|
307
|
+
suggestedFix: finding.suggestedFix,
|
|
308
|
+
impact,
|
|
309
|
+
recurring: isRecurring,
|
|
310
|
+
recurrenceCount: pattern ? pattern.sprints.length : 1,
|
|
311
|
+
recurrenceSprints: pattern ? pattern.sprints : [finding.sprintId],
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// --- From lessons: automation candidates ---
|
|
316
|
+
for (const lesson of lessons) {
|
|
317
|
+
// Only propose automation for lessons 3+ sprints old (graduation candidates)
|
|
318
|
+
// or lessons with clear mechanical rules regardless of age
|
|
319
|
+
const isGraduationCandidate = lesson.ageSprints >= 3;
|
|
320
|
+
const isMechanical = lesson.automationType === 'gate_check' || lesson.automationType === 'script';
|
|
321
|
+
|
|
322
|
+
if (!isGraduationCandidate && !isMechanical) continue;
|
|
323
|
+
|
|
324
|
+
let impact;
|
|
325
|
+
if (isMechanical) {
|
|
326
|
+
// Mechanical checks save tokens every sprint
|
|
327
|
+
impact = IMPACT.P1;
|
|
328
|
+
} else if (isGraduationCandidate) {
|
|
329
|
+
impact = IMPACT.P2;
|
|
330
|
+
} else {
|
|
331
|
+
impact = IMPACT.P3;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
proposals.push({
|
|
335
|
+
id: id++,
|
|
336
|
+
source: 'lesson',
|
|
337
|
+
type: lesson.automationType,
|
|
338
|
+
title: `Automate lesson: "${lesson.title}"`,
|
|
339
|
+
rule: lesson.rule,
|
|
340
|
+
whatHappened: lesson.whatHappened,
|
|
341
|
+
lessonDate: lesson.date,
|
|
342
|
+
ageSprints: lesson.ageSprints,
|
|
343
|
+
impact,
|
|
344
|
+
automationType: lesson.automationType,
|
|
345
|
+
automationDetail: generateAutomationDetail(lesson),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// --- From unresolved improvements ---
|
|
350
|
+
for (const unresolved of unresolvedImprovements) {
|
|
351
|
+
proposals.push({
|
|
352
|
+
id: id++,
|
|
353
|
+
source: 'effectiveness_check',
|
|
354
|
+
type: 're-examine',
|
|
355
|
+
title: `Unresolved: "${unresolved.title}"`,
|
|
356
|
+
detail: unresolved.status,
|
|
357
|
+
appliedInSprint: unresolved.appliedInSprint,
|
|
358
|
+
impact: IMPACT.P1, // Previous fix didn't work — escalate priority
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Sort by impact level (P0 first)
|
|
363
|
+
proposals.sort((a, b) => a.impact.level.localeCompare(b.impact.level));
|
|
364
|
+
|
|
365
|
+
return proposals;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function mapAreaToType(area) {
|
|
369
|
+
const map = {
|
|
370
|
+
'Templates': 'template_patch',
|
|
371
|
+
'Agent Handoffs': 'report_field',
|
|
372
|
+
'RAG Pipeline': 'tooling',
|
|
373
|
+
'Skills': 'skill_update',
|
|
374
|
+
'Process Flow': 'process_change',
|
|
375
|
+
'Tooling & Scripts': 'script',
|
|
376
|
+
};
|
|
377
|
+
return map[area] || 'other';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function generateAutomationDetail(lesson) {
|
|
381
|
+
switch (lesson.automationType) {
|
|
382
|
+
case 'gate_check':
|
|
383
|
+
return {
|
|
384
|
+
action: 'Add to gate-checks.json or pre_gate_runner.sh',
|
|
385
|
+
rationale: `Rule "${lesson.rule}" can be enforced mechanically via grep/lint pattern`,
|
|
386
|
+
effort: 'Low',
|
|
387
|
+
};
|
|
388
|
+
case 'script':
|
|
389
|
+
return {
|
|
390
|
+
action: 'Create or extend a validation script',
|
|
391
|
+
rationale: `Rule describes a procedural check that should run automatically`,
|
|
392
|
+
effort: 'Low-Medium',
|
|
393
|
+
};
|
|
394
|
+
case 'template_field':
|
|
395
|
+
return {
|
|
396
|
+
action: 'Add field or section to relevant template',
|
|
397
|
+
rationale: `Rule indicates missing information that should be captured at planning time`,
|
|
398
|
+
effort: 'Trivial',
|
|
399
|
+
};
|
|
400
|
+
case 'agent_config':
|
|
401
|
+
return {
|
|
402
|
+
action: 'Graduate to agent brain config (brains/claude-agents/*.md)',
|
|
403
|
+
rationale: `Lesson has been active ${lesson.ageSprints}+ sprints — promote to permanent rule`,
|
|
404
|
+
effort: 'Low',
|
|
405
|
+
};
|
|
406
|
+
default:
|
|
407
|
+
return { action: 'Review manually', rationale: 'Could not auto-classify', effort: 'Unknown' };
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ---------------------------------------------------------------------------
|
|
412
|
+
// Main
|
|
413
|
+
// ---------------------------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
const today = new Date().toISOString().split('T')[0];
|
|
416
|
+
const archiveDir = path.join(ROOT, '.bounce', 'archive');
|
|
417
|
+
const lessonsPath = path.join(ROOT, 'LESSONS.md');
|
|
418
|
+
const improvementLogPath = path.join(ROOT, '.bounce', 'improvement-log.md');
|
|
419
|
+
|
|
420
|
+
// Current sprint report
|
|
421
|
+
const reportPath = path.join(ROOT, '.bounce', `sprint-report-${sprintId}.md`);
|
|
422
|
+
const reportArchivePath = path.join(archiveDir, sprintId, `sprint-report-${sprintId}.md`);
|
|
423
|
+
const actualReportPath = fs.existsSync(reportPath) ? reportPath : reportArchivePath;
|
|
424
|
+
|
|
425
|
+
// 1. Parse current sprint retro
|
|
426
|
+
const currentFindings = parseRetroFindings(actualReportPath, sprintId);
|
|
427
|
+
console.log(` Retro findings from ${sprintId}: ${currentFindings.length}`);
|
|
428
|
+
|
|
429
|
+
// 2. Parse lessons
|
|
430
|
+
const lessons = parseLessons(lessonsPath);
|
|
431
|
+
console.log(` Lessons in LESSONS.md: ${lessons.length}`);
|
|
432
|
+
|
|
433
|
+
// 3. Cross-reference archived reports
|
|
434
|
+
const patterns = findRecurringPatterns(archiveDir, currentFindings);
|
|
435
|
+
const recurringCount = Object.values(patterns).filter(p => p.sprints.length > 1).length;
|
|
436
|
+
console.log(` Recurring patterns across sprints: ${recurringCount}`);
|
|
437
|
+
|
|
438
|
+
// 4. Check improvement effectiveness
|
|
439
|
+
const unresolved = checkImprovementEffectiveness(improvementLogPath, currentFindings);
|
|
440
|
+
if (unresolved.length > 0) {
|
|
441
|
+
console.log(` ⚠ Unresolved improvements from previous cycles: ${unresolved.length}`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// 5. Generate proposals
|
|
445
|
+
const proposals = generateProposals(currentFindings, lessons, patterns, unresolved);
|
|
446
|
+
|
|
447
|
+
// 6. Write manifest
|
|
448
|
+
const manifest = {
|
|
449
|
+
sprintId,
|
|
450
|
+
generatedAt: today,
|
|
451
|
+
impactLevels: IMPACT,
|
|
452
|
+
summary: {
|
|
453
|
+
totalProposals: proposals.length,
|
|
454
|
+
byImpact: {
|
|
455
|
+
P0: proposals.filter(p => p.impact.level === 'P0').length,
|
|
456
|
+
P1: proposals.filter(p => p.impact.level === 'P1').length,
|
|
457
|
+
P2: proposals.filter(p => p.impact.level === 'P2').length,
|
|
458
|
+
P3: proposals.filter(p => p.impact.level === 'P3').length,
|
|
459
|
+
},
|
|
460
|
+
bySource: {
|
|
461
|
+
retro: proposals.filter(p => p.source === 'retro').length,
|
|
462
|
+
lesson: proposals.filter(p => p.source === 'lesson').length,
|
|
463
|
+
effectiveness_check: proposals.filter(p => p.source === 'effectiveness_check').length,
|
|
464
|
+
},
|
|
465
|
+
byType: {},
|
|
466
|
+
},
|
|
467
|
+
proposals,
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
// Count by type
|
|
471
|
+
for (const p of proposals) {
|
|
472
|
+
manifest.summary.byType[p.type] = (manifest.summary.byType[p.type] || 0) + 1;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const manifestPath = path.join(ROOT, '.bounce', 'improvement-manifest.json');
|
|
476
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
477
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
478
|
+
|
|
479
|
+
console.log('');
|
|
480
|
+
console.log(`✓ Improvement manifest written to .bounce/improvement-manifest.json`);
|
|
481
|
+
console.log(` ${proposals.length} proposal(s): ${manifest.summary.byImpact.P0} P0, ${manifest.summary.byImpact.P1} P1, ${manifest.summary.byImpact.P2} P2, ${manifest.summary.byImpact.P3} P3`);
|
|
482
|
+
|
|
483
|
+
if (proposals.length > 0) {
|
|
484
|
+
console.log('');
|
|
485
|
+
console.log('Next: run `vbounce suggest ' + sprintId + '` to generate human-readable improvement suggestions.');
|
|
486
|
+
}
|