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/README.md +33 -63
- package/dist/cli/index.js +4586 -18398
- package/dist/cli/templates/claude-md-substrate-section.md +3 -3
- package/dist/decisions-BBLMsN_c.js +336 -0
- package/dist/decisions-WIsicZiG.js +3 -0
- package/dist/errors-BPqtzQ4U.js +111 -0
- package/dist/event-bus-J-bw-pkp.js +1549 -0
- package/dist/experimenter-BSu2ie3J.js +484 -0
- package/dist/git-utils-BtI5eNoN.js +365 -0
- package/dist/index.d.ts +89 -137
- package/dist/index.js +5 -305
- package/dist/logger-C6n1g8uP.js +119 -0
- package/dist/metrics-BSg8VIHd.js +184 -0
- package/dist/run-CRmhkcwN.js +7 -0
- package/dist/run-DlOWhkIF.js +10725 -0
- package/dist/{upgrade-CS2i2Qy5.js → upgrade-BjYVeC6G.js} +2 -3
- package/dist/{upgrade-CIAel_WS.js → upgrade-rV26kdh3.js} +2 -2
- package/dist/version-manager-impl-9N_519Ey.js +3 -0
- package/dist/{version-manager-impl-CcCP8PzG.js → version-manager-impl-BpVx2DkY.js} +101 -3
- package/package.json +1 -1
- package/packs/bmad/prompts/ux-step-3-journeys.md +3 -3
- package/dist/app-CY3MaJtP.js +0 -6342
- package/dist/cli/templates/parallel.yaml +0 -72
- package/dist/cli/templates/research-then-implement.yaml +0 -103
- package/dist/cli/templates/review-cycle.yaml +0 -91
- package/dist/cli/templates/sequential.yaml +0 -68
- package/dist/config-schema-C9tTMcm1.js +0 -102
- package/dist/version-manager-impl-LhLAyrCL.js +0 -4
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import "./
|
|
3
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|