@sandrinio/vbounce 1.8.0 → 2.0.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 +439 -159
- package/bin/vbounce.mjs +255 -25
- package/brains/AGENTS.md +62 -24
- package/brains/CHANGELOG.md +24 -2
- package/brains/CLAUDE.md +124 -30
- package/brains/GEMINI.md +64 -27
- package/brains/SETUP.md +15 -15
- package/brains/claude-agents/architect.md +1 -1
- package/brains/claude-agents/developer.md +7 -5
- package/brains/claude-agents/devops.md +1 -1
- package/brains/claude-agents/qa.md +1 -1
- package/brains/claude-agents/scribe.md +1 -1
- package/brains/copilot/copilot-instructions.md +8 -3
- package/brains/cursor-rules/vbounce-docs.mdc +2 -2
- package/brains/cursor-rules/vbounce-process.mdc +6 -3
- package/brains/cursor-rules/vbounce-rules.mdc +2 -2
- package/brains/windsurf/.windsurfrules +7 -2
- package/docs/HOTFIX_EDGE_CASES.md +1 -1
- package/package.json +5 -5
- package/scripts/close_sprint.mjs +32 -1
- package/scripts/doctor.mjs +3 -3
- package/scripts/hotfix_manager.sh +2 -2
- package/scripts/init_gate_config.sh +1 -1
- package/scripts/post_sprint_improve.mjs +486 -0
- package/scripts/pre_gate_common.sh +1 -1
- package/scripts/pre_gate_runner.sh +1 -1
- package/scripts/suggest_improvements.mjs +207 -44
- package/scripts/validate_report.mjs +1 -1
- package/scripts/verify_framework.mjs +1 -1
- package/skills/agent-team/SKILL.md +48 -25
- package/skills/agent-team/references/discovery.md +97 -0
- package/skills/doc-manager/SKILL.md +146 -22
- package/skills/improve/SKILL.md +149 -58
- package/skills/lesson/SKILL.md +14 -0
- package/templates/epic.md +19 -16
- package/templates/spike.md +143 -0
- package/templates/sprint.md +32 -12
- package/templates/sprint_report.md +6 -4
- package/templates/story.md +23 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# V-Bounce
|
|
1
|
+
# V-Bounce Engine — Windsurf Rules
|
|
2
2
|
|
|
3
|
-
This project uses V-Bounce
|
|
3
|
+
This project uses V-Bounce Engine. You are operating in Tier 4 (Awareness) mode.
|
|
4
4
|
|
|
5
5
|
## Before Writing Code
|
|
6
6
|
|
|
@@ -20,8 +20,13 @@ This project uses V-Bounce OS. You are operating in Tier 4 (Awareness) mode.
|
|
|
20
20
|
vbounce state show # where is the sprint right now?
|
|
21
21
|
vbounce validate report <f> # is this report valid?
|
|
22
22
|
vbounce doctor # is the framework healthy?
|
|
23
|
+
vbounce improve S-XX # run self-improvement pipeline
|
|
23
24
|
```
|
|
24
25
|
|
|
26
|
+
## Self-Improvement
|
|
27
|
+
|
|
28
|
+
After sprint close, V-Bounce auto-analyzes retro findings + LESSONS.md + cross-sprint patterns → generates `.bounce/improvement-suggestions.md` with impact levels (P0 Critical → P3 Low). Use `/improve` skill to apply approved changes.
|
|
29
|
+
|
|
25
30
|
## Critical Rules
|
|
26
31
|
|
|
27
32
|
- Read LESSONS.md before coding. No exceptions.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Hotfix Workflow: Edge Cases & Mitigations
|
|
2
2
|
|
|
3
|
-
This document outlines the critical edge cases, failure modes, and required mitigations for the **V-Bounce
|
|
3
|
+
This document outlines the critical edge cases, failure modes, and required mitigations for the **V-Bounce Engine Hotfix (L1 Trivial)** workflow.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sandrinio/vbounce",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "V-Bounce
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "V-Bounce Engine: Turn your AI coding assistant into a full engineering team through structured SDLC skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"vbounce": "./bin/vbounce.mjs"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
14
|
-
"url": "git+https://github.com/sandrinio/v-bounce-
|
|
14
|
+
"url": "git+https://github.com/sandrinio/v-bounce-engine.git"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"ai",
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
"author": "sandrinio",
|
|
29
29
|
"license": "MIT",
|
|
30
30
|
"bugs": {
|
|
31
|
-
"url": "https://github.com/sandrinio/v-bounce-
|
|
31
|
+
"url": "https://github.com/sandrinio/v-bounce-engine/issues"
|
|
32
32
|
},
|
|
33
|
-
"homepage": "https://github.com/sandrinio/v-bounce-
|
|
33
|
+
"homepage": "https://github.com/sandrinio/v-bounce-engine#readme",
|
|
34
34
|
"files": [
|
|
35
35
|
"bin",
|
|
36
36
|
"brains",
|
package/scripts/close_sprint.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import fs from 'fs';
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
|
+
import { spawnSync } from 'child_process';
|
|
14
15
|
|
|
15
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
17
|
const ROOT = path.resolve(__dirname, '..');
|
|
@@ -81,6 +82,35 @@ console.log(`✓ Updated state.json`);
|
|
|
81
82
|
const sprintPlanPath = `product_plans/sprints/sprint-${sprintNum}`;
|
|
82
83
|
const archivePath = `product_plans/archive/sprints/sprint-${sprintNum}`;
|
|
83
84
|
|
|
85
|
+
// 7. Auto-run improvement pipeline
|
|
86
|
+
console.log('');
|
|
87
|
+
console.log('Running self-improvement pipeline...');
|
|
88
|
+
const suggestScript = path.join(__dirname, 'suggest_improvements.mjs');
|
|
89
|
+
if (fs.existsSync(suggestScript)) {
|
|
90
|
+
// Run trends first (if available)
|
|
91
|
+
const trendsScript = path.join(__dirname, 'sprint_trends.mjs');
|
|
92
|
+
if (fs.existsSync(trendsScript)) {
|
|
93
|
+
const trendsResult = spawnSync(process.execPath, [trendsScript], {
|
|
94
|
+
stdio: 'inherit',
|
|
95
|
+
cwd: process.cwd(),
|
|
96
|
+
});
|
|
97
|
+
if (trendsResult.status !== 0) {
|
|
98
|
+
console.warn(' ⚠ Trends analysis returned non-zero — continuing.');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Run suggest (which internally runs post_sprint_improve.mjs)
|
|
103
|
+
const suggestResult = spawnSync(process.execPath, [suggestScript, sprintId], {
|
|
104
|
+
stdio: 'inherit',
|
|
105
|
+
cwd: process.cwd(),
|
|
106
|
+
});
|
|
107
|
+
if (suggestResult.status !== 0) {
|
|
108
|
+
console.warn(' ⚠ Improvement suggestions returned non-zero.');
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
console.warn(' ⚠ suggest_improvements.mjs not found — skipping improvement pipeline.');
|
|
112
|
+
}
|
|
113
|
+
|
|
84
114
|
console.log('');
|
|
85
115
|
console.log('Manual steps remaining:');
|
|
86
116
|
console.log(` 1. Archive sprint plan folder:`);
|
|
@@ -89,6 +119,7 @@ console.log(` 2. Update Delivery Plan §4 Completed Sprints with a summary row`
|
|
|
89
119
|
console.log(` 3. Remove delivered stories from Delivery Plan §3 Backlog`);
|
|
90
120
|
console.log(` 4. Delete sprint branch (after merge to main):`);
|
|
91
121
|
console.log(` git branch -d sprint/${sprintId}`);
|
|
92
|
-
console.log(` 5.
|
|
122
|
+
console.log(` 5. Review .bounce/improvement-suggestions.md — approve/reject/defer each item`);
|
|
123
|
+
console.log(` 6. Run /improve to apply approved changes with brain-file sync`);
|
|
93
124
|
console.log('');
|
|
94
125
|
console.log(`✓ Sprint ${sprintId} closed.`);
|
package/scripts/doctor.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* doctor.mjs
|
|
5
|
-
* V-Bounce
|
|
5
|
+
* V-Bounce Engine Health Check — validates all configs, templates, state files
|
|
6
6
|
* Usage: vbounce doctor
|
|
7
7
|
*/
|
|
8
8
|
|
|
@@ -42,7 +42,7 @@ const templatesDir = path.join(ROOT, 'templates');
|
|
|
42
42
|
let templateCount = 0;
|
|
43
43
|
for (const t of requiredTemplates) {
|
|
44
44
|
if (fs.existsSync(path.join(templatesDir, t))) templateCount++;
|
|
45
|
-
else fail(`templates/${t} missing`, `Create from V-Bounce
|
|
45
|
+
else fail(`templates/${t} missing`, `Create from V-Bounce Engine template`);
|
|
46
46
|
}
|
|
47
47
|
if (templateCount === requiredTemplates.length) pass(`templates/ complete (${templateCount}/${requiredTemplates.length})`);
|
|
48
48
|
|
|
@@ -130,7 +130,7 @@ if (fs.existsSync(path.join(ROOT, 'vbounce.config.json'))) {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
// Print results
|
|
133
|
-
console.log('\nV-Bounce
|
|
133
|
+
console.log('\nV-Bounce Engine Health Check');
|
|
134
134
|
console.log('========================');
|
|
135
135
|
checks.forEach(c => console.log(c));
|
|
136
136
|
console.log('');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
|
-
# V-Bounce
|
|
3
|
+
# V-Bounce Engine: Hotfix Manager
|
|
4
4
|
# Handles edge cases for L1 Trivial tasks to save tokens and ensure framework integrity.
|
|
5
5
|
|
|
6
6
|
set -euo pipefail
|
|
@@ -14,7 +14,7 @@ REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || {
|
|
|
14
14
|
COMMAND="${1:-}"
|
|
15
15
|
|
|
16
16
|
function show_help {
|
|
17
|
-
echo "V-Bounce
|
|
17
|
+
echo "V-Bounce Engine — Hotfix Manager"
|
|
18
18
|
echo ""
|
|
19
19
|
echo "Usage: ./scripts/hotfix_manager.sh <command> [args]"
|
|
20
20
|
echo ""
|
|
@@ -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
|
+
}
|
|
@@ -31,7 +31,7 @@ fi
|
|
|
31
31
|
# Resolve to absolute path
|
|
32
32
|
WORKTREE_PATH="$(cd "$WORKTREE_PATH" && pwd)"
|
|
33
33
|
|
|
34
|
-
echo -e "${CYAN}V-Bounce
|
|
34
|
+
echo -e "${CYAN}V-Bounce Engine Pre-Gate Scanner${NC}"
|
|
35
35
|
echo -e "Gate: ${YELLOW}${GATE_TYPE}${NC}"
|
|
36
36
|
echo -e "Target: ${WORKTREE_PATH}"
|
|
37
37
|
echo ""
|