vbounce-engine 2.5.1
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 +142 -0
- package/VBOUNCE_MANIFEST.md +404 -0
- package/bin/vbounce.mjs +882 -0
- package/brains/AGENTS.md +71 -0
- package/brains/CHANGELOG.md +398 -0
- package/brains/CLAUDE.md +90 -0
- package/brains/GEMINI.md +102 -0
- package/brains/SETUP.md +195 -0
- package/brains/claude-agents/architect.md +226 -0
- package/brains/claude-agents/developer.md +133 -0
- package/brains/claude-agents/devops.md +267 -0
- package/brains/claude-agents/explorer.md +157 -0
- package/brains/claude-agents/qa.md +225 -0
- package/brains/claude-agents/scribe.md +171 -0
- package/brains/copilot/copilot-instructions.md +54 -0
- package/brains/cursor-rules/vbounce-docs.mdc +45 -0
- package/brains/cursor-rules/vbounce-process.mdc +51 -0
- package/brains/cursor-rules/vbounce-rules.mdc +29 -0
- package/brains/windsurf/.windsurfrules +35 -0
- package/docs/HOTFIX_EDGE_CASES.md +37 -0
- package/docs/IMPROVEMENT.md +46 -0
- package/docs/agent-skill-profiles.docx +0 -0
- package/docs/icons/alert.svg +1 -0
- package/docs/icons/beaker.svg +1 -0
- package/docs/icons/book.svg +1 -0
- package/docs/icons/git-branch.svg +1 -0
- package/docs/icons/git-merge.svg +1 -0
- package/docs/icons/graph.svg +1 -0
- package/docs/icons/light-bulb.svg +1 -0
- package/docs/icons/logo.svg +9 -0
- package/docs/icons/pencil.svg +1 -0
- package/docs/icons/rocket.svg +1 -0
- package/docs/icons/shield.svg +1 -0
- package/docs/icons/sync.svg +1 -0
- package/docs/icons/terminal.svg +1 -0
- package/docs/icons/tools.svg +1 -0
- package/docs/icons/zap.svg +1 -0
- package/docs/images/bounce_loop_diagram.png +0 -0
- package/docs/vbounce-os-manual.docx +0 -0
- package/package.json +48 -0
- package/scripts/close_sprint.mjs +134 -0
- package/scripts/complete_story.mjs +121 -0
- package/scripts/count_tokens.mjs +494 -0
- package/scripts/doctor.mjs +144 -0
- package/scripts/hotfix_manager.sh +157 -0
- package/scripts/init_gate_config.sh +151 -0
- package/scripts/init_sprint.mjs +129 -0
- package/scripts/post_sprint_improve.mjs +486 -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 +152 -0
- package/scripts/prep_sprint_context.mjs +141 -0
- package/scripts/prep_sprint_summary.mjs +154 -0
- package/scripts/product_graph.mjs +387 -0
- package/scripts/product_impact.mjs +167 -0
- package/scripts/sprint_trends.mjs +160 -0
- package/scripts/suggest_improvements.mjs +363 -0
- package/scripts/update_state.mjs +132 -0
- package/scripts/validate_bounce_readiness.mjs +152 -0
- package/scripts/validate_report.mjs +165 -0
- 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/scripts/verify_framework.mjs +122 -0
- package/scripts/verify_framework.sh +13 -0
- package/skills/agent-team/SKILL.md +579 -0
- package/skills/agent-team/references/cleanup.md +42 -0
- package/skills/agent-team/references/delivery-sync.md +43 -0
- package/skills/agent-team/references/discovery.md +97 -0
- package/skills/agent-team/references/git-strategy.md +52 -0
- package/skills/agent-team/references/mid-sprint-triage.md +85 -0
- package/skills/agent-team/references/report-naming.md +34 -0
- package/skills/doc-manager/SKILL.md +444 -0
- package/skills/file-organization/SKILL.md +146 -0
- package/skills/file-organization/TEST-RESULTS.md +193 -0
- package/skills/file-organization/evals/evals.json +41 -0
- package/skills/file-organization/references/gitignore-template.md +53 -0
- package/skills/file-organization/references/quick-checklist.md +48 -0
- package/skills/improve/SKILL.md +296 -0
- package/skills/lesson/SKILL.md +136 -0
- package/skills/product-graph/SKILL.md +102 -0
- package/skills/react-best-practices/SKILL.md +3014 -0
- package/skills/react-best-practices/rules/_sections.md +46 -0
- package/skills/react-best-practices/rules/_template.md +28 -0
- package/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/skills/react-best-practices/rules/advanced-init-once.md +42 -0
- package/skills/react-best-practices/rules/advanced-use-latest.md +39 -0
- package/skills/react-best-practices/rules/async-api-routes.md +38 -0
- package/skills/react-best-practices/rules/async-defer-await.md +80 -0
- package/skills/react-best-practices/rules/async-dependencies.md +51 -0
- package/skills/react-best-practices/rules/async-parallel.md +28 -0
- package/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/skills/react-best-practices/rules/bundle-conditional.md +31 -0
- package/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/skills/react-best-practices/rules/bundle-preload.md +50 -0
- package/skills/react-best-practices/rules/client-event-listeners.md +74 -0
- package/skills/react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/skills/react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
- package/skills/react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
- package/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
- package/skills/react-best-practices/rules/js-cache-storage.md +70 -0
- package/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
- package/skills/react-best-practices/rules/js-early-exit.md +50 -0
- package/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/skills/react-best-practices/rules/js-index-maps.md +37 -0
- package/skills/react-best-practices/rules/js-length-check-first.md +49 -0
- package/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
- package/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/skills/react-best-practices/rules/rendering-activity.md +26 -0
- package/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/skills/react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/skills/react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
- package/skills/react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
- package/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/skills/react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/skills/react-best-practices/rules/rerender-memo.md +44 -0
- package/skills/react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/skills/react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/skills/react-best-practices/rules/rerender-transitions.md +40 -0
- package/skills/react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/skills/react-best-practices/rules/server-auth-actions.md +96 -0
- package/skills/react-best-practices/rules/server-cache-lru.md +41 -0
- package/skills/react-best-practices/rules/server-cache-react.md +76 -0
- package/skills/react-best-practices/rules/server-dedup-props.md +65 -0
- package/skills/react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/skills/react-best-practices/rules/server-serialization.md +38 -0
- package/skills/vibe-code-review/SKILL.md +70 -0
- package/skills/vibe-code-review/references/deep-audit.md +259 -0
- package/skills/vibe-code-review/references/pr-review.md +234 -0
- package/skills/vibe-code-review/references/quick-scan.md +178 -0
- package/skills/vibe-code-review/references/report-template.md +189 -0
- package/skills/vibe-code-review/references/trend-check.md +224 -0
- package/skills/vibe-code-review/scripts/generate-snapshot.sh +89 -0
- package/skills/vibe-code-review/scripts/pr-analyze.sh +180 -0
- package/skills/write-skill/SKILL.md +133 -0
- package/templates/bug.md +100 -0
- package/templates/change_request.md +105 -0
- package/templates/charter.md +144 -0
- package/templates/delivery_plan.md +44 -0
- package/templates/epic.md +203 -0
- package/templates/hotfix.md +58 -0
- package/templates/risk_registry.md +87 -0
- package/templates/roadmap.md +174 -0
- package/templates/spike.md +143 -0
- package/templates/sprint.md +134 -0
- package/templates/sprint_context.md +61 -0
- package/templates/sprint_report.md +215 -0
- package/templates/story.md +193 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* sprint_trends.mjs
|
|
5
|
+
* Cross-sprint trend analysis from archived reports.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ./scripts/sprint_trends.mjs
|
|
9
|
+
*
|
|
10
|
+
* Output: .vbounce/trends.md
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
import yaml from 'js-yaml';
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const ROOT = path.resolve(__dirname, '../..');
|
|
20
|
+
|
|
21
|
+
const archiveBase = path.join(ROOT, '.vbounce', 'archive');
|
|
22
|
+
|
|
23
|
+
if (!fs.existsSync(archiveBase)) {
|
|
24
|
+
console.log('No sprint history found (.vbounce/archive/ does not exist)');
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const sprintDirs = fs.readdirSync(archiveBase)
|
|
29
|
+
.filter(d => /^S-\d{2}$/.test(d))
|
|
30
|
+
.sort();
|
|
31
|
+
|
|
32
|
+
if (sprintDirs.length === 0) {
|
|
33
|
+
console.log('No sprint history found (no S-XX directories in .vbounce/archive/)');
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function findReports(dir) {
|
|
38
|
+
const results = [];
|
|
39
|
+
if (!fs.existsSync(dir)) return results;
|
|
40
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
41
|
+
for (const e of entries) {
|
|
42
|
+
const full = path.join(dir, e.name);
|
|
43
|
+
if (e.isDirectory()) results.push(...findReports(full));
|
|
44
|
+
else if (e.name.endsWith('.md')) results.push(full);
|
|
45
|
+
}
|
|
46
|
+
return results;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseFm(filePath) {
|
|
50
|
+
try {
|
|
51
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
52
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
53
|
+
if (match) return yaml.load(match[1]) || {};
|
|
54
|
+
} catch {}
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const sprintStats = [];
|
|
59
|
+
const rootCauseCounts = {};
|
|
60
|
+
|
|
61
|
+
for (const sprintId of sprintDirs) {
|
|
62
|
+
const sprintDir = path.join(archiveBase, sprintId);
|
|
63
|
+
const reports = findReports(sprintDir);
|
|
64
|
+
|
|
65
|
+
const storyIds = new Set();
|
|
66
|
+
const pattern = /(STORY-[\w-]+)-(?:dev|qa|arch|devops)/;
|
|
67
|
+
for (const r of reports) {
|
|
68
|
+
const m = path.basename(r).match(pattern);
|
|
69
|
+
if (m) storyIds.add(m[1]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let qaFails = 0, archFails = 0, firstPassCount = 0;
|
|
73
|
+
let correctionTaxSum = 0, correctionTaxCount = 0, totalTokens = 0;
|
|
74
|
+
const storyQaBounces = {};
|
|
75
|
+
|
|
76
|
+
for (const r of reports) {
|
|
77
|
+
const fm = parseFm(r);
|
|
78
|
+
if (fm.tokens_used) totalTokens += fm.tokens_used;
|
|
79
|
+
|
|
80
|
+
const bn = path.basename(r);
|
|
81
|
+
if (/-qa(-bounce\d+)?\.md$/.test(bn) && fm.status === 'FAIL') {
|
|
82
|
+
qaFails++;
|
|
83
|
+
const m = bn.match(pattern);
|
|
84
|
+
if (m) storyQaBounces[m[1]] = (storyQaBounces[m[1]] || 0) + 1;
|
|
85
|
+
|
|
86
|
+
// Collect root causes
|
|
87
|
+
if (fm.root_cause) {
|
|
88
|
+
rootCauseCounts[fm.root_cause] = rootCauseCounts[fm.root_cause] || {};
|
|
89
|
+
rootCauseCounts[fm.root_cause][sprintId] = (rootCauseCounts[fm.root_cause][sprintId] || 0) + 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (/-arch(-bounce\d+)?\.md$/.test(bn) && fm.status === 'FAIL') {
|
|
93
|
+
archFails++;
|
|
94
|
+
if (fm.root_cause) {
|
|
95
|
+
rootCauseCounts[fm.root_cause] = rootCauseCounts[fm.root_cause] || {};
|
|
96
|
+
rootCauseCounts[fm.root_cause][sprintId] = (rootCauseCounts[fm.root_cause][sprintId] || 0) + 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (/-dev\.md$/.test(bn)) {
|
|
100
|
+
const tax = parseFloat(String(fm.correction_tax || '0').replace('%', ''));
|
|
101
|
+
if (!isNaN(tax)) { correctionTaxSum += tax; correctionTaxCount++; }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const [id] of Object.entries(storyQaBounces)) {
|
|
106
|
+
if (!storyQaBounces[id]) firstPassCount++;
|
|
107
|
+
}
|
|
108
|
+
// Stories with no QA failures = first pass
|
|
109
|
+
for (const id of storyIds) {
|
|
110
|
+
if (!storyQaBounces[id]) firstPassCount++;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const totalStories = storyIds.size;
|
|
114
|
+
const firstPassRate = totalStories > 0 ? Math.round((firstPassCount / totalStories) * 100) : 100;
|
|
115
|
+
const avgBounces = totalStories > 0 ? ((qaFails + archFails) / totalStories).toFixed(2) : '0.00';
|
|
116
|
+
const avgTax = correctionTaxCount > 0 ? (correctionTaxSum / correctionTaxCount).toFixed(1) : '0.0';
|
|
117
|
+
|
|
118
|
+
sprintStats.push({ sprintId, totalStories, firstPassRate, avgBounces, avgTax, totalTokens });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Build process health table
|
|
122
|
+
const healthRows = sprintStats.map(s =>
|
|
123
|
+
`| ${s.sprintId} | ${s.totalStories} | ${s.firstPassRate}% | ${s.avgBounces} | ${s.avgTax}% | ${s.totalTokens.toLocaleString()} |`
|
|
124
|
+
).join('\n');
|
|
125
|
+
|
|
126
|
+
// Build root cause table if data available
|
|
127
|
+
let rootCauseSection = '';
|
|
128
|
+
const allCauses = Object.keys(rootCauseCounts);
|
|
129
|
+
if (allCauses.length > 0) {
|
|
130
|
+
const causeRows = allCauses.map(cause => {
|
|
131
|
+
const counts = sprintDirs.map(s => rootCauseCounts[cause][s] || 0);
|
|
132
|
+
return `| ${cause} | ${counts.join(' | ')} |`;
|
|
133
|
+
});
|
|
134
|
+
rootCauseSection = [
|
|
135
|
+
'',
|
|
136
|
+
`## Bounce Root Causes`,
|
|
137
|
+
`| Category | ${sprintDirs.join(' | ')} |`,
|
|
138
|
+
`|----------|${sprintDirs.map(() => '---').join('|')}|`,
|
|
139
|
+
...causeRows,
|
|
140
|
+
].join('\n');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const output = [
|
|
144
|
+
`# V-Bounce Trends`,
|
|
145
|
+
`> Generated: ${new Date().toISOString().split('T')[0]} | Sprints analyzed: ${sprintDirs.join(', ')}`,
|
|
146
|
+
'',
|
|
147
|
+
`## Process Health`,
|
|
148
|
+
`| Sprint | Stories | First-Pass Rate | Avg Bounces | Avg Correction Tax | Total Tokens |`,
|
|
149
|
+
`|--------|---------|----------------|-------------|-------------------|--------------|`,
|
|
150
|
+
healthRows || '| (no data) | — | — | — | — | — |',
|
|
151
|
+
rootCauseSection,
|
|
152
|
+
'',
|
|
153
|
+
`---`,
|
|
154
|
+
`Run \`vbounce suggest S-XX\` to generate improvement recommendations based on this data.`,
|
|
155
|
+
].join('\n');
|
|
156
|
+
|
|
157
|
+
const outputFile = path.join(ROOT, '.vbounce', 'trends.md');
|
|
158
|
+
fs.writeFileSync(outputFile, output);
|
|
159
|
+
console.log(`✓ Trends written to .vbounce/trends.md`);
|
|
160
|
+
console.log(` Sprints analyzed: ${sprintDirs.join(', ')}`);
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* suggest_improvements.mjs
|
|
5
|
+
* Generates human-readable improvement suggestions from:
|
|
6
|
+
* 1. Improvement manifest (post_sprint_improve.mjs output)
|
|
7
|
+
* 2. Sprint trends
|
|
8
|
+
* 3. LESSONS.md
|
|
9
|
+
*
|
|
10
|
+
* Overwrites (not appends) to prevent stale suggestion accumulation.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* ./scripts/suggest_improvements.mjs S-05
|
|
14
|
+
*
|
|
15
|
+
* Output: .vbounce/improvement-suggestions.md
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import { fileURLToPath } from 'url';
|
|
21
|
+
import { spawnSync } from 'child_process';
|
|
22
|
+
|
|
23
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const ROOT = path.resolve(__dirname, '../..');
|
|
25
|
+
|
|
26
|
+
const sprintId = process.argv[2];
|
|
27
|
+
if (!sprintId) {
|
|
28
|
+
console.error('Usage: suggest_improvements.mjs S-XX');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const today = new Date().toISOString().split('T')[0];
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// 0. Run post_sprint_improve.mjs to generate fresh manifest
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
const analyzerScript = path.join(__dirname, 'post_sprint_improve.mjs');
|
|
39
|
+
if (fs.existsSync(analyzerScript)) {
|
|
40
|
+
console.log('Running post-sprint improvement analyzer...');
|
|
41
|
+
const result = spawnSync(process.execPath, [analyzerScript, sprintId], {
|
|
42
|
+
stdio: 'inherit',
|
|
43
|
+
cwd: process.cwd(),
|
|
44
|
+
});
|
|
45
|
+
if (result.status !== 0) {
|
|
46
|
+
console.warn('⚠ Analyzer returned non-zero — continuing with available data.');
|
|
47
|
+
}
|
|
48
|
+
console.log('');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// 1. Read improvement manifest (from post_sprint_improve.mjs)
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
const manifestPath = path.join(ROOT, '.vbounce', 'improvement-manifest.json');
|
|
56
|
+
let manifest = null;
|
|
57
|
+
if (fs.existsSync(manifestPath)) {
|
|
58
|
+
try {
|
|
59
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
60
|
+
} catch {
|
|
61
|
+
console.warn('⚠ Could not parse improvement-manifest.json');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// 2. Read trends if available
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
const trendsFile = path.join(ROOT, '.vbounce', 'trends.md');
|
|
70
|
+
let trendsContent = null;
|
|
71
|
+
if (fs.existsSync(trendsFile)) {
|
|
72
|
+
trendsContent = fs.readFileSync(trendsFile, 'utf8');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// 3. Read LESSONS.md
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
const lessonsFile = path.join(ROOT, 'LESSONS.md');
|
|
80
|
+
let lessonCount = 0;
|
|
81
|
+
let oldLessons = [];
|
|
82
|
+
if (fs.existsSync(lessonsFile)) {
|
|
83
|
+
const lines = fs.readFileSync(lessonsFile, 'utf8').split('\n');
|
|
84
|
+
const lessonEntries = lines.filter(l => /^###\s+\[\d{4}-\d{2}-\d{2}\]/.test(l));
|
|
85
|
+
lessonCount = lessonEntries.length;
|
|
86
|
+
|
|
87
|
+
const cutoff = new Date();
|
|
88
|
+
cutoff.setDate(cutoff.getDate() - 90);
|
|
89
|
+
oldLessons = lessonEntries.filter(entry => {
|
|
90
|
+
const dateMatch = entry.match(/\[(\d{4}-\d{2}-\d{2})\]/);
|
|
91
|
+
if (dateMatch) return new Date(dateMatch[1]) < cutoff;
|
|
92
|
+
return false;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// 4. Read improvement-log for rejected items
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
const improvementLog = path.join(ROOT, '.vbounce', 'improvement-log.md');
|
|
101
|
+
let rejectedItems = [];
|
|
102
|
+
if (fs.existsSync(improvementLog)) {
|
|
103
|
+
const logContent = fs.readFileSync(improvementLog, 'utf8');
|
|
104
|
+
const rejectedMatch = logContent.match(/## Rejected\n[\s\S]*?(?=\n## |$)/);
|
|
105
|
+
if (rejectedMatch) {
|
|
106
|
+
rejectedItems = rejectedMatch[0].split('\n')
|
|
107
|
+
.filter(l => l.startsWith('|') && !l.startsWith('| Sprint'))
|
|
108
|
+
.map(l => l.split('|')[2]?.trim())
|
|
109
|
+
.filter(Boolean);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// 5. Parse sprint stats from trends
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
let lastSprintStats = null;
|
|
118
|
+
if (trendsContent) {
|
|
119
|
+
const rows = trendsContent.split('\n').filter(l => l.match(/^\| S-\d{2} \|/));
|
|
120
|
+
if (rows.length > 0) {
|
|
121
|
+
const lastRow = rows[rows.length - 1].split('|').map(s => s.trim()).filter(Boolean);
|
|
122
|
+
lastSprintStats = {
|
|
123
|
+
sprintId: lastRow[0],
|
|
124
|
+
stories: parseInt(lastRow[1]) || 0,
|
|
125
|
+
firstPassRate: parseInt(lastRow[2]) || 100,
|
|
126
|
+
avgBounces: parseFloat(lastRow[3]) || 0,
|
|
127
|
+
avgTax: parseFloat(lastRow[4]) || 0,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// 6. Build suggestions
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
const suggestions = [];
|
|
137
|
+
let itemNum = 1;
|
|
138
|
+
|
|
139
|
+
// Impact level badge
|
|
140
|
+
function badge(impact) {
|
|
141
|
+
const badges = {
|
|
142
|
+
P0: '🔴 P0 Critical',
|
|
143
|
+
P1: '🟠 P1 High',
|
|
144
|
+
P2: '🟡 P2 Medium',
|
|
145
|
+
P3: '⚪ P3 Low',
|
|
146
|
+
};
|
|
147
|
+
return badges[impact?.level] || '⚪ Unrated';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// --- Manifest-driven suggestions (retro + lessons + effectiveness) ---
|
|
151
|
+
if (manifest && manifest.proposals) {
|
|
152
|
+
for (const proposal of manifest.proposals) {
|
|
153
|
+
// Skip previously rejected
|
|
154
|
+
if (rejectedItems.some(r => proposal.title.includes(r))) continue;
|
|
155
|
+
|
|
156
|
+
if (proposal.source === 'retro') {
|
|
157
|
+
suggestions.push({
|
|
158
|
+
num: itemNum++,
|
|
159
|
+
category: 'Retro',
|
|
160
|
+
impact: proposal.impact,
|
|
161
|
+
title: proposal.title,
|
|
162
|
+
detail: [
|
|
163
|
+
`**Area:** ${proposal.area}`,
|
|
164
|
+
`**Source Agent:** ${proposal.sourceAgent}`,
|
|
165
|
+
`**Severity:** ${proposal.severity}`,
|
|
166
|
+
proposal.recurring ? `**Recurring:** Yes — found in ${proposal.recurrenceSprints.join(', ')} (${proposal.recurrenceCount}x)` : null,
|
|
167
|
+
`**Suggested Fix:** ${proposal.suggestedFix}`,
|
|
168
|
+
].filter(Boolean).join('\n'),
|
|
169
|
+
target: mapAreaToTarget(proposal.area),
|
|
170
|
+
effort: proposal.severity === 'Blocker' ? 'Medium' : 'Low',
|
|
171
|
+
});
|
|
172
|
+
} else if (proposal.source === 'lesson') {
|
|
173
|
+
suggestions.push({
|
|
174
|
+
num: itemNum++,
|
|
175
|
+
category: 'Lesson → Automation',
|
|
176
|
+
impact: proposal.impact,
|
|
177
|
+
title: proposal.title,
|
|
178
|
+
detail: [
|
|
179
|
+
`**Rule:** ${proposal.rule}`,
|
|
180
|
+
`**What happened:** ${proposal.whatHappened}`,
|
|
181
|
+
`**Active for:** ${proposal.ageSprints} sprint(s) (since ${proposal.lessonDate})`,
|
|
182
|
+
`**Automation type:** ${proposal.automationType}`,
|
|
183
|
+
`**Action:** ${proposal.automationDetail?.action}`,
|
|
184
|
+
`**Rationale:** ${proposal.automationDetail?.rationale}`,
|
|
185
|
+
].filter(Boolean).join('\n'),
|
|
186
|
+
target: mapAutomationTypeToTarget(proposal.automationType),
|
|
187
|
+
effort: proposal.automationDetail?.effort || 'Low',
|
|
188
|
+
});
|
|
189
|
+
} else if (proposal.source === 'effectiveness_check') {
|
|
190
|
+
suggestions.push({
|
|
191
|
+
num: itemNum++,
|
|
192
|
+
category: 'Effectiveness',
|
|
193
|
+
impact: proposal.impact,
|
|
194
|
+
title: proposal.title,
|
|
195
|
+
detail: [
|
|
196
|
+
`**Status:** ${proposal.detail}`,
|
|
197
|
+
`**Originally applied in:** ${proposal.appliedInSprint}`,
|
|
198
|
+
'**Action:** Re-examine the original fix — it did not resolve the underlying issue.',
|
|
199
|
+
].join('\n'),
|
|
200
|
+
target: 'Review original improvement in .vbounce/improvement-log.md',
|
|
201
|
+
effort: 'Medium',
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// --- Metric-driven suggestions (from trends) ---
|
|
208
|
+
if (lastSprintStats) {
|
|
209
|
+
if (lastSprintStats.firstPassRate < 80) {
|
|
210
|
+
suggestions.push({
|
|
211
|
+
num: itemNum++,
|
|
212
|
+
category: 'Metrics',
|
|
213
|
+
impact: { level: 'P1', label: 'High' },
|
|
214
|
+
title: `Low first-pass rate (${lastSprintStats.firstPassRate}%)`,
|
|
215
|
+
detail: `First-pass rate was below 80% in ${lastSprintStats.sprintId}. This suggests spec ambiguity or insufficient context packs.`,
|
|
216
|
+
target: '.vbounce/scripts/validate_bounce_readiness.mjs',
|
|
217
|
+
effort: 'Low',
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (lastSprintStats.avgTax > 10) {
|
|
222
|
+
suggestions.push({
|
|
223
|
+
num: itemNum++,
|
|
224
|
+
category: 'Metrics',
|
|
225
|
+
impact: { level: 'P1', label: 'High' },
|
|
226
|
+
title: `High correction tax (${lastSprintStats.avgTax}% average)`,
|
|
227
|
+
detail: 'Average correction tax exceeded 10%, indicating significant human intervention.',
|
|
228
|
+
target: '.vbounce/skills/agent-team/SKILL.md Step 1',
|
|
229
|
+
effort: 'Low',
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (lastSprintStats.avgBounces > 0.5) {
|
|
234
|
+
suggestions.push({
|
|
235
|
+
num: itemNum++,
|
|
236
|
+
category: 'Metrics',
|
|
237
|
+
impact: { level: 'P2', label: 'Medium' },
|
|
238
|
+
title: `High bounce rate (${lastSprintStats.avgBounces} avg per story)`,
|
|
239
|
+
detail: 'Run `vbounce trends` to see root cause breakdown.',
|
|
240
|
+
target: '.vbounce/scripts/sprint_trends.mjs',
|
|
241
|
+
effort: 'Low',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// --- Lesson graduation ---
|
|
247
|
+
if (oldLessons.length > 0) {
|
|
248
|
+
const notRejected = oldLessons.filter(l => !rejectedItems.some(r => l.includes(r)));
|
|
249
|
+
if (notRejected.length > 0) {
|
|
250
|
+
suggestions.push({
|
|
251
|
+
num: itemNum++,
|
|
252
|
+
category: 'Graduation',
|
|
253
|
+
impact: { level: 'P2', label: 'Medium' },
|
|
254
|
+
title: `${notRejected.length} lesson(s) older than 90 days — graduation candidates`,
|
|
255
|
+
detail: notRejected.map(l => ` - ${l}`).join('\n'),
|
|
256
|
+
target: 'LESSONS.md → .claude/agents/',
|
|
257
|
+
effort: 'Low',
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// --- Health check ---
|
|
263
|
+
suggestions.push({
|
|
264
|
+
num: itemNum++,
|
|
265
|
+
category: 'Health',
|
|
266
|
+
impact: { level: 'P3', label: 'Low' },
|
|
267
|
+
title: 'Run vbounce doctor',
|
|
268
|
+
detail: 'Verify the V-Bounce Engine installation is healthy after this sprint.',
|
|
269
|
+
target: '.vbounce/scripts/doctor.mjs',
|
|
270
|
+
effort: 'Trivial',
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// 7. Format output
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
function mapAreaToTarget(area) {
|
|
278
|
+
const map = {
|
|
279
|
+
'Templates': 'templates/*.md',
|
|
280
|
+
'Agent Handoffs': '.claude/agents/*.md',
|
|
281
|
+
'RAG Pipeline': '.vbounce/scripts/prep_*.mjs',
|
|
282
|
+
'Skills': 'skills/*/SKILL.md',
|
|
283
|
+
'Process Flow': '.vbounce/skills/agent-team/SKILL.md',
|
|
284
|
+
'Tooling & Scripts': 'scripts/*',
|
|
285
|
+
};
|
|
286
|
+
return map[area] || area;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function mapAutomationTypeToTarget(type) {
|
|
290
|
+
const map = {
|
|
291
|
+
'gate_check': '.vbounce/gate-checks.json OR .vbounce/scripts/pre_gate_runner.sh',
|
|
292
|
+
'script': 'scripts/',
|
|
293
|
+
'template_field': 'templates/*.md',
|
|
294
|
+
'agent_config': '.claude/agents/*.md',
|
|
295
|
+
};
|
|
296
|
+
return map[type] || type;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const suggestionBlocks = suggestions.map(s => {
|
|
300
|
+
return `### ${s.num}. [${badge(s.impact)}] [${s.category}] ${s.title}
|
|
301
|
+
${s.detail}
|
|
302
|
+
|
|
303
|
+
**Target:** \`${s.target}\`
|
|
304
|
+
**Effort:** ${s.effort}`;
|
|
305
|
+
}).join('\n\n---\n\n');
|
|
306
|
+
|
|
307
|
+
// Impact level reference
|
|
308
|
+
const impactRef = `## Impact Levels
|
|
309
|
+
|
|
310
|
+
| Level | Label | Meaning | Timeline |
|
|
311
|
+
|-------|-------|---------|----------|
|
|
312
|
+
| **P0** | 🔴 Critical | Blocks agent work or causes incorrect output | Fix before next sprint |
|
|
313
|
+
| **P1** | 🟠 High | Causes rework — bounces, wasted tokens, repeated manual steps | Fix this improvement cycle |
|
|
314
|
+
| **P2** | 🟡 Medium | Friction that slows agents but does not block | Fix within 2 sprints |
|
|
315
|
+
| **P3** | ⚪ Low | Polish — nice-to-have, batch with other improvements | Batch when convenient |`;
|
|
316
|
+
|
|
317
|
+
// Summary stats
|
|
318
|
+
const summarySection = manifest ? `## Summary
|
|
319
|
+
|
|
320
|
+
| Source | Count |
|
|
321
|
+
|--------|-------|
|
|
322
|
+
| Retro (§5 findings) | ${manifest.summary.bySource.retro} |
|
|
323
|
+
| Lesson → Automation | ${manifest.summary.bySource.lesson} |
|
|
324
|
+
| Effectiveness checks | ${manifest.summary.bySource.effectiveness_check} |
|
|
325
|
+
| Metric-driven | ${suggestions.filter(s => s.category === 'Metrics').length} |
|
|
326
|
+
| **Total** | **${suggestions.length}** |
|
|
327
|
+
|
|
328
|
+
| Impact | Count |
|
|
329
|
+
|--------|-------|
|
|
330
|
+
| 🔴 P0 Critical | ${manifest.summary.byImpact.P0} |
|
|
331
|
+
| 🟠 P1 High | ${manifest.summary.byImpact.P1 + suggestions.filter(s => s.category === 'Metrics' && s.impact.level === 'P1').length} |
|
|
332
|
+
| 🟡 P2 Medium | ${manifest.summary.byImpact.P2 + suggestions.filter(s => s.category === 'Metrics' && s.impact.level === 'P2').length} |
|
|
333
|
+
| ⚪ P3 Low | ${manifest.summary.byImpact.P3 + suggestions.filter(s => s.category === 'Health').length} |` : '';
|
|
334
|
+
|
|
335
|
+
const output = [
|
|
336
|
+
`# Improvement Suggestions (post ${sprintId})`,
|
|
337
|
+
`> Generated: ${today}. Review each item. Approved items are applied by the Lead at sprint boundary.`,
|
|
338
|
+
`> Rejected items go to \`.vbounce/improvement-log.md\` with reason.`,
|
|
339
|
+
`> Applied items go to \`.vbounce/improvement-log.md\` under Applied.`,
|
|
340
|
+
'',
|
|
341
|
+
impactRef,
|
|
342
|
+
'',
|
|
343
|
+
summarySection,
|
|
344
|
+
'',
|
|
345
|
+
'---',
|
|
346
|
+
'',
|
|
347
|
+
suggestionBlocks || '_No suggestions generated — all metrics look healthy!_',
|
|
348
|
+
'',
|
|
349
|
+
'---',
|
|
350
|
+
'',
|
|
351
|
+
`## How to Apply`,
|
|
352
|
+
`- **Approve** → Lead applies change, records in \`.vbounce/improvement-log.md\` under Applied`,
|
|
353
|
+
`- **Reject** → Record in \`.vbounce/improvement-log.md\` under Rejected with reason`,
|
|
354
|
+
`- **Defer** → Record in \`.vbounce/improvement-log.md\` under Deferred`,
|
|
355
|
+
'',
|
|
356
|
+
`> Framework changes (.claude/agents/, .vbounce/skills/, .vbounce/templates/) are applied at sprint boundaries only — never mid-sprint.`,
|
|
357
|
+
`> Use \`/improve\` skill to have the Team Lead apply approved changes with brain-file sync.`,
|
|
358
|
+
].join('\n');
|
|
359
|
+
|
|
360
|
+
const outputFile = path.join(ROOT, '.vbounce', 'improvement-suggestions.md');
|
|
361
|
+
fs.writeFileSync(outputFile, output);
|
|
362
|
+
console.log(`✓ Improvement suggestions written to .vbounce/improvement-suggestions.md`);
|
|
363
|
+
console.log(` ${suggestions.length} suggestion(s) generated`);
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* update_state.mjs
|
|
5
|
+
* Updates .vbounce/state.json atomically at every V-Bounce state transition.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ./.vbounce/scripts/update_state.mjs STORY-005-02 "QA Passed"
|
|
9
|
+
* ./.vbounce/scripts/update_state.mjs STORY-005-02 --qa-bounce
|
|
10
|
+
* ./.vbounce/scripts/update_state.mjs STORY-005-02 --arch-bounce
|
|
11
|
+
* ./.vbounce/scripts/update_state.mjs --set-phase "Phase 3"
|
|
12
|
+
* ./.vbounce/scripts/update_state.mjs --set-action "QA FAIL on STORY-005-02, bouncing back to Dev"
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
import { validateState } from './validate_state.mjs';
|
|
19
|
+
|
|
20
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const ROOT = path.resolve(__dirname, '../..');
|
|
22
|
+
const STATE_FILE = path.join(ROOT, '.vbounce', 'state.json');
|
|
23
|
+
|
|
24
|
+
const VALID_STATES = [
|
|
25
|
+
'Draft', 'Refinement', 'Ready to Bounce', 'Bouncing',
|
|
26
|
+
'QA Passed', 'Architect Passed', 'Done', 'Escalated', 'Parking Lot'
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function readState() {
|
|
30
|
+
if (!fs.existsSync(STATE_FILE)) {
|
|
31
|
+
console.error(`ERROR: ${STATE_FILE} not found. Run: vbounce sprint init S-XX D-XX`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.error(`ERROR: state.json is not valid JSON — ${e.message}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeState(state) {
|
|
43
|
+
state.updated_at = new Date().toISOString();
|
|
44
|
+
const { valid, errors } = validateState(state);
|
|
45
|
+
if (!valid) {
|
|
46
|
+
console.error('ERROR: Would write invalid state:');
|
|
47
|
+
errors.forEach(e => console.error(` - ${e}`));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const args = process.argv.slice(2);
|
|
54
|
+
|
|
55
|
+
if (args.length === 0) {
|
|
56
|
+
console.error('Usage:');
|
|
57
|
+
console.error(' update_state.mjs STORY-ID "New State"');
|
|
58
|
+
console.error(' update_state.mjs STORY-ID --qa-bounce');
|
|
59
|
+
console.error(' update_state.mjs STORY-ID --arch-bounce');
|
|
60
|
+
console.error(' update_state.mjs --set-phase "Phase N"');
|
|
61
|
+
console.error(' update_state.mjs --set-action "description"');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const state = readState();
|
|
66
|
+
|
|
67
|
+
// Global flags
|
|
68
|
+
if (args[0] === '--set-phase') {
|
|
69
|
+
state.phase = args[1];
|
|
70
|
+
writeState(state);
|
|
71
|
+
console.log(`✓ Phase set to: ${args[1]}`);
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (args[0] === '--set-action') {
|
|
76
|
+
state.last_action = args[1];
|
|
77
|
+
writeState(state);
|
|
78
|
+
console.log(`✓ Last action set to: ${args[1]}`);
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (args[0] === '--show') {
|
|
83
|
+
console.log(JSON.stringify(state, null, 2));
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Story-specific updates
|
|
88
|
+
const storyId = args[0];
|
|
89
|
+
if (!state.stories) {
|
|
90
|
+
console.error('ERROR: state.json has no stories field');
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
if (!state.stories[storyId]) {
|
|
94
|
+
console.error(`ERROR: Story "${storyId}" not found in state.json`);
|
|
95
|
+
console.error(`Known stories: ${Object.keys(state.stories).join(', ')}`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const flag = args[1];
|
|
100
|
+
|
|
101
|
+
if (flag === '--qa-bounce') {
|
|
102
|
+
state.stories[storyId].qa_bounces = (state.stories[storyId].qa_bounces || 0) + 1;
|
|
103
|
+
state.last_action = `QA bounce on ${storyId} (total: ${state.stories[storyId].qa_bounces})`;
|
|
104
|
+
writeState(state);
|
|
105
|
+
console.log(`✓ ${storyId} QA bounces: ${state.stories[storyId].qa_bounces}`);
|
|
106
|
+
|
|
107
|
+
} else if (flag === '--arch-bounce') {
|
|
108
|
+
state.stories[storyId].arch_bounces = (state.stories[storyId].arch_bounces || 0) + 1;
|
|
109
|
+
state.last_action = `Architect bounce on ${storyId} (total: ${state.stories[storyId].arch_bounces})`;
|
|
110
|
+
writeState(state);
|
|
111
|
+
console.log(`✓ ${storyId} Arch bounces: ${state.stories[storyId].arch_bounces}`);
|
|
112
|
+
|
|
113
|
+
} else if (flag) {
|
|
114
|
+
// New state
|
|
115
|
+
if (!VALID_STATES.includes(flag)) {
|
|
116
|
+
console.error(`ERROR: Invalid state "${flag}"`);
|
|
117
|
+
console.error(`Valid states: ${VALID_STATES.join(', ')}`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
const prev = state.stories[storyId].state;
|
|
121
|
+
state.stories[storyId].state = flag;
|
|
122
|
+
if (flag === 'Done') {
|
|
123
|
+
state.stories[storyId].worktree = null;
|
|
124
|
+
}
|
|
125
|
+
state.last_action = `${storyId}: ${prev} → ${flag}`;
|
|
126
|
+
writeState(state);
|
|
127
|
+
console.log(`✓ ${storyId}: ${prev} → ${flag}`);
|
|
128
|
+
|
|
129
|
+
} else {
|
|
130
|
+
console.error('ERROR: Specify a state or flag (--qa-bounce, --arch-bounce)');
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|