@sandrinio/vbounce 1.6.0 → 1.7.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 (46) hide show
  1. package/README.md +108 -18
  2. package/bin/vbounce.mjs +291 -146
  3. package/brains/AGENTS.md +5 -5
  4. package/brains/CHANGELOG.md +88 -1
  5. package/brains/CLAUDE.md +22 -17
  6. package/brains/GEMINI.md +40 -4
  7. package/brains/SETUP.md +11 -5
  8. package/brains/claude-agents/architect.md +13 -6
  9. package/brains/claude-agents/developer.md +2 -2
  10. package/brains/claude-agents/qa.md +16 -7
  11. package/brains/copilot/copilot-instructions.md +49 -0
  12. package/brains/cursor-rules/vbounce-process.mdc +2 -2
  13. package/brains/windsurf/.windsurfrules +30 -0
  14. package/package.json +2 -4
  15. package/scripts/close_sprint.mjs +94 -0
  16. package/scripts/complete_story.mjs +113 -0
  17. package/scripts/doctor.mjs +144 -0
  18. package/scripts/init_sprint.mjs +121 -0
  19. package/scripts/prep_arch_context.mjs +178 -0
  20. package/scripts/prep_qa_context.mjs +134 -0
  21. package/scripts/prep_sprint_context.mjs +118 -0
  22. package/scripts/prep_sprint_summary.mjs +154 -0
  23. package/scripts/sprint_trends.mjs +160 -0
  24. package/scripts/suggest_improvements.mjs +200 -0
  25. package/scripts/update_state.mjs +132 -0
  26. package/scripts/validate_bounce_readiness.mjs +125 -0
  27. package/scripts/validate_report.mjs +39 -2
  28. package/scripts/validate_sprint_plan.mjs +117 -0
  29. package/scripts/validate_state.mjs +99 -0
  30. package/skills/agent-team/SKILL.md +35 -17
  31. package/skills/agent-team/references/cleanup.md +42 -0
  32. package/skills/agent-team/references/delivery-sync.md +43 -0
  33. package/skills/agent-team/references/git-strategy.md +52 -0
  34. package/skills/agent-team/references/mid-sprint-triage.md +71 -0
  35. package/skills/agent-team/references/report-naming.md +34 -0
  36. package/skills/doc-manager/SKILL.md +5 -4
  37. package/skills/improve/SKILL.md +1 -1
  38. package/skills/lesson/SKILL.md +23 -0
  39. package/templates/delivery_plan.md +1 -1
  40. package/templates/hotfix.md +1 -1
  41. package/templates/sprint.md +65 -13
  42. package/templates/sprint_report.md +8 -1
  43. package/templates/story.md +1 -1
  44. package/scripts/pre_bounce_sync.sh +0 -37
  45. package/scripts/vbounce_ask.mjs +0 -98
  46. package/scripts/vbounce_index.mjs +0 -184
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * close_sprint.mjs
5
+ * Sprint close automation — validates, archives, updates state.json.
6
+ *
7
+ * Usage:
8
+ * ./scripts/close_sprint.mjs S-05
9
+ */
10
+
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+ import { fileURLToPath } from 'url';
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ const ROOT = path.resolve(__dirname, '..');
17
+
18
+ const args = process.argv.slice(2);
19
+ if (args.length < 1) {
20
+ console.error('Usage: close_sprint.mjs S-XX');
21
+ process.exit(1);
22
+ }
23
+
24
+ const sprintId = args[0];
25
+ if (!/^S-\d{2}$/.test(sprintId)) {
26
+ console.error(`ERROR: sprint_id "${sprintId}" must match S-XX format`);
27
+ process.exit(1);
28
+ }
29
+
30
+ const sprintNum = sprintId.replace('S-', '');
31
+ const stateFile = path.join(ROOT, '.bounce', 'state.json');
32
+
33
+ // 1. Read state.json
34
+ if (!fs.existsSync(stateFile)) {
35
+ console.error(`ERROR: .bounce/state.json not found`);
36
+ process.exit(1);
37
+ }
38
+
39
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
40
+
41
+ if (state.sprint_id !== sprintId) {
42
+ console.error(`ERROR: state.json is for sprint ${state.sprint_id}, not ${sprintId}`);
43
+ process.exit(1);
44
+ }
45
+
46
+ // 2. Check all stories are terminal
47
+ const activeStories = Object.entries(state.stories || {}).filter(
48
+ ([, s]) => !['Done', 'Escalated', 'Parking Lot'].includes(s.state)
49
+ );
50
+
51
+ if (activeStories.length > 0) {
52
+ console.warn(`⚠ ${activeStories.length} stories are not in a terminal state:`);
53
+ activeStories.forEach(([id, s]) => console.warn(` - ${id}: ${s.state}`));
54
+ console.warn(' Proceed? These stories will be left incomplete.');
55
+ }
56
+
57
+ // 3. Create archive directory
58
+ const archiveDir = path.join(ROOT, '.bounce', 'archive', sprintId);
59
+ fs.mkdirSync(archiveDir, { recursive: true });
60
+
61
+ // 4. Move sprint report if it exists
62
+ const reportSrc = path.join(ROOT, '.bounce', `sprint-report-${sprintId}.md`);
63
+ const reportLegacy = path.join(ROOT, '.bounce', 'sprint-report.md');
64
+ const reportDst = path.join(archiveDir, `sprint-report-${sprintId}.md`);
65
+
66
+ if (fs.existsSync(reportSrc)) {
67
+ fs.copyFileSync(reportSrc, reportDst);
68
+ console.log(`✓ Archived sprint report → .bounce/archive/${sprintId}/sprint-report-${sprintId}.md`);
69
+ } else if (fs.existsSync(reportLegacy)) {
70
+ fs.copyFileSync(reportLegacy, reportDst);
71
+ console.log(`✓ Archived sprint report → .bounce/archive/${sprintId}/sprint-report-${sprintId}.md`);
72
+ }
73
+
74
+ // 5. Update state.json
75
+ state.last_action = `Sprint ${sprintId} closed`;
76
+ state.updated_at = new Date().toISOString();
77
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
78
+ console.log(`✓ Updated state.json`);
79
+
80
+ // 6. Print manual steps
81
+ const sprintPlanPath = `product_plans/sprints/sprint-${sprintNum}`;
82
+ const archivePath = `product_plans/archive/sprints/sprint-${sprintNum}`;
83
+
84
+ console.log('');
85
+ console.log('Manual steps remaining:');
86
+ console.log(` 1. Archive sprint plan folder:`);
87
+ console.log(` mv ${sprintPlanPath}/ ${archivePath}/`);
88
+ console.log(` 2. Update Delivery Plan §4 Completed Sprints with a summary row`);
89
+ console.log(` 3. Remove delivered stories from Delivery Plan §3 Backlog`);
90
+ console.log(` 4. Delete sprint branch (after merge to main):`);
91
+ console.log(` git branch -d sprint/${sprintId}`);
92
+ console.log(` 5. Run: vbounce trends && vbounce suggest ${sprintId}`);
93
+ console.log('');
94
+ console.log(`✓ Sprint ${sprintId} closed.`);
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * complete_story.mjs
5
+ * Mark a story as Done — updates Sprint Plan §1 + §4, and state.json atomically.
6
+ *
7
+ * Usage:
8
+ * ./scripts/complete_story.mjs STORY-005-02 --qa-bounces 1 --arch-bounces 0 --correction-tax 5 --notes "Missing validation fixed"
9
+ */
10
+
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+ import { fileURLToPath } from 'url';
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ const ROOT = path.resolve(__dirname, '..');
17
+
18
+ function parseArgs(argv) {
19
+ const result = { storyId: null, qaBounces: 0, archBounces: 0, correctionTax: '0%', notes: '' };
20
+ const args = argv.slice(2);
21
+ result.storyId = args[0];
22
+ for (let i = 1; i < args.length; i++) {
23
+ if (args[i] === '--qa-bounces') result.qaBounces = parseInt(args[++i], 10) || 0;
24
+ else if (args[i] === '--arch-bounces') result.archBounces = parseInt(args[++i], 10) || 0;
25
+ else if (args[i] === '--correction-tax') result.correctionTax = args[++i] + (args[i].includes('%') ? '' : '%');
26
+ else if (args[i] === '--notes') result.notes = args[++i];
27
+ }
28
+ return result;
29
+ }
30
+
31
+ const { storyId, qaBounces, archBounces, correctionTax, notes } = parseArgs(process.argv);
32
+
33
+ if (!storyId) {
34
+ console.error('Usage: complete_story.mjs STORY-ID [--qa-bounces N] [--arch-bounces N] [--correction-tax N] [--notes "text"]');
35
+ process.exit(1);
36
+ }
37
+
38
+ // 1. Update state.json
39
+ const stateFile = path.join(ROOT, '.bounce', 'state.json');
40
+ if (!fs.existsSync(stateFile)) {
41
+ console.error('ERROR: .bounce/state.json not found');
42
+ process.exit(1);
43
+ }
44
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
45
+ if (!state.stories[storyId]) {
46
+ console.error(`ERROR: Story "${storyId}" not found in state.json`);
47
+ process.exit(1);
48
+ }
49
+ state.stories[storyId].state = 'Done';
50
+ state.stories[storyId].qa_bounces = qaBounces;
51
+ state.stories[storyId].arch_bounces = archBounces;
52
+ state.stories[storyId].worktree = null;
53
+ state.last_action = `${storyId} completed`;
54
+ state.updated_at = new Date().toISOString();
55
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
56
+ console.log(`✓ Updated state.json: ${storyId} → Done`);
57
+
58
+ // 2. Find sprint plan
59
+ const sprintNum = state.sprint_id.replace('S-', '');
60
+ const sprintPlanPath = path.join(ROOT, 'product_plans', 'sprints', `sprint-${sprintNum}`, `sprint-${sprintNum}.md`);
61
+
62
+ if (!fs.existsSync(sprintPlanPath)) {
63
+ console.warn(`⚠ Sprint plan not found at ${sprintPlanPath}. Update §1 and §4 manually.`);
64
+ process.exit(0);
65
+ }
66
+
67
+ let content = fs.readFileSync(sprintPlanPath, 'utf8');
68
+
69
+ // 3. Update §1 table — find the row with storyId and change V-Bounce State to Done
70
+ const tableRowRegex = new RegExp(`(\\|[^|]*\\|[^|]*${storyId.replace(/[-]/g, '[-]')}[^|]*\\|[^|]*\\|[^|]*\\|)([^|]+)(\\|[^|]*\\|)`, 'g');
71
+ let updated = false;
72
+ content = content.replace(tableRowRegex, (match, before, stateCell, after) => {
73
+ updated = true;
74
+ return `${before} Done ${after}`;
75
+ });
76
+
77
+ if (!updated) {
78
+ console.warn(`⚠ Could not find ${storyId} row in §1 table. Update V-Bounce State manually.`);
79
+ }
80
+
81
+ // 4. Add row to §4 Execution Log
82
+ const logStart = '<!-- EXECUTION_LOG_START -->';
83
+ const logEnd = '<!-- EXECUTION_LOG_END -->';
84
+ const newRow = `| ${storyId} | Done | ${qaBounces} | ${archBounces} | ${correctionTax} | ${notes || '—'} |`;
85
+
86
+ if (content.includes(logStart)) {
87
+ // Find the table in the execution log section and append a row
88
+ const startIdx = content.indexOf(logStart);
89
+ const endIdx = content.indexOf(logEnd);
90
+
91
+ if (endIdx > startIdx) {
92
+ const before = content.substring(0, endIdx);
93
+ const after = content.substring(endIdx);
94
+
95
+ // Check if header row exists, if not add it
96
+ const section = before.substring(startIdx);
97
+ if (!section.includes('| Story |')) {
98
+ const headerRow = `\n| Story | Final State | QA Bounces | Arch Bounces | Correction Tax | Notes |\n|-------|-------------|------------|--------------|----------------|-------|`;
99
+ content = before + headerRow + '\n' + newRow + '\n' + after;
100
+ } else {
101
+ content = before + newRow + '\n' + after;
102
+ }
103
+ console.log(`✓ Added row to §4 Execution Log`);
104
+ }
105
+ } else {
106
+ // Append §4 section at end
107
+ content += `\n\n<!-- EXECUTION_LOG_START -->\n## 4. Execution Log\n\n| Story | Final State | QA Bounces | Arch Bounces | Correction Tax | Notes |\n|-------|-------------|------------|--------------|----------------|-------|\n${newRow}\n<!-- EXECUTION_LOG_END -->\n`;
108
+ console.log(`✓ Created §4 Execution Log with first row`);
109
+ }
110
+
111
+ fs.writeFileSync(sprintPlanPath, content);
112
+ console.log(`✓ Updated sprint plan: ${storyId} Done`);
113
+ console.log(`\n QA bounces: ${qaBounces} | Arch bounces: ${archBounces} | Correction tax: ${correctionTax}`);
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * doctor.mjs
5
+ * V-Bounce OS Health Check — validates all configs, templates, state files
6
+ * Usage: vbounce doctor
7
+ */
8
+
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+ const ROOT = path.resolve(__dirname, '..');
15
+
16
+ const checks = [];
17
+ let issueCount = 0;
18
+
19
+ function pass(msg) {
20
+ checks.push(` ✓ ${msg}`);
21
+ }
22
+
23
+ function fail(msg, fix) {
24
+ checks.push(` ✗ ${msg}${fix ? `\n → Fix: ${fix}` : ''}`);
25
+ issueCount++;
26
+ }
27
+
28
+ function warn(msg) {
29
+ checks.push(` ⚠ ${msg}`);
30
+ }
31
+
32
+ // Check LESSONS.md
33
+ if (fs.existsSync(path.join(ROOT, 'LESSONS.md'))) {
34
+ pass('LESSONS.md exists');
35
+ } else {
36
+ fail('LESSONS.md missing', 'Create LESSONS.md at project root');
37
+ }
38
+
39
+ // Check templates
40
+ const requiredTemplates = ['sprint.md', 'delivery_plan.md', 'sprint_report.md', 'story.md', 'epic.md', 'charter.md', 'roadmap.md', 'risk_registry.md'];
41
+ const templatesDir = path.join(ROOT, 'templates');
42
+ let templateCount = 0;
43
+ for (const t of requiredTemplates) {
44
+ if (fs.existsSync(path.join(templatesDir, t))) templateCount++;
45
+ else fail(`templates/${t} missing`, `Create from V-Bounce OS template`);
46
+ }
47
+ if (templateCount === requiredTemplates.length) pass(`templates/ complete (${templateCount}/${requiredTemplates.length})`);
48
+
49
+ // Check .bounce directory
50
+ if (fs.existsSync(path.join(ROOT, '.bounce'))) {
51
+ pass('.bounce/ directory exists');
52
+
53
+ // Check state.json
54
+ const stateFile = path.join(ROOT, '.bounce', 'state.json');
55
+ if (fs.existsSync(stateFile)) {
56
+ try {
57
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
58
+ pass(`state.json valid (sprint ${state.sprint_id}, ${Object.keys(state.stories || {}).length} stories)`);
59
+ } catch (e) {
60
+ fail('state.json exists but is invalid JSON', 'Run: vbounce validate state');
61
+ }
62
+ } else {
63
+ warn('state.json not found — run: vbounce sprint init S-XX D-XX');
64
+ }
65
+ } else {
66
+ warn('.bounce/ directory missing — run: vbounce sprint init S-XX D-XX');
67
+ }
68
+
69
+ // Check brain files
70
+ const brainFiles = [
71
+ ['brains/CLAUDE.md', 'Tier 1 (Claude Code)'],
72
+ ['brains/GEMINI.md', 'Tier 2 (Gemini CLI)'],
73
+ ['brains/AGENTS.md', 'Tier 2 (Codex CLI)'],
74
+ ];
75
+ for (const [f, tier] of brainFiles) {
76
+ if (fs.existsSync(path.join(ROOT, f))) pass(`Brain file: ${f} (${tier})`);
77
+ else fail(`Brain file: ${f} missing`, `Run: vbounce init --tool ${f.includes('GEMINI') ? 'gemini' : f.includes('AGENTS') ? 'codex' : 'claude'}`);
78
+ }
79
+
80
+ // Check optional brain files
81
+ const optionalBrains = [
82
+ ['brains/copilot/copilot-instructions.md', 'copilot'],
83
+ ['brains/windsurf/.windsurfrules', 'windsurf'],
84
+ ];
85
+ for (const [f, tool] of optionalBrains) {
86
+ if (fs.existsSync(path.join(ROOT, f))) pass(`Brain file: ${f} (Tier 4)`);
87
+ else warn(`Brain file: ${f} not found (optional) — run: vbounce init --tool ${tool}`);
88
+ }
89
+
90
+ // Check skills
91
+ const requiredSkills = ['agent-team', 'doc-manager', 'lesson', 'vibe-code-review', 'react-best-practices', 'write-skill', 'improve'];
92
+ const skillsDir = path.join(ROOT, 'skills');
93
+ let skillCount = 0;
94
+ for (const s of requiredSkills) {
95
+ const skillFile = path.join(skillsDir, s, 'SKILL.md');
96
+ if (fs.existsSync(skillFile)) skillCount++;
97
+ else fail(`skills/${s}/SKILL.md missing`);
98
+ }
99
+ if (skillCount === requiredSkills.length) pass(`Skills: ${skillCount}/${requiredSkills.length} installed`);
100
+
101
+ // Check scripts
102
+ const requiredScripts = [
103
+ 'validate_report.mjs', 'update_state.mjs', 'validate_state.mjs',
104
+ 'validate_sprint_plan.mjs', 'validate_bounce_readiness.mjs',
105
+ 'init_sprint.mjs', 'close_sprint.mjs', 'complete_story.mjs',
106
+ 'prep_qa_context.mjs', 'prep_arch_context.mjs', 'prep_sprint_context.mjs',
107
+ 'prep_sprint_summary.mjs', 'sprint_trends.mjs', 'suggest_improvements.mjs',
108
+ 'hotfix_manager.sh'
109
+ ];
110
+ const scriptsDir = path.join(ROOT, 'scripts');
111
+ let scriptCount = 0;
112
+ for (const s of requiredScripts) {
113
+ if (fs.existsSync(path.join(scriptsDir, s))) scriptCount++;
114
+ else fail(`scripts/${s} missing`);
115
+ }
116
+ if (scriptCount === requiredScripts.length) pass(`Scripts: ${scriptCount}/${requiredScripts.length} available`);
117
+
118
+ // Check product_plans structure
119
+ if (fs.existsSync(path.join(ROOT, 'product_plans'))) {
120
+ pass('product_plans/ directory exists');
121
+ } else {
122
+ warn('product_plans/ directory missing — create it to store planning documents');
123
+ }
124
+
125
+ // Check vbounce.config.json
126
+ if (fs.existsSync(path.join(ROOT, 'vbounce.config.json'))) {
127
+ pass('vbounce.config.json found');
128
+ } else {
129
+ warn('vbounce.config.json not found — using default context limits');
130
+ }
131
+
132
+ // Print results
133
+ console.log('\nV-Bounce OS Health Check');
134
+ console.log('========================');
135
+ checks.forEach(c => console.log(c));
136
+ console.log('');
137
+ if (issueCount === 0) {
138
+ console.log('✓ All checks passed.');
139
+ } else {
140
+ console.log(`Issues: ${issueCount}`);
141
+ console.log('Run suggested commands to fix.');
142
+ }
143
+
144
+ process.exit(issueCount > 0 ? 1 : 0);
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * init_sprint.mjs
5
+ * Sprint setup automation — creates state.json, sprint plan dir, and prints git commands.
6
+ *
7
+ * Usage:
8
+ * ./scripts/init_sprint.mjs S-06 D-02 --stories STORY-011-05,STORY-005-01,STORY-005-02
9
+ */
10
+
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+ import { fileURLToPath } from 'url';
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ const ROOT = path.resolve(__dirname, '..');
17
+
18
+ const args = process.argv.slice(2);
19
+
20
+ if (args.length < 2) {
21
+ console.error('Usage: init_sprint.mjs S-XX D-NN [--stories STORY-ID1,STORY-ID2,...]');
22
+ process.exit(1);
23
+ }
24
+
25
+ const sprintId = args[0]; // e.g. S-06
26
+ const deliveryId = args[1]; // e.g. D-02
27
+
28
+ if (!/^S-\d{2}$/.test(sprintId)) {
29
+ console.error(`ERROR: sprint_id "${sprintId}" must match S-XX format`);
30
+ process.exit(1);
31
+ }
32
+ if (!/^D-\d{2}$/.test(deliveryId)) {
33
+ console.error(`ERROR: delivery_id "${deliveryId}" must match D-NN format`);
34
+ process.exit(1);
35
+ }
36
+
37
+ const storiesArg = args.indexOf('--stories');
38
+ const storyIds = storiesArg !== -1 ? args[storiesArg + 1].split(',') : [];
39
+
40
+ // 1. Create .bounce/ directory
41
+ const bounceDir = path.join(ROOT, '.bounce');
42
+ fs.mkdirSync(bounceDir, { recursive: true });
43
+ fs.mkdirSync(path.join(bounceDir, 'archive'), { recursive: true });
44
+ fs.mkdirSync(path.join(bounceDir, 'reports'), { recursive: true });
45
+
46
+ // 2. Create state.json
47
+ const stateFile = path.join(bounceDir, 'state.json');
48
+ if (fs.existsSync(stateFile)) {
49
+ console.warn(`⚠ state.json already exists. Overwriting...`);
50
+ }
51
+
52
+ const sprintNum = sprintId.replace('S-', '');
53
+ const stories = {};
54
+ for (const id of storyIds) {
55
+ stories[id.trim()] = {
56
+ state: 'Draft',
57
+ qa_bounces: 0,
58
+ arch_bounces: 0,
59
+ worktree: null
60
+ };
61
+ }
62
+
63
+ const state = {
64
+ sprint_id: sprintId,
65
+ delivery_id: deliveryId,
66
+ sprint_plan: `product_plans/sprints/sprint-${sprintNum}/sprint-${sprintNum}.md`,
67
+ delivery_plan: `product_plans/strategy/${deliveryId}_DELIVERY_PLAN.md`,
68
+ stories,
69
+ phase: 'Phase 1',
70
+ last_action: `Sprint ${sprintId} initialized`,
71
+ updated_at: new Date().toISOString()
72
+ };
73
+
74
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
75
+ console.log(`✓ Created .bounce/state.json`);
76
+
77
+ // 3. Create sprint plan directory
78
+ const sprintDir = path.join(ROOT, 'product_plans', 'sprints', `sprint-${sprintNum}`);
79
+ fs.mkdirSync(sprintDir, { recursive: true });
80
+
81
+ const sprintPlanFile = path.join(sprintDir, `sprint-${sprintNum}.md`);
82
+ if (!fs.existsSync(sprintPlanFile)) {
83
+ // Copy from template
84
+ const templateFile = path.join(ROOT, 'templates', 'sprint.md');
85
+ if (fs.existsSync(templateFile)) {
86
+ let content = fs.readFileSync(templateFile, 'utf8');
87
+ // Replace placeholders
88
+ content = content.replace(/sprint-\{XX\}/g, `sprint-${sprintNum}`);
89
+ content = content.replace(/S-\{XX\}/g, sprintId);
90
+ content = content.replace(/D-\{NN\}/g, deliveryId);
91
+ content = content.replace(/status: "Planning \/ Active \/ Completed"/, 'status: "Planning"');
92
+ // Strip instructions block
93
+ content = content.replace(/<instructions>[\s\S]*?<\/instructions>\n\n/, '');
94
+ fs.writeFileSync(sprintPlanFile, content);
95
+ console.log(`✓ Created product_plans/sprints/sprint-${sprintNum}/sprint-${sprintNum}.md`);
96
+ } else {
97
+ // Create minimal sprint plan
98
+ const minimal = `---\nsprint_id: "${sprintId}"\nsprint_goal: "TBD"\ndates: "TBD"\nstatus: "Planning"\ndelivery: "${deliveryId}"\n---\n\n# Sprint ${sprintId} Plan\n\n## 1. Active Scope\n\n| Priority | Story | Epic | Label | V-Bounce State | Blocker |\n|----------|-------|------|-------|----------------|---------|\n${storyIds.map((id, i) => `| ${i + 1} | ${id.trim()} | — | L2 | Draft | — |`).join('\n')}\n\n### Escalated / Parking Lot\n- (none)\n\n---\n\n## 2. Execution Strategy\n\n### Phase Plan\n- **Phase 1 (parallel)**: ${storyIds.join(', ')}\n\n### Risk Flags\n- (TBD)\n\n---\n\n## 3. Sprint Open Questions\n\n| Question | Options | Impact | Owner | Status |\n|----------|---------|--------|-------|--------|\n\n---\n\n<!-- EXECUTION_LOG_START -->\n## 4. Execution Log\n\n| Story | Final State | QA Bounces | Arch Bounces | Correction Tax | Notes |\n|-------|-------------|------------|--------------|----------------|-------|\n<!-- EXECUTION_LOG_END -->\n`;
99
+ fs.writeFileSync(sprintPlanFile, minimal);
100
+ console.log(`✓ Created product_plans/sprints/sprint-${sprintNum}/sprint-${sprintNum}.md (minimal — template not found)`);
101
+ }
102
+ } else {
103
+ console.log(` Sprint plan already exists: product_plans/sprints/sprint-${sprintNum}/sprint-${sprintNum}.md`);
104
+ }
105
+
106
+ // 4. Print git commands (don't execute)
107
+ console.log('');
108
+ console.log('Run these git commands to initialize the sprint branch:');
109
+ console.log(` git checkout -b sprint/${sprintId} main`);
110
+ if (storyIds.length > 0) {
111
+ console.log('');
112
+ console.log('Then create worktrees for each story:');
113
+ storyIds.forEach(id => {
114
+ const trimmed = id.trim();
115
+ console.log(` git worktree add .worktrees/${trimmed} -b story/${trimmed} sprint/${sprintId}`);
116
+ console.log(` mkdir -p .worktrees/${trimmed}/.bounce/{tasks,reports}`);
117
+ });
118
+ }
119
+
120
+ console.log('');
121
+ console.log(`✓ Sprint ${sprintId} initialized. Stories: ${storyIds.length > 0 ? storyIds.join(', ') : 'none (add manually)'}`);
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * prep_arch_context.mjs
5
+ * Generates an Architect context pack for a story.
6
+ *
7
+ * Usage:
8
+ * ./scripts/prep_arch_context.mjs STORY-005-02
9
+ *
10
+ * Output: .bounce/arch-context-STORY-005-02.md
11
+ */
12
+
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import { fileURLToPath } from 'url';
16
+ import { execSync } from 'child_process';
17
+ import yaml from 'js-yaml';
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const ROOT = path.resolve(__dirname, '..');
21
+
22
+ const storyId = process.argv[2];
23
+ if (!storyId) {
24
+ console.error('Usage: prep_arch_context.mjs STORY-ID');
25
+ process.exit(1);
26
+ }
27
+
28
+ // Load config
29
+ let config = { maxDiffLines: 500 };
30
+ const configFile = path.join(ROOT, 'vbounce.config.json');
31
+ if (fs.existsSync(configFile)) {
32
+ try { config = { ...config, ...JSON.parse(fs.readFileSync(configFile, 'utf8')) }; } catch {}
33
+ }
34
+ const MAX_DIFF_LINES = config.maxDiffLines || 500;
35
+
36
+ function findFilesMatching(dir, pattern) {
37
+ const results = [];
38
+ if (!fs.existsSync(dir)) return results;
39
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
40
+ for (const e of entries) {
41
+ const full = path.join(dir, e.name);
42
+ if (e.isDirectory()) results.push(...findFilesMatching(full, pattern));
43
+ else if (pattern.test(e.name)) results.push(full);
44
+ }
45
+ return results;
46
+ }
47
+
48
+ const searchDirs = [
49
+ path.join(ROOT, '.worktrees', storyId, '.bounce', 'reports'),
50
+ path.join(ROOT, '.bounce', 'reports'),
51
+ path.join(ROOT, '.bounce', 'archive'),
52
+ ];
53
+
54
+ // 1. Find dev report (required)
55
+ const devPattern = new RegExp(`${storyId.replace(/[-]/g, '[-]')}.*-dev\\.md$`);
56
+ let devReport = null;
57
+ for (const dir of searchDirs) {
58
+ const m = findFilesMatching(dir, devPattern);
59
+ if (m.length > 0) { devReport = m[0]; break; }
60
+ }
61
+ if (!devReport) {
62
+ console.error(`ERROR: Dev report not found for ${storyId}`);
63
+ process.exit(1);
64
+ }
65
+
66
+ // 2. Find QA report (optional but warn)
67
+ const qaPattern = new RegExp(`${storyId.replace(/[-]/g, '[-]')}.*-qa.*\\.md$`);
68
+ let qaReport = null;
69
+ for (const dir of searchDirs) {
70
+ const m = findFilesMatching(dir, qaPattern);
71
+ if (m.length > 0) { qaReport = m[m.length - 1]; break; } // latest QA report
72
+ }
73
+ if (!qaReport) console.warn(`⚠ QA report not found for ${storyId} — proceeding without it`);
74
+
75
+ // 3. Find story spec (required)
76
+ const storyPattern = new RegExp(`${storyId.replace(/[-]/g, '[-]')}.*\\.md$`);
77
+ const storyMatches = findFilesMatching(path.join(ROOT, 'product_plans'), storyPattern);
78
+ if (storyMatches.length === 0) {
79
+ console.error(`ERROR: Story spec not found for ${storyId} in product_plans/`);
80
+ process.exit(1);
81
+ }
82
+ const storySpecFile = storyMatches[0];
83
+
84
+ // Parse frontmatters
85
+ let devFm = {}, qaFm = {};
86
+ try {
87
+ const dc = fs.readFileSync(devReport, 'utf8');
88
+ const dm = dc.match(/^---\s*\n([\s\S]*?)\n---/);
89
+ if (dm) devFm = yaml.load(dm[1]) || {};
90
+ } catch {}
91
+ if (qaReport) {
92
+ try {
93
+ const qc = fs.readFileSync(qaReport, 'utf8');
94
+ const qm = qc.match(/^---\s*\n([\s\S]*?)\n---/);
95
+ if (qm) qaFm = yaml.load(qm[1]) || {};
96
+ } catch {}
97
+ }
98
+
99
+ // 4. Get git diff
100
+ let diffContent = '';
101
+ let diffTruncated = false;
102
+ const stateFile = path.join(ROOT, '.bounce', 'state.json');
103
+ try {
104
+ let diffCmd = 'git diff HEAD~5';
105
+ if (fs.existsSync(stateFile)) {
106
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
107
+ const sprintBranch = `sprint/${state.sprint_id}`;
108
+ try {
109
+ execSync(`git rev-parse ${sprintBranch}`, { cwd: ROOT, stdio: 'pipe' });
110
+ diffCmd = `git diff ${sprintBranch}...HEAD`;
111
+ } catch {}
112
+ }
113
+ diffContent = execSync(diffCmd, { cwd: ROOT }).toString();
114
+
115
+ if (!diffContent.trim()) {
116
+ console.warn(`⚠ Git diff is empty — proceeding without diff`);
117
+ } else {
118
+ const diffLines = diffContent.split('\n');
119
+ if (diffLines.length > MAX_DIFF_LINES) {
120
+ diffTruncated = true;
121
+ const fullDiffPath = path.join(ROOT, '.bounce', `arch-full-diff-${storyId}.txt`);
122
+ fs.writeFileSync(fullDiffPath, diffContent);
123
+ console.warn(`⚠ Diff truncated at ${MAX_DIFF_LINES} lines (was ${diffLines.length}). Full diff saved to .bounce/arch-full-diff-${storyId}.txt`);
124
+ diffContent = diffLines.slice(0, MAX_DIFF_LINES).join('\n');
125
+ }
126
+ }
127
+ } catch (e) {
128
+ console.warn(`⚠ Could not get git diff: ${e.message}`);
129
+ }
130
+
131
+ // 5. Read LESSONS.md
132
+ const lessonsFile = path.join(ROOT, 'LESSONS.md');
133
+ let lessonsExcerpt = '_No LESSONS.md found_';
134
+ if (fs.existsSync(lessonsFile)) {
135
+ const lines = fs.readFileSync(lessonsFile, 'utf8').split('\n');
136
+ lessonsExcerpt = lines.slice(0, 20).join('\n');
137
+ if (lines.length > 20) lessonsExcerpt += `\n_(+${lines.length - 20} more lines)_`;
138
+ }
139
+
140
+ // 6. Assemble context pack
141
+ const lines = [
142
+ `# Architect Context: ${storyId}`,
143
+ `> Generated: ${new Date().toISOString().split('T')[0]}`,
144
+ '',
145
+ `## Dev Report Summary`,
146
+ `| Field | Value |`,
147
+ `|-------|-------|`,
148
+ `| Status | ${devFm.status || '—'} |`,
149
+ `| Correction Tax | ${devFm.correction_tax || '—'} |`,
150
+ `| Tests Written | ${devFm.tests_written ?? '—'} |`,
151
+ `| Files Modified | ${Array.isArray(devFm.files_modified) ? devFm.files_modified.length : '—'} |`,
152
+ '',
153
+ `## QA Report Summary`,
154
+ qaReport
155
+ ? [`| Field | Value |`, `|-------|-------|`,
156
+ `| Status | ${qaFm.status || '—'} |`,
157
+ `| Bounce Count | ${qaFm.bounce_count ?? '—'} |`,
158
+ `| Bugs Found | ${qaFm.bugs_found ?? '—'} |`].join('\n')
159
+ : '_QA report not found_',
160
+ '',
161
+ `## Story Spec`,
162
+ `- File: \`${path.relative(ROOT, storySpecFile)}\``,
163
+ `- Read §3 Implementation Guide and §3.1 ADR References before auditing`,
164
+ '',
165
+ `## Git Diff${diffTruncated ? ` (TRUNCATED at ${MAX_DIFF_LINES} lines — full diff in .bounce/arch-full-diff-${storyId}.txt)` : ''}`,
166
+ '```diff',
167
+ diffContent || '(no diff available)',
168
+ '```',
169
+ '',
170
+ `## Relevant Lessons`,
171
+ lessonsExcerpt,
172
+ ];
173
+
174
+ const output = lines.join('\n');
175
+ const outputFile = path.join(ROOT, '.bounce', `arch-context-${storyId}.md`);
176
+ fs.writeFileSync(outputFile, output);
177
+ console.log(`✓ Architect context pack written to .bounce/arch-context-${storyId}.md`);
178
+ if (diffTruncated) console.log(` ⚠ Diff truncated — full diff at .bounce/arch-full-diff-${storyId}.txt`);