agent-relay 3.2.10 → 3.2.12

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.
Files changed (63) hide show
  1. package/README.md +2 -2
  2. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  3. package/bin/agent-relay-broker-darwin-x64 +0 -0
  4. package/bin/agent-relay-broker-linux-arm64 +0 -0
  5. package/bin/agent-relay-broker-linux-x64 +0 -0
  6. package/dist/index.cjs +858 -519
  7. package/package.json +8 -8
  8. package/packages/acp-bridge/package.json +2 -2
  9. package/packages/config/package.json +1 -1
  10. package/packages/hooks/package.json +4 -4
  11. package/packages/memory/package.json +2 -2
  12. package/packages/openclaw/package.json +2 -2
  13. package/packages/policy/package.json +2 -2
  14. package/packages/sdk/dist/cli-registry.d.ts +42 -0
  15. package/packages/sdk/dist/cli-registry.d.ts.map +1 -0
  16. package/packages/sdk/dist/cli-registry.js +126 -0
  17. package/packages/sdk/dist/cli-registry.js.map +1 -0
  18. package/packages/sdk/dist/cli-resolver.d.ts +30 -0
  19. package/packages/sdk/dist/cli-resolver.d.ts.map +1 -0
  20. package/packages/sdk/dist/cli-resolver.js +132 -0
  21. package/packages/sdk/dist/cli-resolver.js.map +1 -0
  22. package/packages/sdk/dist/index.d.ts +2 -0
  23. package/packages/sdk/dist/index.d.ts.map +1 -1
  24. package/packages/sdk/dist/index.js +2 -0
  25. package/packages/sdk/dist/index.js.map +1 -1
  26. package/packages/sdk/dist/spawn-from-env.d.ts.map +1 -1
  27. package/packages/sdk/dist/spawn-from-env.js +6 -15
  28. package/packages/sdk/dist/spawn-from-env.js.map +1 -1
  29. package/packages/sdk/dist/workflows/builder.d.ts +5 -0
  30. package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
  31. package/packages/sdk/dist/workflows/builder.js +36 -5
  32. package/packages/sdk/dist/workflows/builder.js.map +1 -1
  33. package/packages/sdk/dist/workflows/collectors/opencode.d.ts.map +1 -1
  34. package/packages/sdk/dist/workflows/collectors/opencode.js +26 -0
  35. package/packages/sdk/dist/workflows/collectors/opencode.js.map +1 -1
  36. package/packages/sdk/dist/workflows/default-logger.d.ts +9 -0
  37. package/packages/sdk/dist/workflows/default-logger.d.ts.map +1 -0
  38. package/packages/sdk/dist/workflows/default-logger.js +104 -0
  39. package/packages/sdk/dist/workflows/default-logger.js.map +1 -0
  40. package/packages/sdk/dist/workflows/index.d.ts +1 -0
  41. package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
  42. package/packages/sdk/dist/workflows/index.js +1 -0
  43. package/packages/sdk/dist/workflows/index.js.map +1 -1
  44. package/packages/sdk/dist/workflows/runner.d.ts +1 -1
  45. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  46. package/packages/sdk/dist/workflows/runner.js +16 -45
  47. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  48. package/packages/sdk/package.json +2 -2
  49. package/packages/sdk/src/__tests__/workflow-runner.test.ts +2 -2
  50. package/packages/sdk/src/cli-registry.ts +148 -0
  51. package/packages/sdk/src/cli-resolver.ts +155 -0
  52. package/packages/sdk/src/index.ts +2 -0
  53. package/packages/sdk/src/spawn-from-env.ts +6 -17
  54. package/packages/sdk/src/workflows/builder.ts +44 -4
  55. package/packages/sdk/src/workflows/collectors/opencode.ts +26 -0
  56. package/packages/sdk/src/workflows/default-logger.ts +120 -0
  57. package/packages/sdk/src/workflows/index.ts +1 -0
  58. package/packages/sdk/src/workflows/runner.ts +16 -43
  59. package/packages/sdk-py/pyproject.toml +1 -1
  60. package/packages/telemetry/package.json +1 -1
  61. package/packages/trajectory/package.json +2 -2
  62. package/packages/user-directory/package.json +2 -2
  63. package/packages/utils/package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/sdk",
3
- "version": "3.2.10",
3
+ "version": "3.2.12",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -102,7 +102,7 @@
102
102
  "typescript": "^5.7.3"
103
103
  },
104
104
  "dependencies": {
105
- "@agent-relay/config": "3.2.10",
105
+ "@agent-relay/config": "3.2.12",
106
106
  "@relaycast/sdk": "^1.0.0",
107
107
  "@sinclair/typebox": "^0.34.48",
108
108
  "chalk": "^4.1.2",
@@ -995,10 +995,10 @@ agents:
995
995
  expect(args).toEqual(['-p', 'Analyze']);
996
996
  });
997
997
 
998
- it('should build opencode command with --prompt flag', () => {
998
+ it('should build opencode command with run subcommand', () => {
999
999
  const { cmd, args } = WorkflowRunner.buildNonInteractiveCommand('opencode', 'Fix bug');
1000
1000
  expect(cmd).toBe('opencode');
1001
- expect(args).toEqual(['--prompt', 'Fix bug']);
1001
+ expect(args).toEqual(['run', 'Fix bug']);
1002
1002
  });
1003
1003
 
1004
1004
  it('should build droid command with exec subcommand', () => {
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Consolidated CLI registry — single source of truth for all supported
3
+ * agent CLI metadata: binary names, non-interactive args, bypass flags,
4
+ * and well-known install paths.
5
+ *
6
+ * Consumers: runner.ts (buildNonInteractiveCommand, resolveCursorCli),
7
+ * spawn-from-env.ts (BYPASS_FLAGS), cli-resolver.ts (path resolution).
8
+ *
9
+ * NOTE: The Rust PTY spawner (src/pty.rs) maintains its own PATH fallback.
10
+ * When updating `COMMON_SEARCH_PATHS` here, also update the Rust fallback
11
+ * in `resolve_command_path()` at src/pty.rs:53-67.
12
+ */
13
+
14
+ import type { AgentCli } from './workflows/types.js';
15
+
16
+ // ── Types ──────────────────────────────────────────────────────────────────
17
+
18
+ export interface CliDefinition {
19
+ /** Binary name(s) to try, in order of preference */
20
+ binaries: string[];
21
+ /** Build non-interactive mode args for a one-shot task */
22
+ nonInteractiveArgs: (task: string, extraArgs?: string[]) => string[];
23
+ /** Bypass flag for auto-approve / unattended mode */
24
+ bypassFlag?: string;
25
+ /** Bypass flag aliases (alternative forms accepted by the CLI) */
26
+ bypassAliases?: string[];
27
+ /** Extra install paths to check beyond PATH (resolved relative to $HOME) */
28
+ searchPaths?: string[];
29
+ }
30
+
31
+ // ── Well-known install paths ───────────────────────────────────────────────
32
+
33
+ /**
34
+ * Common install directories checked when PATH is empty or incomplete.
35
+ * Paths containing `~` are expanded at resolution time.
36
+ *
37
+ * Keep in sync with the Rust fallback in src/pty.rs `resolve_command_path()`.
38
+ */
39
+ export const COMMON_SEARCH_PATHS = [
40
+ '~/.local/bin',
41
+ '~/.opencode/bin',
42
+ '~/.claude/local',
43
+ '/usr/local/bin',
44
+ '/usr/bin',
45
+ '/bin',
46
+ '/opt/homebrew/bin',
47
+ ];
48
+
49
+ // ── Registry ───────────────────────────────────────────────────────────────
50
+
51
+ const CLI_REGISTRY: Record<AgentCli, CliDefinition> = {
52
+ claude: {
53
+ binaries: ['claude'],
54
+ nonInteractiveArgs: (task, extra = []) => [
55
+ '-p',
56
+ '--dangerously-skip-permissions',
57
+ task,
58
+ ...extra,
59
+ ],
60
+ bypassFlag: '--dangerously-skip-permissions',
61
+ searchPaths: ['~/.claude/local'],
62
+ },
63
+ codex: {
64
+ binaries: ['codex'],
65
+ nonInteractiveArgs: (task, extra = []) => ['exec', task, ...extra],
66
+ bypassFlag: '--dangerously-bypass-approvals-and-sandbox',
67
+ bypassAliases: ['--full-auto'],
68
+ searchPaths: ['~/.local/bin'],
69
+ },
70
+ gemini: {
71
+ binaries: ['gemini'],
72
+ nonInteractiveArgs: (task, extra = []) => ['-p', task, ...extra],
73
+ bypassFlag: '--yolo',
74
+ bypassAliases: ['-y'],
75
+ },
76
+ opencode: {
77
+ binaries: ['opencode'],
78
+ nonInteractiveArgs: (task, extra = []) => ['run', task, ...extra],
79
+ searchPaths: ['~/.opencode/bin'],
80
+ },
81
+ droid: {
82
+ binaries: ['droid'],
83
+ nonInteractiveArgs: (task, extra = []) => ['exec', task, ...extra],
84
+ },
85
+ aider: {
86
+ binaries: ['aider'],
87
+ nonInteractiveArgs: (task, extra = []) => [
88
+ '--message',
89
+ task,
90
+ '--yes-always',
91
+ '--no-git',
92
+ ...extra,
93
+ ],
94
+ },
95
+ goose: {
96
+ binaries: ['goose'],
97
+ nonInteractiveArgs: (task, extra = []) => [
98
+ 'run',
99
+ '--text',
100
+ task,
101
+ '--no-session',
102
+ ...extra,
103
+ ],
104
+ },
105
+ 'cursor-agent': {
106
+ binaries: ['cursor-agent'],
107
+ nonInteractiveArgs: (task, extra = []) => [
108
+ '--force',
109
+ '-p',
110
+ task,
111
+ ...extra,
112
+ ],
113
+ },
114
+ agent: {
115
+ binaries: ['agent'],
116
+ nonInteractiveArgs: (task, extra = []) => [
117
+ '--force',
118
+ '-p',
119
+ task,
120
+ ...extra,
121
+ ],
122
+ },
123
+ cursor: {
124
+ binaries: ['cursor-agent', 'agent'],
125
+ nonInteractiveArgs: (task, extra = []) => [
126
+ '--force',
127
+ '-p',
128
+ task,
129
+ ...extra,
130
+ ],
131
+ },
132
+ };
133
+
134
+ /**
135
+ * Get the CLI definition for a given CLI identifier.
136
+ * Handles `cli:model` variants (e.g. `claude:opus`) by extracting the base CLI.
137
+ */
138
+ export function getCliDefinition(cli: string): CliDefinition | undefined {
139
+ const baseCli = cli.includes(':') ? cli.split(':')[0] : cli;
140
+ return CLI_REGISTRY[baseCli as AgentCli];
141
+ }
142
+
143
+ /**
144
+ * Get the full registry (read-only).
145
+ */
146
+ export function getCliRegistry(): Readonly<Record<AgentCli, CliDefinition>> {
147
+ return CLI_REGISTRY;
148
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * CLI binary resolver — finds the actual binary path for a given agent CLI.
3
+ *
4
+ * Checks PATH first, then falls back to well-known install directories
5
+ * from the CLI registry. Results are memoized.
6
+ */
7
+
8
+ import { execFile } from 'node:child_process';
9
+ import { access, constants } from 'node:fs/promises';
10
+ import { accessSync, constants as constantsSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { homedir } from 'node:os';
13
+ import { promisify } from 'node:util';
14
+ import type { AgentCli } from './workflows/types.js';
15
+ import { getCliDefinition, COMMON_SEARCH_PATHS } from './cli-registry.js';
16
+
17
+ const execFileAsync = promisify(execFile);
18
+
19
+ // ── Types ──────────────────────────────────────────────────────────────────
20
+
21
+ export interface ResolvedCli {
22
+ /** The binary name that was found */
23
+ binary: string;
24
+ /** The full path to the binary */
25
+ path: string;
26
+ }
27
+
28
+ // ── Memoization ────────────────────────────────────────────────────────────
29
+
30
+ // null sentinel means "looked up, not found" — avoids repeating expensive searches
31
+ const resolveCache = new Map<string, ResolvedCli | null>();
32
+
33
+ /**
34
+ * Clear the resolution cache. Useful for testing or after PATH changes.
35
+ */
36
+ export function clearResolveCache(): void {
37
+ resolveCache.clear();
38
+ }
39
+
40
+ // ── Path expansion ─────────────────────────────────────────────────────────
41
+
42
+ function expandHome(p: string): string {
43
+ if (p.startsWith('~/')) {
44
+ return join(homedir(), p.slice(2));
45
+ }
46
+ return p;
47
+ }
48
+
49
+ // ── Async resolver ─────────────────────────────────────────────────────────
50
+
51
+ /**
52
+ * Resolve a CLI to its binary path. Checks PATH via `which`, then falls
53
+ * back to well-known install directories from the CLI registry.
54
+ *
55
+ * Results are memoized. Returns `undefined` if the binary cannot be found.
56
+ */
57
+ export async function resolveCli(cli: AgentCli): Promise<ResolvedCli | undefined> {
58
+ if (resolveCache.has(cli)) {
59
+ return resolveCache.get(cli) ?? undefined;
60
+ }
61
+
62
+ const def = getCliDefinition(cli);
63
+ if (!def) return undefined;
64
+
65
+ for (const binary of def.binaries) {
66
+ // Try PATH first via `which`
67
+ try {
68
+ const { stdout } = await execFileAsync('which', [binary]);
69
+ const path = stdout.trim();
70
+ if (path) {
71
+ const result: ResolvedCli = { binary, path };
72
+ resolveCache.set(cli, result);
73
+ return result;
74
+ }
75
+ } catch {
76
+ // not in PATH
77
+ }
78
+
79
+ // Try well-known install directories (CLI-specific + common)
80
+ const searchDirs = [...(def.searchPaths ?? []), ...COMMON_SEARCH_PATHS];
81
+ const seen = new Set<string>();
82
+ for (const dir of searchDirs) {
83
+ const expanded = expandHome(dir);
84
+ if (seen.has(expanded)) continue;
85
+ seen.add(expanded);
86
+
87
+ const candidate = join(expanded, binary);
88
+ try {
89
+ await access(candidate, constants.X_OK);
90
+ const result: ResolvedCli = { binary, path: candidate };
91
+ resolveCache.set(cli, result);
92
+ return result;
93
+ } catch {
94
+ // not found here
95
+ }
96
+ }
97
+ }
98
+
99
+ resolveCache.set(cli, null);
100
+ return undefined;
101
+ }
102
+
103
+ // ── Sync resolver (for hot paths that can't be async) ──────────────────────
104
+
105
+ /**
106
+ * Synchronous version of `resolveCli`. Uses `which` via execFileSync
107
+ * and synchronous fs.accessSync. Prefer the async version when possible.
108
+ */
109
+ export function resolveCliSync(cli: AgentCli): ResolvedCli | undefined {
110
+ if (resolveCache.has(cli)) {
111
+ return resolveCache.get(cli) ?? undefined;
112
+ }
113
+
114
+ const def = getCliDefinition(cli);
115
+ if (!def) return undefined;
116
+
117
+ const { execFileSync } = require('node:child_process') as typeof import('node:child_process');
118
+
119
+ for (const binary of def.binaries) {
120
+ // Try PATH first via `which`
121
+ try {
122
+ const stdout = execFileSync('which', [binary], { stdio: ['pipe', 'pipe', 'ignore'] });
123
+ const path = stdout.toString().trim();
124
+ if (path) {
125
+ const result: ResolvedCli = { binary, path };
126
+ resolveCache.set(cli, result);
127
+ return result;
128
+ }
129
+ } catch {
130
+ // not in PATH
131
+ }
132
+
133
+ // Try well-known install directories
134
+ const searchDirs = [...(def.searchPaths ?? []), ...COMMON_SEARCH_PATHS];
135
+ const seen = new Set<string>();
136
+ for (const dir of searchDirs) {
137
+ const expanded = expandHome(dir);
138
+ if (seen.has(expanded)) continue;
139
+ seen.add(expanded);
140
+
141
+ const candidate = join(expanded, binary);
142
+ try {
143
+ accessSync(candidate, constantsSync.X_OK);
144
+ const result: ResolvedCli = { binary, path: candidate };
145
+ resolveCache.set(cli, result);
146
+ return result;
147
+ } catch {
148
+ // not found here
149
+ }
150
+ }
151
+ }
152
+
153
+ resolveCache.set(cli, null);
154
+ return undefined;
155
+ }
@@ -11,3 +11,5 @@ export * from './shadow.js';
11
11
  export * from './relay-adapter.js';
12
12
  export * from './workflows/index.js';
13
13
  export * from './spawn-from-env.js';
14
+ export * from './cli-registry.js';
15
+ export * from './cli-resolver.js';
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import { AgentRelay } from "./relay.js";
16
+ import { getCliDefinition } from "./cli-registry.js";
16
17
 
17
18
  // ── Types ──────────────────────────────────────────────────────────────────
18
19
 
@@ -59,33 +60,21 @@ export interface SpawnFromEnvResult {
59
60
  exitCode?: number;
60
61
  }
61
62
 
62
- // ── Bypass Policy (SDK-owned, single source of truth) ──────────────────────
63
+ // ── Bypass Policy (delegated to cli-registry) ──────────────────────────────
63
64
 
64
65
  type BypassFlagConfig = {
65
66
  flag: string;
66
67
  aliases?: string[];
67
68
  };
68
69
 
69
- /** SDK-owned bypass flag mapping. Cloud must NOT duplicate these. */
70
- const BYPASS_FLAGS: Record<string, BypassFlagConfig> = {
71
- claude: { flag: "--dangerously-skip-permissions" },
72
- codex: {
73
- flag: "--dangerously-bypass-approvals-and-sandbox",
74
- aliases: ["--full-auto"],
75
- },
76
- gemini: {
77
- flag: "--yolo",
78
- aliases: ["-y"],
79
- },
80
- };
81
-
82
70
  /**
83
- * Resolve bypass flag config for a CLI.
71
+ * Resolve bypass flag config for a CLI from the consolidated registry.
84
72
  * Handles `claude:model` variants (e.g. `claude:opus`).
85
73
  */
86
74
  function getBypassFlagConfig(cli: string): BypassFlagConfig | undefined {
87
- const baseCli = cli.includes(":") ? cli.split(":")[0] : cli;
88
- return BYPASS_FLAGS[baseCli];
75
+ const def = getCliDefinition(cli);
76
+ if (!def?.bypassFlag) return undefined;
77
+ return { flag: def.bypassFlag, aliases: def.bypassAliases };
89
78
  }
90
79
 
91
80
  // ── Env Parsing ────────────────────────────────────────────────────────────
@@ -22,6 +22,7 @@ import type {
22
22
  } from './types.js';
23
23
  import { WorkflowRunner, type WorkflowEventListener, type VariableContext, type StepExecutor } from './runner.js';
24
24
  import { formatDryRunReport } from './dry-run-format.js';
25
+ import { createDefaultEventLogger, type LogLevel } from './default-logger.js';
25
26
 
26
27
  // ── Option types for the builder API ────────────────────────────────────────
27
28
 
@@ -106,6 +107,10 @@ export interface WorkflowRunOptions {
106
107
  startFrom?: string;
107
108
  /** Previous run ID whose cached outputs are used with startFrom. */
108
109
  previousRunId?: string;
110
+ /** Console log verbosity: "verbose" | "normal" (default) | "quiet" | false (silent). */
111
+ logLevel?: LogLevel;
112
+ /** Renderer: "listr" for listr2 UI, "default" for console logger, false to disable. */
113
+ renderer?: 'listr' | 'default' | false;
109
114
  }
110
115
 
111
116
  // ── WorkflowBuilder ─────────────────────────────────────────────────────────
@@ -333,7 +338,11 @@ export class WorkflowBuilder {
333
338
  if (this._timeoutMs !== undefined) config.swarm.timeoutMs = this._timeoutMs;
334
339
  if (this._channel !== undefined) config.swarm.channel = this._channel;
335
340
  if (this._idleNudge !== undefined) config.swarm.idleNudge = this._idleNudge;
336
- if (this._errorHandling !== undefined) config.errorHandling = this._errorHandling;
341
+ config.errorHandling = this._errorHandling ?? {
342
+ strategy: 'retry',
343
+ maxRetries: 2,
344
+ retryDelayMs: 10_000,
345
+ };
337
346
  if (this._coordination !== undefined) config.coordination = this._coordination;
338
347
  if (this._state !== undefined) config.state = this._state;
339
348
  if (this._trajectories !== undefined) config.trajectories = this._trajectories;
@@ -367,15 +376,23 @@ export class WorkflowBuilder {
367
376
  return report;
368
377
  }
369
378
 
379
+ // Wire up default console logger unless explicitly disabled
380
+ // renderer: "listr" owns the terminal — skip console logger to avoid garbled output
381
+ // renderer: false implies no output at all
382
+ const logLevel = options.renderer === 'listr' || options.renderer === false
383
+ ? false
384
+ : (options.logLevel ?? 'normal');
385
+ if (logLevel !== false) {
386
+ runner.on(createDefaultEventLogger(logLevel));
387
+ }
388
+
389
+ // Wire up user-provided event handler (additive — does not replace the default logger)
370
390
  if (options.onEvent) {
371
391
  runner.on(options.onEvent);
372
392
  }
373
393
 
374
394
  // Auto-detect RESUME_RUN_ID env var for resuming failed runs
375
395
  const resumeRunId = process.env.RESUME_RUN_ID;
376
- if (resumeRunId) {
377
- return runner.resume(resumeRunId, options.vars);
378
- }
379
396
 
380
397
  const startFrom = this._startFrom ?? options.startFrom ?? process.env.START_FROM;
381
398
  const previousRunId = this._previousRunId ?? options.previousRunId ?? process.env.PREVIOUS_RUN_ID;
@@ -383,6 +400,29 @@ export class WorkflowBuilder {
383
400
  ? { startFrom, previousRunId }
384
401
  : undefined;
385
402
 
403
+ // If listr renderer requested, wire it up and run concurrently
404
+ // Must be set up BEFORE the resume check so resume runs also get event output
405
+ if (options.renderer === 'listr') {
406
+ const { createWorkflowRenderer } = await import('./listr-renderer.js');
407
+ const renderer = createWorkflowRenderer();
408
+ runner.on(renderer.onEvent);
409
+
410
+ const runPromise = resumeRunId
411
+ ? runner.resume(resumeRunId, options.vars)
412
+ : runner.execute(config, options.workflow, options.vars, executeOptions);
413
+
414
+ try {
415
+ const [result] = await Promise.all([runPromise, renderer.start()]);
416
+ return result;
417
+ } finally {
418
+ renderer.unmount();
419
+ }
420
+ }
421
+
422
+ if (resumeRunId) {
423
+ return runner.resume(resumeRunId, options.vars);
424
+ }
425
+
386
426
  return runner.execute(config, options.workflow, options.vars, executeOptions);
387
427
  }
388
428
  }
@@ -73,6 +73,32 @@ interface OpenCodePartData {
73
73
  function loadDatabaseConstructor(): DatabaseConstructor | null {
74
74
  try {
75
75
  return require('better-sqlite3') as DatabaseConstructor;
76
+ } catch {
77
+ // fall through
78
+ }
79
+
80
+ // Fall back to Node 22+ native node:sqlite (experimental)
81
+ try {
82
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
83
+ const { DatabaseSync } = require('node:sqlite');
84
+ return function NativeSqliteWrapper(filename: string, options?: { readonly?: boolean; fileMustExist?: boolean }) {
85
+ const db = new DatabaseSync(filename, { open: true, readOnly: options?.readonly ?? false });
86
+ return {
87
+ prepare(sql: string) {
88
+ const stmt = db.prepare(sql);
89
+ return {
90
+ get<T>(params?: unknown): T | undefined {
91
+ return params != null ? stmt.get(params) as T | undefined : stmt.get() as T | undefined;
92
+ },
93
+ all<T>(params?: unknown): T[] {
94
+ return (params != null ? stmt.all(params) : stmt.all()) as T[];
95
+ },
96
+ };
97
+ },
98
+ pragma(source: string) { db.exec(`PRAGMA ${source}`); return undefined; },
99
+ close() { db.close(); },
100
+ };
101
+ } as unknown as DatabaseConstructor;
76
102
  } catch {
77
103
  return null;
78
104
  }
@@ -0,0 +1,120 @@
1
+ import chalk from 'chalk';
2
+ import type { WorkflowEvent, WorkflowEventListener } from './runner.js';
3
+
4
+ export type LogLevel = 'verbose' | 'normal' | 'quiet' | false;
5
+
6
+ const noop: WorkflowEventListener = () => {};
7
+
8
+ /**
9
+ * Create a default event logger that writes workflow progress to the console.
10
+ *
11
+ * @param level - Log verbosity: "verbose" | "normal" (default) | "quiet" | false (no-op)
12
+ */
13
+ export function createDefaultEventLogger(level: LogLevel = 'normal'): WorkflowEventListener {
14
+ if (level === false) return noop;
15
+
16
+ return (event: WorkflowEvent) => {
17
+ switch (event.type) {
18
+ // ── Run lifecycle ──
19
+ case 'run:started':
20
+ if (level !== 'quiet') {
21
+ console.log(chalk.cyan(`[workflow] run ${event.runId.slice(0, 8)}...`));
22
+ }
23
+ break;
24
+
25
+ case 'run:completed':
26
+ console.log(chalk.green(`[workflow] completed`));
27
+ break;
28
+
29
+ case 'run:failed':
30
+ console.log(chalk.red(`[workflow] FAILED: ${event.error}`));
31
+ break;
32
+
33
+ case 'run:cancelled':
34
+ if (level !== 'quiet') {
35
+ console.log(chalk.yellow(`[workflow] cancelled`));
36
+ }
37
+ break;
38
+
39
+ // ── Step lifecycle ──
40
+ case 'step:started':
41
+ if (level !== 'quiet') {
42
+ console.log(chalk.blue(` ● ${event.stepName} — started`));
43
+ }
44
+ break;
45
+
46
+ case 'step:completed':
47
+ if (level !== 'quiet') {
48
+ console.log(chalk.green(` ✓ ${event.stepName} — completed`));
49
+ }
50
+ break;
51
+
52
+ case 'step:failed':
53
+ console.log(chalk.red(` ✗ ${event.stepName} — FAILED: ${event.error}`));
54
+ break;
55
+
56
+ case 'step:skipped':
57
+ if (level !== 'quiet') {
58
+ console.log(chalk.gray(` ○ ${event.stepName} — skipped`));
59
+ }
60
+ break;
61
+
62
+ case 'step:retrying':
63
+ if (level !== 'quiet') {
64
+ console.log(chalk.yellow(` ↻ ${event.stepName} — retrying (attempt ${event.attempt})`));
65
+ }
66
+ break;
67
+
68
+ case 'step:nudged':
69
+ if (level !== 'quiet') {
70
+ console.log(chalk.yellow(` ⚡ ${event.stepName} — nudged (${event.nudgeCount})`));
71
+ }
72
+ break;
73
+
74
+ case 'step:agent-report': {
75
+ if (level !== 'quiet') {
76
+ const r = event.report;
77
+ const parts: string[] = [];
78
+ if (r.model) parts.push(r.model);
79
+ if (r.cost != null) parts.push(`$${r.cost.toFixed(2)}`);
80
+ if (r.tokens) parts.push(`${r.tokens.input}+${r.tokens.output} tokens`);
81
+ parts.push(`${r.errors.length} errors`);
82
+ console.log(chalk.dim(` 📊 ${event.stepName} — ${parts.join(' · ')}`));
83
+ }
84
+ break;
85
+ }
86
+
87
+ // ── Broker-level events (verbose only) ──
88
+ case 'broker:event':
89
+ if (level === 'verbose') {
90
+ console.log(chalk.dim(` [broker] ${JSON.stringify(event.event)}`));
91
+ }
92
+ break;
93
+
94
+ // ── Other events (verbose only) ──
95
+ case 'step:owner-assigned':
96
+ if (level === 'verbose') {
97
+ console.log(chalk.dim(` ${event.stepName} — owner: ${event.ownerName}, specialist: ${event.specialistName}`));
98
+ }
99
+ break;
100
+
101
+ case 'step:review-completed':
102
+ if (level === 'verbose') {
103
+ console.log(chalk.dim(` ${event.stepName} — review: ${event.decision} by ${event.reviewerName}`));
104
+ }
105
+ break;
106
+
107
+ case 'step:owner-timeout':
108
+ if (level !== 'quiet') {
109
+ console.log(chalk.yellow(` ⏱ ${event.stepName} — owner timeout (${event.ownerName})`));
110
+ }
111
+ break;
112
+
113
+ case 'step:force-released':
114
+ if (level === 'verbose') {
115
+ console.log(chalk.dim(` ${event.stepName} — force-released`));
116
+ }
117
+ break;
118
+ }
119
+ };
120
+ }
@@ -25,3 +25,4 @@ export * from './templates.js';
25
25
  export { WorkflowTrajectory, type StepOutcome } from './trajectory.js';
26
26
  export { formatDryRunReport } from './dry-run-format.js';
27
27
  export { createWorkflowRenderer, type WorkflowRenderer } from './listr-renderer.js';
28
+ export { createDefaultEventLogger } from './default-logger.js';