feat: upulse workflow timeline <key> — view event timeline
CI / test (push) Has been cancelled

- Shows offset, duration, role, meta for each event
- --json for structured output (feeds report workflow)
- --content for content preview
This commit is contained in:
2026-04-17 08:22:35 +00:00
parent 3c6c8840ef
commit 9efb535ead
2 changed files with 82 additions and 1 deletions
+3 -1
View File
@@ -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;
}
+79
View File
@@ -67,6 +67,85 @@ export function registerWorkflowCommand(program: Command): void {
scopedStore.close();
});
topic
.command('timeline <key>')
.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')