@sandrinio/vbounce 1.5.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.
- package/README.md +108 -18
- package/bin/vbounce.mjs +291 -146
- package/brains/AGENTS.md +12 -10
- package/brains/CHANGELOG.md +99 -1
- package/brains/CLAUDE.md +29 -22
- package/brains/GEMINI.md +47 -9
- package/brains/SETUP.md +11 -5
- package/brains/claude-agents/architect.md +22 -6
- package/brains/claude-agents/developer.md +2 -2
- package/brains/claude-agents/devops.md +3 -0
- package/brains/claude-agents/qa.md +25 -9
- package/brains/copilot/copilot-instructions.md +49 -0
- package/brains/cursor-rules/vbounce-process.mdc +9 -7
- 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_gate_config.sh +151 -0
- package/scripts/init_sprint.mjs +121 -0
- package/scripts/pre_gate_common.sh +576 -0
- package/scripts/pre_gate_runner.sh +176 -0
- package/scripts/prep_arch_context.mjs +178 -0
- package/scripts/prep_qa_context.mjs +134 -0
- package/scripts/prep_sprint_context.mjs +118 -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 +125 -0
- package/scripts/validate_report.mjs +39 -2
- package/scripts/validate_sprint_plan.mjs +117 -0
- package/scripts/validate_state.mjs +99 -0
- package/skills/agent-team/SKILL.md +56 -21
- 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 +27 -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 +8 -1
- 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,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* validate_bounce_readiness.mjs
|
|
5
|
+
* Pre-bounce gate check — verifies a story is ready to bounce.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ./scripts/validate_bounce_readiness.mjs STORY-005-02
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import { execSync } from 'child_process';
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
18
|
+
|
|
19
|
+
const storyId = process.argv[2];
|
|
20
|
+
if (!storyId) {
|
|
21
|
+
console.error('Usage: validate_bounce_readiness.mjs STORY-ID');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const errors = [];
|
|
26
|
+
const warnings = [];
|
|
27
|
+
|
|
28
|
+
// 1. Check state.json
|
|
29
|
+
const stateFile = path.join(ROOT, '.bounce', 'state.json');
|
|
30
|
+
if (!fs.existsSync(stateFile)) {
|
|
31
|
+
errors.push('.bounce/state.json not found — run: vbounce sprint init S-XX D-XX');
|
|
32
|
+
} else {
|
|
33
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
34
|
+
if (!state.stories[storyId]) {
|
|
35
|
+
errors.push(`Story "${storyId}" not found in state.json`);
|
|
36
|
+
} else {
|
|
37
|
+
const story = state.stories[storyId];
|
|
38
|
+
if (story.state !== 'Ready to Bounce') {
|
|
39
|
+
errors.push(`Story state is "${story.state}" — must be "Ready to Bounce" before bouncing`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 2. Find sprint plan
|
|
45
|
+
const sprintsDir = path.join(ROOT, 'product_plans', 'sprints');
|
|
46
|
+
let sprintPlanFound = false;
|
|
47
|
+
if (fs.existsSync(sprintsDir)) {
|
|
48
|
+
const sprintDirs = fs.readdirSync(sprintsDir);
|
|
49
|
+
for (const dir of sprintDirs) {
|
|
50
|
+
const planFile = path.join(sprintsDir, dir, `${dir}.md`);
|
|
51
|
+
if (fs.existsSync(planFile)) {
|
|
52
|
+
sprintPlanFound = true;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!sprintPlanFound) {
|
|
58
|
+
warnings.push('No active Sprint Plan found in product_plans/sprints/');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. Find story spec
|
|
62
|
+
let storyFile = null;
|
|
63
|
+
function findFile(dir, id) {
|
|
64
|
+
if (!fs.existsSync(dir)) return null;
|
|
65
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
66
|
+
for (const e of entries) {
|
|
67
|
+
if (e.isDirectory()) {
|
|
68
|
+
const found = findFile(path.join(dir, e.name), id);
|
|
69
|
+
if (found) return found;
|
|
70
|
+
} else if (e.name.includes(id)) {
|
|
71
|
+
return path.join(dir, e.name);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
storyFile = findFile(path.join(ROOT, 'product_plans'), storyId);
|
|
78
|
+
if (!storyFile) {
|
|
79
|
+
errors.push(`Story spec not found for "${storyId}" in product_plans/`);
|
|
80
|
+
} else {
|
|
81
|
+
const storyContent = fs.readFileSync(storyFile, 'utf8');
|
|
82
|
+
|
|
83
|
+
// Check for §1, §2, §3
|
|
84
|
+
const hasSpec = /##\s*(1\.|§1|The Spec)/i.test(storyContent);
|
|
85
|
+
const hasCriteria = /##\s*(2\.|§2|The Truth|Acceptance)/i.test(storyContent);
|
|
86
|
+
const hasGuide = /##\s*(3\.|§3|Implementation)/i.test(storyContent);
|
|
87
|
+
|
|
88
|
+
if (!hasSpec) errors.push(`Story ${storyId}: §1 (spec) section not found`);
|
|
89
|
+
if (!hasCriteria) errors.push(`Story ${storyId}: §2 (acceptance criteria) section not found`);
|
|
90
|
+
if (!hasGuide) errors.push(`Story ${storyId}: §3 (implementation guide) section not found`);
|
|
91
|
+
|
|
92
|
+
// Check for minimum content in each section
|
|
93
|
+
const specMatch = storyContent.match(/##\s*(1\.|§1|The Spec)[^\n]*\n([\s\S]*?)(?=\n##|\n---|\Z)/i);
|
|
94
|
+
if (specMatch && specMatch[2].trim().length < 30) {
|
|
95
|
+
warnings.push(`Story ${storyId}: §1 spec section appears very short — verify it's complete`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 4. Check worktree
|
|
100
|
+
const worktreeDir = path.join(ROOT, '.worktrees', storyId);
|
|
101
|
+
if (!fs.existsSync(worktreeDir)) {
|
|
102
|
+
warnings.push(`.worktrees/${storyId}/ not found — create with: git worktree add .worktrees/${storyId} -b story/${storyId} sprint/S-XX`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Print results
|
|
106
|
+
console.log(`Bounce readiness check: ${storyId}`);
|
|
107
|
+
console.log('');
|
|
108
|
+
|
|
109
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
110
|
+
console.log(`✓ ${storyId} is READY TO BOUNCE`);
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (warnings.length > 0) {
|
|
115
|
+
warnings.forEach(w => console.warn(` ⚠ ${w}`));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (errors.length > 0) {
|
|
119
|
+
errors.forEach(e => console.error(` ✗ ${e}`));
|
|
120
|
+
console.error(`\nNOT READY: Fix ${errors.length} error(s) before bouncing ${storyId}`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
} else {
|
|
123
|
+
console.log(` ✓ ${storyId} is ready (with warnings)`);
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
@@ -13,15 +13,21 @@ import path from 'path';
|
|
|
13
13
|
import yaml from 'js-yaml';
|
|
14
14
|
|
|
15
15
|
// Defined schemas for each report type
|
|
16
|
+
const ROOT_CAUSE_ENUM = [
|
|
17
|
+
'missing_tests', 'missing_validation', 'spec_ambiguity', 'adr_violation',
|
|
18
|
+
'gold_plating', 'logic_error', 'integration_gap', 'type_error',
|
|
19
|
+
'state_management', 'error_handling', 'coupling', 'duplication'
|
|
20
|
+
];
|
|
21
|
+
|
|
16
22
|
const SCHEMAS = {
|
|
17
23
|
dev: ['status', 'correction_tax', 'tokens_used', 'tests_written', 'files_modified', 'lessons_flagged'],
|
|
18
24
|
qa: {
|
|
19
25
|
base: ['status', 'bounce_count', 'tokens_used', 'bugs_found', 'gold_plating_detected'],
|
|
20
|
-
conditional: { 'FAIL': ['failed_scenarios'] }
|
|
26
|
+
conditional: { 'FAIL': ['failed_scenarios', 'root_cause'] }
|
|
21
27
|
},
|
|
22
28
|
arch: {
|
|
23
29
|
base: ['status', 'tokens_used'],
|
|
24
|
-
conditional: { 'PASS': ['safe_zone_score', 'ai_isms_detected', 'regression_risk'], 'FAIL': ['bounce_count', 'critical_failures'] }
|
|
30
|
+
conditional: { 'PASS': ['safe_zone_score', 'ai_isms_detected', 'regression_risk'], 'FAIL': ['bounce_count', 'critical_failures', 'root_cause'] }
|
|
25
31
|
},
|
|
26
32
|
devops: {
|
|
27
33
|
base: ['type', 'status', 'tokens_used'],
|
|
@@ -45,6 +51,29 @@ function validateDev(data) {
|
|
|
45
51
|
if (!Array.isArray(data.files_modified)) throw new Error(`DEV_SCHEMA_ERROR: 'files_modified' must be an array.`);
|
|
46
52
|
}
|
|
47
53
|
|
|
54
|
+
function validateBugsArray(bugs, prefix) {
|
|
55
|
+
if (!Array.isArray(bugs)) throw new Error(`${prefix}: 'bugs' must be an array.`);
|
|
56
|
+
bugs.forEach((bug, i) => {
|
|
57
|
+
const bugRequired = ['scenario', 'expected', 'actual', 'files', 'severity'];
|
|
58
|
+
const bugMissing = bugRequired.filter(k => !(k in bug));
|
|
59
|
+
if (bugMissing.length > 0) throw new Error(`${prefix}: bugs[${i}] missing keys: ${bugMissing.join(', ')}`);
|
|
60
|
+
if (!Array.isArray(bug.files)) throw new Error(`${prefix}: bugs[${i}].files must be an array.`);
|
|
61
|
+
const validSeverities = ['Critical', 'High', 'Medium', 'Low'];
|
|
62
|
+
if (!validSeverities.includes(bug.severity)) throw new Error(`${prefix}: bugs[${i}].severity must be one of: ${validSeverities.join(', ')}`);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function validateFailuresArray(failures, prefix) {
|
|
67
|
+
if (!Array.isArray(failures)) throw new Error(`${prefix}: 'failures' must be an array.`);
|
|
68
|
+
const validDimensions = ['Architectural Consistency', 'Error Handling', 'Data Flow', 'Duplication', 'Test Quality', 'Coupling'];
|
|
69
|
+
failures.forEach((f, i) => {
|
|
70
|
+
const fRequired = ['dimension', 'severity', 'what_wrong', 'fix_required'];
|
|
71
|
+
const fMissing = fRequired.filter(k => !(k in f));
|
|
72
|
+
if (fMissing.length > 0) throw new Error(`${prefix}: failures[${i}] missing keys: ${fMissing.join(', ')}`);
|
|
73
|
+
if (!validDimensions.includes(f.dimension)) throw new Error(`${prefix}: failures[${i}].dimension must be one of: ${validDimensions.join(', ')}`);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
48
77
|
function validateQA(data) {
|
|
49
78
|
const missing = SCHEMAS.qa.base.filter(k => !(k in data));
|
|
50
79
|
if (missing.length > 0) throw new Error(`QA_SCHEMA_ERROR: Missing required keys: ${missing.join(', ')}`);
|
|
@@ -52,6 +81,10 @@ function validateQA(data) {
|
|
|
52
81
|
if (data.status === 'FAIL') {
|
|
53
82
|
const conditionalMissing = SCHEMAS.qa.conditional.FAIL.filter(k => !(k in data));
|
|
54
83
|
if (conditionalMissing.length > 0) throw new Error(`QA_SCHEMA_ERROR: 'FAIL' status requires keys: ${conditionalMissing.join(', ')}`);
|
|
84
|
+
if (data.root_cause && !ROOT_CAUSE_ENUM.includes(data.root_cause)) {
|
|
85
|
+
throw new Error(`QA_SCHEMA_ERROR: Invalid root_cause '${data.root_cause}'. Must be one of: ${ROOT_CAUSE_ENUM.join(', ')}`);
|
|
86
|
+
}
|
|
87
|
+
if ('bugs' in data) validateBugsArray(data.bugs, 'QA_SCHEMA_ERROR');
|
|
55
88
|
}
|
|
56
89
|
}
|
|
57
90
|
|
|
@@ -62,6 +95,10 @@ function validateArch(data) {
|
|
|
62
95
|
const s = data.status === 'PASS' ? 'PASS' : 'FAIL';
|
|
63
96
|
const conditionalMissing = SCHEMAS.arch.conditional[s].filter(k => !(k in data));
|
|
64
97
|
if (conditionalMissing.length > 0) throw new Error(`ARCH_SCHEMA_ERROR: '${s}' status requires keys: ${conditionalMissing.join(', ')}`);
|
|
98
|
+
if (s === 'FAIL' && data.root_cause && !ROOT_CAUSE_ENUM.includes(data.root_cause)) {
|
|
99
|
+
throw new Error(`ARCH_SCHEMA_ERROR: Invalid root_cause '${data.root_cause}'. Must be one of: ${ROOT_CAUSE_ENUM.join(', ')}`);
|
|
100
|
+
}
|
|
101
|
+
if (s === 'FAIL' && 'failures' in data) validateFailuresArray(data.failures, 'ARCH_SCHEMA_ERROR');
|
|
65
102
|
}
|
|
66
103
|
|
|
67
104
|
function validateDevops(data) {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* validate_sprint_plan.mjs
|
|
5
|
+
* Validates a Sprint Plan markdown file structure and cross-checks with state.json.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ./scripts/validate_sprint_plan.mjs product_plans/sprints/sprint-05/sprint-05.md
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import yaml from 'js-yaml';
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
18
|
+
|
|
19
|
+
const filePath = process.argv[2];
|
|
20
|
+
if (!filePath) {
|
|
21
|
+
console.error('Usage: validate_sprint_plan.mjs <path-to-sprint-plan.md>');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const absPath = path.resolve(filePath);
|
|
26
|
+
if (!fs.existsSync(absPath)) {
|
|
27
|
+
console.error(`ERROR: File not found: ${absPath}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const content = fs.readFileSync(absPath, 'utf8');
|
|
32
|
+
const errors = [];
|
|
33
|
+
const warnings = [];
|
|
34
|
+
|
|
35
|
+
// 1. Extract YAML frontmatter
|
|
36
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
37
|
+
if (!fmMatch) {
|
|
38
|
+
errors.push('Missing YAML frontmatter (--- delimiters)');
|
|
39
|
+
} else {
|
|
40
|
+
let fm;
|
|
41
|
+
try {
|
|
42
|
+
fm = yaml.load(fmMatch[1]);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
errors.push(`Invalid YAML frontmatter: ${e.message}`);
|
|
45
|
+
fm = {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const required = ['sprint_id', 'sprint_goal', 'dates', 'status', 'delivery'];
|
|
49
|
+
for (const f of required) {
|
|
50
|
+
if (!fm[f]) errors.push(`Frontmatter missing required field: "${f}"`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. Cross-check with state.json
|
|
54
|
+
const stateFile = path.join(ROOT, '.bounce', 'state.json');
|
|
55
|
+
if (fs.existsSync(stateFile)) {
|
|
56
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
57
|
+
|
|
58
|
+
// Find story IDs in §1 table
|
|
59
|
+
const tableRowRegex = /\|\s*\d+\s*\|\s*\[?(STORY-[\w-]+)/g;
|
|
60
|
+
const planStoryIds = new Set();
|
|
61
|
+
let m;
|
|
62
|
+
while ((m = tableRowRegex.exec(content)) !== null) {
|
|
63
|
+
planStoryIds.add(m[1]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const stateStoryIds = new Set(Object.keys(state.stories || {}));
|
|
67
|
+
|
|
68
|
+
// Check for stories in plan but not in state
|
|
69
|
+
for (const id of planStoryIds) {
|
|
70
|
+
if (!stateStoryIds.has(id)) {
|
|
71
|
+
warnings.push(`Story ${id} is in Sprint Plan §1 but NOT in state.json`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check for stories in state but not in plan
|
|
76
|
+
for (const id of stateStoryIds) {
|
|
77
|
+
if (!planStoryIds.has(id)) {
|
|
78
|
+
warnings.push(`Story ${id} is in state.json but NOT in Sprint Plan §1`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 3. Check §4 Execution Log if sprint is Completed
|
|
84
|
+
if (fm.status === 'Completed') {
|
|
85
|
+
if (!content.includes('<!-- EXECUTION_LOG_START -->') && !content.includes('## 4.') && !content.includes('## §4')) {
|
|
86
|
+
errors.push('Sprint is Completed but §4 Execution Log section is missing');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 4. Check §1 table columns
|
|
92
|
+
if (!content.includes('| Priority |') && !content.includes('|Priority|')) {
|
|
93
|
+
errors.push('§1 Active Scope table missing or malformed (expected "Priority" column header)');
|
|
94
|
+
}
|
|
95
|
+
if (!content.includes('V-Bounce State')) {
|
|
96
|
+
errors.push('§1 Active Scope table missing "V-Bounce State" column');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Print results
|
|
100
|
+
console.log(`Validating: ${filePath}`);
|
|
101
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
102
|
+
console.log('✓ Sprint Plan is valid');
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (warnings.length > 0) {
|
|
107
|
+
console.warn('Warnings:');
|
|
108
|
+
warnings.forEach(w => console.warn(` ⚠ ${w}`));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (errors.length > 0) {
|
|
112
|
+
console.error('Errors:');
|
|
113
|
+
errors.forEach(e => console.error(` ✗ ${e}`));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
} else {
|
|
116
|
+
process.exit(0);
|
|
117
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* validate_state.mjs
|
|
5
|
+
* Validates .bounce/state.json schema.
|
|
6
|
+
* Usage: ./scripts/validate_state.mjs
|
|
7
|
+
* Also exportable: import { validateState } from './validate_state.mjs'
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
16
|
+
const STATE_FILE = path.join(ROOT, '.bounce', 'state.json');
|
|
17
|
+
|
|
18
|
+
const VALID_STATES = [
|
|
19
|
+
'Draft', 'Refinement', 'Ready to Bounce', 'Bouncing',
|
|
20
|
+
'QA Passed', 'Architect Passed', 'Done', 'Escalated', 'Parking Lot'
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validates a state object. Returns { valid, errors }.
|
|
25
|
+
* @param {object} state
|
|
26
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
27
|
+
*/
|
|
28
|
+
export function validateState(state) {
|
|
29
|
+
const errors = [];
|
|
30
|
+
|
|
31
|
+
if (!state || typeof state !== 'object') {
|
|
32
|
+
return { valid: false, errors: ['state.json must be a JSON object'] };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!state.sprint_id || !/^S-\d{2}$/.test(state.sprint_id)) {
|
|
36
|
+
errors.push(`sprint_id "${state.sprint_id}" must match S-XX format (e.g. S-05)`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!state.delivery_id || !/^D-\d{2}$/.test(state.delivery_id)) {
|
|
40
|
+
errors.push(`delivery_id "${state.delivery_id}" must match D-NN format (e.g. D-02)`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!state.stories || typeof state.stories !== 'object') {
|
|
44
|
+
errors.push('stories field must be an object');
|
|
45
|
+
} else {
|
|
46
|
+
for (const [id, story] of Object.entries(state.stories)) {
|
|
47
|
+
if (!VALID_STATES.includes(story.state)) {
|
|
48
|
+
errors.push(`Story ${id}: invalid state "${story.state}". Must be one of: ${VALID_STATES.join(', ')}`);
|
|
49
|
+
}
|
|
50
|
+
if (typeof story.qa_bounces !== 'number' || !Number.isInteger(story.qa_bounces) || story.qa_bounces < 0) {
|
|
51
|
+
errors.push(`Story ${id}: qa_bounces must be a non-negative integer, got "${story.qa_bounces}"`);
|
|
52
|
+
}
|
|
53
|
+
if (typeof story.arch_bounces !== 'number' || !Number.isInteger(story.arch_bounces) || story.arch_bounces < 0) {
|
|
54
|
+
errors.push(`Story ${id}: arch_bounces must be a non-negative integer, got "${story.arch_bounces}"`);
|
|
55
|
+
}
|
|
56
|
+
if (story.state === 'Done' && story.worktree) {
|
|
57
|
+
errors.push(`Story ${id}: state is "Done" but worktree "${story.worktree}" is still set (should be null)`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (state.updated_at) {
|
|
63
|
+
const d = new Date(state.updated_at);
|
|
64
|
+
if (isNaN(d.getTime())) {
|
|
65
|
+
errors.push(`updated_at "${state.updated_at}" is not a valid ISO 8601 timestamp`);
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
errors.push('updated_at field is required');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { valid: errors.length === 0, errors };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// CLI entry point
|
|
75
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
|
|
76
|
+
if (!fs.existsSync(STATE_FILE)) {
|
|
77
|
+
console.error(`ERROR: ${STATE_FILE} not found. Run: vbounce sprint init S-XX D-XX`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let state;
|
|
82
|
+
try {
|
|
83
|
+
state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
|
84
|
+
} catch (e) {
|
|
85
|
+
console.error(`ERROR: state.json is not valid JSON — ${e.message}`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { valid, errors } = validateState(state);
|
|
90
|
+
|
|
91
|
+
if (valid) {
|
|
92
|
+
console.log(`VALID: state.json — sprint ${state.sprint_id}, ${Object.keys(state.stories || {}).length} stories`);
|
|
93
|
+
process.exit(0);
|
|
94
|
+
} else {
|
|
95
|
+
console.error('INVALID: state.json has errors:');
|
|
96
|
+
errors.forEach(e => console.error(` - ${e}`));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -61,12 +61,12 @@ repo/ ← main working directory
|
|
|
61
61
|
│
|
|
62
62
|
└── .bounce/
|
|
63
63
|
├── reports/ ← active working reports (GITIGNORED)
|
|
64
|
-
├── sprint-report.md
|
|
64
|
+
├── sprint-report-S-{XX}.md ← current sprint report (GITIGNORED)
|
|
65
65
|
└── archive/ ← completed sprint history (COMMITTED TO GIT)
|
|
66
66
|
└── S-01/
|
|
67
67
|
├── STORY-001-01/ ← all agent reports for this story
|
|
68
|
-
├── sprint-report.md
|
|
69
|
-
└── sprint-devops.md
|
|
68
|
+
├── sprint-report-S-{XX}.md ← final sprint report
|
|
69
|
+
└── sprint-S-{XX}-devops.md ← release report
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
### V-Bounce State → Git Operations
|
|
@@ -154,7 +154,7 @@ For tools without native subagent support (Cursor, Codex, Gemini, Antigravity):
|
|
|
154
154
|
|
|
155
155
|
**After story completes:** Reports are archived to the shared `.bounce/archive/S-{XX}/STORY-{ID}-{StoryName}/` in the main repo before the worktree is removed.
|
|
156
156
|
|
|
157
|
-
**Sprint Report:** Always written to `.bounce/sprint-report.md` in the main repo (not in any worktree).
|
|
157
|
+
**Sprint Report:** Always written to `.bounce/sprint-report-S-{XX}.md` in the main repo (not in any worktree).
|
|
158
158
|
|
|
159
159
|
### Report File Naming
|
|
160
160
|
|
|
@@ -201,20 +201,24 @@ Examples:
|
|
|
201
201
|
e. DevOps runs `hotfix_manager.sh sync` to update any active story worktrees.
|
|
202
202
|
f. Update Delivery Plan Status to "Done".
|
|
203
203
|
|
|
204
|
-
6. **
|
|
204
|
+
6. **Gate Config Check**:
|
|
205
|
+
- If `.bounce/gate-checks.json` does not exist, run `./scripts/init_gate_config.sh` to auto-detect the project stack and generate default gate checks.
|
|
206
|
+
- If it exists, verify it's current (stack detection may have changed).
|
|
207
|
+
|
|
208
|
+
7. **Parallel Readiness Check** (before bouncing multiple stories simultaneously):
|
|
205
209
|
- Verify test runner config excludes `.worktrees/` (vitest, jest, pytest, etc.)
|
|
206
210
|
- Verify no shared mutable state between worktrees (e.g., shared temp files, singletons writing to same path)
|
|
207
211
|
- Verify `.gitignore` includes `.worktrees/`
|
|
208
212
|
If any check fails, fix before spawning parallel stories. Intermittent test failures from worktree cross-contamination erode trust in the test suite fast.
|
|
209
213
|
|
|
210
|
-
|
|
214
|
+
8. **Dependency Check & Execution Mode**:
|
|
211
215
|
- For each story, check the `Depends On:` field in its template.
|
|
212
216
|
- If Story B depends on Story A, you MUST execute them sequentially. Do not create Story B's worktree or spawn its Developer until Story A has successfully passed the DevOps merge step.
|
|
213
217
|
- Determine Execution Mode:
|
|
214
218
|
- **Full Bounce (Default)**: Normal L2-L4 stories go through full Dev → QA → Architect → DevOps flow.
|
|
215
219
|
- **Fast Track (L1/L2 Minor)**: For cosmetic UI tweaks or isolated refactors, execute Dev → DevOps only. Skip QA and Architect loops to save overhead. Validate manually during Sprint Review.
|
|
216
220
|
|
|
217
|
-
|
|
221
|
+
9. Update sprint-{XX}.md: Status → "Active"
|
|
218
222
|
```
|
|
219
223
|
|
|
220
224
|
### Step 1: Story Initialization
|
|
@@ -245,11 +249,18 @@ mkdir -p .worktrees/STORY-{ID}-{StoryName}/.bounce/{tasks,reports}
|
|
|
245
249
|
|
|
246
250
|
### Step 3: QA Pass
|
|
247
251
|
```
|
|
252
|
+
0. Run pre-QA gate scan:
|
|
253
|
+
./scripts/pre_gate_runner.sh qa .worktrees/STORY-{ID}-{StoryName}/ sprint/S-{XX}
|
|
254
|
+
- If scan FAILS on trivial issues (debug statements, missing JSDoc, TODOs):
|
|
255
|
+
Return to Developer for quick fix. Do NOT spawn QA for mechanical failures.
|
|
256
|
+
- If scan PASSES: Include scan output path in the QA task file.
|
|
248
257
|
1. Spawn qa subagent in .worktrees/STORY-{ID}-{StoryName}/ with:
|
|
249
258
|
- Developer Implementation Report
|
|
259
|
+
- Pre-QA scan results (.bounce/reports/pre-qa-scan.txt)
|
|
250
260
|
- Story §2 The Truth (acceptance criteria)
|
|
251
261
|
- LESSONS.md
|
|
252
262
|
2. QA validates against Gherkin scenarios, runs vibe-code-review
|
|
263
|
+
(skipping checks already covered by pre-qa-scan.txt)
|
|
253
264
|
3. If FAIL:
|
|
254
265
|
- QA writes Bug Report (STORY-{ID}-{StoryName}-qa-bounce{N}.md)
|
|
255
266
|
- Increment bounce counter
|
|
@@ -262,8 +273,14 @@ mkdir -p .worktrees/STORY-{ID}-{StoryName}/.bounce/{tasks,reports}
|
|
|
262
273
|
|
|
263
274
|
### Step 4: Architect Pass
|
|
264
275
|
```
|
|
276
|
+
0. Run pre-Architect gate scan:
|
|
277
|
+
./scripts/pre_gate_runner.sh arch .worktrees/STORY-{ID}-{StoryName}/ sprint/S-{XX}
|
|
278
|
+
- If scan reveals new dependencies or structural violations:
|
|
279
|
+
Return to Developer for resolution. Do NOT spawn Architect for mechanical failures.
|
|
280
|
+
- If scan PASSES: Include scan output path in the Architect task file.
|
|
265
281
|
1. Spawn architect subagent in .worktrees/STORY-{ID}-{StoryName}/ with:
|
|
266
282
|
- All reports for this story
|
|
283
|
+
- Pre-Architect scan results (.bounce/reports/pre-arch-scan.txt)
|
|
267
284
|
- Full Story spec + Roadmap §3 ADRs
|
|
268
285
|
- LESSONS.md
|
|
269
286
|
2. If FAIL:
|
|
@@ -286,7 +303,7 @@ mkdir -p .worktrees/STORY-{ID}-{StoryName}/.bounce/{tasks,reports}
|
|
|
286
303
|
- Pre-merge checks (worktree clean, gate reports verified)
|
|
287
304
|
- Archive reports to .bounce/archive/S-{XX}/STORY-{ID}-{StoryName}/
|
|
288
305
|
- Merge story branch into sprint branch (--no-ff)
|
|
289
|
-
- Post-merge validation (tests + build on sprint branch)
|
|
306
|
+
- Post-merge validation (tests + lint + build on sprint branch)
|
|
290
307
|
- Worktree removal and story branch cleanup
|
|
291
308
|
3. DevOps writes Merge Report to .bounce/archive/S-{XX}/STORY-{ID}-{StoryName}/STORY-{ID}-{StoryName}-devops.md
|
|
292
309
|
4. If merge conflicts:
|
|
@@ -310,7 +327,7 @@ After ALL stories are merged into `sprint/S-01`:
|
|
|
310
327
|
```
|
|
311
328
|
1. Read all archived reports in .bounce/archive/S-{XX}/
|
|
312
329
|
2. **Sum the `tokens_used` field** from every agent report to calculate the sprint's total resource cost.
|
|
313
|
-
3. Generate Sprint Report to .bounce/sprint-report.md:
|
|
330
|
+
3. Generate Sprint Report to .bounce/sprint-report-S-{XX}.md:
|
|
314
331
|
- Ensure the Sprint Report starts with a YAML frontmatter block containing:
|
|
315
332
|
```yaml
|
|
316
333
|
---
|
|
@@ -330,9 +347,9 @@ After ALL stories are merged into `sprint/S-01`:
|
|
|
330
347
|
- Run full test suite + build + lint on main
|
|
331
348
|
- Sprint branch cleanup
|
|
332
349
|
- Environment verification (if applicable)
|
|
333
|
-
- DevOps writes Sprint Release Report to .bounce/archive/S-{XX}/sprint-devops.md
|
|
350
|
+
- DevOps writes Sprint Release Report to .bounce/archive/S-{XX}/sprint-S-{XX}-devops.md
|
|
334
351
|
6. Lead finalizes:
|
|
335
|
-
- Move sprint-report.md to .bounce/archive/S-{XX}/
|
|
352
|
+
- Move sprint-report-S-{XX}.md to .bounce/archive/S-{XX}/
|
|
336
353
|
- Record lessons (with user approval)
|
|
337
354
|
- Update delivery_plan.md to reflect the completed sprint.
|
|
338
355
|
7. **Framework Self-Assessment** (aggregated from agent reports):
|
|
@@ -403,15 +420,18 @@ When ALL sprints in a delivery (release) are done:
|
|
|
403
420
|
|
|
404
421
|
The Team Lead MUST update the active `sprint-{XX}.md` at every state transition. This is the source of truth for execution.
|
|
405
422
|
|
|
406
|
-
| Action | Sprint Plan Update |
|
|
407
|
-
|
|
408
|
-
| Worktree created | §1
|
|
409
|
-
| Dev report written | No update (still "Bouncing") |
|
|
410
|
-
| QA passes | §1
|
|
411
|
-
| Architect passes | §1
|
|
412
|
-
| DevOps merges story | §1
|
|
413
|
-
| Escalated | §1 Escalated
|
|
414
|
-
|
|
|
423
|
+
| Action | Sprint Plan Update | Delivery Plan Update |
|
|
424
|
+
|--------|-------------------|--------------------|
|
|
425
|
+
| Worktree created | §1: V-Bounce State → "Bouncing" | **Nothing** — Sprint Plan is source of truth |
|
|
426
|
+
| Dev report written | No update (still "Bouncing") | **Nothing** |
|
|
427
|
+
| QA passes | §1: V-Bounce State → "QA Passed" | **Nothing** |
|
|
428
|
+
| Architect passes | §1: V-Bounce State → "Architect Passed" | **Nothing** |
|
|
429
|
+
| DevOps merges story | §1: V-Bounce State → "Done". §4: Add Execution Log row (via `vbounce story complete`) | **Nothing** |
|
|
430
|
+
| Escalated | §1: Move story to Escalated section | **Nothing** |
|
|
431
|
+
| Sprint CLOSES | Status → "Completed" in frontmatter | §2: sprint → Completed. §4: add summary. §3: remove delivered stories |
|
|
432
|
+
|
|
433
|
+
> **Key rule**: The Delivery Plan is updated ONLY at sprint close, never during active bouncing.
|
|
434
|
+
> See `skills/agent-team/references/delivery-sync.md` for full sync rules.
|
|
415
435
|
|
|
416
436
|
---
|
|
417
437
|
|
|
@@ -431,6 +451,21 @@ When QA bounce count >= 3 OR Architect bounce count >= 3:
|
|
|
431
451
|
- **If returned to Refinement:** The spec has been rewritten. You MUST reset the QA and Architect bounce counters to 0 for this story.
|
|
432
452
|
- If killed: `git worktree remove`, branch preserved unmerged
|
|
433
453
|
|
|
454
|
+
### Mid-Sprint Change Requests
|
|
455
|
+
When the user provides input mid-bounce that isn't a direct answer to an agent question (e.g., "this is broken", "change the approach", "I meant X not Y"), the Team Lead MUST triage it before acting.
|
|
456
|
+
|
|
457
|
+
> See `skills/agent-team/references/mid-sprint-triage.md` for the full triage flow, routing rules, and logging requirements.
|
|
458
|
+
|
|
459
|
+
**Quick reference — categories:**
|
|
460
|
+
| Category | Route | Bounce Impact |
|
|
461
|
+
|----------|-------|---------------|
|
|
462
|
+
| **Bug** | Hotfix or bug-fix task in current story | No bounce increment |
|
|
463
|
+
| **Spec Clarification** | Update Story spec, continue bounce | No impact |
|
|
464
|
+
| **Scope Change** | Pause, update spec, confirm with user | Resets Dev pass |
|
|
465
|
+
| **Approach Change** | Update §3 Implementation Guide, re-delegate | Resets Dev pass |
|
|
466
|
+
|
|
467
|
+
Every change request is logged in `sprint-{XX}.md` §4 Execution Log with event type `CR` and reported in Sprint Report §2.1.
|
|
468
|
+
|
|
434
469
|
### Mid-Sprint Strategic Changes
|
|
435
470
|
Charter and Roadmap are typically **frozen** during active sprints. However, if an emergency requires modifying them:
|
|
436
471
|
1. You MUST pause active bouncing across all stories.
|
|
@@ -453,7 +488,7 @@ If merging story branch into sprint branch creates conflicts:
|
|
|
453
488
|
- **Reports are the only handoff.** No agent communicates with another directly.
|
|
454
489
|
- **One bounce = one report.** Every agent pass produces exactly one report file.
|
|
455
490
|
- **Archive before remove.** Always copy reports to shared archive before removing a worktree.
|
|
456
|
-
- **Sync the
|
|
491
|
+
- **Sync the Sprint Plan.** Update V-Bounce State in sprint-{XX}.md §1 at EVERY transition. The Sprint Plan is the source of truth DURING the sprint. The Delivery Plan is updated at sprint boundaries only — see `skills/agent-team/references/delivery-sync.md`.
|
|
457
492
|
- **Track bounce counts.** QA and Architect bounces are tracked separately per story.
|
|
458
493
|
- **Git tracking rules.** `.worktrees/`, `.bounce/reports/`, and `.bounce/sprint-report.md` are gitignored (ephemeral). `.bounce/archive/` is **committed to git** (permanent audit trail).
|
|
459
494
|
- **Check risks before bouncing.** Read RISK_REGISTRY.md at sprint start. Flag high-severity risks that affect planned stories.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Cleanup & Archive Rules
|
|
2
|
+
|
|
3
|
+
> On-demand reference from agent-team/SKILL.md. Read during sprint close or when archiving.
|
|
4
|
+
|
|
5
|
+
## After Each Story Completes (DevOps Step 5)
|
|
6
|
+
|
|
7
|
+
1. Archive all reports to `.bounce/archive/S-{XX}/STORY-{ID}-{StoryName}/`
|
|
8
|
+
2. Merge story branch into sprint branch (--no-ff)
|
|
9
|
+
3. Validate tests/build on sprint branch
|
|
10
|
+
4. Remove worktree: `git worktree remove .worktrees/STORY-{ID}-{StoryName}`
|
|
11
|
+
5. Delete story branch: `git branch -d story/STORY-{ID}-{StoryName}`
|
|
12
|
+
6. Write DevOps Merge Report to `.bounce/archive/S-{XX}/STORY-{ID}-{StoryName}/`
|
|
13
|
+
|
|
14
|
+
## After Sprint Completes (DevOps Step 7)
|
|
15
|
+
|
|
16
|
+
1. Merge sprint branch into main (--no-ff)
|
|
17
|
+
2. Tag release: `git tag -a v{VERSION}`
|
|
18
|
+
3. Run full validation (tests + build + lint)
|
|
19
|
+
4. Delete sprint branch: `git branch -d sprint/S-{XX}`
|
|
20
|
+
5. Verify `.worktrees/` is empty
|
|
21
|
+
6. Write Sprint Release Report to `.bounce/archive/S-{XX}/sprint-S-{XX}-devops.md`
|
|
22
|
+
7. Lead archives Sprint Plan: `mv product_plans/sprints/sprint-{XX}/ product_plans/archive/sprints/sprint-{XX}/`
|
|
23
|
+
8. Lead archives sprint report: `mv .bounce/sprint-report-S-{XX}.md .bounce/archive/S-{XX}/`
|
|
24
|
+
9. Run: `vbounce trends && vbounce suggest S-{XX}` — generates improvement recommendations
|
|
25
|
+
|
|
26
|
+
## Retention Policy
|
|
27
|
+
|
|
28
|
+
| Location | Status | Rule |
|
|
29
|
+
|----------|--------|------|
|
|
30
|
+
| `.bounce/archive/` | **Committed to git** | Permanent audit trail — never delete |
|
|
31
|
+
| `.bounce/reports/` | **Gitignored** | Active working files only — ephemeral |
|
|
32
|
+
| `.bounce/sprint-report-S-{XX}.md` | **Gitignored** | Active sprint report — archived on close |
|
|
33
|
+
| `.bounce/sprint-context-*.md` | **Gitignored** | Regenerated each session |
|
|
34
|
+
| `.worktrees/` | **Gitignored** | Ephemeral — exists only during active bouncing |
|
|
35
|
+
| `product_plans/archive/` | **Committed** | Completed deliveries with all docs |
|
|
36
|
+
|
|
37
|
+
## After Delivery Completes (Team Lead)
|
|
38
|
+
|
|
39
|
+
1. Verify all stories in delivery are "Done" in Delivery Plan §4
|
|
40
|
+
2. Move delivery folder: `mv product_plans/D-{NN}_{name}/ product_plans/archive/D-{NN}_{name}/`
|
|
41
|
+
3. Add Delivery Log entry to Roadmap §7
|
|
42
|
+
4. Update Roadmap §2 Release Plan: status → "Delivered"
|