@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.
Files changed (39) hide show
  1. package/README.md +439 -159
  2. package/bin/vbounce.mjs +255 -25
  3. package/brains/AGENTS.md +62 -24
  4. package/brains/CHANGELOG.md +24 -2
  5. package/brains/CLAUDE.md +124 -30
  6. package/brains/GEMINI.md +64 -27
  7. package/brains/SETUP.md +15 -15
  8. package/brains/claude-agents/architect.md +1 -1
  9. package/brains/claude-agents/developer.md +7 -5
  10. package/brains/claude-agents/devops.md +1 -1
  11. package/brains/claude-agents/qa.md +1 -1
  12. package/brains/claude-agents/scribe.md +1 -1
  13. package/brains/copilot/copilot-instructions.md +8 -3
  14. package/brains/cursor-rules/vbounce-docs.mdc +2 -2
  15. package/brains/cursor-rules/vbounce-process.mdc +6 -3
  16. package/brains/cursor-rules/vbounce-rules.mdc +2 -2
  17. package/brains/windsurf/.windsurfrules +7 -2
  18. package/docs/HOTFIX_EDGE_CASES.md +1 -1
  19. package/package.json +5 -5
  20. package/scripts/close_sprint.mjs +32 -1
  21. package/scripts/doctor.mjs +3 -3
  22. package/scripts/hotfix_manager.sh +2 -2
  23. package/scripts/init_gate_config.sh +1 -1
  24. package/scripts/post_sprint_improve.mjs +486 -0
  25. package/scripts/pre_gate_common.sh +1 -1
  26. package/scripts/pre_gate_runner.sh +1 -1
  27. package/scripts/suggest_improvements.mjs +207 -44
  28. package/scripts/validate_report.mjs +1 -1
  29. package/scripts/verify_framework.mjs +1 -1
  30. package/skills/agent-team/SKILL.md +48 -25
  31. package/skills/agent-team/references/discovery.md +97 -0
  32. package/skills/doc-manager/SKILL.md +146 -22
  33. package/skills/improve/SKILL.md +149 -58
  34. package/skills/lesson/SKILL.md +14 -0
  35. package/templates/epic.md +19 -16
  36. package/templates/spike.md +143 -0
  37. package/templates/sprint.md +32 -12
  38. package/templates/sprint_report.md +6 -4
  39. package/templates/story.md +23 -8
@@ -1,6 +1,6 @@
1
- # V-Bounce OS — Windsurf Rules
1
+ # V-Bounce Engine — Windsurf Rules
2
2
 
3
- This project uses V-Bounce OS. You are operating in Tier 4 (Awareness) mode.
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 OS Hotfix (L1 Trivial)** workflow.
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": "1.8.0",
4
- "description": "V-Bounce OS: Turn your AI coding assistant into a full engineering team through structured SDLC skills.",
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-os.git"
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-os/issues"
31
+ "url": "https://github.com/sandrinio/v-bounce-engine/issues"
32
32
  },
33
- "homepage": "https://github.com/sandrinio/v-bounce-os#readme",
33
+ "homepage": "https://github.com/sandrinio/v-bounce-engine#readme",
34
34
  "files": [
35
35
  "bin",
36
36
  "brains",
@@ -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. Run: vbounce trends && vbounce suggest ${sprintId}`);
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.`);
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * doctor.mjs
5
- * V-Bounce OS Health Check — validates all configs, templates, state files
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 OS template`);
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 OS Health Check');
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 OS: Hotfix Manager
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 OS — Hotfix Manager"
17
+ echo "V-Bounce Engine — Hotfix Manager"
18
18
  echo ""
19
19
  echo "Usage: ./scripts/hotfix_manager.sh <command> [args]"
20
20
  echo ""
@@ -17,7 +17,7 @@ YELLOW='\033[1;33m'
17
17
  CYAN='\033[0;36m'
18
18
  NC='\033[0m'
19
19
 
20
- echo -e "${CYAN}V-Bounce OS Gate Config Initializer${NC}"
20
+ echo -e "${CYAN}V-Bounce Engine Gate Config Initializer${NC}"
21
21
  echo -e "Project: ${PROJECT_PATH}"
22
22
  echo ""
23
23
 
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # pre_gate_common.sh — Shared gate check functions for V-Bounce OS
2
+ # pre_gate_common.sh — Shared gate check functions for V-Bounce Engine
3
3
  # Sourced by pre_gate_runner.sh. Never run directly.
4
4
 
5
5
  set -euo pipefail
@@ -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 OS Pre-Gate Scanner${NC}"
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 ""