diff --git a/packages/pulse/src/e2e/council-v2-live.ts b/packages/pulse/src/e2e/council-v2-live.ts index 7b52c75..8b4f2e9 100644 --- a/packages/pulse/src/e2e/council-v2-live.ts +++ b/packages/pulse/src/e2e/council-v2-live.ts @@ -103,7 +103,9 @@ async function reviewerFn(chain: WorkflowMessage[]) { console.log(` šŸ” reviewer reviewing "${title}"...`); const t = Date.now(); const result = await reviewerRole(chain, 'live', null as any); - console.log(` šŸ” reviewer verdict=${result.meta?.verdict} (${((Date.now() - t) / 1000).toFixed(1)}s)`); + console.log( + ` šŸ” reviewer verdict=${result.meta?.verdict} (${((Date.now() - t) / 1000).toFixed(1)}s)`, + ); return result; } diff --git a/packages/upulse/src/commands/workflow.ts b/packages/upulse/src/commands/workflow.ts index 3138906..ae3603f 100644 --- a/packages/upulse/src/commands/workflow.ts +++ b/packages/upulse/src/commands/workflow.ts @@ -67,6 +67,85 @@ export function registerWorkflowCommand(program: Command): void { scopedStore.close(); }); + topic + .command('timeline ') + .description('Show workflow event timeline') + .option('--json', 'Output as JSON') + .option('--content', 'Include content preview') + .action((key: string, opts: { json?: boolean; content?: boolean }) => { + const config = loadConfig(resolveDir(program.opts().dir)); + migrateToScoped(config); + const scopedStore = openScopedStore( + config.store.scopesDir, + config.store.objectsDir, + ); + if (!scopedStore) { + console.error('No store found.'); + process.exit(1); + } + const store = scopedStore.scope('workflows'); + const events = store + .getAfter(0) + .filter((e) => e.kind.startsWith('coding.') && e.key === key); + + if (events.length === 0) { + console.error(`No events found for key: ${key}`); + scopedStore.close(); + process.exit(1); + } + + const t0 = events[0].occurredAt; + const entries = events.map((e, i) => { + const role = e.kind.replace('coding.', ''); + const prevTime = i > 0 ? events[i - 1].occurredAt : t0; + const durationMs = e.occurredAt - prevTime; + const meta = e.meta ? JSON.parse(e.meta) : null; + let contentPreview: string | undefined; + if (opts.content && e.hash) { + try { + const obj = store.getObject(e.hash); + const text = typeof obj === 'string' ? obj : JSON.stringify(obj); + contentPreview = + text.slice(0, 200) + (text.length > 200 ? '...' : ''); + } catch {} + } + return { + id: e.id, + role, + offsetMs: e.occurredAt - t0, + durationMs: i === 0 ? 0 : durationMs, + meta, + ...(contentPreview ? { contentPreview } : {}), + }; + }); + + const totalMs = events[events.length - 1].occurredAt - t0; + + if (opts.json) { + console.log(JSON.stringify({ key, totalMs, events: entries }, null, 2)); + } else { + console.log(`\nšŸ“Š Workflow Timeline: ${key}`); + console.log(` Total: ${(totalMs / 1000).toFixed(1)}s\n`); + for (const entry of entries) { + const offset = `+${(entry.offsetMs / 1000).toFixed(1)}s`; + const dur = + entry.durationMs > 0 + ? ` (${(entry.durationMs / 1000).toFixed(1)}s)` + : ''; + const metaStr = entry.meta ? ` ${JSON.stringify(entry.meta)}` : ''; + console.log( + ` [${offset.padStart(8)}] #${String(entry.id).padStart(2)} ${entry.role.padEnd(12)}${dur}${metaStr}`, + ); + if (entry.contentPreview) { + console.log(` ${entry.contentPreview.split('\n')[0]}`); + } + } + console.log(); + } + + scopedStore.close(); + }); + topic .command('list') .description('List coding topics')