@sandrinio/vbounce 1.6.0 → 1.8.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 +108 -18
- package/bin/vbounce.mjs +306 -145
- package/brains/AGENTS.md +5 -5
- package/brains/CHANGELOG.md +88 -1
- package/brains/CLAUDE.md +22 -17
- package/brains/GEMINI.md +40 -4
- package/brains/SETUP.md +11 -5
- package/brains/claude-agents/architect.md +13 -6
- package/brains/claude-agents/developer.md +2 -2
- package/brains/claude-agents/qa.md +17 -7
- package/brains/copilot/copilot-instructions.md +49 -0
- package/brains/cursor-rules/vbounce-process.mdc +2 -2
- package/brains/windsurf/.windsurfrules +30 -0
- package/package.json +2 -4
- package/scripts/close_sprint.mjs +94 -0
- package/scripts/complete_story.mjs +113 -0
- package/scripts/doctor.mjs +144 -0
- package/scripts/init_sprint.mjs +121 -0
- package/scripts/prep_arch_context.mjs +178 -0
- package/scripts/prep_qa_context.mjs +152 -0
- package/scripts/prep_sprint_context.mjs +141 -0
- package/scripts/prep_sprint_summary.mjs +154 -0
- package/scripts/sprint_trends.mjs +160 -0
- package/scripts/suggest_improvements.mjs +200 -0
- package/scripts/update_state.mjs +132 -0
- package/scripts/validate_bounce_readiness.mjs +152 -0
- package/scripts/validate_report.mjs +39 -2
- package/scripts/validate_sprint_plan.mjs +117 -0
- package/scripts/validate_state.mjs +99 -0
- package/scripts/vdoc_match.mjs +269 -0
- package/scripts/vdoc_staleness.mjs +199 -0
- package/skills/agent-team/SKILL.md +53 -28
- package/skills/agent-team/references/cleanup.md +42 -0
- package/skills/agent-team/references/delivery-sync.md +43 -0
- package/skills/agent-team/references/git-strategy.md +52 -0
- package/skills/agent-team/references/mid-sprint-triage.md +71 -0
- package/skills/agent-team/references/report-naming.md +34 -0
- package/skills/doc-manager/SKILL.md +5 -4
- package/skills/improve/SKILL.md +1 -1
- package/skills/lesson/SKILL.md +23 -0
- package/templates/delivery_plan.md +1 -1
- package/templates/hotfix.md +1 -1
- package/templates/sprint.md +65 -13
- package/templates/sprint_report.md +14 -3
- package/templates/story.md +1 -1
- package/scripts/pre_bounce_sync.sh +0 -37
- package/scripts/vbounce_ask.mjs +0 -98
- 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`);
|