vibe-coder-kit 6.0.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.
@@ -0,0 +1,157 @@
1
+ // Cost Tracker
2
+ // Tracks token usage and estimated costs
3
+
4
+ import { randomUUID } from 'crypto';
5
+
6
+ export interface TokenUsage {
7
+ input: number;
8
+ output: number;
9
+ total: number;
10
+ }
11
+
12
+ export interface CostEstimate {
13
+ model: string;
14
+ inputCost: number;
15
+ outputCost: number;
16
+ totalCost: number;
17
+ currency: string;
18
+ }
19
+
20
+ export interface SessionCost {
21
+ sessionId: string;
22
+ startTime: string;
23
+ usage: TokenUsage;
24
+ estimatedCost: CostEstimate;
25
+ phaseBreakdown: Record<string, TokenUsage>;
26
+ modelUsage: Record<string, TokenUsage>;
27
+ }
28
+
29
+ export class CostTracker {
30
+ private sessions: Map<string, SessionCost> = new Map();
31
+ private currentSessionId: string;
32
+ private costPerToken: Record<string, { input: number; output: number }> = {
33
+ 'claude-sonnet': { input: 0.003 / 1000, output: 0.015 / 1000 },
34
+ 'claude-haiku': { input: 0.00025 / 1000, output: 0.00125 / 1000 },
35
+ 'gpt-4': { input: 0.03 / 1000, output: 0.06 / 1000 },
36
+ 'gpt-4o': { input: 0.005 / 1000, output: 0.015 / 1000 },
37
+ };
38
+
39
+ constructor() {
40
+ this.currentSessionId = randomUUID();
41
+ this.sessions.set(this.currentSessionId, {
42
+ sessionId: this.currentSessionId,
43
+ startTime: new Date().toISOString(),
44
+ usage: { input: 0, output: 0, total: 0 },
45
+ estimatedCost: { model: 'unknown', inputCost: 0, outputCost: 0, totalCost: 0, currency: 'USD' },
46
+ phaseBreakdown: {},
47
+ modelUsage: {},
48
+ });
49
+ }
50
+
51
+ recordUsage(model: string, inputTokens: number, outputTokens: number, phase?: string): void {
52
+ const session = this.sessions.get(this.currentSessionId);
53
+ if (!session) return;
54
+
55
+ session.usage.input += inputTokens;
56
+ session.usage.output += outputTokens;
57
+ session.usage.total += inputTokens + outputTokens;
58
+
59
+ if (phase) {
60
+ if (!session.phaseBreakdown[phase]) {
61
+ session.phaseBreakdown[phase] = { input: 0, output: 0, total: 0 };
62
+ }
63
+ session.phaseBreakdown[phase].input += inputTokens;
64
+ session.phaseBreakdown[phase].output += outputTokens;
65
+ session.phaseBreakdown[phase].total += inputTokens + outputTokens;
66
+ }
67
+
68
+ if (!session.modelUsage[model]) {
69
+ session.modelUsage[model] = { input: 0, output: 0, total: 0 };
70
+ }
71
+ session.modelUsage[model].input += inputTokens;
72
+ session.modelUsage[model].output += outputTokens;
73
+ session.modelUsage[model].total += inputTokens + outputTokens;
74
+
75
+ const rates = this.costPerToken[model] || this.costPerToken['gpt-4o'];
76
+ if (!this.costPerToken[model]) {
77
+ console.warn(`[CostTracker] Unknown model '${model}', using gpt-4o rates`);
78
+ }
79
+ const inputCost = inputTokens * rates.input;
80
+ const outputCost = outputTokens * rates.output;
81
+
82
+ session.estimatedCost = {
83
+ model,
84
+ inputCost: session.estimatedCost.inputCost + inputCost,
85
+ outputCost: session.estimatedCost.outputCost + outputCost,
86
+ totalCost: session.estimatedCost.totalCost + inputCost + outputCost,
87
+ currency: 'USD',
88
+ };
89
+ }
90
+
91
+ getCurrentSession(): SessionCost | undefined {
92
+ return this.sessions.get(this.currentSessionId);
93
+ }
94
+
95
+ getSessionCost(): CostEstimate | undefined {
96
+ return this.sessions.get(this.currentSessionId)?.estimatedCost;
97
+ }
98
+
99
+ checkBudget(maxTokens: number): { within: boolean; used: number; remaining: number; percentage: number } {
100
+ const session = this.sessions.get(this.currentSessionId);
101
+ const used = session?.usage.total || 0;
102
+ return {
103
+ within: used <= maxTokens,
104
+ used,
105
+ remaining: Math.max(0, maxTokens - used),
106
+ percentage: Math.min(100, (used / maxTokens) * 100),
107
+ };
108
+ }
109
+
110
+ getSummary(): {
111
+ totalSessions: number;
112
+ totalTokens: number;
113
+ totalCost: number;
114
+ averagePerSession: number;
115
+ } {
116
+ const allSessions = Array.from(this.sessions.values());
117
+ const totalTokens = allSessions.reduce((sum, s) => sum + s.usage.total, 0);
118
+ const totalCost = allSessions.reduce((sum, s) => sum + s.estimatedCost.totalCost, 0);
119
+
120
+ return {
121
+ totalSessions: allSessions.length,
122
+ totalTokens,
123
+ totalCost,
124
+ averagePerSession: allSessions.length > 0 ? totalCost / allSessions.length : 0,
125
+ };
126
+ }
127
+
128
+ exportReport(): string {
129
+ const session = this.sessions.get(this.currentSessionId);
130
+ if (!session) return 'No active session';
131
+
132
+ let report = `# Cost Report — Session ${this.currentSessionId}\n\n`;
133
+ report += `**Started:** ${session.startTime}\n`;
134
+ report += `**Model:** ${session.estimatedCost.model}\n\n`;
135
+ report += `## Token Usage\n`;
136
+ report += `| Type | Tokens |\n|------|--------|\n`;
137
+ report += `| Input | ${session.usage.input.toLocaleString()} |\n`;
138
+ report += `| Output | ${session.usage.output.toLocaleString()} |\n`;
139
+ report += `| **Total** | **${session.usage.total.toLocaleString()}** |\n\n`;
140
+ report += `## Estimated Cost\n`;
141
+ report += `- Input: $${session.estimatedCost.inputCost.toFixed(4)}\n`;
142
+ report += `- Output: $${session.estimatedCost.outputCost.toFixed(4)}\n`;
143
+ report += `- **Total: $${session.estimatedCost.totalCost.toFixed(4)}**\n\n`;
144
+
145
+ if (Object.keys(session.phaseBreakdown).length > 0) {
146
+ report += `## Phase Breakdown\n`;
147
+ report += `| Phase | Tokens | Est. Cost |\n|-------|--------|----------|\n`;
148
+ for (const [phase, usage] of Object.entries(session.phaseBreakdown)) {
149
+ const rates = this.costPerToken[session.estimatedCost.model] || this.costPerToken['gpt-4o'];
150
+ const cost = (usage.input * rates.input) + (usage.output * rates.output);
151
+ report += `| ${phase} | ${usage.total.toLocaleString()} | $${cost.toFixed(4)} |\n`;
152
+ }
153
+ }
154
+
155
+ return report;
156
+ }
157
+ }
@@ -0,0 +1,320 @@
1
+ // DAG Workflow Engine
2
+ // Replaces linear 00-99 phase numbering with dependency graph
3
+
4
+ import { readFileSync, existsSync, writeFileSync } from 'fs';
5
+ import { join } from 'path';
6
+
7
+ export interface PhaseNode {
8
+ id: string;
9
+ name: string;
10
+ depends: string[];
11
+ next: string[];
12
+ entryCriteria: string[];
13
+ exitCriteria: string[];
14
+ optional: boolean;
15
+ parallelGroup?: string;
16
+ }
17
+
18
+ export interface PhaseGraph {
19
+ phases: Record<string, PhaseNode>;
20
+ parallelGroups: string[][];
21
+ }
22
+
23
+ export class DAGWorkflow {
24
+ private graph: PhaseGraph;
25
+ private graphFile: string;
26
+
27
+ constructor(configDir: string) {
28
+ this.graphFile = join(configDir, 'phase-graph.json');
29
+ this.graph = this.loadGraph();
30
+
31
+ // Validate graph on load
32
+ const validation = this.validateGraph();
33
+ if (!validation.valid) {
34
+ console.error(`[DAG] Graph validation failed: ${validation.errors.join(', ')}`);
35
+ }
36
+ }
37
+
38
+ private loadGraph(): PhaseGraph {
39
+ if (!existsSync(this.graphFile)) {
40
+ return this.createDefaultGraph();
41
+ }
42
+
43
+ try {
44
+ const content = readFileSync(this.graphFile, 'utf-8');
45
+ const graph = JSON.parse(content) as PhaseGraph;
46
+
47
+ // Validate loaded graph
48
+ const validation = this.validateGraphStructure(graph);
49
+ if (!validation.valid) {
50
+ console.error(`[DAG] Invalid graph structure: ${validation.errors.join(', ')}`);
51
+ return this.createDefaultGraph();
52
+ }
53
+
54
+ return graph;
55
+ } catch (e) {
56
+ console.error(`[DAG] Failed to parse graph file: ${(e as Error).message}`);
57
+ return this.createDefaultGraph();
58
+ }
59
+ }
60
+
61
+ private validateGraphStructure(graph: PhaseGraph): { valid: boolean; errors: string[] } {
62
+ const errors: string[] = [];
63
+
64
+ // Check for missing phase references
65
+ for (const [id, phase] of Object.entries(graph.phases)) {
66
+ for (const dep of phase.depends) {
67
+ if (!graph.phases[dep]) {
68
+ errors.push(`Phase '${id}' depends on non-existent phase '${dep}'`);
69
+ }
70
+ }
71
+ for (const next of phase.next) {
72
+ if (!graph.phases[next]) {
73
+ errors.push(`Phase '${id}' references non-existent next phase '${next}'`);
74
+ }
75
+ }
76
+ }
77
+
78
+ return { valid: errors.length === 0, errors };
79
+ }
80
+
81
+ private createDefaultGraph(): PhaseGraph {
82
+ const defaultGraph: PhaseGraph = {
83
+ phases: {
84
+ init: {
85
+ id: 'init',
86
+ name: 'Project Initialization',
87
+ depends: [],
88
+ next: ['clarify'],
89
+ entryCriteria: [],
90
+ exitCriteria: ['context.md populated', 'git initialized'],
91
+ optional: false,
92
+ },
93
+ clarify: {
94
+ id: 'clarify',
95
+ name: 'Requirements Clarification',
96
+ depends: ['init'],
97
+ next: ['brainstorm', 'plan'],
98
+ entryCriteria: ['task defined'],
99
+ exitCriteria: ['no open questions', 'scope defined'],
100
+ optional: false,
101
+ },
102
+ brainstorm: {
103
+ id: 'brainstorm',
104
+ name: 'Research & Alternatives',
105
+ depends: ['clarify'],
106
+ next: ['plan'],
107
+ entryCriteria: ['ambiguity exists'],
108
+ exitCriteria: ['alternatives evaluated', 'approach selected'],
109
+ optional: true,
110
+ parallelGroup: 'planning',
111
+ },
112
+ plan: {
113
+ id: 'plan',
114
+ name: 'Planning',
115
+ depends: ['clarify'],
116
+ next: ['approve'],
117
+ entryCriteria: ['scope clear', 'approach selected'],
118
+ exitCriteria: ['task list created', 'test strategy defined'],
119
+ optional: false,
120
+ parallelGroup: 'planning',
121
+ },
122
+ approve: {
123
+ id: 'approve',
124
+ name: 'Architecture Review & Approval',
125
+ depends: ['plan'],
126
+ next: ['code'],
127
+ entryCriteria: ['plan ready'],
128
+ exitCriteria: ['architecture approved', 'user approved'],
129
+ optional: false,
130
+ },
131
+ code: {
132
+ id: 'code',
133
+ name: 'Implementation',
134
+ depends: ['approve'],
135
+ next: ['review'],
136
+ entryCriteria: ['approval received'],
137
+ exitCriteria: ['all tasks completed', 'tests passing'],
138
+ optional: false,
139
+ parallelGroup: 'development',
140
+ },
141
+ review: {
142
+ id: 'review',
143
+ name: 'Code Review & QA',
144
+ depends: ['code'],
145
+ next: ['learn', 'deploy'],
146
+ entryCriteria: ['code complete'],
147
+ exitCriteria: ['review approved', 'no critical bugs'],
148
+ optional: false,
149
+ },
150
+ fix: {
151
+ id: 'fix',
152
+ name: 'Bug Fixes',
153
+ depends: ['review'],
154
+ next: ['code'],
155
+ entryCriteria: ['bugs found'],
156
+ exitCriteria: ['all bugs fixed', 'tests passing'],
157
+ optional: true,
158
+ parallelGroup: 'development',
159
+ },
160
+ learn: {
161
+ id: 'learn',
162
+ name: 'Knowledge Capture',
163
+ depends: ['review'],
164
+ next: ['done'],
165
+ entryCriteria: ['review complete'],
166
+ exitCriteria: ['knowledge recorded', 'changelog updated'],
167
+ optional: false,
168
+ },
169
+ deploy: {
170
+ id: 'deploy',
171
+ name: 'Deployment',
172
+ depends: ['review'],
173
+ next: ['learn'],
174
+ entryCriteria: ['review approved', 'deploy needed'],
175
+ exitCriteria: ['deploy successful', 'smoke test passed'],
176
+ optional: true,
177
+ },
178
+ done: {
179
+ id: 'done',
180
+ name: 'Workflow Complete',
181
+ depends: ['learn'],
182
+ next: [],
183
+ entryCriteria: ['knowledge captured'],
184
+ exitCriteria: ['state reset'],
185
+ optional: false,
186
+ },
187
+ },
188
+ parallelGroups: [
189
+ ['brainstorm', 'plan'],
190
+ ['code', 'fix'],
191
+ ],
192
+ };
193
+
194
+ writeFileSync(this.graphFile, JSON.stringify(defaultGraph, null, 2));
195
+ return defaultGraph;
196
+ }
197
+
198
+ validateGraph(): { valid: boolean; errors: string[] } {
199
+ const errors: string[] = [];
200
+ const visited = new Set<string>();
201
+ const recursionStack = new Set<string>();
202
+
203
+ // Check for cycles using DFS
204
+ const hasCycle = (nodeId: string): boolean => {
205
+ visited.add(nodeId);
206
+ recursionStack.add(nodeId);
207
+
208
+ const node = this.graph.phases[nodeId];
209
+ if (!node) return false;
210
+
211
+ for (const nextId of node.next) {
212
+ if (!visited.has(nextId)) {
213
+ if (hasCycle(nextId)) return true;
214
+ } else if (recursionStack.has(nextId)) {
215
+ return true;
216
+ }
217
+ }
218
+
219
+ recursionStack.delete(nodeId);
220
+ return false;
221
+ };
222
+
223
+ // Check each phase for cycles
224
+ for (const nodeId of Object.keys(this.graph.phases)) {
225
+ if (!visited.has(nodeId)) {
226
+ if (hasCycle(nodeId)) {
227
+ errors.push(`Circular dependency detected involving phase '${nodeId}'`);
228
+ }
229
+ }
230
+ }
231
+
232
+ // Check for missing dependencies
233
+ for (const [id, phase] of Object.entries(this.graph.phases)) {
234
+ for (const dep of phase.depends) {
235
+ if (!this.graph.phases[dep]) {
236
+ errors.push(`Phase '${id}' depends on non-existent phase '${dep}'`);
237
+ }
238
+ }
239
+ }
240
+
241
+ return { valid: errors.length === 0, errors };
242
+ }
243
+
244
+ getPhase(id: string): PhaseNode | undefined {
245
+ return this.graph.phases[id];
246
+ }
247
+
248
+ getValidNextPhases(currentPhase: string): PhaseNode[] {
249
+ const current = this.graph.phases[currentPhase];
250
+ if (!current) return [];
251
+ return current.next
252
+ .map(id => this.graph.phases[id])
253
+ .filter(Boolean);
254
+ }
255
+
256
+ canTransition(from: string, to: string): boolean {
257
+ const phase = this.graph.phases[from];
258
+ if (!phase) return false;
259
+ return phase.next.includes(to);
260
+ }
261
+
262
+ validateTransition(from: string, to: string): { valid: boolean; reason: string } {
263
+ if (!this.canTransition(from, to)) {
264
+ return {
265
+ valid: false,
266
+ reason: `No transition from ${from} to ${to}`,
267
+ };
268
+ }
269
+
270
+ const targetPhase = this.graph.phases[to];
271
+ if (!targetPhase) {
272
+ return { valid: false, reason: `Phase ${to} not found` };
273
+ }
274
+
275
+ // Check if required dependencies are met
276
+ const completedPhases = new Set<string>(); // This should be passed in from state
277
+ for (const dep of targetPhase.depends) {
278
+ if (!completedPhases.has(dep) && dep !== from) {
279
+ // Allow if dependency is optional
280
+ const depPhase = this.graph.phases[dep];
281
+ if (depPhase && !depPhase.optional) {
282
+ return { valid: false, reason: `Required dependency '${dep}' not completed` };
283
+ }
284
+ }
285
+ }
286
+
287
+ return { valid: true, reason: 'Transition allowed' };
288
+ }
289
+
290
+ getParallelGroup(phaseId: string): string | undefined {
291
+ const phase = this.graph.phases[phaseId];
292
+ return phase?.parallelGroup;
293
+ }
294
+
295
+ areParallel(phase1: string, phase2: string): boolean {
296
+ const group1 = this.getParallelGroup(phase1);
297
+ const group2 = this.getParallelGroup(phase2);
298
+ return group1 !== undefined && group1 === group2;
299
+ }
300
+
301
+ getEntryCriteria(phaseId: string): string[] {
302
+ return this.graph.phases[phaseId]?.entryCriteria || [];
303
+ }
304
+
305
+ getExitCriteria(phaseId: string): string[] {
306
+ return this.graph.phases[phaseId]?.exitCriteria || [];
307
+ }
308
+
309
+ listPhases(): PhaseNode[] {
310
+ return Object.values(this.graph.phases);
311
+ }
312
+
313
+ getOptionalPhases(): PhaseNode[] {
314
+ return this.listPhases().filter(p => p.optional);
315
+ }
316
+
317
+ getRequiredPhases(): PhaseNode[] {
318
+ return this.listPhases().filter(p => !p.optional);
319
+ }
320
+ }