substrate-ai 0.1.33 → 0.2.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.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { AdapterRegistry, AdtError, BudgetExceededError, ClaudeCodeAdapter, CodexCLIAdapter, ConfigError, ConfigIncompatibleFormatError, GeminiCLIAdapter, GitError, RecoveryError, TaskConfigError, TaskGraphCycleError, TaskGraphError, TaskGraphIncompatibleFormatError, WorkerError, WorkerNotFoundError, childLogger, computeChangedKeys, createConfigWatcher, createDatabaseService, createEventBus, createGitWorktreeManager, createLogger, createMonitorAgent, createMonitorDatabase, createRoutingEngine, createTaskGraphEngine, createTuiApp, createWorkerPoolManager, isTuiCapable, logger, printNonTtyWarning } from "./app-CY3MaJtP.js";
2
- import "./config-schema-C9tTMcm1.js";
3
- import { join } from "node:path";
1
+ import { childLogger, createLogger, logger } from "./logger-C6n1g8uP.js";
2
+ import { AdapterRegistry, ClaudeCodeAdapter, CodexCLIAdapter, GeminiCLIAdapter, createEventBus, createTuiApp, isTuiCapable, printNonTtyWarning } from "./event-bus-J-bw-pkp.js";
3
+ import { AdtError, BudgetExceededError, ConfigError, ConfigIncompatibleFormatError, GitError, RecoveryError, TaskConfigError, TaskGraphCycleError, TaskGraphError, TaskGraphIncompatibleFormatError, WorkerError, WorkerNotFoundError } from "./errors-BPqtzQ4U.js";
4
4
  import { randomUUID } from "crypto";
5
5
 
6
6
  //#region src/utils/helpers.ts
@@ -9,7 +9,7 @@ import { randomUUID } from "crypto";
9
9
  * @param ms - Milliseconds to sleep
10
10
  */
11
11
  function sleep(ms) {
12
- return new Promise((resolve$1) => setTimeout(resolve$1, ms));
12
+ return new Promise((resolve) => setTimeout(resolve, ms));
13
13
  }
14
14
  /**
15
15
  * Assert that a value is defined (not null or undefined)
@@ -161,305 +161,5 @@ var ServiceRegistry = class {
161
161
  };
162
162
 
163
163
  //#endregion
164
- //#region src/modules/budget/budget-tracker.ts
165
- const logger$3 = createLogger("budget");
166
- var BudgetTrackerImpl = class {
167
- _eventBus;
168
- constructor(eventBus) {
169
- this._eventBus = eventBus;
170
- }
171
- async initialize() {
172
- logger$3.info("BudgetTracker.initialize() — stub");
173
- this._eventBus.on("budget:warning", ({ taskId, currentSpend, limit }) => {
174
- logger$3.warn({
175
- taskId,
176
- currentSpend,
177
- limit
178
- }, "budget:warning");
179
- });
180
- this._eventBus.on("budget:exceeded", ({ taskId, spend, limit }) => {
181
- logger$3.error({
182
- taskId,
183
- spend,
184
- limit
185
- }, "budget:exceeded");
186
- });
187
- this._eventBus.on("task:complete", ({ taskId, result }) => {
188
- logger$3.debug({
189
- taskId,
190
- costUsd: result.costUsd
191
- }, "task:complete — accumulate cost");
192
- });
193
- this._eventBus.on("task:progress", ({ taskId, tokensUsed }) => {
194
- logger$3.debug({
195
- taskId,
196
- tokensUsed
197
- }, "task:progress — update token usage");
198
- });
199
- }
200
- async shutdown() {
201
- logger$3.info("BudgetTracker.shutdown() — stub");
202
- }
203
- };
204
- function createBudgetTracker(options) {
205
- return new BudgetTrackerImpl(options.eventBus);
206
- }
207
-
208
- //#endregion
209
- //#region src/modules/git/git-manager.ts
210
- const logger$2 = createLogger("git");
211
- var GitManagerImpl = class {
212
- _eventBus;
213
- repoRoot;
214
- constructor(eventBus, repoRoot) {
215
- this._eventBus = eventBus;
216
- this.repoRoot = repoRoot;
217
- }
218
- async initialize() {
219
- logger$2.info({ repoRoot: this.repoRoot }, "GitManager.initialize() — stub");
220
- this._eventBus.on("worktree:created", ({ taskId, worktreePath, branchName }) => {
221
- logger$2.debug({
222
- taskId,
223
- worktreePath,
224
- branchName
225
- }, "worktree:created");
226
- });
227
- this._eventBus.on("worktree:merged", ({ taskId, branch }) => {
228
- logger$2.debug({
229
- taskId,
230
- branch
231
- }, "worktree:merged");
232
- });
233
- this._eventBus.on("worktree:conflict", ({ taskId, conflictingFiles }) => {
234
- logger$2.warn({
235
- taskId,
236
- conflictingFiles
237
- }, "worktree:conflict");
238
- });
239
- this._eventBus.on("worktree:removed", ({ taskId, branchName }) => {
240
- logger$2.debug({
241
- taskId,
242
- branchName
243
- }, "worktree:removed");
244
- });
245
- }
246
- async shutdown() {
247
- logger$2.info("GitManager.shutdown() — stub");
248
- }
249
- };
250
- function createGitManager(options) {
251
- return new GitManagerImpl(options.eventBus, options.repoRoot);
252
- }
253
-
254
- //#endregion
255
- //#region src/core/orchestrator-impl.ts
256
- const logger$1 = createLogger("orchestrator");
257
- /** Internal symbol used to expose lifecycle hooks to the factory only */
258
- const INTERNAL = Symbol("OrchestratorImpl.internal");
259
- var OrchestratorImpl = class {
260
- eventBus;
261
- _registry;
262
- _ready = false;
263
- _shutdown = false;
264
- _shutdownHandlersRegistered = false;
265
- _configWatcher = null;
266
- constructor(eventBus, registry) {
267
- this.eventBus = eventBus;
268
- this._registry = registry;
269
- }
270
- get isReady() {
271
- return this._ready;
272
- }
273
- async shutdown() {
274
- if (this._shutdown) return;
275
- this._shutdown = true;
276
- logger$1.info("Orchestrator shutdown initiated");
277
- this.eventBus.emit("orchestrator:shutdown", { reason: "shutdown() called" });
278
- if (this._configWatcher !== null) {
279
- this._configWatcher.stop();
280
- this._configWatcher = null;
281
- }
282
- try {
283
- await this._registry.shutdownAll();
284
- } catch (err) {
285
- logger$1.error({ err }, "Error during orchestrator shutdown");
286
- }
287
- logger$1.info("Orchestrator shutdown complete");
288
- }
289
- _markReady() {
290
- this._ready = true;
291
- }
292
- _sigtermHandler = null;
293
- _sigintHandler = null;
294
- _registerShutdownHandlers() {
295
- if (this._shutdownHandlersRegistered) return;
296
- this._shutdownHandlersRegistered = true;
297
- const makeHandler = (signal) => () => {
298
- logger$1.info({ signal }, "Received signal — initiating graceful shutdown");
299
- this.shutdown().then(() => {
300
- process.exit(0);
301
- }).catch((err) => {
302
- logger$1.error({ err }, "Error during signal-triggered shutdown");
303
- process.exit(1);
304
- });
305
- };
306
- this._sigtermHandler = makeHandler("SIGTERM");
307
- this._sigintHandler = makeHandler("SIGINT");
308
- process.once("SIGTERM", this._sigtermHandler);
309
- process.once("SIGINT", this._sigintHandler);
310
- }
311
- _removeShutdownHandlers() {
312
- if (this._sigtermHandler !== null) {
313
- process.removeListener("SIGTERM", this._sigtermHandler);
314
- this._sigtermHandler = null;
315
- }
316
- if (this._sigintHandler !== null) {
317
- process.removeListener("SIGINT", this._sigintHandler);
318
- this._sigintHandler = null;
319
- }
320
- }
321
- /**
322
- * Internal accessor used exclusively by the createOrchestrator factory.
323
- * Do not call outside of this module.
324
- * @internal
325
- */
326
- [INTERNAL]() {
327
- return {
328
- markReady: () => this._markReady(),
329
- registerShutdownHandlers: () => this._registerShutdownHandlers(),
330
- removeShutdownHandlers: () => this._removeShutdownHandlers(),
331
- setConfigWatcher: (watcher) => {
332
- this._configWatcher = watcher;
333
- }
334
- };
335
- }
336
- };
337
- /**
338
- * Initialize the orchestrator with all modules wired via dependency injection.
339
- *
340
- * Steps performed:
341
- * 1. Create the TypedEventBus
342
- * 2. Create the SQLite database service
343
- * 3. Instantiate all modules (TaskGraphEngine, RoutingEngine, WorkerManager,
344
- * BudgetTracker, GitManager) with constructor injection
345
- * 4. Register all modules in the ServiceRegistry
346
- * 5. Call initialize() on all services in registration order
347
- * 6. Register SIGTERM/SIGINT graceful shutdown handlers
348
- * 7. Emit orchestrator:ready
349
- *
350
- * @param config - Orchestrator configuration
351
- * @returns Initialized Orchestrator instance
352
- */
353
- async function createOrchestrator(config) {
354
- logger$1.info({ databasePath: config.databasePath }, "Initializing orchestrator");
355
- const eventBus = createEventBus();
356
- const databaseService = createDatabaseService(config.databasePath);
357
- const taskGraphEngine = createTaskGraphEngine({
358
- eventBus,
359
- databaseService
360
- });
361
- const adapterRegistry = new AdapterRegistry();
362
- const routingEngine = createRoutingEngine({
363
- eventBus,
364
- adapterRegistry
365
- });
366
- const budgetTracker = createBudgetTracker({ eventBus });
367
- const gitManager = createGitManager({
368
- eventBus,
369
- repoRoot: config.projectRoot
370
- });
371
- const gitWorktreeManager = createGitWorktreeManager({
372
- eventBus,
373
- projectRoot: config.projectRoot,
374
- db: databaseService
375
- });
376
- const workerPoolManager = createWorkerPoolManager({
377
- eventBus,
378
- adapterRegistry,
379
- engine: taskGraphEngine,
380
- db: databaseService,
381
- gitWorktreeManager
382
- });
383
- const monitorDbPath = config.monitor?.databasePath ?? ":memory:";
384
- const monitorDatabase = createMonitorDatabase(monitorDbPath);
385
- const monitorAgent = createMonitorAgent({
386
- eventBus,
387
- monitorDb: monitorDatabase,
388
- config: {
389
- retentionDays: config.monitor?.retentionDays ?? 90,
390
- customTaxonomy: config.monitor?.customTaxonomy,
391
- use_recommendations: config.monitor?.use_recommendations ?? false,
392
- recommendation_threshold_percentage: config.monitor?.recommendation_threshold_percentage,
393
- min_sample_size: config.monitor?.min_sample_size,
394
- recommendation_history_days: config.monitor?.recommendation_history_days
395
- }
396
- });
397
- const registry = new ServiceRegistry();
398
- registry.register("database", databaseService);
399
- registry.register("taskGraph", taskGraphEngine);
400
- registry.register("routingEngine", routingEngine);
401
- registry.register("gitWorktreeManager", gitWorktreeManager);
402
- registry.register("workerPoolManager", workerPoolManager);
403
- registry.register("budgetTracker", budgetTracker);
404
- registry.register("gitManager", gitManager);
405
- registry.register("monitorAgent", monitorAgent);
406
- const orchestrator = new OrchestratorImpl(eventBus, registry);
407
- const internal = orchestrator[INTERNAL]();
408
- try {
409
- await registry.initializeAll();
410
- } catch (err) {
411
- logger$1.error({ err }, "Service initialization failed — cleaning up");
412
- internal.removeShutdownHandlers();
413
- try {
414
- await registry.shutdownAll();
415
- } catch (shutdownErr) {
416
- logger$1.error({ err: shutdownErr }, "Error during cleanup after failed initialization");
417
- }
418
- throw err;
419
- }
420
- internal.registerShutdownHandlers();
421
- const enableConfigHotReload = config.enableConfigHotReload ?? true;
422
- if (enableConfigHotReload) {
423
- const configFilePath = config.configPath ?? join(config.projectRoot, "substrate.config.yaml");
424
- let currentConfig = null;
425
- const configWatcher = createConfigWatcher({
426
- configPath: configFilePath,
427
- onReload: (newConfig) => {
428
- const previousConfig = currentConfig;
429
- if (previousConfig === null) {
430
- currentConfig = newConfig;
431
- return;
432
- }
433
- const changedKeys = computeChangedKeys(previousConfig, newConfig);
434
- currentConfig = newConfig;
435
- const n = changedKeys.length;
436
- logger$1.info({
437
- changedKeys,
438
- configPath: configFilePath
439
- }, `Config reloaded: ${n} setting(s) changed`);
440
- eventBus.emit("config:reloaded", {
441
- path: configFilePath,
442
- previousConfig,
443
- newConfig,
444
- changedKeys
445
- });
446
- },
447
- onError: (err) => {
448
- logger$1.error({
449
- err,
450
- configPath: configFilePath
451
- }, `Config reload failed: ${err.message}. Continuing with previous config.`);
452
- }
453
- });
454
- configWatcher.start();
455
- internal.setConfigWatcher(configWatcher);
456
- }
457
- internal.markReady();
458
- eventBus.emit("orchestrator:ready", {});
459
- logger$1.info("Orchestrator ready");
460
- return orchestrator;
461
- }
462
-
463
- //#endregion
464
- export { AdapterRegistry, AdtError, BudgetExceededError, ClaudeCodeAdapter, CodexCLIAdapter, ConfigError, ConfigIncompatibleFormatError, GeminiCLIAdapter, GitError, RecoveryError, ServiceRegistry, TaskConfigError, TaskGraphCycleError, TaskGraphError, TaskGraphIncompatibleFormatError, WorkerError, WorkerNotFoundError, assertDefined, childLogger, createEventBus, createLogger, createOrchestrator, createTuiApp, deepClone, formatDuration, generateId, isPlainObject, isTuiCapable, logger, printNonTtyWarning, sleep, withRetry };
164
+ export { AdapterRegistry, AdtError, BudgetExceededError, ClaudeCodeAdapter, CodexCLIAdapter, ConfigError, ConfigIncompatibleFormatError, GeminiCLIAdapter, GitError, RecoveryError, ServiceRegistry, TaskConfigError, TaskGraphCycleError, TaskGraphError, TaskGraphIncompatibleFormatError, WorkerError, WorkerNotFoundError, assertDefined, childLogger, createEventBus, createLogger, createTuiApp, deepClone, formatDuration, generateId, isPlainObject, isTuiCapable, logger, printNonTtyWarning, sleep, withRetry };
465
165
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,119 @@
1
+ import pino from "pino";
2
+
3
+ //#region src/cli/utils/masking.ts
4
+ /**
5
+ * Credential masking utilities for CLI output and Pino logger redaction.
6
+ *
7
+ * Ensures that API keys and other secrets never appear in logs, status
8
+ * output, or error messages (NFR8, NFR9).
9
+ */
10
+ /** Placeholder shown instead of a real credential */
11
+ const MASKED_VALUE = "***";
12
+ /**
13
+ * Known Pino redaction paths for provider API key fields.
14
+ * Pass this array to the `pino({ redact: ... })` option.
15
+ *
16
+ * @example
17
+ * import pino from 'pino'
18
+ * import { PINO_REDACT_PATHS } from './masking.js'
19
+ * const logger = pino({ redact: PINO_REDACT_PATHS })
20
+ */
21
+ const PINO_REDACT_PATHS = [
22
+ "apiKey",
23
+ "api_key",
24
+ "*.apiKey",
25
+ "*.api_key",
26
+ "providers.*.api_key_env",
27
+ "env.ANTHROPIC_API_KEY",
28
+ "env.OPENAI_API_KEY",
29
+ "env.GOOGLE_API_KEY"
30
+ ];
31
+ /**
32
+ * Credential field names that should be replaced with `***` in displayed output.
33
+ */
34
+ const CREDENTIAL_FIELDS = new Set([
35
+ "api_key",
36
+ "apiKey",
37
+ "api_key_env",
38
+ "api_key_value",
39
+ "token",
40
+ "secret",
41
+ "password"
42
+ ]);
43
+ /**
44
+ * Deep-clone a plain-object tree and replace known credential fields with `***`.
45
+ *
46
+ * Only operates on plain objects and arrays; primitives are returned as-is.
47
+ *
48
+ * @param value - Value to mask
49
+ * @returns Masked clone of `value`
50
+ */
51
+ function deepMask(value) {
52
+ if (value === null || value === void 0) return value;
53
+ if (Array.isArray(value)) return value.map(deepMask);
54
+ if (typeof value === "object") {
55
+ const obj = value;
56
+ const masked = {};
57
+ for (const [k, v] of Object.entries(obj)) if (CREDENTIAL_FIELDS.has(k)) masked[k] = MASKED_VALUE;
58
+ else masked[k] = deepMask(v);
59
+ return masked;
60
+ }
61
+ return value;
62
+ }
63
+
64
+ //#endregion
65
+ //#region src/utils/logger.ts
66
+ /** Default log level based on environment */
67
+ function getDefaultLogLevel() {
68
+ const envLevel = process.env.LOG_LEVEL;
69
+ if (envLevel) return envLevel;
70
+ if (process.env.NODE_ENV === "production") return "info";
71
+ if (process.env.NODE_ENV === "test" || process.env.NODE_ENV === "development") return "debug";
72
+ return "warn";
73
+ }
74
+ /** Whether to use pretty printing (development mode) */
75
+ function isPrettyMode() {
76
+ if (process.env.LOG_PRETTY !== void 0) return process.env.LOG_PRETTY === "true";
77
+ return process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test";
78
+ }
79
+ /**
80
+ * Create a named logger instance
81
+ * @param name - Logger name (module identifier)
82
+ * @param options - Optional logger configuration overrides
83
+ */
84
+ function createLogger(name, options = {}) {
85
+ const level = options.level ?? getDefaultLogLevel();
86
+ const pretty = options.pretty ?? isPrettyMode();
87
+ const baseOptions = {
88
+ name: options.name ?? name,
89
+ level,
90
+ redact: PINO_REDACT_PATHS,
91
+ formatters: { level(label) {
92
+ return { level: label };
93
+ } },
94
+ timestamp: pino.stdTimeFunctions.isoTime,
95
+ base: { pid: process.pid }
96
+ };
97
+ if (pretty) return pino({
98
+ ...baseOptions,
99
+ transport: {
100
+ target: "pino-pretty",
101
+ options: {
102
+ colorize: true,
103
+ translateTime: "SYS:standard",
104
+ ignore: "pid,hostname"
105
+ }
106
+ }
107
+ });
108
+ return pino(baseOptions);
109
+ }
110
+ /** Root application logger */
111
+ const logger = createLogger("substrate");
112
+ /** Create a child logger with additional context */
113
+ function childLogger(parent, bindings) {
114
+ return parent.child(bindings);
115
+ }
116
+
117
+ //#endregion
118
+ export { childLogger, createLogger, deepMask, logger };
119
+ //# sourceMappingURL=logger-C6n1g8uP.js.map
@@ -0,0 +1,184 @@
1
+ //#region src/persistence/queries/metrics.ts
2
+ /**
3
+ * Write or update run-level metrics.
4
+ *
5
+ * Uses INSERT ... ON CONFLICT DO UPDATE to avoid a TOCTOU race on the
6
+ * `restarts` counter: when a row already exists, `restarts` is preserved from
7
+ * the DB (so any `incrementRunRestarts()` calls made by the supervisor between
8
+ * the caller's read and this write are not silently overwritten).
9
+ */
10
+ function writeRunMetrics(db, input) {
11
+ const stmt = db.prepare(`
12
+ INSERT INTO run_metrics (
13
+ run_id, methodology, status, started_at, completed_at,
14
+ wall_clock_seconds, total_input_tokens, total_output_tokens, total_cost_usd,
15
+ stories_attempted, stories_succeeded, stories_failed, stories_escalated,
16
+ total_review_cycles, total_dispatches, concurrency_setting, max_concurrent_actual, restarts,
17
+ is_baseline
18
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
19
+ ON CONFLICT(run_id) DO UPDATE SET
20
+ methodology = excluded.methodology,
21
+ status = excluded.status,
22
+ started_at = excluded.started_at,
23
+ completed_at = excluded.completed_at,
24
+ wall_clock_seconds = excluded.wall_clock_seconds,
25
+ total_input_tokens = excluded.total_input_tokens,
26
+ total_output_tokens = excluded.total_output_tokens,
27
+ total_cost_usd = excluded.total_cost_usd,
28
+ stories_attempted = excluded.stories_attempted,
29
+ stories_succeeded = excluded.stories_succeeded,
30
+ stories_failed = excluded.stories_failed,
31
+ stories_escalated = excluded.stories_escalated,
32
+ total_review_cycles = excluded.total_review_cycles,
33
+ total_dispatches = excluded.total_dispatches,
34
+ concurrency_setting = excluded.concurrency_setting,
35
+ max_concurrent_actual = excluded.max_concurrent_actual,
36
+ restarts = run_metrics.restarts,
37
+ is_baseline = run_metrics.is_baseline
38
+ `);
39
+ stmt.run(input.run_id, input.methodology, input.status, input.started_at, input.completed_at ?? null, input.wall_clock_seconds ?? 0, input.total_input_tokens ?? 0, input.total_output_tokens ?? 0, input.total_cost_usd ?? 0, input.stories_attempted ?? 0, input.stories_succeeded ?? 0, input.stories_failed ?? 0, input.stories_escalated ?? 0, input.total_review_cycles ?? 0, input.total_dispatches ?? 0, input.concurrency_setting ?? 1, input.max_concurrent_actual ?? 1, input.restarts ?? 0, input.is_baseline ?? 0);
40
+ }
41
+ /**
42
+ * Get run metrics for a specific run.
43
+ */
44
+ function getRunMetrics(db, runId) {
45
+ return db.prepare("SELECT * FROM run_metrics WHERE run_id = ?").get(runId);
46
+ }
47
+ /**
48
+ * List the most recent N run metrics rows, newest first.
49
+ */
50
+ function listRunMetrics(db, limit = 10) {
51
+ return db.prepare("SELECT * FROM run_metrics ORDER BY started_at DESC LIMIT ?").all(limit);
52
+ }
53
+ /**
54
+ * Tag a run as the baseline (clears any existing baseline first).
55
+ */
56
+ function tagRunAsBaseline(db, runId) {
57
+ db.transaction(() => {
58
+ db.prepare("UPDATE run_metrics SET is_baseline = 0").run();
59
+ db.prepare("UPDATE run_metrics SET is_baseline = 1 WHERE run_id = ?").run(runId);
60
+ })();
61
+ }
62
+ /**
63
+ * Get the current baseline run metrics (if any).
64
+ */
65
+ function getBaselineRunMetrics(db) {
66
+ return db.prepare("SELECT * FROM run_metrics WHERE is_baseline = 1 LIMIT 1").get();
67
+ }
68
+ /**
69
+ * Increment the restart count for a run by 1.
70
+ * Called by the supervisor each time it successfully restarts the pipeline.
71
+ * If the run_id does not yet exist in run_metrics, a placeholder row is
72
+ * inserted so the restart count is not lost — writeRunMetrics will overwrite
73
+ * all other fields when the run reaches a terminal state.
74
+ */
75
+ function incrementRunRestarts(db, runId) {
76
+ db.prepare(`
77
+ INSERT INTO run_metrics (run_id, methodology, status, started_at, restarts)
78
+ VALUES (?, 'unknown', 'running', datetime('now'), 1)
79
+ ON CONFLICT(run_id) DO UPDATE SET restarts = run_metrics.restarts + 1
80
+ `).run(runId);
81
+ }
82
+ /**
83
+ * Write or update story-level metrics.
84
+ */
85
+ function writeStoryMetrics(db, input) {
86
+ const stmt = db.prepare(`
87
+ INSERT INTO story_metrics (
88
+ run_id, story_key, result, phase_durations_json, started_at, completed_at,
89
+ wall_clock_seconds, input_tokens, output_tokens, cost_usd,
90
+ review_cycles, dispatches
91
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
92
+ ON CONFLICT(run_id, story_key) DO UPDATE SET
93
+ result = excluded.result,
94
+ phase_durations_json = excluded.phase_durations_json,
95
+ started_at = COALESCE(excluded.started_at, story_metrics.started_at),
96
+ completed_at = excluded.completed_at,
97
+ wall_clock_seconds = excluded.wall_clock_seconds,
98
+ input_tokens = excluded.input_tokens,
99
+ output_tokens = excluded.output_tokens,
100
+ cost_usd = excluded.cost_usd,
101
+ review_cycles = excluded.review_cycles,
102
+ dispatches = excluded.dispatches
103
+ `);
104
+ stmt.run(input.run_id, input.story_key, input.result, input.phase_durations_json ?? null, input.started_at ?? null, input.completed_at ?? null, input.wall_clock_seconds ?? 0, input.input_tokens ?? 0, input.output_tokens ?? 0, input.cost_usd ?? 0, input.review_cycles ?? 0, input.dispatches ?? 0);
105
+ }
106
+ /**
107
+ * Get all story metrics for a given run.
108
+ */
109
+ function getStoryMetricsForRun(db, runId) {
110
+ return db.prepare("SELECT * FROM story_metrics WHERE run_id = ? ORDER BY id ASC").all(runId);
111
+ }
112
+ /**
113
+ * Compare two runs and return percentage deltas for key numeric fields.
114
+ * Positive deltas mean run B is larger/longer than run A.
115
+ * Returns null if either run does not exist.
116
+ */
117
+ function compareRunMetrics(db, runIdA, runIdB) {
118
+ const a = getRunMetrics(db, runIdA);
119
+ const b = getRunMetrics(db, runIdB);
120
+ if (!a || !b) return null;
121
+ const pct = (base, diff) => base === 0 ? null : Math.round(diff / base * 100 * 10) / 10;
122
+ const inputDelta = b.total_input_tokens - a.total_input_tokens;
123
+ const outputDelta = b.total_output_tokens - a.total_output_tokens;
124
+ const clockDelta = (b.wall_clock_seconds ?? 0) - (a.wall_clock_seconds ?? 0);
125
+ const cycleDelta = b.total_review_cycles - a.total_review_cycles;
126
+ const costDelta = (b.total_cost_usd ?? 0) - (a.total_cost_usd ?? 0);
127
+ return {
128
+ run_id_a: runIdA,
129
+ run_id_b: runIdB,
130
+ token_input_delta: inputDelta,
131
+ token_output_delta: outputDelta,
132
+ token_input_pct: pct(a.total_input_tokens, inputDelta),
133
+ token_output_pct: pct(a.total_output_tokens, outputDelta),
134
+ wall_clock_delta_seconds: clockDelta,
135
+ wall_clock_pct: pct(a.wall_clock_seconds ?? 0, clockDelta),
136
+ review_cycles_delta: cycleDelta,
137
+ review_cycles_pct: pct(a.total_review_cycles, cycleDelta),
138
+ cost_delta: costDelta,
139
+ cost_pct: pct(a.total_cost_usd ?? 0, costDelta)
140
+ };
141
+ }
142
+ /**
143
+ * Aggregate token usage from the token_usage table for a pipeline run.
144
+ */
145
+ function aggregateTokenUsageForRun(db, runId) {
146
+ const row = db.prepare(`
147
+ SELECT
148
+ COALESCE(SUM(input_tokens), 0) as input,
149
+ COALESCE(SUM(output_tokens), 0) as output,
150
+ COALESCE(SUM(cost_usd), 0) as cost
151
+ FROM token_usage
152
+ WHERE pipeline_run_id = ?
153
+ `).get(runId);
154
+ return row ?? {
155
+ input: 0,
156
+ output: 0,
157
+ cost: 0
158
+ };
159
+ }
160
+ /**
161
+ * Aggregate token usage for a specific story within a pipeline run.
162
+ * Matches rows where the metadata JSON contains the given storyKey.
163
+ */
164
+ function aggregateTokenUsageForStory(db, runId, storyKey) {
165
+ const row = db.prepare(`
166
+ SELECT
167
+ COALESCE(SUM(input_tokens), 0) as input,
168
+ COALESCE(SUM(output_tokens), 0) as output,
169
+ COALESCE(SUM(cost_usd), 0) as cost
170
+ FROM token_usage
171
+ WHERE pipeline_run_id = ?
172
+ AND metadata IS NOT NULL
173
+ AND json_extract(metadata, '$.storyKey') = ?
174
+ `).get(runId, storyKey);
175
+ return row ?? {
176
+ input: 0,
177
+ output: 0,
178
+ cost: 0
179
+ };
180
+ }
181
+
182
+ //#endregion
183
+ export { aggregateTokenUsageForRun, aggregateTokenUsageForStory, compareRunMetrics, getBaselineRunMetrics, getRunMetrics, getStoryMetricsForRun, incrementRunRestarts, listRunMetrics, tagRunAsBaseline, writeRunMetrics, writeStoryMetrics };
184
+ //# sourceMappingURL=metrics-BSg8VIHd.js.map
@@ -0,0 +1,7 @@
1
+ import "./logger-C6n1g8uP.js";
2
+ import "./event-bus-J-bw-pkp.js";
3
+ import { registerRunCommand, runRunAction } from "./run-DlOWhkIF.js";
4
+ import "./decisions-BBLMsN_c.js";
5
+ import "./metrics-BSg8VIHd.js";
6
+
7
+ export { runRunAction };