diff --git a/src/needleFabric.integration.test.ts b/src/needleFabric.integration.test.ts index c41a976..1796f9c 100644 --- a/src/needleFabric.integration.test.ts +++ b/src/needleFabric.integration.test.ts @@ -33,7 +33,7 @@ describe('NEEDLE-FABRIC Integration', () => { expect(result).not.toBeNull(); expect(result?.ts).toBe(new Date('2026-03-04T16:17:34.008Z').getTime()); - expect(result?.worker).toBe('claude-test12'); + expect(result?.worker).toBe('claude-anthropic-sonnet-test12'); expect(result?.level).toBe('info'); expect(result?.msg).toBe('worker.started'); expect(result?.session).toBe('needle-claude-anthropic-sonnet-test12'); @@ -153,9 +153,11 @@ describe('NEEDLE-FABRIC Integration', () => { }); it('should parse bead.claim_retry event with warn level', () => { + // NEEDLE emits bead.claim_retry with explicit level: "warn" const log = JSON.stringify({ ts: '2026-03-04T19:37:22.192Z', event: 'bead.claim_retry', + level: 'warn', session: 'forge-glm-test', worker: { runner: 'claude', @@ -313,19 +315,15 @@ describe('NEEDLE-FABRIC Integration', () => { }); describe('error events', () => { - it('should infer error level for events with "error" in name', () => { + it('should parse error.* events with error level (explicit in NEEDLE output)', () => { + // NEEDLE emits error.* events with explicit level: "error" const log = JSON.stringify({ ts: '2026-03-04T16:17:34.008Z', - event: 'bead.error', + event: 'error.agent_crash', + level: 'error', session: 'test-session', - worker: { - runner: 'claude', - provider: 'code', - model: 'sonnet', - identifier: 'test', - }, + worker: 'claude-code-sonnet-test', data: { - bead_id: 'bd-abc', error: 'Failed to process bead', }, }); @@ -334,14 +332,16 @@ describe('NEEDLE-FABRIC Integration', () => { expect(result).not.toBeNull(); expect(result?.level).toBe('error'); - expect(result?.msg).toBe('bead.error'); + expect(result?.msg).toBe('error.agent_crash'); expect(result?.error).toBe('Failed to process bead'); }); - it('should infer error level for events with "fail" in name', () => { + it('should parse bead.failed with error level (explicit in NEEDLE output)', () => { + // NEEDLE emits bead.failed with explicit level: "error" const log = JSON.stringify({ ts: '2026-03-04T16:17:34.008Z', event: 'bead.failed', + level: 'error', session: 'test-session', worker: 'claude-test', data: { @@ -356,20 +356,22 @@ describe('NEEDLE-FABRIC Integration', () => { expect(result?.msg).toBe('bead.failed'); }); - it('should infer error level for queue.exhausted events', () => { + it('should parse bead.claim_exhausted with error level (explicit in NEEDLE output)', () => { + // NEEDLE emits bead.claim_exhausted with explicit level: "error" const log = JSON.stringify({ ts: '2026-03-04T16:17:34.008Z', - event: 'queue.exhausted', + event: 'bead.claim_exhausted', + level: 'error', session: 'test-session', worker: 'claude-test', - data: {}, + data: { max_retries: 5 }, }); const result = parseLogLine(log); expect(result).not.toBeNull(); expect(result?.level).toBe('error'); - expect(result?.msg).toBe('queue.exhausted'); + expect(result?.msg).toBe('bead.claim_exhausted'); }); }); @@ -388,7 +390,7 @@ describe('NEEDLE-FABRIC Integration', () => { // Verify first event expect(results[0].msg).toBe('worker.started'); - expect(results[0].worker).toBe('claude-w1'); + expect(results[0].worker).toBe('claude-code-sonnet-w1'); expect(results[0].pid).toBe(123); // Verify second event @@ -405,7 +407,7 @@ describe('NEEDLE-FABRIC Integration', () => { expect(results[3].consecutive_empty).toBe(1); // All events should have same worker - expect(results.every((r) => r.worker === 'claude-w1')).toBe(true); + expect(results.every((r) => r.worker === 'claude-code-sonnet-w1')).toBe(true); // All events should have same session expect(results.every((r) => r.session === 'test')).toBe(true); @@ -573,7 +575,7 @@ describe('NEEDLE-FABRIC Integration', () => { expect(events).toHaveLength(7); // Verify all events have correct worker - expect(events.every((e) => e.worker === 'claude-test')).toBe(true); + expect(events.every((e) => e.worker === 'claude-code-glm-4.7-test')).toBe(true); // Verify all events have correct session expect(events.every((e) => e.session === 'forge-glm-test')).toBe(true); diff --git a/src/parser.test.ts b/src/parser.test.ts index e70381d..af004fa 100644 --- a/src/parser.test.ts +++ b/src/parser.test.ts @@ -126,7 +126,7 @@ describe('parseLogLine', () => { expect(result).not.toBeNull(); expect(result?.ts).toBe(1772641054008); // Unix ms from '2026-03-04T16:17:34.008Z' - expect(result?.worker).toBe('claude-test'); + expect(result?.worker).toBe('claude-code-glm-4.7-test'); expect(result?.msg).toBe('worker.started'); expect(result?.level).toBe('info'); expect(result?.session).toBe('forge-glm-test'); @@ -184,10 +184,11 @@ describe('parseLogLine', () => { }); it('should infer error level from event name', () => { + // NEEDLE rule: events with error.* prefix -> error const errorEvents = [ - 'bead.error', - 'worker.failed', - 'bead.claim_exhausted', + 'error.claim_failed', + 'error.agent_crash', + 'error.timeout', ]; for (const eventName of errorEvents) { @@ -210,7 +211,8 @@ describe('parseLogLine', () => { }); it('should infer warn level from event name', () => { - const warnEvents = ['bead.claim_retry', 'worker.warning']; + // NEEDLE rule: events with *.failed or *.retry suffix -> warn + const warnEvents = ['bead.failed', 'hook.failed']; for (const eventName of warnEvents) { const line = JSON.stringify({ @@ -232,9 +234,10 @@ describe('parseLogLine', () => { }); it('should infer debug level from event name', () => { + // NEEDLE rule: events with debug.* prefix -> debug const line = JSON.stringify({ ts: '2026-03-04T16:17:34.008Z', - event: 'worker.debug', + event: 'debug.probe', session: 'test', worker: { runner: 'claude', @@ -304,7 +307,7 @@ describe('parseLogLine', () => { expect(result?.workspace).toBe('/home/coder/test'); }); - it('should flatten worker to runner-identifier format', () => { + it('should flatten worker to runner-provider-model-identifier format', () => { const line = JSON.stringify({ ts: '2026-03-04T16:17:34.008Z', event: 'worker.started', @@ -320,7 +323,7 @@ describe('parseLogLine', () => { const result = parseLogLine(line); - expect(result?.worker).toBe('needle-prod-worker-1'); + expect(result?.worker).toBe('needle-anthropic-opus-prod-worker-1'); }); }); @@ -1818,7 +1821,7 @@ describe('parseLogLine - NEEDLE format', () => { expect(result).not.toBeNull(); expect(result?.ts).toBe(new Date('2026-03-04T16:17:34.008Z').getTime()); - expect(result?.worker).toBe('claude-test12'); + expect(result?.worker).toBe('claude-anthropic-sonnet-test12'); expect(result?.level).toBe('info'); expect(result?.msg).toBe('worker.started'); expect(result?.session).toBe('needle-claude-anthropic-sonnet-test12'); @@ -1849,7 +1852,7 @@ describe('parseLogLine - NEEDLE format', () => { const result = parseLogLine(line); expect(result).not.toBeNull(); - expect(result?.worker).toBe('claude-test'); + expect(result?.worker).toBe('claude-code-glm-4.7-test'); expect(result?.provider).toBe('code'); expect(result?.model).toBe('glm-4.7'); expect(result?.session).toBe('forge-glm-test'); @@ -1886,7 +1889,7 @@ describe('parseLogLine - NEEDLE format', () => { expect(result?.msg).toBe('bead.claimed'); expect(result?.level).toBe('info'); expect(result?.bead).toBe('bd-2ok0'); - expect(result?.worker).toBe('claude-test'); + expect(result?.worker).toBe('claude-code-glm-4.7-test'); expect(result?.attempt).toBe(1); expect(result?.actor).toBe('forge-glm-test'); }); @@ -1952,10 +1955,11 @@ describe('parseLogLine - NEEDLE format', () => { describe('bead.claim_retry event', () => { it('should parse bead.claim_retry event with warn level', () => { - // Sample from ~/.needle/logs/forge-glm-test.log + // NEEDLE emits bead.claim_retry with explicit level: "warn" const line = JSON.stringify({ ts: '2026-03-04T19:37:22.192Z', event: 'bead.claim_retry', + level: 'warn', session: 'forge-glm-test', worker: { runner: 'claude', @@ -1975,7 +1979,7 @@ describe('parseLogLine - NEEDLE format', () => { expect(result).not.toBeNull(); expect(result?.msg).toBe('bead.claim_retry'); - expect(result?.level).toBe('warn'); // 'retry' in event name triggers warn level + expect(result?.level).toBe('warn'); // NEEDLE emits this with explicit level: "warn" expect(result?.bead).toBe('bd-e6jq'); expect(result?.attempt).toBe(1); expect(result?.max_retries).toBe(5); @@ -1993,6 +1997,7 @@ describe('parseLogLine - NEEDLE format', () => { const line = JSON.stringify({ ts: '2026-03-04T19:37:22.536Z', event: 'bead.claim_retry', + level: 'warn', session: 'forge-glm-test', worker: { runner: 'claude', @@ -2018,10 +2023,11 @@ describe('parseLogLine - NEEDLE format', () => { describe('bead.claim_exhausted event', () => { it('should parse bead.claim_exhausted event with error level', () => { - // Sample from ~/.needle/logs/forge-glm-test.log + // NEEDLE emits bead.claim_exhausted with explicit level: "error" const line = JSON.stringify({ ts: '2026-03-04T19:37:23.647Z', event: 'bead.claim_exhausted', + level: 'error', session: 'forge-glm-test', worker: { runner: 'claude', @@ -2040,7 +2046,7 @@ describe('parseLogLine - NEEDLE format', () => { expect(result).not.toBeNull(); expect(result?.msg).toBe('bead.claim_exhausted'); - expect(result?.level).toBe('error'); // 'exhausted' in event name triggers error level + expect(result?.level).toBe('error'); // NEEDLE emits this with explicit level: "error" expect(result?.max_retries).toBe(5); }); }); @@ -2070,7 +2076,7 @@ describe('parseLogLine - NEEDLE format', () => { expect(result).not.toBeNull(); expect(result?.msg).toBe('heartbeat.emitted'); expect(result?.level).toBe('info'); - expect(result?.worker).toBe('claude-test12'); + expect(result?.worker).toBe('claude-anthropic-sonnet-test12'); expect(result?.session).toBe('needle-claude-anthropic-sonnet-test12'); expect(result?.uptime_seconds).toBe(3600); expect(result?.beads_completed).toBe(5); @@ -2137,10 +2143,15 @@ describe('parseLogLine - NEEDLE format', () => { }); describe('level inference from event names', () => { - it('should infer error level for events with "error"', () => { + // inferLogLevel is the fallback used when no explicit level field is present (legacy logs). + // NEEDLE always includes level explicitly; these tests verify the inference rules match + // NEEDLE's _needle_telemetry_infer_level: error.* -> error, *.failed/*.retry -> warn, + // debug.* -> debug, else -> info. + + it('should infer error level for error.* prefix events', () => { const line = JSON.stringify({ ts: '2026-03-04T16:17:34.008Z', - event: 'worker.error', + event: 'error.agent_crash', session: 'test-session', worker: { runner: 'claude', provider: 'code', model: 'sonnet', identifier: 'test' }, data: {} @@ -2150,7 +2161,7 @@ describe('parseLogLine - NEEDLE format', () => { expect(result?.level).toBe('error'); }); - it('should infer error level for events with "fail"', () => { + it('should infer warn level for *.failed suffix events', () => { const line = JSON.stringify({ ts: '2026-03-04T16:17:34.008Z', event: 'bead.failed', @@ -2160,13 +2171,13 @@ describe('parseLogLine - NEEDLE format', () => { }); const result = parseLogLine(line); - expect(result?.level).toBe('error'); + expect(result?.level).toBe('warn'); }); - it('should infer warn level for events with "retry"', () => { + it('should infer warn level for *.retry suffix events', () => { const line = JSON.stringify({ ts: '2026-03-04T16:17:34.008Z', - event: 'bead.claim_retry', + event: 'claim.retry', session: 'test-session', worker: { runner: 'claude', provider: 'code', model: 'sonnet', identifier: 'test' }, data: {} @@ -2176,23 +2187,10 @@ describe('parseLogLine - NEEDLE format', () => { expect(result?.level).toBe('warn'); }); - it('should infer warn level for events with "warn"', () => { + it('should infer debug level for debug.* prefix events', () => { const line = JSON.stringify({ ts: '2026-03-04T16:17:34.008Z', - event: 'worker.warning', - session: 'test-session', - worker: { runner: 'claude', provider: 'code', model: 'sonnet', identifier: 'test' }, - data: {} - }); - - const result = parseLogLine(line); - expect(result?.level).toBe('warn'); - }); - - it('should infer debug level for events with "debug"', () => { - const line = JSON.stringify({ - ts: '2026-03-04T16:17:34.008Z', - event: 'worker.debug', + event: 'debug.probe', session: 'test-session', worker: { runner: 'claude', provider: 'code', model: 'sonnet', identifier: 'test' }, data: {} @@ -2214,6 +2212,22 @@ describe('parseLogLine - NEEDLE format', () => { const result = parseLogLine(line); expect(result?.level).toBe('info'); }); + + it('should use explicit level field when present rather than inferring', () => { + // bead.claim_retry would infer info (ends with _retry not .retry) but NEEDLE + // always emits it with explicit level: "warn" + const line = JSON.stringify({ + ts: '2026-03-04T16:17:34.008Z', + event: 'bead.claim_retry', + level: 'warn', + session: 'test-session', + worker: { runner: 'claude', provider: 'code', model: 'sonnet', identifier: 'test' }, + data: {} + }); + + const result = parseLogLine(line); + expect(result?.level).toBe('warn'); + }); }); describe('timestamp conversion', () => { @@ -2248,7 +2262,7 @@ describe('parseLogLine - NEEDLE format', () => { }); describe('worker identifier flattening', () => { - it('should flatten worker object to runner-identifier format', () => { + it('should flatten worker object to runner-provider-model-identifier format', () => { const line = JSON.stringify({ ts: '2026-03-04T16:17:34.008Z', event: 'worker.started', @@ -2258,7 +2272,7 @@ describe('parseLogLine - NEEDLE format', () => { }); const result = parseLogLine(line); - expect(result?.worker).toBe('claude-prod'); + expect(result?.worker).toBe('claude-anthropic-opus-prod'); }); it('should preserve provider and model as separate fields', () => { @@ -2296,7 +2310,7 @@ describe('parseLogLine - NEEDLE format', () => { const needleResult = parseLogLine(needleLine); const legacyResult = parseLogLine(legacyLine); - expect(needleResult?.worker).toBe('claude-test'); + expect(needleResult?.worker).toBe('claude-code-sonnet-test'); expect(needleResult?.msg).toBe('worker.started'); expect(legacyResult?.worker).toBe('w-legacy'); diff --git a/src/parser.ts b/src/parser.ts index 7d8a01b..162801a 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -85,19 +85,28 @@ export function parseLogLine(line: string): LogEvent | null { } /** - * NEEDLE log format interface + * NEEDLE worker object — legacy format only, present in some tests. + * Production NEEDLE emits worker as a flat string: runner-provider-model-identifier. + */ +interface NeedleWorkerObject { + runner: string; // e.g., "claude" + provider: string; // e.g., "anthropic", "openai" + model: string; // e.g., "sonnet", "gpt-4o" + identifier: string; // e.g., "alpha", "bravo" +} + +/** + * NEEDLE log format interface. + * worker is a flat string in current NEEDLE output (runner-provider-model-identifier). + * The object form is retained for backward compat with legacy test fixtures. */ interface NeedleLogEntry { - ts: string; // ISO 8601 timestamp - event: string; // Event type (e.g., "worker.started", "bead.claimed") - session: string; // Session identifier - worker: { - runner: string; // e.g., "claude" - provider: string; // e.g., "code", "anthropic" - model: string; // e.g., "glm-4.7", "sonnet" - identifier: string; // e.g., "test", "align" - }; - data: Record; // Event-specific payload + ts: string; // ISO 8601 timestamp + event: string; // Event type (e.g., "worker.started", "bead.claimed") + level?: string; // Log level — always present in current NEEDLE output + session: string; // Session identifier + worker: string | NeedleWorkerObject; // Flat string in production; object in legacy fixtures + data: Record; // Event-specific payload } /** @@ -125,16 +134,16 @@ function parseNeedleFormat(entry: NeedleLogEntry): LogEvent { // Convert ISO timestamp to Unix milliseconds const ts = new Date(entry.ts).getTime(); - // Handle worker as string (aligned format) or object (legacy) + // Handle worker as string (current NEEDLE format) or object (legacy test fixtures) const worker = typeof entry.worker === 'string' ? entry.worker - : `${entry.worker.runner}-${entry.worker.identifier}`; + : `${entry.worker.runner}-${entry.worker.provider}-${entry.worker.model}-${entry.worker.identifier}`; - // Use event as message + // Use event type as message const msg = entry.event; - // Infer log level from event name - const level = inferLogLevel(entry.event); + // Use the level NEEDLE provides; fall back to inference only for legacy entries without it + const level = isValidLogLevel(entry.level) ? entry.level as LogLevel : inferLogLevel(entry.event); // Build LogEvent const event: LogEvent = { @@ -172,10 +181,12 @@ function parseNeedleFormat(entry: NeedleLogEntry): LogEvent { event.path = data.path; } - // Copy session and other NEEDLE-specific fields + // Copy session and, when available from the object form, provider/model event.session = entry.session; - event.provider = entry.worker.provider; - event.model = entry.worker.model; + if (typeof entry.worker !== 'string') { + event.provider = entry.worker.provider; + event.model = entry.worker.model; + } // Copy remaining data fields (excluding already extracted ones) const extractedFields = ['bead_id', 'duration_ms', 'error', 'tool', 'path']; @@ -189,33 +200,22 @@ function parseNeedleFormat(entry: NeedleLogEntry): LogEvent { } /** - * Infer log level from event name + * Infer log level from event name. * - * Maps NEEDLE event types to log levels: - * - error: events containing "error", "fail", "exhausted" - * - warn: events containing "retry", "warn" - * - debug: events containing "debug" - * - info: everything else + * Mirrors NEEDLE's _needle_telemetry_infer_level rules exactly: + * error.* → error + * *.failed → warn + * *.retry → warn + * debug.* → debug + * everything else → info + * + * This is only used as a fallback when the event's level field is absent + * (legacy log entries). Current NEEDLE always includes level explicitly. */ function inferLogLevel(eventName: string): LogLevel { - const lower = eventName.toLowerCase(); - - // Error-level events - if (lower.includes('error') || lower.includes('fail') || lower.includes('exhausted')) { - return 'error'; - } - - // Warn-level events - if (lower.includes('retry') || lower.includes('warn')) { - return 'warn'; - } - - // Debug-level events - if (lower.includes('debug')) { - return 'debug'; - } - - // Default to info + if (eventName.startsWith('error.')) return 'error'; + if (eventName.endsWith('.failed') || eventName.endsWith('.retry')) return 'warn'; + if (eventName.startsWith('debug.')) return 'debug'; return 'info'; } diff --git a/src/store.test.ts b/src/store.test.ts index ca1224d..8315252 100644 --- a/src/store.test.ts +++ b/src/store.test.ts @@ -168,33 +168,33 @@ describe('InMemoryEventStore', () => { expect(worker?.status).toBe('error'); }); - it('should set status to idle on completed message', () => { - store.add(createEvent({ worker: 'w-test', msg: 'Task completed successfully' })); + it('should set status to idle on bead.completed event', () => { + store.add(createEvent({ worker: 'w-test', msg: 'bead.completed' })); const worker = store.getWorker('w-test'); expect(worker?.status).toBe('idle'); }); - it('should set status to idle on complete message', () => { - store.add(createEvent({ worker: 'w-test', msg: 'Task complete' })); + it('should set status to idle on worker.idle event', () => { + store.add(createEvent({ worker: 'w-test', msg: 'worker.idle' })); const worker = store.getWorker('w-test'); expect(worker?.status).toBe('idle'); }); - it('should set status to active on Starting message', () => { + it('should set status to active on worker.started event', () => { // First make it idle - store.add(createEvent({ worker: 'w-test', msg: 'Task completed' })); - // Then starting - store.add(createEvent({ worker: 'w-test', msg: 'Starting new task' })); + store.add(createEvent({ worker: 'w-test', msg: 'bead.completed' })); + // Then active + store.add(createEvent({ worker: 'w-test', msg: 'worker.started' })); const worker = store.getWorker('w-test'); expect(worker?.status).toBe('active'); }); - it('should increment beadsCompleted when task completes with bead', () => { - store.add(createEvent({ worker: 'w-test', msg: 'Task completed', bead: 'bd-1' })); - store.add(createEvent({ worker: 'w-test', msg: 'Task completed', bead: 'bd-2' })); + it('should increment beadsCompleted when bead.completed event has bead', () => { + store.add(createEvent({ worker: 'w-test', msg: 'bead.completed', bead: 'bd-1' })); + store.add(createEvent({ worker: 'w-test', msg: 'bead.completed', bead: 'bd-2' })); const worker = store.getWorker('w-test'); expect(worker?.beadsCompleted).toBe(2); diff --git a/src/store.ts b/src/store.ts index 7cbcfae..efb7c46 100644 --- a/src/store.ts +++ b/src/store.ts @@ -411,18 +411,30 @@ export class InMemoryEventStore implements EventStore { } } - // Update status based on event + // Update status based on NEEDLE event type (event.msg holds the event type string) + const needleEvent = event.msg; if (event.level === 'error') { worker.status = 'error'; - } else if (event.msg.includes('completed') || event.msg.includes('complete')) { + } else if ( + needleEvent === 'bead.completed' || + needleEvent === 'worker.idle' || + needleEvent === 'worker.stopped' || + needleEvent === 'worker.draining' + ) { worker.status = 'idle'; - if (event.bead) { + if (needleEvent === 'bead.completed' && event.bead) { worker.beadsCompleted++; } - // Clear active files and bead on completion - worker.activeFiles = []; - worker.activeBead = undefined; - } else if (event.msg.includes('Starting') || event.msg.includes('starting')) { + if (needleEvent === 'bead.completed') { + worker.activeFiles = []; + worker.activeBead = undefined; + } + } else if ( + needleEvent === 'worker.started' || + needleEvent === 'bead.claimed' || + needleEvent === 'bead.agent_started' || + needleEvent === 'execution.started' + ) { worker.status = 'active'; } @@ -455,15 +467,9 @@ export class InMemoryEventStore implements EventStore { this.taskStartTimes.set(beadId, event.ts); } - // Check for task completion - const msg = event.msg?.toLowerCase() || ''; - if ( - msg.includes('completed') || - msg.includes('finished') || - msg.includes('done') || - msg.includes('success') || - msg.includes('closed') - ) { + // Check for task completion — match on NEEDLE event type exactly + const msg = event.msg || ''; + if (msg === 'bead.completed' || msg === 'bead.failed') { const startTime = this.taskStartTimes.get(beadId); if (startTime) { const durationMs = event.ts - startTime; diff --git a/src/types.ts b/src/types.ts index 18ed76d..f8acecd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,6 +8,92 @@ export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; export type WorkerStatus = 'active' | 'idle' | 'error'; +/** + * NEEDLE worker status values as emitted in heartbeat files and worker.* events. + * FABRIC maps these to the simpler WorkerStatus for display. + */ +export type NeedleWorkerStatus = 'idle' | 'executing' | 'draining' | 'starting'; + +/** + * All event types emitted by NEEDLE's telemetry pipeline. + * Format: category.action — matches NEEDLE's _needle_telemetry_emit event_type argument. + */ +export type NeedleEventType = + // Worker lifecycle + | 'worker.started' + | 'worker.idle' + | 'worker.stopped' + | 'worker.draining' + // Bead lifecycle + | 'bead.claimed' + | 'bead.prompt_built' + | 'bead.agent_started' + | 'bead.agent_completed' + | 'bead.completed' + | 'bead.failed' + | 'bead.released' + | 'bead.claim_retry' + | 'bead.claim_exhausted' + // Bead mitosis + | 'bead.mitosis.check' + | 'bead.mitosis.started' + | 'bead.mitosis.child_created' + | 'bead.mitosis.complete' + | 'bead.mitosis.failed' + | 'bead.mitosis.skipped' + // Strand lifecycle + | 'strand.started' + | 'strand.completed' + | 'strand.fallthrough' + | 'strand.skipped' + // Hook lifecycle + | 'hook.started' + | 'hook.completed' + | 'hook.failed' + // Heartbeat + | 'heartbeat.emitted' + | 'heartbeat.stuck_detected' + | 'heartbeat.recovery' + // Mend (maintenance) + | 'mend.orphan_released' + | 'mend.heartbeat_cleaned' + | 'mend.logs_pruned' + | 'mend.completed' + // Unravel (alternatives) + | 'unravel.alternatives_created' + | 'unravel.alternative_created' + | 'unravel.analysis_started' + | 'unravel.analysis_completed' + // Weave (documentation gaps) + | 'weave.bead_created' + | 'weave.analysis_started' + | 'weave.analysis_completed' + // Pulse (health monitoring) + | 'pulse.bead_created' + | 'pulse.scan_started' + | 'pulse.scan_completed' + | 'pulse.issue_detected' + | 'pulse.detector_started' + | 'pulse.detector_completed' + // Error events + | 'error.claim_failed' + | 'error.agent_crash' + | 'error.timeout' + | 'error.release_failed' + // Effort & budget + | 'effort.recorded' + | 'budget.warning' + | 'budget.exceeded' + | 'budget.per_bead_exceeded' + // File locks + | 'file.checkout' + | 'file.conflict' + | 'file.release' + | 'file.stale' + | 'lock.priority_bump' + | 'lock.priority_bump_received' + | 'lock.expired'; + // ============================================ // Conversation Event Types // ============================================ @@ -258,6 +344,15 @@ export interface LogEvent { /** Optional: Error details */ error?: string; + /** NEEDLE session identifier (e.g. 'needle-claude-anthropic-sonnet-alpha') */ + session?: string; + + /** AI provider extracted from NEEDLE worker string (e.g. 'anthropic', 'openai') */ + provider?: string; + + /** AI model identifier extracted from NEEDLE worker string (e.g. 'sonnet', 'gpt-4o') */ + model?: string; + /** Any additional fields */ [key: string]: unknown; }