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,318 @@
1
+ // Event Store - Append-only state management
2
+ // Replaces mutable STATE.md with immutable event log
3
+
4
+ import { createHash } from 'crypto';
5
+ import { readFileSync, appendFileSync, existsSync, writeFileSync, renameSync, unlinkSync, mkdirSync } from 'fs';
6
+ import { join, dirname } from 'path';
7
+ import { v4 as uuidv4 } from 'uuid';
8
+
9
+ export interface PhaseEvent {
10
+ id: string;
11
+ phase: string;
12
+ type: 'STARTED' | 'COMPLETED' | 'FAILED' | 'ROLLED_BACK' | 'SKIPPED';
13
+ payload: Record<string, unknown>;
14
+ timestamp: string;
15
+ checksum: string;
16
+ agent?: string;
17
+ idempotencyKey: string;
18
+ roleId?: string;
19
+ }
20
+
21
+ export interface WorkflowState {
22
+ currentPhase: string;
23
+ phaseHistory: PhaseEvent[];
24
+ task: {
25
+ title: string;
26
+ description: string;
27
+ priority: 'low' | 'medium' | 'high' | 'critical';
28
+ };
29
+ decisions: Array<{ date: string; decision: string; reason: string }>;
30
+ openQuestions: Array<{ question: string; status: 'wait' | 'answered' }>;
31
+ scope: {
32
+ inScope: string[];
33
+ outScope: string[];
34
+ decidedLater: string[];
35
+ };
36
+ blockerLog: Array<{ date: string; blocker: string; resolution: string }>;
37
+ health: {
38
+ lastUpdated: string;
39
+ phaseDurationMinutes: number;
40
+ openBlockers: number;
41
+ };
42
+ }
43
+
44
+ export interface RBACConfig {
45
+ requiredRole?: string;
46
+ requiredPermission?: string;
47
+ }
48
+
49
+ export class EventStore {
50
+ private eventsFile: string;
51
+ private derivedStateFile: string;
52
+ private events: PhaseEvent[] = [];
53
+ private writeLock = false;
54
+ private maxFileSize = 10 * 1024 * 1024; // 10MB limit
55
+
56
+ constructor(stateDir: string) {
57
+ if (!existsSync(stateDir)) {
58
+ mkdirSync(stateDir, { recursive: true });
59
+ }
60
+ this.eventsFile = join(stateDir, 'events.jsonl');
61
+ this.derivedStateFile = join(stateDir, 'derived-state.json');
62
+ this.loadEvents();
63
+ }
64
+
65
+ private loadEvents(): void {
66
+ if (!existsSync(this.eventsFile)) return;
67
+
68
+ try {
69
+ const content = readFileSync(this.eventsFile, 'utf-8');
70
+ this.events = content
71
+ .split('\n')
72
+ .filter(line => line.trim())
73
+ .map(line => {
74
+ try {
75
+ return JSON.parse(line) as PhaseEvent;
76
+ } catch (e) {
77
+ console.error(`[EventStore] Failed to parse event line: ${line.substring(0, 50)}...`);
78
+ return null;
79
+ }
80
+ })
81
+ .filter((e): e is PhaseEvent => e !== null);
82
+ } catch (e) {
83
+ console.error(`[EventStore] Failed to read events file: ${(e as Error).message}`);
84
+ this.events = [];
85
+ }
86
+ }
87
+
88
+ private calculateChecksum(event: Omit<PhaseEvent, 'checksum'>): string {
89
+ const eventWithoutChecksum = { ...event, checksum: '' };
90
+ return createHash('sha256')
91
+ .update(JSON.stringify(eventWithoutChecksum))
92
+ .digest('hex');
93
+ }
94
+
95
+ verifyChecksum(event: PhaseEvent): boolean {
96
+ const { checksum, ...eventWithoutChecksum } = event;
97
+ const expectedChecksum = createHash('sha256')
98
+ .update(JSON.stringify(eventWithoutChecksum))
99
+ .digest('hex');
100
+ return checksum === expectedChecksum;
101
+ }
102
+
103
+ private acquireLock(): void {
104
+ while (this.writeLock) {
105
+ // Busy wait with small delay
106
+ const start = Date.now();
107
+ while (Date.now() - start < 10) {
108
+ // Spin wait
109
+ }
110
+ }
111
+ this.writeLock = true;
112
+ }
113
+
114
+ private releaseLock(): void {
115
+ this.writeLock = false;
116
+ }
117
+
118
+ append(
119
+ event: Omit<PhaseEvent, 'id' | 'checksum' | 'timestamp'>,
120
+ rbacConfig?: RBACConfig,
121
+ checkPermission?: (permission: string) => boolean
122
+ ): PhaseEvent {
123
+ // RBAC check
124
+ if (rbacConfig?.requiredPermission && checkPermission) {
125
+ if (!checkPermission(rbacConfig.requiredPermission)) {
126
+ throw new Error(`RBAC: Missing required permission: ${rbacConfig.requiredPermission}`);
127
+ }
128
+ }
129
+
130
+ this.acquireLock();
131
+
132
+ try {
133
+ // Idempotency check
134
+ const existing = this.events.find(e => e.idempotencyKey === event.idempotencyKey);
135
+ if (existing) {
136
+ console.log(`[EventStore] Duplicate idempotency key detected: ${event.idempotencyKey}`);
137
+ return existing;
138
+ }
139
+
140
+ // File size check
141
+ if (existsSync(this.eventsFile)) {
142
+ const stats = require('fs').statSync(this.eventsFile);
143
+ if (stats.size > this.maxFileSize) {
144
+ throw new Error(`Events file exceeds maximum size of ${this.maxFileSize} bytes`);
145
+ }
146
+ }
147
+
148
+ const fullEvent: PhaseEvent = {
149
+ ...event,
150
+ id: uuidv4(),
151
+ timestamp: new Date().toISOString(),
152
+ checksum: '',
153
+ };
154
+ fullEvent.checksum = this.calculateChecksum(fullEvent);
155
+
156
+ // Atomic write: write to temp file, then rename
157
+ const tempFile = `${this.eventsFile}.tmp.${Date.now()}`;
158
+ const eventLine = JSON.stringify(fullEvent) + '\n';
159
+
160
+ try {
161
+ if (existsSync(this.eventsFile)) {
162
+ const existingContent = readFileSync(this.eventsFile, 'utf-8');
163
+ writeFileSync(tempFile, existingContent + eventLine);
164
+ } else {
165
+ writeFileSync(tempFile, eventLine);
166
+ }
167
+ renameSync(tempFile, this.eventsFile);
168
+ } catch (e) {
169
+ // Cleanup temp file on error
170
+ if (existsSync(tempFile)) {
171
+ try { unlinkSync(tempFile); } catch {}
172
+ }
173
+ throw e;
174
+ }
175
+
176
+ this.events.push(fullEvent);
177
+ this.updateDerivedState();
178
+
179
+ return fullEvent;
180
+ } finally {
181
+ this.releaseLock();
182
+ }
183
+ }
184
+
185
+ deriveState(): WorkflowState {
186
+ const initialState: WorkflowState = {
187
+ currentPhase: 'init',
188
+ phaseHistory: [],
189
+ task: { title: '', description: '', priority: 'medium' },
190
+ decisions: [],
191
+ openQuestions: [],
192
+ scope: { inScope: [], outScope: [], decidedLater: [] },
193
+ blockerLog: [],
194
+ health: {
195
+ lastUpdated: new Date().toISOString(),
196
+ phaseDurationMinutes: 0,
197
+ openBlockers: 0,
198
+ },
199
+ };
200
+
201
+ return this.events.reduce((state, event) => {
202
+ return this.applyEvent(state, event);
203
+ }, initialState);
204
+ }
205
+
206
+ private applyEvent(state: WorkflowState, event: PhaseEvent): WorkflowState {
207
+ // Deep clone to prevent mutable state issues
208
+ const newState: WorkflowState = {
209
+ ...state,
210
+ phaseHistory: [...state.phaseHistory],
211
+ task: { ...state.task },
212
+ decisions: [...state.decisions],
213
+ openQuestions: [...state.openQuestions],
214
+ scope: {
215
+ inScope: [...state.scope.inScope],
216
+ outScope: [...state.scope.outScope],
217
+ decidedLater: [...state.scope.decidedLater],
218
+ },
219
+ blockerLog: [...state.blockerLog],
220
+ health: { ...state.health },
221
+ };
222
+
223
+ switch (event.type) {
224
+ case 'STARTED':
225
+ newState.currentPhase = event.phase;
226
+ newState.health.lastUpdated = event.timestamp;
227
+ break;
228
+ case 'COMPLETED':
229
+ newState.phaseHistory.push(event);
230
+ break;
231
+ case 'FAILED':
232
+ newState.blockerLog.push({
233
+ date: event.timestamp,
234
+ blocker: `Phase ${event.phase} failed`,
235
+ resolution: (event.payload.reason as string) || 'Pending',
236
+ });
237
+ newState.health.openBlockers++;
238
+ break;
239
+ case 'ROLLED_BACK':
240
+ // Find the specific event to remove by its ID
241
+ const originalEventId = event.payload.originalEventId as string;
242
+ if (originalEventId) {
243
+ newState.phaseHistory = newState.phaseHistory.filter(
244
+ e => e.id !== originalEventId
245
+ );
246
+ } else {
247
+ // Fallback: remove last event for this phase
248
+ const lastIndex = newState.phaseHistory.findLastIndex(e => e.phase === event.phase);
249
+ if (lastIndex !== -1) {
250
+ newState.phaseHistory.splice(lastIndex, 1);
251
+ }
252
+ }
253
+ break;
254
+ case 'SKIPPED':
255
+ // SKIPPED events are tracked but don't modify state
256
+ break;
257
+ }
258
+
259
+ return newState;
260
+ }
261
+
262
+ private updateDerivedState(): void {
263
+ const state = this.deriveState();
264
+ const tempFile = `${this.derivedStateFile}.tmp.${Date.now()}`;
265
+
266
+ try {
267
+ writeFileSync(tempFile, JSON.stringify(state, null, 2));
268
+ renameSync(tempFile, this.derivedStateFile);
269
+ } catch (e) {
270
+ if (existsSync(tempFile)) {
271
+ try { unlinkSync(tempFile); } catch {}
272
+ }
273
+ console.error(`[EventStore] Failed to update derived state: ${(e as Error).message}`);
274
+ }
275
+ }
276
+
277
+ getEvents(): PhaseEvent[] {
278
+ return [...this.events];
279
+ }
280
+
281
+ getLastEvent(): PhaseEvent | undefined {
282
+ return this.events[this.events.length - 1];
283
+ }
284
+
285
+ rollback(eventId: string, reason: string, roleId?: string): PhaseEvent {
286
+ const event = this.events.find(e => e.id === eventId);
287
+ if (!event) throw new Error(`Event not found: ${eventId}`);
288
+
289
+ return this.append({
290
+ phase: event.phase,
291
+ type: 'ROLLED_BACK',
292
+ payload: { originalEventId: eventId, reason },
293
+ agent: 'system',
294
+ idempotencyKey: `rollback-${eventId}-${uuidv4()}`,
295
+ roleId,
296
+ });
297
+ }
298
+
299
+ // Cleanup old events (for persistence management)
300
+ pruneOldEvents(keepLastN: number): void {
301
+ if (this.events.length <= keepLastN) return;
302
+
303
+ const eventsToKeep = this.events.slice(-keepLastN);
304
+ const tempFile = `${this.eventsFile}.tmp.${Date.now()}`;
305
+
306
+ try {
307
+ const content = eventsToKeep.map(e => JSON.stringify(e)).join('\n') + '\n';
308
+ writeFileSync(tempFile, content);
309
+ renameSync(tempFile, this.eventsFile);
310
+ this.events = eventsToKeep;
311
+ this.updateDerivedState();
312
+ } catch (e) {
313
+ if (existsSync(tempFile)) {
314
+ try { unlinkSync(tempFile); } catch {}
315
+ }
316
+ }
317
+ }
318
+ }
@@ -0,0 +1,113 @@
1
+ // Health Check Chain
2
+ // Monitors MCP tools and system components
3
+
4
+ export interface HealthCheckResult {
5
+ name: string;
6
+ status: 'healthy' | 'degraded' | 'unhealthy';
7
+ latencyMs: number;
8
+ message: string;
9
+ lastChecked: string;
10
+ }
11
+
12
+ export interface HealthCheckConfig {
13
+ name: string;
14
+ check: () => Promise<boolean>;
15
+ timeoutMs?: number;
16
+ }
17
+
18
+ export class HealthCheckChain {
19
+ private checks: HealthCheckConfig[] = [];
20
+ private results: Map<string, HealthCheckResult> = new Map();
21
+ private maxConcurrentChecks = 10;
22
+
23
+ addCheck(config: HealthCheckConfig): void {
24
+ this.checks.push(config);
25
+ }
26
+
27
+ async runAll(): Promise<HealthCheckResult[]> {
28
+ // Limit concurrent checks
29
+ const checksToRun = this.checks.slice(0, this.maxConcurrentChecks);
30
+
31
+ const results = await Promise.all(
32
+ checksToRun.map(async (check) => {
33
+ const result = await this.runSingle(check);
34
+ this.results.set(check.name, result);
35
+ return result;
36
+ })
37
+ );
38
+ return results;
39
+ }
40
+
41
+ private async runSingle(check: HealthCheckConfig): Promise<HealthCheckResult> {
42
+ const startTime = Date.now();
43
+ const timeoutMs = check.timeoutMs || 5000;
44
+
45
+ return new Promise<HealthCheckResult>((resolve) => {
46
+ let resolved = false;
47
+ const timeoutId = setTimeout(() => {
48
+ if (!resolved) {
49
+ resolved = true;
50
+ resolve({
51
+ name: check.name,
52
+ status: 'unhealthy',
53
+ latencyMs: Date.now() - startTime,
54
+ message: `Timeout after ${timeoutMs}ms`,
55
+ lastChecked: new Date().toISOString(),
56
+ });
57
+ }
58
+ }, timeoutMs);
59
+
60
+ check.check()
61
+ .then(result => {
62
+ if (!resolved) {
63
+ resolved = true;
64
+ clearTimeout(timeoutId);
65
+ resolve({
66
+ name: check.name,
67
+ status: result ? 'healthy' : 'degraded',
68
+ latencyMs: Date.now() - startTime,
69
+ message: result ? 'OK' : 'Check returned false',
70
+ lastChecked: new Date().toISOString(),
71
+ });
72
+ }
73
+ })
74
+ .catch(error => {
75
+ if (!resolved) {
76
+ resolved = true;
77
+ clearTimeout(timeoutId);
78
+ resolve({
79
+ name: check.name,
80
+ status: 'unhealthy',
81
+ latencyMs: Date.now() - startTime,
82
+ message: (error as Error).message,
83
+ lastChecked: new Date().toISOString(),
84
+ });
85
+ }
86
+ });
87
+ });
88
+ }
89
+
90
+ async checkTool(toolName: string): Promise<boolean> {
91
+ const check = this.checks.find(c => c.name === toolName);
92
+ if (!check) return false;
93
+
94
+ const result = await this.runSingle(check);
95
+ return result.status === 'healthy';
96
+ }
97
+
98
+ getOverallHealth(): 'healthy' | 'degraded' | 'unhealthy' | 'unknown' {
99
+ const results = Array.from(this.results.values());
100
+ if (results.length === 0) return 'unknown';
101
+ if (results.every(r => r.status === 'healthy')) return 'healthy';
102
+ if (results.some(r => r.status === 'unhealthy')) return 'unhealthy';
103
+ return 'degraded';
104
+ }
105
+
106
+ getLastResults(): HealthCheckResult[] {
107
+ return Array.from(this.results.values());
108
+ }
109
+
110
+ clearResults(): void {
111
+ this.results.clear();
112
+ }
113
+ }
@@ -0,0 +1,92 @@
1
+ // Idempotency Manager
2
+ // Ensures operations are safely retryable
3
+
4
+ import { createHash, randomUUID } from 'crypto';
5
+
6
+ export interface IdempotentOperation {
7
+ id: string;
8
+ key: string;
9
+ timestamp: string;
10
+ result?: unknown;
11
+ status: 'pending' | 'completed' | 'failed';
12
+ }
13
+
14
+ export class IdempotencyManager {
15
+ private operations: Map<string, IdempotentOperation> = new Map();
16
+ private pendingTimeoutMs: number;
17
+
18
+ constructor(pendingTimeoutMs: number = 60000) {
19
+ this.pendingTimeoutMs = pendingTimeoutMs;
20
+ }
21
+
22
+ generateKey(...args: unknown[]): string {
23
+ const content = JSON.stringify(args);
24
+ return createHash('sha256')
25
+ .update(content)
26
+ .digest('hex')
27
+ .substring(0, 16);
28
+ }
29
+
30
+ async execute<T>(
31
+ key: string,
32
+ operation: () => Promise<T>
33
+ ): Promise<{ result: T; fromCache: boolean }> {
34
+ const existing = this.operations.get(key);
35
+
36
+ if (existing?.status === 'completed') {
37
+ console.log(`[Idempotency] Cache hit for key: ${key}`);
38
+ return { result: existing.result as T, fromCache: true };
39
+ }
40
+
41
+ if (existing?.status === 'pending') {
42
+ const pendingTime = new Date(existing.timestamp).getTime();
43
+ if (Date.now() - pendingTime > this.pendingTimeoutMs) {
44
+ console.log(`[Idempotency] Pending operation timed out, allowing retry: ${key}`);
45
+ } else {
46
+ console.log(`[Idempotency] Operation in progress: ${key}`);
47
+ throw new Error(`Operation ${key} is already in progress`);
48
+ }
49
+ }
50
+
51
+ const op: IdempotentOperation = {
52
+ id: randomUUID(),
53
+ key,
54
+ timestamp: new Date().toISOString(),
55
+ status: 'pending',
56
+ };
57
+ this.operations.set(key, op);
58
+
59
+ try {
60
+ const result = await operation();
61
+ op.status = 'completed';
62
+ op.result = result;
63
+ return { result, fromCache: false };
64
+ } catch (error) {
65
+ op.status = 'failed';
66
+ throw error;
67
+ }
68
+ }
69
+
70
+ isCompleted(key: string): boolean {
71
+ return this.operations.get(key)?.status === 'completed';
72
+ }
73
+
74
+ getResult<T>(key: string): T | undefined {
75
+ const op = this.operations.get(key);
76
+ return op?.status === 'completed' ? (op.result as T) : undefined;
77
+ }
78
+
79
+ clear(): void {
80
+ this.operations.clear();
81
+ }
82
+
83
+ getStats(): { total: number; completed: number; pending: number; failed: number } {
84
+ const ops = Array.from(this.operations.values());
85
+ return {
86
+ total: ops.length,
87
+ completed: ops.filter(o => o.status === 'completed').length,
88
+ pending: ops.filter(o => o.status === 'pending').length,
89
+ failed: ops.filter(o => o.status === 'failed').length,
90
+ };
91
+ }
92
+ }
@@ -0,0 +1,16 @@
1
+ // Vibe Coder Kit — Main Entry Point
2
+ // Exports all core modules
3
+
4
+ export { EventStore, PhaseEvent, WorkflowState } from './event-store';
5
+ export { DAGWorkflow, PhaseGraph, PhaseNode } from './dag';
6
+ export { PluginRegistry, Plugin, Rule, Hook } from './plugin-registry';
7
+ export { CircuitBreaker, CircuitOpenError } from './circuit-breaker';
8
+ export { Saga, SagaStep } from './saga';
9
+ export { IdempotencyManager } from './idempotency';
10
+ export { OutputValidator, ValidationResult } from './validator';
11
+ export { VibeCLI, CLIConfig } from './cli';
12
+ export { Telemetry, Span } from './telemetry';
13
+ export { HealthCheckChain } from './health-check';
14
+ export { CostTracker } from './cost-tracker';
15
+ export { KnowledgeStore, KnowledgeEntry } from './knowledge-store';
16
+ export { TeamManager, Role, TeamMember } from './team-config';