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.
Files changed (165) hide show
  1. package/README.md +142 -0
  2. package/VBOUNCE_MANIFEST.md +404 -0
  3. package/bin/vbounce.mjs +882 -0
  4. package/brains/AGENTS.md +71 -0
  5. package/brains/CHANGELOG.md +398 -0
  6. package/brains/CLAUDE.md +90 -0
  7. package/brains/GEMINI.md +102 -0
  8. package/brains/SETUP.md +195 -0
  9. package/brains/claude-agents/architect.md +226 -0
  10. package/brains/claude-agents/developer.md +133 -0
  11. package/brains/claude-agents/devops.md +267 -0
  12. package/brains/claude-agents/explorer.md +157 -0
  13. package/brains/claude-agents/qa.md +225 -0
  14. package/brains/claude-agents/scribe.md +171 -0
  15. package/brains/copilot/copilot-instructions.md +54 -0
  16. package/brains/cursor-rules/vbounce-docs.mdc +45 -0
  17. package/brains/cursor-rules/vbounce-process.mdc +51 -0
  18. package/brains/cursor-rules/vbounce-rules.mdc +29 -0
  19. package/brains/windsurf/.windsurfrules +35 -0
  20. package/docs/HOTFIX_EDGE_CASES.md +37 -0
  21. package/docs/IMPROVEMENT.md +46 -0
  22. package/docs/agent-skill-profiles.docx +0 -0
  23. package/docs/icons/alert.svg +1 -0
  24. package/docs/icons/beaker.svg +1 -0
  25. package/docs/icons/book.svg +1 -0
  26. package/docs/icons/git-branch.svg +1 -0
  27. package/docs/icons/git-merge.svg +1 -0
  28. package/docs/icons/graph.svg +1 -0
  29. package/docs/icons/light-bulb.svg +1 -0
  30. package/docs/icons/logo.svg +9 -0
  31. package/docs/icons/pencil.svg +1 -0
  32. package/docs/icons/rocket.svg +1 -0
  33. package/docs/icons/shield.svg +1 -0
  34. package/docs/icons/sync.svg +1 -0
  35. package/docs/icons/terminal.svg +1 -0
  36. package/docs/icons/tools.svg +1 -0
  37. package/docs/icons/zap.svg +1 -0
  38. package/docs/images/bounce_loop_diagram.png +0 -0
  39. package/docs/vbounce-os-manual.docx +0 -0
  40. package/package.json +48 -0
  41. package/scripts/close_sprint.mjs +134 -0
  42. package/scripts/complete_story.mjs +121 -0
  43. package/scripts/count_tokens.mjs +494 -0
  44. package/scripts/doctor.mjs +144 -0
  45. package/scripts/hotfix_manager.sh +157 -0
  46. package/scripts/init_gate_config.sh +151 -0
  47. package/scripts/init_sprint.mjs +129 -0
  48. package/scripts/post_sprint_improve.mjs +486 -0
  49. package/scripts/pre_gate_common.sh +576 -0
  50. package/scripts/pre_gate_runner.sh +176 -0
  51. package/scripts/prep_arch_context.mjs +178 -0
  52. package/scripts/prep_qa_context.mjs +152 -0
  53. package/scripts/prep_sprint_context.mjs +141 -0
  54. package/scripts/prep_sprint_summary.mjs +154 -0
  55. package/scripts/product_graph.mjs +387 -0
  56. package/scripts/product_impact.mjs +167 -0
  57. package/scripts/sprint_trends.mjs +160 -0
  58. package/scripts/suggest_improvements.mjs +363 -0
  59. package/scripts/update_state.mjs +132 -0
  60. package/scripts/validate_bounce_readiness.mjs +152 -0
  61. package/scripts/validate_report.mjs +165 -0
  62. package/scripts/validate_sprint_plan.mjs +117 -0
  63. package/scripts/validate_state.mjs +99 -0
  64. package/scripts/vdoc_match.mjs +269 -0
  65. package/scripts/vdoc_staleness.mjs +199 -0
  66. package/scripts/verify_framework.mjs +122 -0
  67. package/scripts/verify_framework.sh +13 -0
  68. package/skills/agent-team/SKILL.md +579 -0
  69. package/skills/agent-team/references/cleanup.md +42 -0
  70. package/skills/agent-team/references/delivery-sync.md +43 -0
  71. package/skills/agent-team/references/discovery.md +97 -0
  72. package/skills/agent-team/references/git-strategy.md +52 -0
  73. package/skills/agent-team/references/mid-sprint-triage.md +85 -0
  74. package/skills/agent-team/references/report-naming.md +34 -0
  75. package/skills/doc-manager/SKILL.md +444 -0
  76. package/skills/file-organization/SKILL.md +146 -0
  77. package/skills/file-organization/TEST-RESULTS.md +193 -0
  78. package/skills/file-organization/evals/evals.json +41 -0
  79. package/skills/file-organization/references/gitignore-template.md +53 -0
  80. package/skills/file-organization/references/quick-checklist.md +48 -0
  81. package/skills/improve/SKILL.md +296 -0
  82. package/skills/lesson/SKILL.md +136 -0
  83. package/skills/product-graph/SKILL.md +102 -0
  84. package/skills/react-best-practices/SKILL.md +3014 -0
  85. package/skills/react-best-practices/rules/_sections.md +46 -0
  86. package/skills/react-best-practices/rules/_template.md +28 -0
  87. package/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  88. package/skills/react-best-practices/rules/advanced-init-once.md +42 -0
  89. package/skills/react-best-practices/rules/advanced-use-latest.md +39 -0
  90. package/skills/react-best-practices/rules/async-api-routes.md +38 -0
  91. package/skills/react-best-practices/rules/async-defer-await.md +80 -0
  92. package/skills/react-best-practices/rules/async-dependencies.md +51 -0
  93. package/skills/react-best-practices/rules/async-parallel.md +28 -0
  94. package/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
  95. package/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
  96. package/skills/react-best-practices/rules/bundle-conditional.md +31 -0
  97. package/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
  98. package/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  99. package/skills/react-best-practices/rules/bundle-preload.md +50 -0
  100. package/skills/react-best-practices/rules/client-event-listeners.md +74 -0
  101. package/skills/react-best-practices/rules/client-localstorage-schema.md +71 -0
  102. package/skills/react-best-practices/rules/client-passive-event-listeners.md +48 -0
  103. package/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
  104. package/skills/react-best-practices/rules/js-batch-dom-css.md +107 -0
  105. package/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
  106. package/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
  107. package/skills/react-best-practices/rules/js-cache-storage.md +70 -0
  108. package/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
  109. package/skills/react-best-practices/rules/js-early-exit.md +50 -0
  110. package/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
  111. package/skills/react-best-practices/rules/js-index-maps.md +37 -0
  112. package/skills/react-best-practices/rules/js-length-check-first.md +49 -0
  113. package/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
  114. package/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
  115. package/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
  116. package/skills/react-best-practices/rules/rendering-activity.md +26 -0
  117. package/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  118. package/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
  119. package/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
  120. package/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  121. package/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  122. package/skills/react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
  123. package/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
  124. package/skills/react-best-practices/rules/rendering-usetransition-loading.md +75 -0
  125. package/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
  126. package/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
  127. package/skills/react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
  128. package/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
  129. package/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
  130. package/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  131. package/skills/react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
  132. package/skills/react-best-practices/rules/rerender-memo.md +44 -0
  133. package/skills/react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
  134. package/skills/react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
  135. package/skills/react-best-practices/rules/rerender-transitions.md +40 -0
  136. package/skills/react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
  137. package/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
  138. package/skills/react-best-practices/rules/server-auth-actions.md +96 -0
  139. package/skills/react-best-practices/rules/server-cache-lru.md +41 -0
  140. package/skills/react-best-practices/rules/server-cache-react.md +76 -0
  141. package/skills/react-best-practices/rules/server-dedup-props.md +65 -0
  142. package/skills/react-best-practices/rules/server-parallel-fetching.md +83 -0
  143. package/skills/react-best-practices/rules/server-serialization.md +38 -0
  144. package/skills/vibe-code-review/SKILL.md +70 -0
  145. package/skills/vibe-code-review/references/deep-audit.md +259 -0
  146. package/skills/vibe-code-review/references/pr-review.md +234 -0
  147. package/skills/vibe-code-review/references/quick-scan.md +178 -0
  148. package/skills/vibe-code-review/references/report-template.md +189 -0
  149. package/skills/vibe-code-review/references/trend-check.md +224 -0
  150. package/skills/vibe-code-review/scripts/generate-snapshot.sh +89 -0
  151. package/skills/vibe-code-review/scripts/pr-analyze.sh +180 -0
  152. package/skills/write-skill/SKILL.md +133 -0
  153. package/templates/bug.md +100 -0
  154. package/templates/change_request.md +105 -0
  155. package/templates/charter.md +144 -0
  156. package/templates/delivery_plan.md +44 -0
  157. package/templates/epic.md +203 -0
  158. package/templates/hotfix.md +58 -0
  159. package/templates/risk_registry.md +87 -0
  160. package/templates/roadmap.md +174 -0
  161. package/templates/spike.md +143 -0
  162. package/templates/sprint.md +134 -0
  163. package/templates/sprint_context.md +61 -0
  164. package/templates/sprint_report.md +215 -0
  165. package/templates/story.md +193 -0
@@ -0,0 +1,494 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * count_tokens.mjs
5
+ * Counts tokens used in the current Claude Code session or subagent.
6
+ *
7
+ * How it works:
8
+ * Parses Claude Code's JSONL session files (stored at ~/.claude/projects/)
9
+ * and sums token usage from all assistant messages.
10
+ *
11
+ * Usage:
12
+ * node .vbounce/scripts/count_tokens.mjs # current session (auto-detect)
13
+ * node .vbounce/scripts/count_tokens.mjs --session <ID> # specific session
14
+ * node .vbounce/scripts/count_tokens.mjs --agent <ID> # specific subagent
15
+ * node .vbounce/scripts/count_tokens.mjs --all # all subagents in current session
16
+ * node .vbounce/scripts/count_tokens.mjs --json # JSON output (for YAML frontmatter)
17
+ */
18
+
19
+ import fs from 'fs';
20
+ import path from 'path';
21
+ import os from 'os';
22
+ import { execSync } from 'child_process';
23
+
24
+ const args = process.argv.slice(2);
25
+ let sessionId = null;
26
+ let agentId = null;
27
+ let showAll = false;
28
+ let showSelf = false;
29
+ let jsonOutput = false;
30
+ let appendTo = null;
31
+ let agentName = null;
32
+ let sprintSummary = null;
33
+
34
+ for (let i = 0; i < args.length; i++) {
35
+ switch (args[i]) {
36
+ case '--session': sessionId = args[++i]; break;
37
+ case '--agent': agentId = args[++i]; break;
38
+ case '--all': showAll = true; break;
39
+ case '--self': showSelf = true; break;
40
+ case '--json': jsonOutput = true; break;
41
+ case '--append': appendTo = args[++i]; break;
42
+ case '--name': agentName = args[++i]; break;
43
+ case '--sprint': sprintSummary = args[++i]; break;
44
+ case '--help': case '-h':
45
+ console.log(`Usage:
46
+ count_tokens.mjs # current session totals
47
+ count_tokens.mjs --all # all subagents in current session
48
+ count_tokens.mjs --self # auto-detect own subagent (for agents to self-report)
49
+ count_tokens.mjs --self --append <story.md> --name Developer # write tokens to story doc
50
+ count_tokens.mjs --sprint S-01 # aggregate tokens from all stories in a sprint
51
+ count_tokens.mjs --agent <ID> # specific subagent
52
+ count_tokens.mjs --session <ID> # specific session
53
+ count_tokens.mjs --json # JSON output for reports`);
54
+ process.exit(0);
55
+ }
56
+ }
57
+
58
+ // ── Sprint aggregation (reads Token Usage tables from story docs) ─
59
+ // This runs independently of Claude Code session files — just parses markdown.
60
+
61
+ if (sprintSummary) {
62
+ const ROOT = path.resolve(process.cwd());
63
+ const sprintNum = sprintSummary.replace('S-', '');
64
+
65
+ let sprintDir = path.join(ROOT, 'product_plans', 'sprints', `sprint-${sprintNum}`);
66
+ if (!fs.existsSync(sprintDir)) {
67
+ sprintDir = path.join(ROOT, 'product_plans', 'archive', 'sprints', `sprint-${sprintNum}`);
68
+ }
69
+ if (!fs.existsSync(sprintDir)) {
70
+ console.error(`ERROR: Sprint directory not found for ${sprintSummary}`);
71
+ process.exit(1);
72
+ }
73
+
74
+ aggregateSprintTokens(sprintDir, sprintSummary);
75
+ process.exit(0);
76
+ }
77
+
78
+ // ── Find Claude Code project directory ───────────────────────────
79
+
80
+ const CLAUDE_DIR = path.join(os.homedir(), '.claude', 'projects');
81
+ const CWD = process.cwd();
82
+
83
+ /**
84
+ * Convert a path to Claude Code's project directory name format.
85
+ * e.g., /Users/foo/bar → -Users-foo-bar
86
+ */
87
+ function pathToProjectDir(dirPath) {
88
+ return dirPath.replace(/\//g, '-').replace(/^-/, '-');
89
+ }
90
+
91
+ function tryProjectDir(dirPath) {
92
+ const candidate = pathToProjectDir(dirPath);
93
+ const candidatePath = path.join(CLAUDE_DIR, candidate);
94
+ if (fs.existsSync(candidatePath)) return candidatePath;
95
+ // Claude Code may normalize special chars (underscores → dashes)
96
+ const normalized = candidate.replace(/_/g, '-');
97
+ const normalizedPath = path.join(CLAUDE_DIR, normalized);
98
+ if (normalized !== candidate && fs.existsSync(normalizedPath)) return normalizedPath;
99
+ return null;
100
+ }
101
+
102
+ function findProjectDir() {
103
+ if (!fs.existsSync(CLAUDE_DIR)) return null;
104
+
105
+ // Try exact and parent directory matches (walks up from CWD)
106
+ let dir = CWD;
107
+ while (dir !== path.dirname(dir)) {
108
+ const found = tryProjectDir(dir);
109
+ if (found) return found;
110
+ dir = path.dirname(dir);
111
+ }
112
+
113
+ // If CWD walk-up failed, try git-based resolution for worktrees.
114
+ // When running inside a git worktree, CWD may not be a child of the
115
+ // main repo directory. `git rev-parse --git-common-dir` returns the
116
+ // shared .git directory which lives in the main repo root.
117
+ try {
118
+ const gitCommonDir = execSync('git rev-parse --git-common-dir', {
119
+ encoding: 'utf8',
120
+ cwd: CWD,
121
+ stdio: ['pipe', 'pipe', 'pipe'],
122
+ }).trim();
123
+ const resolvedGitDir = path.resolve(CWD, gitCommonDir);
124
+ // The main repo root is the parent of the .git/ directory
125
+ const mainRepoRoot = resolvedGitDir.endsWith('.git')
126
+ ? path.dirname(resolvedGitDir)
127
+ : path.dirname(resolvedGitDir.replace(/\/\.git\/.*$/, '/.git'));
128
+ const found = tryProjectDir(mainRepoRoot);
129
+ if (found) return found;
130
+ } catch {
131
+ // Not a git repo or git not available — fall through
132
+ }
133
+
134
+ return null;
135
+ }
136
+
137
+ const projectDir = findProjectDir();
138
+ if (!projectDir) {
139
+ console.error('ERROR: Could not find Claude Code project directory.');
140
+ console.error(`Looked for: ${pathToProjectDir(CWD)} in ${CLAUDE_DIR}`);
141
+ process.exit(1);
142
+ }
143
+
144
+ // ── Find session JSONL ───────────────────────────────────────────
145
+
146
+ function findLatestSession() {
147
+ const files = fs.readdirSync(projectDir)
148
+ .filter(f => f.endsWith('.jsonl') && !f.startsWith('.'))
149
+ .map(f => ({
150
+ name: f,
151
+ id: f.replace('.jsonl', ''),
152
+ mtime: fs.statSync(path.join(projectDir, f)).mtimeMs,
153
+ }))
154
+ .sort((a, b) => b.mtime - a.mtime);
155
+
156
+ return files.length > 0 ? files[0] : null;
157
+ }
158
+
159
+ // ── Parse token usage from JSONL ─────────────────────────────────
160
+
161
+ /**
162
+ * Parse a JSONL file and sum token usage from all assistant messages.
163
+ * @param {string} filePath
164
+ * @returns {{ input_tokens: number, output_tokens: number, cache_read: number, cache_creation: number, messages: number }}
165
+ */
166
+ function parseTokenUsage(filePath) {
167
+ const totals = {
168
+ input_tokens: 0,
169
+ output_tokens: 0,
170
+ cache_read: 0,
171
+ cache_creation: 0,
172
+ messages: 0,
173
+ };
174
+
175
+ if (!fs.existsSync(filePath)) return totals;
176
+
177
+ const content = fs.readFileSync(filePath, 'utf8');
178
+ const lines = content.split('\n').filter(l => l.trim());
179
+
180
+ const seenRequests = new Set();
181
+
182
+ for (const line of lines) {
183
+ try {
184
+ const msg = JSON.parse(line);
185
+ if (msg.type !== 'assistant') continue;
186
+
187
+ // Deduplicate by requestId (parallel tool calls share the same message)
188
+ const reqId = msg.message?.requestId || msg.requestId;
189
+ if (reqId && seenRequests.has(reqId)) continue;
190
+ if (reqId) seenRequests.add(reqId);
191
+
192
+ const usage = msg.message?.usage || msg.usage;
193
+ if (!usage) continue;
194
+
195
+ totals.input_tokens += usage.input_tokens || 0;
196
+ totals.output_tokens += usage.output_tokens || 0;
197
+ totals.cache_read += usage.cache_read_input_tokens || 0;
198
+ totals.cache_creation += usage.cache_creation_input_tokens || 0;
199
+ totals.messages++;
200
+ } catch {
201
+ // Skip malformed lines
202
+ }
203
+ }
204
+
205
+ return totals;
206
+ }
207
+
208
+ /**
209
+ * Read all story files in a sprint folder, parse their Token Usage tables,
210
+ * and output aggregated data.
211
+ */
212
+ function aggregateSprintTokens(sprintDir, sprintId) {
213
+ const files = fs.readdirSync(sprintDir).filter(f =>
214
+ f.endsWith('.md') && (f.startsWith('STORY-') || f.startsWith('BUG-') || f.startsWith('HOTFIX-'))
215
+ );
216
+
217
+ const stories = [];
218
+ let totalInput = 0;
219
+ let totalOutput = 0;
220
+ let totalAll = 0;
221
+
222
+ for (const file of files) {
223
+ const content = fs.readFileSync(path.join(sprintDir, file), 'utf8');
224
+ const storyId = file.replace('.md', '');
225
+
226
+ // Parse Token Usage table
227
+ const agents = [];
228
+ const lines = content.split('\n');
229
+ let inTokenTable = false;
230
+ let pastHeader = false;
231
+
232
+ for (const line of lines) {
233
+ if (line.includes('## Token Usage')) { inTokenTable = true; continue; }
234
+ if (!inTokenTable) continue;
235
+ if (!line.startsWith('|')) { if (pastHeader) break; continue; }
236
+
237
+ const cells = line.split('|').map(c => c.trim()).filter(c => c);
238
+ // Skip header and separator rows
239
+ if (cells[0] === 'Agent' || cells[0].startsWith('-')) { pastHeader = true; continue; }
240
+ pastHeader = true;
241
+
242
+ const name = cells[0];
243
+ const input = parseInt(cells[1]?.replace(/,/g, ''), 10) || 0;
244
+ const output = parseInt(cells[2]?.replace(/,/g, ''), 10) || 0;
245
+ const total = parseInt(cells[3]?.replace(/,/g, ''), 10) || (input + output);
246
+
247
+ agents.push({ name, input, output, total });
248
+ totalInput += input;
249
+ totalOutput += output;
250
+ totalAll += total;
251
+ }
252
+
253
+ if (agents.length > 0) {
254
+ stories.push({ storyId, agents, total: agents.reduce((s, a) => s + a.total, 0) });
255
+ }
256
+ }
257
+
258
+ if (jsonOutput) {
259
+ console.log(JSON.stringify({
260
+ sprint: sprintId,
261
+ total_input_tokens: totalInput,
262
+ total_output_tokens: totalOutput,
263
+ total_tokens: totalAll,
264
+ stories,
265
+ }, null, 2));
266
+ } else {
267
+ console.log(`\n📊 Sprint Token Summary — ${sprintId}\n`);
268
+
269
+ if (stories.length === 0) {
270
+ console.log('No token usage data found in story documents.');
271
+ console.log('Agents must run: count_tokens.mjs --self --append <story.md> --name <Agent>');
272
+ process.exit(0);
273
+ }
274
+
275
+ // Per-story table
276
+ console.log(`${' Story'.padEnd(45)} ${'Input'.padStart(10)} ${'Output'.padStart(10)} ${'Total'.padStart(10)}`);
277
+ console.log(`${' ' + '─'.repeat(43)} ${'─'.repeat(10)} ${'─'.repeat(10)} ${'─'.repeat(10)}`);
278
+ for (const story of stories) {
279
+ const name = story.storyId.length > 41 ? story.storyId.substring(0, 41) + '...' : story.storyId;
280
+ const storyInput = story.agents.reduce((s, a) => s + a.input, 0);
281
+ const storyOutput = story.agents.reduce((s, a) => s + a.output, 0);
282
+ console.log(` ${name.padEnd(43)} ${fmt(storyInput).padStart(10)} ${fmt(storyOutput).padStart(10)} ${fmt(story.total).padStart(10)}`);
283
+ for (const a of story.agents) {
284
+ console.log(` ${('└ ' + a.name).padEnd(41)} ${fmt(a.input).padStart(10)} ${fmt(a.output).padStart(10)} ${fmt(a.total).padStart(10)}`);
285
+ }
286
+ }
287
+ console.log(`${' ' + '─'.repeat(43)} ${'─'.repeat(10)} ${'─'.repeat(10)} ${'─'.repeat(10)}`);
288
+ console.log(` ${'SPRINT TOTAL'.padEnd(43)} ${fmt(totalInput).padStart(10)} ${fmt(totalOutput).padStart(10)} ${fmt(totalAll).padStart(10)}`);
289
+ console.log('');
290
+ }
291
+ }
292
+
293
+ // ── Main (session-based tracking) ────────────────────────────────
294
+
295
+ const session = sessionId
296
+ ? { id: sessionId, name: `${sessionId}.jsonl` }
297
+ : findLatestSession();
298
+
299
+ if (!session) {
300
+ console.error('ERROR: No session JSONL files found.');
301
+ process.exit(1);
302
+ }
303
+
304
+ const sessionPath = path.join(projectDir, session.name);
305
+
306
+ if (showSelf) {
307
+ // ── Self-detect: find the most recently modified subagent JSONL (likely "me") ──
308
+ const agentDir = path.join(projectDir, session.id, 'subagents');
309
+
310
+ if (!fs.existsSync(agentDir)) {
311
+ // Not running as a subagent — fall back to session totals
312
+ const usage = parseTokenUsage(sessionPath);
313
+ outputUsage('session', usage);
314
+ process.exit(0);
315
+ }
316
+
317
+ const agentFiles = fs.readdirSync(agentDir)
318
+ .filter(f => f.endsWith('.jsonl'))
319
+ .map(f => ({
320
+ name: f,
321
+ path: path.join(agentDir, f),
322
+ mtime: fs.statSync(path.join(agentDir, f)).mtimeMs,
323
+ }))
324
+ .sort((a, b) => b.mtime - a.mtime);
325
+
326
+ if (agentFiles.length === 0) {
327
+ const usage = parseTokenUsage(sessionPath);
328
+ outputUsage('session', usage);
329
+ process.exit(0);
330
+ }
331
+
332
+ // Most recently modified = the currently running agent
333
+ const self = agentFiles[0];
334
+ const usage = parseTokenUsage(self.path);
335
+
336
+ if (appendTo) {
337
+ appendTokenRow(appendTo, agentName || 'Agent', usage);
338
+ } else {
339
+ outputUsage(self.name.replace('.jsonl', ''), usage);
340
+ }
341
+
342
+ } else if (agentId) {
343
+ // ── Single subagent ──
344
+ const agentDir = path.join(projectDir, session.id, 'subagents');
345
+ const agentFile = path.join(agentDir, `agent-${agentId}.jsonl`);
346
+
347
+ if (!fs.existsSync(agentFile)) {
348
+ // Try fuzzy match
349
+ if (fs.existsSync(agentDir)) {
350
+ const matches = fs.readdirSync(agentDir).filter(f => f.includes(agentId));
351
+ if (matches.length === 1) {
352
+ const usage = parseTokenUsage(path.join(agentDir, matches[0]));
353
+ outputUsage(matches[0].replace('.jsonl', ''), usage);
354
+ process.exit(0);
355
+ }
356
+ }
357
+ console.error(`ERROR: Agent "${agentId}" not found in session ${session.id}`);
358
+ process.exit(1);
359
+ }
360
+
361
+ const usage = parseTokenUsage(agentFile);
362
+ outputUsage(`agent-${agentId}`, usage);
363
+
364
+ } else if (showAll) {
365
+ // ── All subagents ──
366
+ const agentDir = path.join(projectDir, session.id, 'subagents');
367
+
368
+ if (!fs.existsSync(agentDir)) {
369
+ console.error(`No subagents found for session ${session.id}`);
370
+ process.exit(0);
371
+ }
372
+
373
+ const agentFiles = fs.readdirSync(agentDir).filter(f => f.endsWith('.jsonl'));
374
+ const results = [];
375
+
376
+ for (const file of agentFiles) {
377
+ const usage = parseTokenUsage(path.join(agentDir, file));
378
+ const name = file.replace('.jsonl', '');
379
+ results.push({ name, ...usage });
380
+ }
381
+
382
+ // Sort by total tokens descending
383
+ results.sort((a, b) => (b.input_tokens + b.output_tokens) - (a.input_tokens + a.output_tokens));
384
+
385
+ if (jsonOutput) {
386
+ const sessionUsage = parseTokenUsage(sessionPath);
387
+ console.log(JSON.stringify({
388
+ session: session.id,
389
+ session_totals: sessionUsage,
390
+ subagents: results,
391
+ }, null, 2));
392
+ } else {
393
+ const sessionUsage = parseTokenUsage(sessionPath);
394
+ console.log(`\n📊 Token Usage — Session ${session.id.substring(0, 8)}...\n`);
395
+ console.log(`Session totals: ${fmt(sessionUsage.input_tokens)} in / ${fmt(sessionUsage.output_tokens)} out (${sessionUsage.messages} messages)\n`);
396
+
397
+ if (results.length > 0) {
398
+ console.log(`Subagents (${results.length}):`);
399
+ console.log(`${' Name'.padEnd(50)} ${'Input'.padStart(10)} ${'Output'.padStart(10)} ${'Msgs'.padStart(6)}`);
400
+ console.log(`${' ' + '─'.repeat(48)} ${'─'.repeat(10)} ${'─'.repeat(10)} ${'─'.repeat(6)}`);
401
+ for (const r of results) {
402
+ const shortName = r.name.length > 46 ? r.name.substring(0, 46) + '...' : r.name;
403
+ console.log(` ${shortName.padEnd(48)} ${fmt(r.input_tokens).padStart(10)} ${fmt(r.output_tokens).padStart(10)} ${String(r.messages).padStart(6)}`);
404
+ }
405
+ const totalIn = results.reduce((s, r) => s + r.input_tokens, 0);
406
+ const totalOut = results.reduce((s, r) => s + r.output_tokens, 0);
407
+ console.log(`${' ' + '─'.repeat(48)} ${'─'.repeat(10)} ${'─'.repeat(10)} ${'─'.repeat(6)}`);
408
+ console.log(` ${'TOTAL (subagents)'.padEnd(48)} ${fmt(totalIn).padStart(10)} ${fmt(totalOut).padStart(10)}`);
409
+ } else {
410
+ console.log('No subagents found in this session.');
411
+ }
412
+ console.log('');
413
+ }
414
+
415
+ } else {
416
+ // ── Session totals ──
417
+ const usage = parseTokenUsage(sessionPath);
418
+ outputUsage(`session-${session.id.substring(0, 8)}`, usage);
419
+ }
420
+
421
+ // ── Helpers ──────────────────────────────────────────────────────
422
+
423
+ function fmt(n) {
424
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
425
+ if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K';
426
+ return String(n);
427
+ }
428
+
429
+ /**
430
+ * Append a token usage row to a markdown file's Token Usage table.
431
+ * Creates the table if it doesn't exist.
432
+ */
433
+ function appendTokenRow(filePath, name, usage) {
434
+ const resolvedPath = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath);
435
+
436
+ if (!fs.existsSync(resolvedPath)) {
437
+ console.error(`ERROR: File not found: ${filePath}`);
438
+ process.exit(1);
439
+ }
440
+
441
+ let content = fs.readFileSync(resolvedPath, 'utf8');
442
+ const total = usage.input_tokens + usage.output_tokens;
443
+ const row = `| ${name} | ${usage.input_tokens.toLocaleString()} | ${usage.output_tokens.toLocaleString()} | ${total.toLocaleString()} |`;
444
+
445
+ const TABLE_HEADER = '## Token Usage';
446
+ const TABLE_COLUMNS = '| Agent | Input | Output | Total |\n|-------|-------|--------|-------|';
447
+
448
+ if (content.includes(TABLE_HEADER)) {
449
+ // Table exists — find the last pipe-row and append after it
450
+ const lines = content.split('\n');
451
+ let lastPipeRow = -1;
452
+ let inTokenSection = false;
453
+ for (let i = 0; i < lines.length; i++) {
454
+ if (lines[i].includes(TABLE_HEADER)) inTokenSection = true;
455
+ if (inTokenSection && lines[i].startsWith('|')) lastPipeRow = i;
456
+ if (inTokenSection && lastPipeRow > -1 && !lines[i].startsWith('|') && lines[i].trim() !== '') break;
457
+ }
458
+ if (lastPipeRow > -1) {
459
+ lines.splice(lastPipeRow + 1, 0, row);
460
+ content = lines.join('\n');
461
+ }
462
+ } else {
463
+ // Table doesn't exist — create it at the end
464
+ content = content.trimEnd() + '\n\n---\n\n' + TABLE_HEADER + '\n\n' + TABLE_COLUMNS + '\n' + row + '\n';
465
+ }
466
+
467
+ fs.writeFileSync(resolvedPath, content);
468
+ console.log(`✓ Token usage written to ${filePath}: ${name} — ${total.toLocaleString()} tokens (${usage.input_tokens.toLocaleString()} in / ${usage.output_tokens.toLocaleString()} out)`);
469
+ }
470
+
471
+ function outputUsage(label, usage) {
472
+ const total = usage.input_tokens + usage.output_tokens;
473
+
474
+ if (jsonOutput) {
475
+ console.log(JSON.stringify({
476
+ label,
477
+ input_tokens: usage.input_tokens,
478
+ output_tokens: usage.output_tokens,
479
+ cache_read_tokens: usage.cache_read,
480
+ cache_creation_tokens: usage.cache_creation,
481
+ total_tokens: total,
482
+ messages: usage.messages,
483
+ }, null, 2));
484
+ } else {
485
+ console.log(`\n📊 Token Usage — ${label}`);
486
+ console.log(` Input: ${fmt(usage.input_tokens)}`);
487
+ console.log(` Output: ${fmt(usage.output_tokens)}`);
488
+ console.log(` Cache read: ${fmt(usage.cache_read)}`);
489
+ console.log(` Cache creation: ${fmt(usage.cache_creation)}`);
490
+ console.log(` Total: ${fmt(total)}`);
491
+ console.log(` Messages: ${usage.messages}`);
492
+ console.log('');
493
+ }
494
+ }
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * doctor.mjs
5
+ * V-Bounce Engine 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, '.vbounce', 'templates');
42
+ let templateCount = 0;
43
+ for (const t of requiredTemplates) {
44
+ if (fs.existsSync(path.join(templatesDir, t))) templateCount++;
45
+ else fail(`.vbounce/templates/${t} missing`, `Create from V-Bounce Engine template`);
46
+ }
47
+ if (templateCount === requiredTemplates.length) pass(`.vbounce/templates/ complete (${templateCount}/${requiredTemplates.length})`);
48
+
49
+ // Check .bounce directory
50
+ if (fs.existsSync(path.join(ROOT, '.vbounce'))) {
51
+ pass('.vbounce/ directory exists');
52
+
53
+ // Check state.json
54
+ const stateFile = path.join(ROOT, '.vbounce', '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('.vbounce/ directory missing — run: vbounce sprint init S-XX D-XX');
67
+ }
68
+
69
+ // Check brain files (deployed to project root)
70
+ const brainFiles = [
71
+ ['CLAUDE.md', 'claude', 'Tier 1 (Claude Code)'],
72
+ ['GEMINI.md', 'gemini', 'Tier 2 (Gemini CLI)'],
73
+ ['AGENTS.md', 'codex', 'Tier 2 (Codex CLI)'],
74
+ ];
75
+ for (const [f, tool, 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 ${tool}`);
78
+ }
79
+
80
+ // Check optional brain files
81
+ const optionalBrains = [
82
+ ['.github/copilot-instructions.md', 'copilot'],
83
+ ['.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, '.vbounce', '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(`.vbounce/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, '.vbounce', 'scripts');
111
+ let scriptCount = 0;
112
+ for (const s of requiredScripts) {
113
+ if (fs.existsSync(path.join(scriptsDir, s))) scriptCount++;
114
+ else fail(`.vbounce/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 Engine 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);