substrate-ai 0.4.10 → 0.5.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/adapter-registry-BRQXdPnB.js +3 -0
- package/dist/{adapter-registry-Cd-7lG5v.js → adapter-registry-BkUvZSKJ.js} +2 -2
- package/dist/cli/index.js +571 -572
- package/dist/{decisions-D7Ao_KcL.js → decisions-BxYj_a1X.js} +1 -1
- package/dist/{decisions-Db8GTbH2.js → decisions-C6MF2Cax.js} +113 -88
- package/dist/{experimenter-CvxtqzXz.js → experimenter-CoR0k66d.js} +10 -10
- package/dist/index.js +1 -1
- package/dist/{operational-C0_y8DAs.js → operational-CidppHy-.js} +104 -89
- package/dist/run-C-yCMYlt.js +9 -0
- package/dist/{run-BErnJT9a.js → run-GqmIa5YW.js} +1685 -1562
- package/package.json +2 -4
- package/dist/adapter-registry-X5X81xdJ.js +0 -3
- package/dist/run-BbdWeKiB.js +0 -9
package/dist/cli/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { AppError, DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY,
|
|
2
|
+
import { AdapterTelemetryPersistence, AppError, DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DoltClient, DoltNotInstalled, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, FileStateStore, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SUBSTRATE_OWNED_SETTINGS_KEYS, SymbolParser, VALID_PHASES, buildPipelineStatusOutput, checkDoltInstalled, createConfigSystem, createContextCompiler, createDatabaseAdapter, createDispatcher, createDoltClient, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStateStore, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, initSchema, initializeDolt, isSyncAdapter, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-GqmIa5YW.js";
|
|
3
3
|
import { createLogger } from "../logger-D2fS2ccL.js";
|
|
4
|
-
import { AdapterRegistry } from "../adapter-registry-
|
|
4
|
+
import { AdapterRegistry } from "../adapter-registry-BkUvZSKJ.js";
|
|
5
5
|
import { CURRENT_CONFIG_FORMAT_VERSION, CURRENT_TASK_GRAPH_VERSION, PartialSubstrateConfigSchema } from "../config-migrator-DtZW1maj.js";
|
|
6
6
|
import { ConfigError, createEventBus } from "../helpers-BihqWgVe.js";
|
|
7
7
|
import { RoutingRecommender } from "../routing-BUE9pIxW.js";
|
|
8
|
-
import { addTokenUsage, createDecision, createPipelineRun, getDecisionsByCategory, getDecisionsByPhaseForRun, getLatestRun, getTokenUsageSummary, listRequirements, updatePipelineRun } from "../decisions-
|
|
9
|
-
import { ESCALATION_DIAGNOSIS, EXPERIMENT_RESULT, OPERATIONAL_FINDING, STORY_METRICS, aggregateTokenUsageForRun, compareRunMetrics, getBaselineRunMetrics, getRunMetrics, getStoryMetricsForRun, incrementRunRestarts, listRunMetrics, tagRunAsBaseline } from "../operational-
|
|
8
|
+
import { addTokenUsage, createDecision, createPipelineRun, getDecisionsByCategory, getDecisionsByPhaseForRun, getLatestRun, getPipelineRunById, getTokenUsageSummary, listRequirements, updatePipelineRun } from "../decisions-C6MF2Cax.js";
|
|
9
|
+
import { ESCALATION_DIAGNOSIS, EXPERIMENT_RESULT, OPERATIONAL_FINDING, STORY_METRICS, aggregateTokenUsageForRun, compareRunMetrics, getBaselineRunMetrics, getRunMetrics, getStoryMetricsForRun, incrementRunRestarts, listRunMetrics, tagRunAsBaseline } from "../operational-CidppHy-.js";
|
|
10
10
|
import { abortMerge, createWorktree, getConflictingFiles, getMergedFiles, getOrphanedWorktrees, performMerge, removeBranch, removeWorktree, simulateMerge, verifyGitVersion } from "../git-utils-C-fdrHF_.js";
|
|
11
11
|
import "../version-manager-impl-DTlmGvHb.js";
|
|
12
12
|
import { registerUpgradeCommand } from "../upgrade-C8_VcI8B.js";
|
|
@@ -19,9 +19,8 @@ import yaml from "js-yaml";
|
|
|
19
19
|
import { createRequire } from "node:module";
|
|
20
20
|
import * as path$1 from "node:path";
|
|
21
21
|
import { isAbsolute, join as join$1 } from "node:path";
|
|
22
|
-
import Database from "better-sqlite3";
|
|
23
|
-
import { access as access$1, readFile as readFile$1 } from "node:fs/promises";
|
|
24
22
|
import { existsSync as existsSync$1, mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1 } from "node:fs";
|
|
23
|
+
import { access as access$1, readFile as readFile$1 } from "node:fs/promises";
|
|
25
24
|
import { createInterface } from "node:readline";
|
|
26
25
|
import { homedir } from "os";
|
|
27
26
|
import { randomUUID } from "crypto";
|
|
@@ -666,10 +665,12 @@ async function runInitAction(options) {
|
|
|
666
665
|
else process.stderr.write(`Error: ${errorMsg}\n`);
|
|
667
666
|
return INIT_EXIT_ERROR;
|
|
668
667
|
}
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
668
|
+
const dbAdapter = createDatabaseAdapter({
|
|
669
|
+
backend: "auto",
|
|
670
|
+
basePath: projectRoot
|
|
671
|
+
});
|
|
672
|
+
await initSchema(dbAdapter);
|
|
673
|
+
await dbAdapter.close();
|
|
673
674
|
await scaffoldClaudeMd(projectRoot);
|
|
674
675
|
await scaffoldStatuslineScript(projectRoot);
|
|
675
676
|
await scaffoldClaudeSettings(projectRoot);
|
|
@@ -1017,16 +1018,19 @@ async function runResumeAction(options) {
|
|
|
1017
1018
|
const packPath = join(projectRoot, "packs", packName);
|
|
1018
1019
|
const dbRoot = await resolveMainRepoRoot(projectRoot);
|
|
1019
1020
|
const dbPath = join(dbRoot, ".substrate", "substrate.db");
|
|
1020
|
-
|
|
1021
|
+
const doltDir = join(dbRoot, ".substrate", "state", ".dolt");
|
|
1022
|
+
if (!existsSync(dbPath) && !existsSync(doltDir)) {
|
|
1021
1023
|
const errorMsg = `Decision store not initialized. Run 'substrate init' first.`;
|
|
1022
1024
|
if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
1023
1025
|
else process.stderr.write(`Error: ${errorMsg}\n`);
|
|
1024
1026
|
return 1;
|
|
1025
1027
|
}
|
|
1026
|
-
const
|
|
1028
|
+
const adapter = createDatabaseAdapter({
|
|
1029
|
+
backend: "auto",
|
|
1030
|
+
basePath: dbRoot
|
|
1031
|
+
});
|
|
1027
1032
|
try {
|
|
1028
|
-
|
|
1029
|
-
const db = dbWrapper.db;
|
|
1033
|
+
await initSchema(adapter);
|
|
1030
1034
|
const packLoader = createPackLoader();
|
|
1031
1035
|
let pack;
|
|
1032
1036
|
try {
|
|
@@ -1039,8 +1043,10 @@ async function runResumeAction(options) {
|
|
|
1039
1043
|
return 1;
|
|
1040
1044
|
}
|
|
1041
1045
|
let run;
|
|
1042
|
-
if (specifiedRunId !== void 0 && specifiedRunId !== "")
|
|
1043
|
-
|
|
1046
|
+
if (specifiedRunId !== void 0 && specifiedRunId !== "") {
|
|
1047
|
+
const rows = await adapter.query("SELECT * FROM pipeline_runs WHERE id = ?", [specifiedRunId]);
|
|
1048
|
+
run = rows[0];
|
|
1049
|
+
} else run = await getLatestRun(adapter);
|
|
1044
1050
|
if (run === void 0) {
|
|
1045
1051
|
const errorMsg = specifiedRunId !== void 0 ? `Pipeline run '${specifiedRunId}' not found.` : "No pipeline runs found. Run `substrate run --from analysis` first.";
|
|
1046
1052
|
if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
@@ -1050,7 +1056,7 @@ async function runResumeAction(options) {
|
|
|
1050
1056
|
const runId = run.id;
|
|
1051
1057
|
if (outputFormat === "human") process.stdout.write(`Resuming pipeline run: ${runId}\n`);
|
|
1052
1058
|
const phaseOrchestrator = createPhaseOrchestrator({
|
|
1053
|
-
db,
|
|
1059
|
+
db: adapter,
|
|
1054
1060
|
pack
|
|
1055
1061
|
});
|
|
1056
1062
|
const runStatus = await phaseOrchestrator.resumeRun(runId);
|
|
@@ -1092,18 +1098,19 @@ async function runResumeAction(options) {
|
|
|
1092
1098
|
return 1;
|
|
1093
1099
|
} finally {
|
|
1094
1100
|
try {
|
|
1095
|
-
|
|
1101
|
+
await adapter.close();
|
|
1096
1102
|
} catch {}
|
|
1097
1103
|
}
|
|
1098
1104
|
}
|
|
1099
1105
|
async function runFullPipelineFromPhase(options) {
|
|
1100
1106
|
const { packName, packPath, dbDir, dbPath, startPhase, stopAfter, concept, concurrency, outputFormat, existingRunId, projectRoot, registry: injectedRegistry } = options;
|
|
1101
1107
|
if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
|
|
1102
|
-
const
|
|
1108
|
+
const adapter = createDatabaseAdapter({
|
|
1109
|
+
backend: "auto",
|
|
1110
|
+
basePath: projectRoot
|
|
1111
|
+
});
|
|
1103
1112
|
try {
|
|
1104
|
-
|
|
1105
|
-
runMigrations(dbWrapper.db);
|
|
1106
|
-
const db = dbWrapper.db;
|
|
1113
|
+
await initSchema(adapter);
|
|
1107
1114
|
const packLoader = createPackLoader();
|
|
1108
1115
|
let pack;
|
|
1109
1116
|
try {
|
|
@@ -1116,20 +1123,20 @@ async function runFullPipelineFromPhase(options) {
|
|
|
1116
1123
|
return 1;
|
|
1117
1124
|
}
|
|
1118
1125
|
const eventBus = createEventBus();
|
|
1119
|
-
const contextCompiler = createContextCompiler({ db });
|
|
1126
|
+
const contextCompiler = createContextCompiler({ db: adapter });
|
|
1120
1127
|
if (!injectedRegistry) throw new Error("AdapterRegistry is required — must be initialized at CLI startup");
|
|
1121
1128
|
const dispatcher = createDispatcher({
|
|
1122
1129
|
eventBus,
|
|
1123
1130
|
adapterRegistry: injectedRegistry
|
|
1124
1131
|
});
|
|
1125
1132
|
const phaseDeps = {
|
|
1126
|
-
db,
|
|
1133
|
+
db: adapter,
|
|
1127
1134
|
pack,
|
|
1128
1135
|
contextCompiler,
|
|
1129
1136
|
dispatcher
|
|
1130
1137
|
};
|
|
1131
1138
|
const phaseOrchestrator = createPhaseOrchestrator({
|
|
1132
|
-
db,
|
|
1139
|
+
db: adapter,
|
|
1133
1140
|
pack
|
|
1134
1141
|
});
|
|
1135
1142
|
const startedAt = Date.now();
|
|
@@ -1153,7 +1160,7 @@ async function runFullPipelineFromPhase(options) {
|
|
|
1153
1160
|
});
|
|
1154
1161
|
if (result.tokenUsage.input > 0 || result.tokenUsage.output > 0) {
|
|
1155
1162
|
const costUsd = (result.tokenUsage.input * 3 + result.tokenUsage.output * 15) / 1e6;
|
|
1156
|
-
addTokenUsage(
|
|
1163
|
+
await addTokenUsage(adapter, runId, {
|
|
1157
1164
|
phase: "analysis",
|
|
1158
1165
|
agent: "claude-code",
|
|
1159
1166
|
input_tokens: result.tokenUsage.input,
|
|
@@ -1162,7 +1169,7 @@ async function runFullPipelineFromPhase(options) {
|
|
|
1162
1169
|
});
|
|
1163
1170
|
}
|
|
1164
1171
|
if (result.result === "failed") {
|
|
1165
|
-
updatePipelineRun(
|
|
1172
|
+
await updatePipelineRun(adapter, runId, { status: "failed" });
|
|
1166
1173
|
const errorMsg = `Analysis phase failed: ${result.error ?? "unknown error"}`;
|
|
1167
1174
|
if (outputFormat === "human") process.stderr.write(`Error: ${errorMsg}\n`);
|
|
1168
1175
|
else process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
@@ -1173,7 +1180,7 @@ async function runFullPipelineFromPhase(options) {
|
|
|
1173
1180
|
const result = await runPlanningPhase(phaseDeps, { runId });
|
|
1174
1181
|
if (result.tokenUsage.input > 0 || result.tokenUsage.output > 0) {
|
|
1175
1182
|
const costUsd = (result.tokenUsage.input * 3 + result.tokenUsage.output * 15) / 1e6;
|
|
1176
|
-
addTokenUsage(
|
|
1183
|
+
await addTokenUsage(adapter, runId, {
|
|
1177
1184
|
phase: "planning",
|
|
1178
1185
|
agent: "claude-code",
|
|
1179
1186
|
input_tokens: result.tokenUsage.input,
|
|
@@ -1182,7 +1189,7 @@ async function runFullPipelineFromPhase(options) {
|
|
|
1182
1189
|
});
|
|
1183
1190
|
}
|
|
1184
1191
|
if (result.result === "failed") {
|
|
1185
|
-
updatePipelineRun(
|
|
1192
|
+
await updatePipelineRun(adapter, runId, { status: "failed" });
|
|
1186
1193
|
const errorMsg = `Planning phase failed: ${result.error ?? "unknown error"}`;
|
|
1187
1194
|
if (outputFormat === "human") process.stderr.write(`Error: ${errorMsg}\n`);
|
|
1188
1195
|
else process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
@@ -1193,7 +1200,7 @@ async function runFullPipelineFromPhase(options) {
|
|
|
1193
1200
|
const result = await runSolutioningPhase(phaseDeps, { runId });
|
|
1194
1201
|
if (result.tokenUsage.input > 0 || result.tokenUsage.output > 0) {
|
|
1195
1202
|
const costUsd = (result.tokenUsage.input * 3 + result.tokenUsage.output * 15) / 1e6;
|
|
1196
|
-
addTokenUsage(
|
|
1203
|
+
await addTokenUsage(adapter, runId, {
|
|
1197
1204
|
phase: "solutioning",
|
|
1198
1205
|
agent: "claude-code",
|
|
1199
1206
|
input_tokens: result.tokenUsage.input,
|
|
@@ -1202,7 +1209,7 @@ async function runFullPipelineFromPhase(options) {
|
|
|
1202
1209
|
});
|
|
1203
1210
|
}
|
|
1204
1211
|
if (result.result === "failed") {
|
|
1205
|
-
updatePipelineRun(
|
|
1212
|
+
await updatePipelineRun(adapter, runId, { status: "failed" });
|
|
1206
1213
|
const errorMsg = `Solutioning phase failed: ${result.error ?? "unknown error"}`;
|
|
1207
1214
|
if (outputFormat === "human") process.stderr.write(`Error: ${errorMsg}\n`);
|
|
1208
1215
|
else process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
@@ -1222,9 +1229,9 @@ async function runFullPipelineFromPhase(options) {
|
|
|
1222
1229
|
}
|
|
1223
1230
|
} catch {}
|
|
1224
1231
|
const ingestionServer = telemetryEnabled ? new IngestionServer({ port: telemetryPort }) : void 0;
|
|
1225
|
-
const telemetryPersistence = telemetryEnabled ? new
|
|
1232
|
+
const telemetryPersistence = telemetryEnabled ? new AdapterTelemetryPersistence(adapter) : void 0;
|
|
1226
1233
|
const orchestrator = createImplementationOrchestrator({
|
|
1227
|
-
db,
|
|
1234
|
+
db: adapter,
|
|
1228
1235
|
pack,
|
|
1229
1236
|
contextCompiler,
|
|
1230
1237
|
dispatcher,
|
|
@@ -1244,19 +1251,21 @@ async function runFullPipelineFromPhase(options) {
|
|
|
1244
1251
|
if (result?.tokenUsage !== void 0) {
|
|
1245
1252
|
const { input, output } = result.tokenUsage;
|
|
1246
1253
|
const costUsd = (input * 3 + output * 15) / 1e6;
|
|
1247
|
-
addTokenUsage(
|
|
1254
|
+
addTokenUsage(adapter, runId, {
|
|
1248
1255
|
phase: payload.phase,
|
|
1249
1256
|
agent: "claude-code",
|
|
1250
1257
|
input_tokens: input,
|
|
1251
1258
|
output_tokens: output,
|
|
1252
1259
|
cost_usd: costUsd
|
|
1260
|
+
}).catch((err) => {
|
|
1261
|
+
logger$16.warn({ err }, "Failed to record token usage");
|
|
1253
1262
|
});
|
|
1254
1263
|
}
|
|
1255
1264
|
} catch (err) {
|
|
1256
1265
|
logger$16.warn({ err }, "Failed to record token usage");
|
|
1257
1266
|
}
|
|
1258
1267
|
});
|
|
1259
|
-
const storyKeys = resolveStoryKeys(
|
|
1268
|
+
const storyKeys = await resolveStoryKeys(adapter, projectRoot, { pipelineRunId: runId });
|
|
1260
1269
|
if (storyKeys.length === 0 && outputFormat === "human") process.stdout.write("[IMPLEMENTATION] No stories found for this run. Check solutioning phase output.\n");
|
|
1261
1270
|
await orchestrator.run(storyKeys);
|
|
1262
1271
|
if (outputFormat === "human") process.stdout.write("[IMPLEMENTATION] Complete\n");
|
|
@@ -1264,8 +1273,9 @@ async function runFullPipelineFromPhase(options) {
|
|
|
1264
1273
|
if (stopAfter !== void 0 && currentPhase === stopAfter) {
|
|
1265
1274
|
const gate = createStopAfterGate(stopAfter);
|
|
1266
1275
|
if (gate.shouldHalt()) {
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1276
|
+
const countRows = await adapter.query(`SELECT COUNT(*) as cnt FROM decisions WHERE pipeline_run_id = ?`, [runId]);
|
|
1277
|
+
const decisionsCount$1 = countRows[0]?.cnt ?? 0;
|
|
1278
|
+
await updatePipelineRun(adapter, runId, { status: "stopped" });
|
|
1269
1279
|
const phaseStartedAt = new Date(startedAt).toISOString();
|
|
1270
1280
|
const phaseCompletedAt = new Date().toISOString();
|
|
1271
1281
|
const summary = formatPhaseCompletionSummary({
|
|
@@ -1291,11 +1301,14 @@ async function runFullPipelineFromPhase(options) {
|
|
|
1291
1301
|
}
|
|
1292
1302
|
}
|
|
1293
1303
|
}
|
|
1294
|
-
const tokenSummary = getTokenUsageSummary(
|
|
1304
|
+
const tokenSummary = await getTokenUsageSummary(adapter, runId);
|
|
1295
1305
|
const durationMs = Date.now() - startedAt;
|
|
1296
|
-
const
|
|
1297
|
-
const
|
|
1298
|
-
const
|
|
1306
|
+
const decRows = await adapter.query(`SELECT COUNT(*) as cnt FROM decisions WHERE pipeline_run_id = ?`, [runId]);
|
|
1307
|
+
const decisionsCount = decRows[0]?.cnt ?? 0;
|
|
1308
|
+
const storyRows = await adapter.query(`SELECT COUNT(*) as cnt FROM requirements WHERE pipeline_run_id = ? AND source = 'solutioning-phase'`, [runId]);
|
|
1309
|
+
const storiesCount = storyRows[0]?.cnt ?? 0;
|
|
1310
|
+
const finalRunRows = await adapter.query("SELECT * FROM pipeline_runs WHERE id = ?", [runId]);
|
|
1311
|
+
const finalRun = finalRunRows[0];
|
|
1299
1312
|
if (outputFormat === "json") {
|
|
1300
1313
|
const statusOutput = buildPipelineStatusOutput(finalRun ?? { id: runId }, tokenSummary, decisionsCount, storiesCount);
|
|
1301
1314
|
process.stdout.write(formatOutput(statusOutput, "json", true) + "\n");
|
|
@@ -1312,7 +1325,7 @@ async function runFullPipelineFromPhase(options) {
|
|
|
1312
1325
|
return 1;
|
|
1313
1326
|
} finally {
|
|
1314
1327
|
try {
|
|
1315
|
-
|
|
1328
|
+
await adapter.close();
|
|
1316
1329
|
} catch {}
|
|
1317
1330
|
}
|
|
1318
1331
|
}
|
|
@@ -1364,28 +1377,33 @@ async function runStatusAction(options) {
|
|
|
1364
1377
|
}
|
|
1365
1378
|
const dbRoot = await resolveMainRepoRoot(projectRoot);
|
|
1366
1379
|
const dbPath = join(dbRoot, ".substrate", "substrate.db");
|
|
1367
|
-
|
|
1380
|
+
const doltDir = join(dbRoot, ".substrate", "state", ".dolt");
|
|
1381
|
+
if (!existsSync(dbPath) && !existsSync(doltDir)) {
|
|
1368
1382
|
const errorMsg = `Decision store not initialized. Run 'substrate init' first.`;
|
|
1369
1383
|
if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
1370
1384
|
else process.stderr.write(`Error: ${errorMsg}\n`);
|
|
1371
1385
|
return 1;
|
|
1372
1386
|
}
|
|
1373
|
-
const
|
|
1387
|
+
const adapter = createDatabaseAdapter({
|
|
1388
|
+
backend: "auto",
|
|
1389
|
+
basePath: dbRoot
|
|
1390
|
+
});
|
|
1374
1391
|
try {
|
|
1375
|
-
|
|
1376
|
-
const db = dbWrapper.db;
|
|
1392
|
+
await initSchema(adapter);
|
|
1377
1393
|
let run;
|
|
1378
|
-
if (runId !== void 0 && runId !== "") run =
|
|
1379
|
-
else run = getLatestRun(
|
|
1394
|
+
if (runId !== void 0 && runId !== "") run = await getPipelineRunById(adapter, runId);
|
|
1395
|
+
else run = await getLatestRun(adapter);
|
|
1380
1396
|
if (run === void 0) {
|
|
1381
1397
|
const errorMsg = runId !== void 0 ? `Pipeline run '${runId}' not found.` : "No pipeline runs found. Run `substrate run` first.";
|
|
1382
1398
|
if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
1383
1399
|
else process.stderr.write(`Error: ${errorMsg}\n`);
|
|
1384
1400
|
return 1;
|
|
1385
1401
|
}
|
|
1386
|
-
const tokenSummary = getTokenUsageSummary(
|
|
1387
|
-
const
|
|
1388
|
-
const
|
|
1402
|
+
const tokenSummary = await getTokenUsageSummary(adapter, run.id);
|
|
1403
|
+
const decisionsCountRows = await adapter.query(`SELECT COUNT(*) as cnt FROM decisions WHERE pipeline_run_id = ?`, [run.id]);
|
|
1404
|
+
const decisionsCount = decisionsCountRows[0]?.cnt ?? 0;
|
|
1405
|
+
const storiesCountRows = await adapter.query(`SELECT COUNT(*) as cnt FROM requirements WHERE pipeline_run_id = ? AND source = 'solutioning-phase'`, [run.id]);
|
|
1406
|
+
const storiesCount = storiesCountRows[0]?.cnt ?? 0;
|
|
1389
1407
|
let storeStories = [];
|
|
1390
1408
|
if (stateStore) try {
|
|
1391
1409
|
storeStories = await stateStore.queryStories({});
|
|
@@ -1394,7 +1412,7 @@ async function runStatusAction(options) {
|
|
|
1394
1412
|
}
|
|
1395
1413
|
if (outputFormat === "json") {
|
|
1396
1414
|
const statusOutput = buildPipelineStatusOutput(run, tokenSummary, decisionsCount, storiesCount);
|
|
1397
|
-
const storyMetricsRows = getStoryMetricsForRun(
|
|
1415
|
+
const storyMetricsRows = await getStoryMetricsForRun(adapter, run.id);
|
|
1398
1416
|
const storyMetricsV2 = storyMetricsRows.map((row) => {
|
|
1399
1417
|
const phaseBreakdown = {};
|
|
1400
1418
|
try {
|
|
@@ -1497,7 +1515,7 @@ async function runStatusAction(options) {
|
|
|
1497
1515
|
return 1;
|
|
1498
1516
|
} finally {
|
|
1499
1517
|
try {
|
|
1500
|
-
|
|
1518
|
+
await adapter.close();
|
|
1501
1519
|
} catch {}
|
|
1502
1520
|
}
|
|
1503
1521
|
}
|
|
@@ -1542,14 +1560,20 @@ function registerStatusCommand(program, _version = "0.0.0", projectRoot = proces
|
|
|
1542
1560
|
* Throws if the parent run is not found or not completed.
|
|
1543
1561
|
* Returns the new run's ID on success.
|
|
1544
1562
|
*/
|
|
1545
|
-
function createAmendmentRun(
|
|
1546
|
-
const
|
|
1563
|
+
async function createAmendmentRun(adapter, input) {
|
|
1564
|
+
const rows = await adapter.query("SELECT id, status FROM pipeline_runs WHERE id = ?", [input.parentRunId]);
|
|
1565
|
+
const parentRun = rows[0];
|
|
1547
1566
|
if (!parentRun) throw new Error(`Parent run not found: ${input.parentRunId}`);
|
|
1548
1567
|
if (parentRun.status !== "completed") throw new Error(`Parent run is not completed (status: ${parentRun.status}). Only completed runs can be amended.`);
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1568
|
+
await adapter.query(`INSERT INTO pipeline_runs (id, methodology, current_phase, status, config_json, parent_run_id, created_at, updated_at)
|
|
1569
|
+
VALUES (?, ?, NULL, 'running', ?, ?, ?, ?)`, [
|
|
1570
|
+
input.id,
|
|
1571
|
+
input.methodology,
|
|
1572
|
+
input.configJson ?? null,
|
|
1573
|
+
input.parentRunId,
|
|
1574
|
+
new Date().toISOString(),
|
|
1575
|
+
new Date().toISOString()
|
|
1576
|
+
]);
|
|
1553
1577
|
return input.id;
|
|
1554
1578
|
}
|
|
1555
1579
|
/**
|
|
@@ -1558,13 +1582,10 @@ function createAmendmentRun(db, input) {
|
|
|
1558
1582
|
* Returns decisions WHERE superseded_by IS NULL for the specified run,
|
|
1559
1583
|
* ordered by created_at ASC.
|
|
1560
1584
|
*/
|
|
1561
|
-
function loadParentRunDecisions(
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
ORDER BY created_at ASC
|
|
1566
|
-
`);
|
|
1567
|
-
return stmt.all(parentRunId);
|
|
1585
|
+
async function loadParentRunDecisions(adapter, parentRunId) {
|
|
1586
|
+
return adapter.query(`SELECT * FROM decisions
|
|
1587
|
+
WHERE pipeline_run_id = ? AND superseded_by IS NULL
|
|
1588
|
+
ORDER BY created_at ASC`, [parentRunId]);
|
|
1568
1589
|
}
|
|
1569
1590
|
/**
|
|
1570
1591
|
* Mark a decision as superseded by another decision.
|
|
@@ -1576,15 +1597,19 @@ function loadParentRunDecisions(db, parentRunId) {
|
|
|
1576
1597
|
*
|
|
1577
1598
|
* On success, updates the original decision's superseded_by field.
|
|
1578
1599
|
*/
|
|
1579
|
-
function supersedeDecision(
|
|
1580
|
-
const
|
|
1600
|
+
async function supersedeDecision(adapter, originalDecisionId, supersedingDecisionId) {
|
|
1601
|
+
const origRows = await adapter.query("SELECT id, superseded_by FROM decisions WHERE id = ?", [originalDecisionId]);
|
|
1602
|
+
const original = origRows[0];
|
|
1581
1603
|
if (!original) throw new Error(`Decision not found: ${originalDecisionId}`);
|
|
1582
|
-
const
|
|
1604
|
+
const superRows = await adapter.query("SELECT id FROM decisions WHERE id = ?", [supersedingDecisionId]);
|
|
1605
|
+
const superseding = superRows[0];
|
|
1583
1606
|
if (!superseding) throw new Error(`Superseding decision not found: ${supersedingDecisionId}`);
|
|
1584
1607
|
if (original.superseded_by !== null) throw new Error(`Decision ${originalDecisionId} is already superseded`);
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1608
|
+
await adapter.query("UPDATE decisions SET superseded_by = ?, updated_at = ? WHERE id = ?", [
|
|
1609
|
+
supersedingDecisionId,
|
|
1610
|
+
new Date().toISOString(),
|
|
1611
|
+
originalDecisionId
|
|
1612
|
+
]);
|
|
1588
1613
|
}
|
|
1589
1614
|
/**
|
|
1590
1615
|
* Get all active (non-superseded) decisions, with optional filtering.
|
|
@@ -1593,7 +1618,7 @@ function supersedeDecision(db, originalDecisionId, supersedingDecisionId) {
|
|
|
1593
1618
|
* If no filter is provided, returns all active decisions across all runs.
|
|
1594
1619
|
* Results are ordered by created_at ASC.
|
|
1595
1620
|
*/
|
|
1596
|
-
function getActiveDecisions(
|
|
1621
|
+
async function getActiveDecisions(adapter, filter) {
|
|
1597
1622
|
const conditions = ["superseded_by IS NULL"];
|
|
1598
1623
|
const values = [];
|
|
1599
1624
|
if (filter?.pipeline_run_id !== void 0) {
|
|
@@ -1609,25 +1634,22 @@ function getActiveDecisions(db, filter) {
|
|
|
1609
1634
|
values.push(filter.category);
|
|
1610
1635
|
}
|
|
1611
1636
|
if (filter?.key !== void 0) {
|
|
1612
|
-
conditions.push("key = ?");
|
|
1637
|
+
conditions.push("`key` = ?");
|
|
1613
1638
|
values.push(filter.key);
|
|
1614
1639
|
}
|
|
1615
1640
|
const where = `WHERE ${conditions.join(" AND ")}`;
|
|
1616
|
-
|
|
1617
|
-
return stmt.all(...values);
|
|
1641
|
+
return adapter.query(`SELECT * FROM decisions ${where} ORDER BY created_at ASC`, values);
|
|
1618
1642
|
}
|
|
1619
1643
|
/**
|
|
1620
1644
|
* Get the most recently created pipeline run with status = 'completed'.
|
|
1621
1645
|
* Returns undefined if no completed run exists.
|
|
1622
1646
|
*/
|
|
1623
|
-
function getLatestCompletedRun(
|
|
1624
|
-
const
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
`);
|
|
1630
|
-
return stmt.get();
|
|
1647
|
+
async function getLatestCompletedRun(adapter) {
|
|
1648
|
+
const rows = await adapter.query(`SELECT * FROM pipeline_runs
|
|
1649
|
+
WHERE status = 'completed'
|
|
1650
|
+
ORDER BY created_at DESC, id DESC
|
|
1651
|
+
LIMIT 1`);
|
|
1652
|
+
return rows[0];
|
|
1631
1653
|
}
|
|
1632
1654
|
|
|
1633
1655
|
//#endregion
|
|
@@ -1676,13 +1698,13 @@ function buildContextBlock(decisions, phaseName, framingConcept) {
|
|
|
1676
1698
|
*
|
|
1677
1699
|
* Throws if parentRunId is not found (delegates to loadParentRunDecisions()).
|
|
1678
1700
|
*
|
|
1679
|
-
* @param db -
|
|
1701
|
+
* @param db - DatabaseAdapter instance
|
|
1680
1702
|
* @param parentRunId - ID of the completed parent pipeline run
|
|
1681
1703
|
* @param options - Optional configuration (phaseFilter, framingConcept)
|
|
1682
1704
|
* @returns AmendmentContextHandler instance
|
|
1683
1705
|
*/
|
|
1684
|
-
function createAmendmentContextHandler(db, parentRunId, options) {
|
|
1685
|
-
const allDecisions = loadParentRunDecisions(db, parentRunId);
|
|
1706
|
+
async function createAmendmentContextHandler(db, parentRunId, options) {
|
|
1707
|
+
const allDecisions = await loadParentRunDecisions(db, parentRunId);
|
|
1686
1708
|
const parentDecisions = options?.phaseFilter && options.phaseFilter.length > 0 ? allDecisions.filter((d) => options.phaseFilter.includes(d.phase)) : allDecisions;
|
|
1687
1709
|
const framingConcept = options?.framingConcept;
|
|
1688
1710
|
const supersessionLog = [];
|
|
@@ -1948,8 +1970,8 @@ const logger$14 = createLogger("amend-cmd");
|
|
|
1948
1970
|
* Errors in individual supersession calls are logged as warnings but do not
|
|
1949
1971
|
* fail the phase (AC7: atomic with phase completion, non-blocking on error).
|
|
1950
1972
|
*/
|
|
1951
|
-
function runPostPhaseSupersessionDetection(
|
|
1952
|
-
const newDecisions = getActiveDecisions(
|
|
1973
|
+
async function runPostPhaseSupersessionDetection(adapter, amendmentRunId, currentPhase, handler) {
|
|
1974
|
+
const newDecisions = await getActiveDecisions(adapter, {
|
|
1953
1975
|
pipeline_run_id: amendmentRunId,
|
|
1954
1976
|
phase: currentPhase
|
|
1955
1977
|
});
|
|
@@ -1957,7 +1979,7 @@ function runPostPhaseSupersessionDetection(db, amendmentRunId, currentPhase, han
|
|
|
1957
1979
|
for (const newDec of newDecisions) {
|
|
1958
1980
|
const parentMatch = parentDecisions.find((p) => p.phase === newDec.phase && p.category === newDec.category && p.key === newDec.key);
|
|
1959
1981
|
if (parentMatch) try {
|
|
1960
|
-
supersedeDecision(
|
|
1982
|
+
await supersedeDecision(adapter, parentMatch.id, newDec.id);
|
|
1961
1983
|
handler.logSupersession({
|
|
1962
1984
|
originalDecisionId: parentMatch.id,
|
|
1963
1985
|
supersedingDecisionId: newDec.id,
|
|
@@ -2010,19 +2032,21 @@ async function runAmendAction(options) {
|
|
|
2010
2032
|
const dbDir = join(dbRoot, ".substrate");
|
|
2011
2033
|
const dbPath = join(dbDir, "substrate.db");
|
|
2012
2034
|
const packPath = join(projectRoot, "packs", packName);
|
|
2013
|
-
|
|
2035
|
+
const doltDir = join(dbRoot, ".substrate", "state", ".dolt");
|
|
2036
|
+
if (!existsSync(dbPath) && !existsSync(doltDir)) {
|
|
2014
2037
|
process.stderr.write(`Error: Decision store not initialized. Run 'substrate init' first.\n`);
|
|
2015
2038
|
return 1;
|
|
2016
2039
|
}
|
|
2017
|
-
const
|
|
2040
|
+
const adapter = createDatabaseAdapter({
|
|
2041
|
+
backend: "auto",
|
|
2042
|
+
basePath: dbRoot
|
|
2043
|
+
});
|
|
2018
2044
|
try {
|
|
2019
|
-
|
|
2020
|
-
runMigrations(dbWrapper.db);
|
|
2021
|
-
const db = dbWrapper.db;
|
|
2045
|
+
await initSchema(adapter);
|
|
2022
2046
|
let parentRunId;
|
|
2023
2047
|
if (specifiedRunId !== void 0 && specifiedRunId !== "") parentRunId = specifiedRunId;
|
|
2024
2048
|
else {
|
|
2025
|
-
const latestCompleted = getLatestCompletedRun(
|
|
2049
|
+
const latestCompleted = await getLatestCompletedRun(adapter);
|
|
2026
2050
|
if (latestCompleted === void 0) {
|
|
2027
2051
|
process.stderr.write("No completed pipeline run found. Run 'substrate run' first.\n");
|
|
2028
2052
|
return 1;
|
|
@@ -2037,7 +2061,7 @@ async function runAmendAction(options) {
|
|
|
2037
2061
|
methodology = pack$1.manifest.name;
|
|
2038
2062
|
} catch {}
|
|
2039
2063
|
try {
|
|
2040
|
-
createAmendmentRun(
|
|
2064
|
+
await createAmendmentRun(adapter, {
|
|
2041
2065
|
id: amendmentRunId,
|
|
2042
2066
|
parentRunId,
|
|
2043
2067
|
methodology,
|
|
@@ -2052,7 +2076,7 @@ async function runAmendAction(options) {
|
|
|
2052
2076
|
process.stderr.write(`Error: ${msg}\n`);
|
|
2053
2077
|
return 1;
|
|
2054
2078
|
}
|
|
2055
|
-
const handler = createAmendmentContextHandler(
|
|
2079
|
+
const handler = await createAmendmentContextHandler(adapter, parentRunId, { framingConcept: concept });
|
|
2056
2080
|
const packLoader = createPackLoader();
|
|
2057
2081
|
let pack;
|
|
2058
2082
|
try {
|
|
@@ -2063,14 +2087,14 @@ async function runAmendAction(options) {
|
|
|
2063
2087
|
return 1;
|
|
2064
2088
|
}
|
|
2065
2089
|
const eventBus = createEventBus();
|
|
2066
|
-
const contextCompiler = createContextCompiler({ db });
|
|
2090
|
+
const contextCompiler = createContextCompiler({ db: adapter });
|
|
2067
2091
|
if (!injectedRegistry) throw new Error("AdapterRegistry is required — must be initialized at CLI startup");
|
|
2068
2092
|
const dispatcher = createDispatcher({
|
|
2069
2093
|
eventBus,
|
|
2070
2094
|
adapterRegistry: injectedRegistry
|
|
2071
2095
|
});
|
|
2072
2096
|
const phaseDeps = {
|
|
2073
|
-
db,
|
|
2097
|
+
db: adapter,
|
|
2074
2098
|
pack,
|
|
2075
2099
|
contextCompiler,
|
|
2076
2100
|
dispatcher
|
|
@@ -2085,8 +2109,8 @@ async function runAmendAction(options) {
|
|
|
2085
2109
|
if (startIdx > 0) {
|
|
2086
2110
|
const phasesToCopy = phaseOrder.slice(0, startIdx);
|
|
2087
2111
|
for (const phase of phasesToCopy) {
|
|
2088
|
-
const parentDecisions$1 = getDecisionsByPhaseForRun(
|
|
2089
|
-
for (const d of parentDecisions$1) createDecision(
|
|
2112
|
+
const parentDecisions$1 = await getDecisionsByPhaseForRun(adapter, parentRunId, phase);
|
|
2113
|
+
for (const d of parentDecisions$1) await createDecision(adapter, {
|
|
2090
2114
|
pipeline_run_id: amendmentRunId,
|
|
2091
2115
|
phase: d.phase,
|
|
2092
2116
|
category: d.category,
|
|
@@ -2115,7 +2139,7 @@ async function runAmendAction(options) {
|
|
|
2115
2139
|
});
|
|
2116
2140
|
if (result.tokenUsage.input > 0 || result.tokenUsage.output > 0) {
|
|
2117
2141
|
const costUsd = (result.tokenUsage.input * 3 + result.tokenUsage.output * 15) / 1e6;
|
|
2118
|
-
addTokenUsage(
|
|
2142
|
+
await addTokenUsage(adapter, amendmentRunId, {
|
|
2119
2143
|
phase: "analysis",
|
|
2120
2144
|
agent: "claude-code",
|
|
2121
2145
|
input_tokens: result.tokenUsage.input,
|
|
@@ -2124,11 +2148,11 @@ async function runAmendAction(options) {
|
|
|
2124
2148
|
});
|
|
2125
2149
|
}
|
|
2126
2150
|
if (result.result === "failed") {
|
|
2127
|
-
updatePipelineRun(
|
|
2151
|
+
await updatePipelineRun(adapter, amendmentRunId, { status: "failed" });
|
|
2128
2152
|
process.stderr.write(`Error: Analysis phase failed: ${result.error ?? "unknown error"}${result.details ? ` — ${result.details}` : ""}\n`);
|
|
2129
2153
|
return 1;
|
|
2130
2154
|
}
|
|
2131
|
-
runPostPhaseSupersessionDetection(
|
|
2155
|
+
await runPostPhaseSupersessionDetection(adapter, amendmentRunId, "analysis", handler);
|
|
2132
2156
|
process.stdout.write(`[AMENDMENT:ANALYSIS] Complete\n`);
|
|
2133
2157
|
} else if (currentPhase === "planning") {
|
|
2134
2158
|
const result = await runPlanningPhase(phaseDeps, {
|
|
@@ -2137,7 +2161,7 @@ async function runAmendAction(options) {
|
|
|
2137
2161
|
});
|
|
2138
2162
|
if (result.tokenUsage.input > 0 || result.tokenUsage.output > 0) {
|
|
2139
2163
|
const costUsd = (result.tokenUsage.input * 3 + result.tokenUsage.output * 15) / 1e6;
|
|
2140
|
-
addTokenUsage(
|
|
2164
|
+
await addTokenUsage(adapter, amendmentRunId, {
|
|
2141
2165
|
phase: "planning",
|
|
2142
2166
|
agent: "claude-code",
|
|
2143
2167
|
input_tokens: result.tokenUsage.input,
|
|
@@ -2146,11 +2170,11 @@ async function runAmendAction(options) {
|
|
|
2146
2170
|
});
|
|
2147
2171
|
}
|
|
2148
2172
|
if (result.result === "failed") {
|
|
2149
|
-
updatePipelineRun(
|
|
2173
|
+
await updatePipelineRun(adapter, amendmentRunId, { status: "failed" });
|
|
2150
2174
|
process.stderr.write(`Error: Planning phase failed: ${result.error ?? "unknown error"}${result.details ? ` — ${result.details}` : ""}\n`);
|
|
2151
2175
|
return 1;
|
|
2152
2176
|
}
|
|
2153
|
-
runPostPhaseSupersessionDetection(
|
|
2177
|
+
await runPostPhaseSupersessionDetection(adapter, amendmentRunId, "planning", handler);
|
|
2154
2178
|
process.stdout.write(`[AMENDMENT:PLANNING] Complete\n`);
|
|
2155
2179
|
} else if (currentPhase === "solutioning") {
|
|
2156
2180
|
const result = await runSolutioningPhase(phaseDeps, {
|
|
@@ -2159,7 +2183,7 @@ async function runAmendAction(options) {
|
|
|
2159
2183
|
});
|
|
2160
2184
|
if (result.tokenUsage.input > 0 || result.tokenUsage.output > 0) {
|
|
2161
2185
|
const costUsd = (result.tokenUsage.input * 3 + result.tokenUsage.output * 15) / 1e6;
|
|
2162
|
-
addTokenUsage(
|
|
2186
|
+
await addTokenUsage(adapter, amendmentRunId, {
|
|
2163
2187
|
phase: "solutioning",
|
|
2164
2188
|
agent: "claude-code",
|
|
2165
2189
|
input_tokens: result.tokenUsage.input,
|
|
@@ -2168,18 +2192,19 @@ async function runAmendAction(options) {
|
|
|
2168
2192
|
});
|
|
2169
2193
|
}
|
|
2170
2194
|
if (result.result === "failed") {
|
|
2171
|
-
updatePipelineRun(
|
|
2195
|
+
await updatePipelineRun(adapter, amendmentRunId, { status: "failed" });
|
|
2172
2196
|
process.stderr.write(`Error: Solutioning phase failed: ${result.error ?? "unknown error"}${result.details ? ` — ${result.details}` : ""}\n`);
|
|
2173
2197
|
return 1;
|
|
2174
2198
|
}
|
|
2175
|
-
runPostPhaseSupersessionDetection(
|
|
2199
|
+
await runPostPhaseSupersessionDetection(adapter, amendmentRunId, "solutioning", handler);
|
|
2176
2200
|
process.stdout.write(`[AMENDMENT:SOLUTIONING] Complete\n`);
|
|
2177
2201
|
} else if (currentPhase === "implementation") process.stdout.write(`[AMENDMENT:IMPLEMENTATION] Context injected (${amendmentContext.length} chars)\n`);
|
|
2178
2202
|
if (stopAfter !== void 0 && currentPhase === stopAfter) {
|
|
2179
2203
|
const gate = createStopAfterGate(stopAfter);
|
|
2180
2204
|
if (gate.shouldHalt()) {
|
|
2181
|
-
const
|
|
2182
|
-
|
|
2205
|
+
const decisionsCountRows = await adapter.query(`SELECT COUNT(*) as cnt FROM decisions WHERE pipeline_run_id = ?`, [amendmentRunId]);
|
|
2206
|
+
const decisionsCount = decisionsCountRows[0]?.cnt ?? 0;
|
|
2207
|
+
await updatePipelineRun(adapter, amendmentRunId, { status: "stopped" });
|
|
2183
2208
|
const phaseStartedAt = new Date(startedAt).toISOString();
|
|
2184
2209
|
const phaseCompletedAt = new Date().toISOString();
|
|
2185
2210
|
const summary = formatPhaseCompletionSummary({
|
|
@@ -2196,8 +2221,8 @@ async function runAmendAction(options) {
|
|
|
2196
2221
|
}
|
|
2197
2222
|
}
|
|
2198
2223
|
}
|
|
2199
|
-
if (!stopped) updatePipelineRun(
|
|
2200
|
-
const amendmentDecisions = getActiveDecisions(
|
|
2224
|
+
if (!stopped) await updatePipelineRun(adapter, amendmentRunId, { status: "completed" });
|
|
2225
|
+
const amendmentDecisions = await getActiveDecisions(adapter, { pipeline_run_id: amendmentRunId });
|
|
2201
2226
|
const parentDecisions = handler.getParentDecisions();
|
|
2202
2227
|
const supersessionLog = handler.getSupersessionLog();
|
|
2203
2228
|
const supersededDecisionIds = new Set(supersessionLog.map((s) => s.originalDecisionId));
|
|
@@ -2226,7 +2251,7 @@ async function runAmendAction(options) {
|
|
|
2226
2251
|
return 1;
|
|
2227
2252
|
} finally {
|
|
2228
2253
|
try {
|
|
2229
|
-
|
|
2254
|
+
await adapter.close();
|
|
2230
2255
|
} catch {}
|
|
2231
2256
|
}
|
|
2232
2257
|
}
|
|
@@ -2257,20 +2282,23 @@ function defaultSupervisorDeps() {
|
|
|
2257
2282
|
resumePipeline: runResumeAction,
|
|
2258
2283
|
sleep: (ms) => new Promise((resolve$2) => setTimeout(resolve$2, ms)),
|
|
2259
2284
|
incrementRestarts: (() => {
|
|
2260
|
-
let
|
|
2285
|
+
let cachedAdapter = null;
|
|
2261
2286
|
return async (runId, projectRoot) => {
|
|
2262
2287
|
try {
|
|
2263
|
-
if (
|
|
2288
|
+
if (cachedAdapter === null) {
|
|
2264
2289
|
const dbRoot = await resolveMainRepoRoot(projectRoot);
|
|
2265
|
-
|
|
2266
|
-
|
|
2290
|
+
cachedAdapter = createDatabaseAdapter({
|
|
2291
|
+
backend: "auto",
|
|
2292
|
+
basePath: dbRoot
|
|
2293
|
+
});
|
|
2294
|
+
await initSchema(cachedAdapter);
|
|
2267
2295
|
}
|
|
2268
|
-
incrementRunRestarts(
|
|
2296
|
+
await incrementRunRestarts(cachedAdapter, runId);
|
|
2269
2297
|
} catch {
|
|
2270
2298
|
try {
|
|
2271
|
-
|
|
2299
|
+
await cachedAdapter?.close();
|
|
2272
2300
|
} catch {}
|
|
2273
|
-
|
|
2301
|
+
cachedAdapter = null;
|
|
2274
2302
|
}
|
|
2275
2303
|
};
|
|
2276
2304
|
})(),
|
|
@@ -2278,15 +2306,19 @@ function defaultSupervisorDeps() {
|
|
|
2278
2306
|
try {
|
|
2279
2307
|
const dbRoot = await resolveMainRepoRoot(projectRoot);
|
|
2280
2308
|
const dbPath = join(dbRoot, ".substrate", "substrate.db");
|
|
2281
|
-
|
|
2309
|
+
const doltDir = join(dbRoot, ".substrate", "state", ".dolt");
|
|
2310
|
+
if (!existsSync(dbPath) && !existsSync(doltDir)) return {
|
|
2282
2311
|
input: 0,
|
|
2283
2312
|
output: 0,
|
|
2284
2313
|
cost_usd: 0
|
|
2285
2314
|
};
|
|
2286
|
-
const
|
|
2315
|
+
const tsAdapter = createDatabaseAdapter({
|
|
2316
|
+
backend: "auto",
|
|
2317
|
+
basePath: dbRoot
|
|
2318
|
+
});
|
|
2287
2319
|
try {
|
|
2288
|
-
|
|
2289
|
-
const agg = aggregateTokenUsageForRun(
|
|
2320
|
+
await initSchema(tsAdapter);
|
|
2321
|
+
const agg = await aggregateTokenUsageForRun(tsAdapter, runId);
|
|
2290
2322
|
return {
|
|
2291
2323
|
input: agg.input,
|
|
2292
2324
|
output: agg.output,
|
|
@@ -2294,7 +2326,7 @@ function defaultSupervisorDeps() {
|
|
|
2294
2326
|
};
|
|
2295
2327
|
} finally {
|
|
2296
2328
|
try {
|
|
2297
|
-
|
|
2329
|
+
await tsAdapter.close();
|
|
2298
2330
|
} catch {}
|
|
2299
2331
|
}
|
|
2300
2332
|
} catch {
|
|
@@ -2312,7 +2344,7 @@ function defaultSupervisorDeps() {
|
|
|
2312
2344
|
if (cached === null) {
|
|
2313
2345
|
const { AdapterRegistry: AR } = await import(
|
|
2314
2346
|
/* @vite-ignore */
|
|
2315
|
-
"../adapter-registry-
|
|
2347
|
+
"../adapter-registry-BRQXdPnB.js"
|
|
2316
2348
|
);
|
|
2317
2349
|
cached = new AR();
|
|
2318
2350
|
await cached.discoverAndRegister();
|
|
@@ -2324,14 +2356,17 @@ function defaultSupervisorDeps() {
|
|
|
2324
2356
|
try {
|
|
2325
2357
|
const dbRoot = await resolveMainRepoRoot(opts.projectRoot);
|
|
2326
2358
|
const dbPath = join(dbRoot, ".substrate", "substrate.db");
|
|
2327
|
-
|
|
2328
|
-
|
|
2359
|
+
const doltDir = join(dbRoot, ".substrate", "state", ".dolt");
|
|
2360
|
+
if (!existsSync(dbPath) && !existsSync(doltDir)) return;
|
|
2361
|
+
const sfAdapter = createDatabaseAdapter({
|
|
2362
|
+
backend: "auto",
|
|
2363
|
+
basePath: dbRoot
|
|
2364
|
+
});
|
|
2329
2365
|
try {
|
|
2330
|
-
|
|
2331
|
-
const db = dbWrapper.db;
|
|
2366
|
+
await initSchema(sfAdapter);
|
|
2332
2367
|
const activeStories = Object.entries(opts.storyDetails).filter(([, s]) => s.phase !== "PENDING" && s.phase !== "COMPLETE" && s.phase !== "ESCALATED");
|
|
2333
2368
|
const now = Date.now();
|
|
2334
|
-
for (const [storyKey, storyState] of activeStories) createDecision(
|
|
2369
|
+
for (const [storyKey, storyState] of activeStories) await createDecision(sfAdapter, {
|
|
2335
2370
|
pipeline_run_id: opts.runId ?? null,
|
|
2336
2371
|
phase: "supervisor",
|
|
2337
2372
|
category: OPERATIONAL_FINDING,
|
|
@@ -2346,7 +2381,7 @@ function defaultSupervisorDeps() {
|
|
|
2346
2381
|
});
|
|
2347
2382
|
} finally {
|
|
2348
2383
|
try {
|
|
2349
|
-
|
|
2384
|
+
await sfAdapter.close();
|
|
2350
2385
|
} catch {}
|
|
2351
2386
|
}
|
|
2352
2387
|
} catch {}
|
|
@@ -2358,13 +2393,16 @@ function defaultSupervisorDeps() {
|
|
|
2358
2393
|
try {
|
|
2359
2394
|
const dbRoot = await resolveMainRepoRoot(opts.projectRoot);
|
|
2360
2395
|
const dbPath = join(dbRoot, ".substrate", "substrate.db");
|
|
2361
|
-
|
|
2362
|
-
|
|
2396
|
+
const doltDir = join(dbRoot, ".substrate", "state", ".dolt");
|
|
2397
|
+
if (!existsSync(dbPath) && !existsSync(doltDir)) return;
|
|
2398
|
+
const rsAdapter = createDatabaseAdapter({
|
|
2399
|
+
backend: "auto",
|
|
2400
|
+
basePath: dbRoot
|
|
2401
|
+
});
|
|
2363
2402
|
try {
|
|
2364
|
-
|
|
2365
|
-
const
|
|
2366
|
-
|
|
2367
|
-
createDecision(db, {
|
|
2403
|
+
await initSchema(rsAdapter);
|
|
2404
|
+
const tokenAgg = await aggregateTokenUsageForRun(rsAdapter, opts.runId);
|
|
2405
|
+
await createDecision(rsAdapter, {
|
|
2368
2406
|
pipeline_run_id: opts.runId,
|
|
2369
2407
|
phase: "supervisor",
|
|
2370
2408
|
category: OPERATIONAL_FINDING,
|
|
@@ -2382,24 +2420,26 @@ function defaultSupervisorDeps() {
|
|
|
2382
2420
|
});
|
|
2383
2421
|
} finally {
|
|
2384
2422
|
try {
|
|
2385
|
-
|
|
2423
|
+
await rsAdapter.close();
|
|
2386
2424
|
} catch {}
|
|
2387
2425
|
}
|
|
2388
2426
|
} catch {}
|
|
2389
2427
|
},
|
|
2390
2428
|
runAnalysis: async (runId, projectRoot) => {
|
|
2391
2429
|
const dbPath = join(projectRoot, ".substrate", "substrate.db");
|
|
2392
|
-
|
|
2393
|
-
|
|
2430
|
+
const doltDir = join(projectRoot, ".substrate", "state", ".dolt");
|
|
2431
|
+
if (!existsSync(dbPath) && !existsSync(doltDir)) return;
|
|
2432
|
+
const raAdapter = createDatabaseAdapter({
|
|
2433
|
+
backend: "auto",
|
|
2434
|
+
basePath: projectRoot
|
|
2435
|
+
});
|
|
2394
2436
|
try {
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
const db = dbWrapper.db;
|
|
2398
|
-
const run = getRunMetrics(db, runId);
|
|
2437
|
+
await initSchema(raAdapter);
|
|
2438
|
+
const run = await getRunMetrics(raAdapter, runId);
|
|
2399
2439
|
if (!run) return;
|
|
2400
|
-
const stories = getStoryMetricsForRun(
|
|
2401
|
-
const baseline = getBaselineRunMetrics(
|
|
2402
|
-
const baselineStories = baseline && baseline.run_id !== runId ? getStoryMetricsForRun(
|
|
2440
|
+
const stories = await getStoryMetricsForRun(raAdapter, runId);
|
|
2441
|
+
const baseline = await getBaselineRunMetrics(raAdapter);
|
|
2442
|
+
const baselineStories = baseline && baseline.run_id !== runId ? await getStoryMetricsForRun(raAdapter, baseline.run_id) : [];
|
|
2403
2443
|
const analysisPath = "../../modules/supervisor/analysis.js";
|
|
2404
2444
|
const { generateAnalysisReport, writeAnalysisReport } = await import(
|
|
2405
2445
|
/* @vite-ignore */
|
|
@@ -2409,7 +2449,7 @@ function defaultSupervisorDeps() {
|
|
|
2409
2449
|
writeAnalysisReport(report, projectRoot);
|
|
2410
2450
|
} catch {} finally {
|
|
2411
2451
|
try {
|
|
2412
|
-
|
|
2452
|
+
await raAdapter.close();
|
|
2413
2453
|
} catch {}
|
|
2414
2454
|
}
|
|
2415
2455
|
}
|
|
@@ -2695,21 +2735,21 @@ async function runSupervisorAction(options, deps = {}) {
|
|
|
2695
2735
|
try {
|
|
2696
2736
|
const { createExperimenter } = await import(
|
|
2697
2737
|
/* @vite-ignore */
|
|
2698
|
-
"../experimenter-
|
|
2738
|
+
"../experimenter-CoR0k66d.js"
|
|
2699
2739
|
);
|
|
2700
2740
|
const { getLatestRun: getLatest } = await import(
|
|
2701
2741
|
/* @vite-ignore */
|
|
2702
|
-
"../decisions-
|
|
2742
|
+
"../decisions-BxYj_a1X.js"
|
|
2703
2743
|
);
|
|
2704
|
-
const
|
|
2705
|
-
|
|
2744
|
+
const expAdapter = createDatabaseAdapter({
|
|
2745
|
+
backend: "auto",
|
|
2746
|
+
basePath: projectRoot
|
|
2747
|
+
});
|
|
2706
2748
|
try {
|
|
2707
|
-
|
|
2708
|
-
runMigrations(expDbWrapper.db);
|
|
2709
|
-
const expDb = expDbWrapper.db;
|
|
2749
|
+
await initSchema(expAdapter);
|
|
2710
2750
|
const { runRunAction: runPipeline } = await import(
|
|
2711
2751
|
/* @vite-ignore */
|
|
2712
|
-
"../run-
|
|
2752
|
+
"../run-C-yCMYlt.js"
|
|
2713
2753
|
);
|
|
2714
2754
|
const runStoryFn = async (opts) => {
|
|
2715
2755
|
const exitCode = await runPipeline({
|
|
@@ -2719,7 +2759,7 @@ async function runSupervisorAction(options, deps = {}) {
|
|
|
2719
2759
|
outputFormat: "json",
|
|
2720
2760
|
projectRoot: opts.projectRoot
|
|
2721
2761
|
});
|
|
2722
|
-
const latestRun = getLatest(
|
|
2762
|
+
const latestRun = await getLatest(expAdapter);
|
|
2723
2763
|
const newRunId = latestRun?.id ?? `experiment-${Date.now()}`;
|
|
2724
2764
|
return {
|
|
2725
2765
|
runId: newRunId,
|
|
@@ -2735,7 +2775,7 @@ async function runSupervisorAction(options, deps = {}) {
|
|
|
2735
2775
|
runStory: runStoryFn,
|
|
2736
2776
|
log: (msg) => log(msg)
|
|
2737
2777
|
});
|
|
2738
|
-
const results = await experimenter.runExperiments(
|
|
2778
|
+
const results = await experimenter.runExperiments(expAdapter, recommendations, health.run_id);
|
|
2739
2779
|
const improved = results.filter((r) => r.verdict === "IMPROVED").length;
|
|
2740
2780
|
const mixed = results.filter((r) => r.verdict === "MIXED").length;
|
|
2741
2781
|
const regressed = results.filter((r) => r.verdict === "REGRESSED").length;
|
|
@@ -2749,7 +2789,7 @@ async function runSupervisorAction(options, deps = {}) {
|
|
|
2749
2789
|
});
|
|
2750
2790
|
} finally {
|
|
2751
2791
|
try {
|
|
2752
|
-
|
|
2792
|
+
await expAdapter.close();
|
|
2753
2793
|
} catch {}
|
|
2754
2794
|
}
|
|
2755
2795
|
} catch (expErr) {
|
|
@@ -2957,11 +2997,17 @@ function registerSupervisorCommand(program, _version = "0.0.0", projectRoot = pr
|
|
|
2957
2997
|
//#endregion
|
|
2958
2998
|
//#region src/cli/commands/metrics.ts
|
|
2959
2999
|
const logger$13 = createLogger("metrics-cmd");
|
|
2960
|
-
async function
|
|
2961
|
-
if (!existsSync(dbPath)) return null;
|
|
3000
|
+
async function openTelemetryAdapter(basePath) {
|
|
2962
3001
|
try {
|
|
2963
|
-
const
|
|
2964
|
-
|
|
3002
|
+
const adapter = createDatabaseAdapter({
|
|
3003
|
+
backend: "auto",
|
|
3004
|
+
basePath
|
|
3005
|
+
});
|
|
3006
|
+
const persistence = new AdapterTelemetryPersistence(adapter);
|
|
3007
|
+
return {
|
|
3008
|
+
persistence,
|
|
3009
|
+
close: () => adapter.close()
|
|
3010
|
+
};
|
|
2965
3011
|
} catch {
|
|
2966
3012
|
return null;
|
|
2967
3013
|
}
|
|
@@ -3048,11 +3094,10 @@ async function runMetricsAction(options) {
|
|
|
3048
3094
|
}
|
|
3049
3095
|
if (hasTelemetryMode) {
|
|
3050
3096
|
const dbRoot$1 = await resolveMainRepoRoot(projectRoot);
|
|
3051
|
-
const dbPath$1 = join(dbRoot$1, ".substrate", "substrate.db");
|
|
3052
3097
|
const doltStatePath = join(dbRoot$1, ".substrate", "state", ".dolt");
|
|
3053
3098
|
const doltExists = existsSync(doltStatePath);
|
|
3054
|
-
if (!doltExists
|
|
3055
|
-
const msg = "No telemetry data yet — run a pipeline with `telemetry.enabled: true`";
|
|
3099
|
+
if (!doltExists) {
|
|
3100
|
+
const msg = "No telemetry data yet — run a pipeline with Dolt initialized and `telemetry.enabled: true`";
|
|
3056
3101
|
if (turns !== void 0 || consumers !== void 0) {
|
|
3057
3102
|
process.stderr.write(`Error: ${msg}\n`);
|
|
3058
3103
|
return 1;
|
|
@@ -3061,8 +3106,8 @@ async function runMetricsAction(options) {
|
|
|
3061
3106
|
else process.stdout.write(msg + "\n");
|
|
3062
3107
|
return 0;
|
|
3063
3108
|
}
|
|
3064
|
-
const
|
|
3065
|
-
if (
|
|
3109
|
+
const telemetryHandle = await openTelemetryAdapter(dbRoot$1);
|
|
3110
|
+
if (telemetryHandle === null) {
|
|
3066
3111
|
const msg = "No telemetry data yet — run a pipeline with `telemetry.enabled: true`";
|
|
3067
3112
|
if (turns !== void 0 || consumers !== void 0) {
|
|
3068
3113
|
process.stderr.write(`Error: ${msg}\n`);
|
|
@@ -3072,8 +3117,8 @@ async function runMetricsAction(options) {
|
|
|
3072
3117
|
else process.stdout.write(msg + "\n");
|
|
3073
3118
|
return 0;
|
|
3074
3119
|
}
|
|
3120
|
+
const telemetryPersistence = telemetryHandle.persistence;
|
|
3075
3121
|
try {
|
|
3076
|
-
const telemetryPersistence = new TelemetryPersistence(sqliteDb);
|
|
3077
3122
|
if (efficiency === true) {
|
|
3078
3123
|
const scores = await telemetryPersistence.getEfficiencyScores(20);
|
|
3079
3124
|
if (outputFormat === "json") process.stdout.write(formatOutput({ efficiency: rowsToEfficiencyScore(scores) }, "json", true) + "\n");
|
|
@@ -3163,7 +3208,7 @@ async function runMetricsAction(options) {
|
|
|
3163
3208
|
}
|
|
3164
3209
|
} finally {
|
|
3165
3210
|
try {
|
|
3166
|
-
|
|
3211
|
+
await telemetryHandle.close();
|
|
3167
3212
|
} catch {}
|
|
3168
3213
|
}
|
|
3169
3214
|
}
|
|
@@ -3249,36 +3294,37 @@ async function runMetricsAction(options) {
|
|
|
3249
3294
|
}
|
|
3250
3295
|
}
|
|
3251
3296
|
const dbRoot = await resolveMainRepoRoot(projectRoot);
|
|
3252
|
-
const
|
|
3253
|
-
if (!existsSync(
|
|
3297
|
+
const doltStateDir = join(dbRoot, ".substrate", "state", ".dolt");
|
|
3298
|
+
if (!existsSync(doltStateDir)) {
|
|
3254
3299
|
if (outputFormat === "json") process.stdout.write(formatOutput({
|
|
3255
3300
|
runs: [],
|
|
3256
|
-
message: "No metrics yet — no pipeline database found."
|
|
3301
|
+
message: "No metrics yet — no pipeline database found. Initialize Dolt with `substrate init`."
|
|
3257
3302
|
}, "json", true) + "\n");
|
|
3258
|
-
else process.stdout.write("No metrics yet — no pipeline database found
|
|
3303
|
+
else process.stdout.write("No metrics yet — no pipeline database found. Initialize Dolt with `substrate init`.\n");
|
|
3259
3304
|
return 0;
|
|
3260
3305
|
}
|
|
3261
|
-
const
|
|
3306
|
+
const adapter = createDatabaseAdapter({
|
|
3307
|
+
backend: "auto",
|
|
3308
|
+
basePath: dbRoot
|
|
3309
|
+
});
|
|
3262
3310
|
try {
|
|
3263
|
-
|
|
3264
|
-
runMigrations(dbWrapper.db);
|
|
3265
|
-
const db = dbWrapper.db;
|
|
3311
|
+
await initSchema(adapter);
|
|
3266
3312
|
if (tagBaseline !== void 0) {
|
|
3267
|
-
const row = getRunMetrics(
|
|
3313
|
+
const row = await getRunMetrics(adapter, tagBaseline);
|
|
3268
3314
|
if (!row) {
|
|
3269
3315
|
const msg = `Run '${tagBaseline}' not found in run_metrics.`;
|
|
3270
3316
|
if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, msg) + "\n");
|
|
3271
3317
|
else process.stderr.write(`Error: ${msg}\n`);
|
|
3272
3318
|
return 1;
|
|
3273
3319
|
}
|
|
3274
|
-
tagRunAsBaseline(
|
|
3320
|
+
await tagRunAsBaseline(adapter, tagBaseline);
|
|
3275
3321
|
if (outputFormat === "json") process.stdout.write(formatOutput({ tagged_baseline: tagBaseline }, "json", true) + "\n");
|
|
3276
3322
|
else process.stdout.write(`Baseline tagged: ${tagBaseline}\n`);
|
|
3277
3323
|
return 0;
|
|
3278
3324
|
}
|
|
3279
3325
|
if (compare !== void 0) {
|
|
3280
3326
|
const [idA, idB] = compare;
|
|
3281
|
-
const delta = compareRunMetrics(
|
|
3327
|
+
const delta = await compareRunMetrics(adapter, idA, idB);
|
|
3282
3328
|
if (delta === null) {
|
|
3283
3329
|
const msg = `One or both run IDs not found in metrics: ${idA}, ${idB}`;
|
|
3284
3330
|
if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, msg) + "\n");
|
|
@@ -3298,7 +3344,7 @@ async function runMetricsAction(options) {
|
|
|
3298
3344
|
}
|
|
3299
3345
|
return 0;
|
|
3300
3346
|
}
|
|
3301
|
-
const runs = listRunMetrics(
|
|
3347
|
+
const runs = await listRunMetrics(adapter, limit);
|
|
3302
3348
|
let doltMetrics;
|
|
3303
3349
|
const doltStatePath = join(dbRoot, ".substrate", "state", ".dolt");
|
|
3304
3350
|
const hasDoltFilters = sprint !== void 0 || story !== void 0 || taskType !== void 0 || since !== void 0 || aggregate === true;
|
|
@@ -3319,7 +3365,7 @@ async function runMetricsAction(options) {
|
|
|
3319
3365
|
} catch (doltErr) {
|
|
3320
3366
|
logger$13.warn({ err: doltErr }, "StateStore query failed — falling back to SQLite metrics only");
|
|
3321
3367
|
}
|
|
3322
|
-
const storyMetricDecisions = getDecisionsByCategory(
|
|
3368
|
+
const storyMetricDecisions = await getDecisionsByCategory(adapter, STORY_METRICS);
|
|
3323
3369
|
const storyMetrics = storyMetricDecisions.map((d) => {
|
|
3324
3370
|
const colonIdx = d.key.indexOf(":");
|
|
3325
3371
|
const storyKey = colonIdx !== -1 ? d.key.slice(0, colonIdx) : d.key;
|
|
@@ -3464,7 +3510,7 @@ async function runMetricsAction(options) {
|
|
|
3464
3510
|
return 1;
|
|
3465
3511
|
} finally {
|
|
3466
3512
|
try {
|
|
3467
|
-
|
|
3513
|
+
await adapter.close();
|
|
3468
3514
|
} catch {}
|
|
3469
3515
|
}
|
|
3470
3516
|
}
|
|
@@ -3514,30 +3560,20 @@ function registerMetricsCommand(program, _version = "0.0.0", projectRoot = proce
|
|
|
3514
3560
|
//#endregion
|
|
3515
3561
|
//#region src/cli/commands/migrate.ts
|
|
3516
3562
|
/**
|
|
3517
|
-
*
|
|
3518
|
-
*
|
|
3519
|
-
*
|
|
3563
|
+
* Reads the SQLite snapshot for migration.
|
|
3564
|
+
*
|
|
3565
|
+
* NOTE (Epic 29): SQLite support has been removed from Substrate.
|
|
3566
|
+
*
|
|
3567
|
+
* If you need to migrate historical SQLite data, downgrade to a pre-Epic-29
|
|
3568
|
+
* version of Substrate (v0.4.x or earlier), run `substrate migrate`, then
|
|
3569
|
+
* upgrade. The Dolt database will retain the migrated data across upgrades.
|
|
3570
|
+
*
|
|
3571
|
+
* This function now always returns an empty snapshot.
|
|
3520
3572
|
*/
|
|
3521
|
-
function readSqliteSnapshot(dbPath) {
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
} catch {
|
|
3526
|
-
return { storyMetrics: [] };
|
|
3527
|
-
}
|
|
3528
|
-
try {
|
|
3529
|
-
const rows = db.prepare(`SELECT story_key, result, completed_at, created_at,
|
|
3530
|
-
wall_clock_seconds, input_tokens, output_tokens,
|
|
3531
|
-
cost_usd, review_cycles
|
|
3532
|
-
FROM story_metrics`).all();
|
|
3533
|
-
return { storyMetrics: rows };
|
|
3534
|
-
} catch (err) {
|
|
3535
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
3536
|
-
process.stderr.write(`Warning: could not read story_metrics from SQLite: ${msg}\n`);
|
|
3537
|
-
return { storyMetrics: [] };
|
|
3538
|
-
} finally {
|
|
3539
|
-
db.close();
|
|
3540
|
-
}
|
|
3573
|
+
async function readSqliteSnapshot(dbPath) {
|
|
3574
|
+
const { existsSync: fileExists } = await import("node:fs");
|
|
3575
|
+
if (fileExists(dbPath)) process.stderr.write(`Warning: Legacy SQLite database found at ${dbPath} but SQLite support has been\nremoved in Epic 29. To migrate historical data, downgrade to Substrate v0.4.x,\nrun 'substrate migrate', then upgrade back to this version.\n`);
|
|
3576
|
+
return { storyMetrics: [] };
|
|
3541
3577
|
}
|
|
3542
3578
|
const BATCH_SIZE = 100;
|
|
3543
3579
|
/**
|
|
@@ -3611,7 +3647,7 @@ function registerMigrateCommand(program) {
|
|
|
3611
3647
|
return;
|
|
3612
3648
|
}
|
|
3613
3649
|
const dbPath = join$1(projectRoot, ".substrate", "substrate.db");
|
|
3614
|
-
const snapshot = readSqliteSnapshot(dbPath);
|
|
3650
|
+
const snapshot = await readSqliteSnapshot(dbPath);
|
|
3615
3651
|
if (snapshot.storyMetrics.length === 0) {
|
|
3616
3652
|
if (options.outputFormat === "json") console.log(JSON.stringify({
|
|
3617
3653
|
migrated: false,
|
|
@@ -3665,25 +3701,14 @@ function registerMigrateCommand(program) {
|
|
|
3665
3701
|
|
|
3666
3702
|
//#endregion
|
|
3667
3703
|
//#region src/persistence/queries/cost.ts
|
|
3668
|
-
const stmtCache = new WeakMap();
|
|
3669
|
-
function getCache(db) {
|
|
3670
|
-
let cache = stmtCache.get(db);
|
|
3671
|
-
if (!cache) {
|
|
3672
|
-
cache = {};
|
|
3673
|
-
stmtCache.set(db, cache);
|
|
3674
|
-
}
|
|
3675
|
-
return cache;
|
|
3676
|
-
}
|
|
3677
3704
|
/**
|
|
3678
3705
|
* Return aggregated cost totals for a session (AC2).
|
|
3679
3706
|
*
|
|
3680
3707
|
* Returns a SessionCostSummary with subscription/API breakdown and savings.
|
|
3681
3708
|
* Uses idx_cost_entries_session_task index.
|
|
3682
3709
|
*/
|
|
3683
|
-
function getSessionCostSummary(
|
|
3684
|
-
const
|
|
3685
|
-
if (!cache.getSessionCostSummaryTotals) cache.getSessionCostSummaryTotals = db.prepare(`
|
|
3686
|
-
SELECT
|
|
3710
|
+
async function getSessionCostSummary(adapter, sessionId) {
|
|
3711
|
+
const totalsRows = await adapter.query(`SELECT
|
|
3687
3712
|
COALESCE(SUM(estimated_cost), 0) AS total_cost_usd,
|
|
3688
3713
|
COALESCE(SUM(CASE WHEN billing_mode = 'subscription' THEN COALESCE(estimated_cost, 0) ELSE 0 END), 0) AS subscription_cost_usd,
|
|
3689
3714
|
COALESCE(SUM(CASE WHEN billing_mode = 'api' THEN COALESCE(estimated_cost, 0) ELSE 0 END), 0) AS api_cost_usd,
|
|
@@ -3693,11 +3718,9 @@ function getSessionCostSummary(db, sessionId) {
|
|
|
3693
3718
|
SUM(CASE WHEN billing_mode = 'api' THEN 1 ELSE 0 END) AS api_task_count,
|
|
3694
3719
|
MIN(timestamp) AS earliest_recorded_at
|
|
3695
3720
|
FROM cost_entries
|
|
3696
|
-
WHERE session_id =
|
|
3697
|
-
|
|
3698
|
-
const
|
|
3699
|
-
if (!cache.getSessionCostSummaryAgents) cache.getSessionCostSummaryAgents = db.prepare(`
|
|
3700
|
-
SELECT
|
|
3721
|
+
WHERE session_id = ?`, [sessionId]);
|
|
3722
|
+
const totalsRow = totalsRows[0];
|
|
3723
|
+
const agentRows = await adapter.query(`SELECT
|
|
3701
3724
|
agent,
|
|
3702
3725
|
COUNT(*) AS task_count,
|
|
3703
3726
|
COALESCE(SUM(estimated_cost), 0) AS cost_usd,
|
|
@@ -3705,11 +3728,9 @@ function getSessionCostSummary(db, sessionId) {
|
|
|
3705
3728
|
SUM(CASE WHEN billing_mode = 'subscription' THEN 1 ELSE 0 END) AS subscription_tasks,
|
|
3706
3729
|
SUM(CASE WHEN billing_mode = 'api' THEN 1 ELSE 0 END) AS api_tasks
|
|
3707
3730
|
FROM cost_entries
|
|
3708
|
-
WHERE session_id =
|
|
3731
|
+
WHERE session_id = ?
|
|
3709
3732
|
GROUP BY agent
|
|
3710
|
-
ORDER BY cost_usd DESC
|
|
3711
|
-
`);
|
|
3712
|
-
const agentRows = cache.getSessionCostSummaryAgents.all({ sessionId });
|
|
3733
|
+
ORDER BY cost_usd DESC`, [sessionId]);
|
|
3713
3734
|
const perAgentBreakdown = agentRows.map((row) => ({
|
|
3714
3735
|
agent: row.agent,
|
|
3715
3736
|
task_count: row.task_count,
|
|
@@ -3746,69 +3767,31 @@ function getSessionCostSummary(db, sessionId) {
|
|
|
3746
3767
|
*
|
|
3747
3768
|
* Uses idx_cost_entries_session_task index.
|
|
3748
3769
|
*/
|
|
3749
|
-
function getSessionCostSummaryFiltered(
|
|
3750
|
-
const
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
SUM(CASE WHEN billing_mode = 'api' THEN 1 ELSE 0 END) AS api_tasks
|
|
3775
|
-
FROM cost_entries
|
|
3776
|
-
WHERE session_id = @sessionId AND category != 'planning'
|
|
3777
|
-
GROUP BY agent
|
|
3778
|
-
ORDER BY cost_usd DESC
|
|
3779
|
-
`);
|
|
3780
|
-
totalsRow = cache.getSessionCostSummaryFilteredTotals.get({ sessionId });
|
|
3781
|
-
agentRows = cache.getSessionCostSummaryFilteredAgents.all({ sessionId });
|
|
3782
|
-
} else {
|
|
3783
|
-
if (!cache.getSessionCostSummaryUnfilteredTotals) cache.getSessionCostSummaryUnfilteredTotals = db.prepare(`
|
|
3784
|
-
SELECT
|
|
3785
|
-
COALESCE(SUM(estimated_cost), 0) AS total_cost_usd,
|
|
3786
|
-
COALESCE(SUM(CASE WHEN billing_mode = 'subscription' THEN COALESCE(estimated_cost, 0) ELSE 0 END), 0) AS subscription_cost_usd,
|
|
3787
|
-
COALESCE(SUM(CASE WHEN billing_mode = 'api' THEN COALESCE(estimated_cost, 0) ELSE 0 END), 0) AS api_cost_usd,
|
|
3788
|
-
COALESCE(SUM(savings_usd), 0) AS savings_usd,
|
|
3789
|
-
COUNT(*) AS task_count,
|
|
3790
|
-
SUM(CASE WHEN billing_mode = 'subscription' THEN 1 ELSE 0 END) AS subscription_task_count,
|
|
3791
|
-
SUM(CASE WHEN billing_mode = 'api' THEN 1 ELSE 0 END) AS api_task_count,
|
|
3792
|
-
MIN(timestamp) AS earliest_recorded_at
|
|
3793
|
-
FROM cost_entries
|
|
3794
|
-
WHERE session_id = @sessionId
|
|
3795
|
-
`);
|
|
3796
|
-
if (!cache.getSessionCostSummaryUnfilteredAgents) cache.getSessionCostSummaryUnfilteredAgents = db.prepare(`
|
|
3797
|
-
SELECT
|
|
3798
|
-
agent,
|
|
3799
|
-
COUNT(*) AS task_count,
|
|
3800
|
-
COALESCE(SUM(estimated_cost), 0) AS cost_usd,
|
|
3801
|
-
COALESCE(SUM(savings_usd), 0) AS savings_usd,
|
|
3802
|
-
SUM(CASE WHEN billing_mode = 'subscription' THEN 1 ELSE 0 END) AS subscription_tasks,
|
|
3803
|
-
SUM(CASE WHEN billing_mode = 'api' THEN 1 ELSE 0 END) AS api_tasks
|
|
3804
|
-
FROM cost_entries
|
|
3805
|
-
WHERE session_id = @sessionId
|
|
3806
|
-
GROUP BY agent
|
|
3807
|
-
ORDER BY cost_usd DESC
|
|
3808
|
-
`);
|
|
3809
|
-
totalsRow = cache.getSessionCostSummaryUnfilteredTotals.get({ sessionId });
|
|
3810
|
-
agentRows = cache.getSessionCostSummaryUnfilteredAgents.all({ sessionId });
|
|
3811
|
-
}
|
|
3770
|
+
async function getSessionCostSummaryFiltered(adapter, sessionId, includePlanning) {
|
|
3771
|
+
const categoryFilter = includePlanning ? "" : "AND category != 'planning'";
|
|
3772
|
+
const totalsRows = await adapter.query(`SELECT
|
|
3773
|
+
COALESCE(SUM(estimated_cost), 0) AS total_cost_usd,
|
|
3774
|
+
COALESCE(SUM(CASE WHEN billing_mode = 'subscription' THEN COALESCE(estimated_cost, 0) ELSE 0 END), 0) AS subscription_cost_usd,
|
|
3775
|
+
COALESCE(SUM(CASE WHEN billing_mode = 'api' THEN COALESCE(estimated_cost, 0) ELSE 0 END), 0) AS api_cost_usd,
|
|
3776
|
+
COALESCE(SUM(savings_usd), 0) AS savings_usd,
|
|
3777
|
+
COUNT(*) AS task_count,
|
|
3778
|
+
SUM(CASE WHEN billing_mode = 'subscription' THEN 1 ELSE 0 END) AS subscription_task_count,
|
|
3779
|
+
SUM(CASE WHEN billing_mode = 'api' THEN 1 ELSE 0 END) AS api_task_count,
|
|
3780
|
+
MIN(timestamp) AS earliest_recorded_at
|
|
3781
|
+
FROM cost_entries
|
|
3782
|
+
WHERE session_id = ? ${categoryFilter}`, [sessionId]);
|
|
3783
|
+
const totalsRow = totalsRows[0];
|
|
3784
|
+
const agentRows = await adapter.query(`SELECT
|
|
3785
|
+
agent,
|
|
3786
|
+
COUNT(*) AS task_count,
|
|
3787
|
+
COALESCE(SUM(estimated_cost), 0) AS cost_usd,
|
|
3788
|
+
COALESCE(SUM(savings_usd), 0) AS savings_usd,
|
|
3789
|
+
SUM(CASE WHEN billing_mode = 'subscription' THEN 1 ELSE 0 END) AS subscription_tasks,
|
|
3790
|
+
SUM(CASE WHEN billing_mode = 'api' THEN 1 ELSE 0 END) AS api_tasks
|
|
3791
|
+
FROM cost_entries
|
|
3792
|
+
WHERE session_id = ? ${categoryFilter}
|
|
3793
|
+
GROUP BY agent
|
|
3794
|
+
ORDER BY cost_usd DESC`, [sessionId]);
|
|
3812
3795
|
const perAgentBreakdown = agentRows.map((row) => ({
|
|
3813
3796
|
agent: row.agent,
|
|
3814
3797
|
task_count: row.task_count,
|
|
@@ -3843,13 +3826,11 @@ function getSessionCostSummaryFiltered(db, sessionId, includePlanning) {
|
|
|
3843
3826
|
*
|
|
3844
3827
|
* Uses idx_cost_entries_session_task index.
|
|
3845
3828
|
*/
|
|
3846
|
-
function getAllCostEntriesFiltered(
|
|
3829
|
+
async function getAllCostEntriesFiltered(adapter, sessionId, includePlanning) {
|
|
3847
3830
|
const categoryFilter = includePlanning ? "" : "AND category != 'planning'";
|
|
3848
|
-
const rows =
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
ORDER BY timestamp DESC
|
|
3852
|
-
`).all({ sessionId });
|
|
3831
|
+
const rows = await adapter.query(`SELECT * FROM cost_entries
|
|
3832
|
+
WHERE session_id = ? ${categoryFilter}
|
|
3833
|
+
ORDER BY timestamp DESC`, [sessionId]);
|
|
3853
3834
|
return rows.map((row) => ({
|
|
3854
3835
|
id: row.id,
|
|
3855
3836
|
session_id: row.session_id,
|
|
@@ -3873,20 +3854,16 @@ function getAllCostEntriesFiltered(db, sessionId, includePlanning) {
|
|
|
3873
3854
|
*
|
|
3874
3855
|
* Uses idx_cost_category index.
|
|
3875
3856
|
*/
|
|
3876
|
-
function getPlanningCostTotal(
|
|
3877
|
-
const
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
WHERE session_id = @sessionId AND category = 'planning'
|
|
3882
|
-
`);
|
|
3883
|
-
const row = cache.getPlanningCostTotal.get({ sessionId });
|
|
3884
|
-
return row.planning_cost;
|
|
3857
|
+
async function getPlanningCostTotal(adapter, sessionId) {
|
|
3858
|
+
const rows = await adapter.query(`SELECT COALESCE(SUM(estimated_cost), 0) AS planning_cost
|
|
3859
|
+
FROM cost_entries
|
|
3860
|
+
WHERE session_id = ? AND category = 'planning'`, [sessionId]);
|
|
3861
|
+
return rows[0]?.planning_cost ?? 0;
|
|
3885
3862
|
}
|
|
3886
3863
|
|
|
3887
3864
|
//#endregion
|
|
3888
3865
|
//#region src/cli/commands/cost.ts
|
|
3889
|
-
function getLatestSessionId(
|
|
3866
|
+
function getLatestSessionId(_adapter) {
|
|
3890
3867
|
return null;
|
|
3891
3868
|
}
|
|
3892
3869
|
const logger$12 = createLogger("cost-cmd");
|
|
@@ -4061,7 +4038,8 @@ function formatCostCsv(summary, taskEntries) {
|
|
|
4061
4038
|
async function runCostAction(options) {
|
|
4062
4039
|
const { sessionId: explicitSessionId, outputFormat, byTask, byAgent, byBilling, includePlanning, projectRoot, version = "0.0.0" } = options;
|
|
4063
4040
|
const dbPath = join(projectRoot, ".substrate", "substrate.db");
|
|
4064
|
-
|
|
4041
|
+
const doltDir = join(projectRoot, ".substrate", "state", ".dolt");
|
|
4042
|
+
if (!existsSync(dbPath) && !existsSync(doltDir)) {
|
|
4065
4043
|
process.stderr.write(`Error: No Substrate database found at ${dbPath}. Run 'substrate init' first.\n`);
|
|
4066
4044
|
return COST_EXIT_ERROR;
|
|
4067
4045
|
}
|
|
@@ -4074,18 +4052,15 @@ async function runCostAction(options) {
|
|
|
4074
4052
|
process.stderr.write(`Error: Invalid output format '${outputFormat}'. Valid formats: ${validFormats.join(", ")}\n`);
|
|
4075
4053
|
return COST_EXIT_ERROR;
|
|
4076
4054
|
}
|
|
4077
|
-
let
|
|
4055
|
+
let adapter = null;
|
|
4078
4056
|
try {
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
return COST_EXIT_ERROR;
|
|
4085
|
-
}
|
|
4086
|
-
runMigrations(db);
|
|
4057
|
+
adapter = createDatabaseAdapter({
|
|
4058
|
+
backend: "auto",
|
|
4059
|
+
basePath: projectRoot
|
|
4060
|
+
});
|
|
4061
|
+
await initSchema(adapter);
|
|
4087
4062
|
let sessionId = explicitSessionId ?? null;
|
|
4088
|
-
if (!sessionId) sessionId = getLatestSessionId(
|
|
4063
|
+
if (!sessionId) sessionId = getLatestSessionId(adapter);
|
|
4089
4064
|
if (!sessionId) {
|
|
4090
4065
|
if (outputFormat === "json") {
|
|
4091
4066
|
const output = buildJsonOutput("substrate cost", {
|
|
@@ -4096,8 +4071,8 @@ async function runCostAction(options) {
|
|
|
4096
4071
|
} else process.stdout.write("No cost data found\n");
|
|
4097
4072
|
return COST_EXIT_SUCCESS;
|
|
4098
4073
|
}
|
|
4099
|
-
const summary = includePlanning ? getSessionCostSummary(
|
|
4100
|
-
const planningCostUsd = includePlanning ? 0 : getPlanningCostTotal(
|
|
4074
|
+
const summary = includePlanning ? await getSessionCostSummary(adapter, sessionId) : await getSessionCostSummaryFiltered(adapter, sessionId, false);
|
|
4075
|
+
const planningCostUsd = includePlanning ? 0 : await getPlanningCostTotal(adapter, sessionId);
|
|
4101
4076
|
if (summary.task_count === 0) {
|
|
4102
4077
|
if (outputFormat === "json") {
|
|
4103
4078
|
const output = buildJsonOutput("substrate cost", {
|
|
@@ -4108,25 +4083,25 @@ async function runCostAction(options) {
|
|
|
4108
4083
|
} else process.stdout.write("No cost data found\n");
|
|
4109
4084
|
return COST_EXIT_SUCCESS;
|
|
4110
4085
|
}
|
|
4111
|
-
const getFilteredEntries = () => getAllCostEntriesFiltered(
|
|
4086
|
+
const getFilteredEntries = () => getAllCostEntriesFiltered(adapter, sessionId, includePlanning);
|
|
4112
4087
|
if (outputFormat === "json") {
|
|
4113
4088
|
const jsonData = {
|
|
4114
4089
|
session_id: sessionId,
|
|
4115
4090
|
summary
|
|
4116
4091
|
};
|
|
4117
|
-
if (byTask) jsonData.tasks = getFilteredEntries();
|
|
4092
|
+
if (byTask) jsonData.tasks = await getFilteredEntries();
|
|
4118
4093
|
if (byAgent) jsonData.agents = summary.per_agent_breakdown;
|
|
4119
4094
|
const output = buildJsonOutput("substrate cost", jsonData, version);
|
|
4120
4095
|
process.stdout.write(JSON.stringify(output, null, 2) + "\n");
|
|
4121
4096
|
return COST_EXIT_SUCCESS;
|
|
4122
4097
|
}
|
|
4123
4098
|
if (outputFormat === "csv") {
|
|
4124
|
-
const csvOutput = formatCostCsv(summary, byTask ? getFilteredEntries() : void 0);
|
|
4099
|
+
const csvOutput = formatCostCsv(summary, byTask ? await getFilteredEntries() : void 0);
|
|
4125
4100
|
process.stdout.write(csvOutput + "\n");
|
|
4126
4101
|
return COST_EXIT_SUCCESS;
|
|
4127
4102
|
}
|
|
4128
4103
|
if (byTask) {
|
|
4129
|
-
const entries = getFilteredEntries();
|
|
4104
|
+
const entries = await getFilteredEntries();
|
|
4130
4105
|
process.stdout.write(formatByTaskTable(entries) + "\n");
|
|
4131
4106
|
} else if (byAgent) process.stdout.write(formatByAgentTable(summary.per_agent_breakdown) + "\n");
|
|
4132
4107
|
else if (byBilling) process.stdout.write(formatByBillingTable(summary) + "\n");
|
|
@@ -4138,8 +4113,8 @@ async function runCostAction(options) {
|
|
|
4138
4113
|
logger$12.error({ err }, "runCostAction failed");
|
|
4139
4114
|
return COST_EXIT_ERROR;
|
|
4140
4115
|
} finally {
|
|
4141
|
-
if (
|
|
4142
|
-
|
|
4116
|
+
if (adapter !== null) try {
|
|
4117
|
+
await adapter.close();
|
|
4143
4118
|
} catch {}
|
|
4144
4119
|
}
|
|
4145
4120
|
}
|
|
@@ -4167,190 +4142,211 @@ function registerCostCommand(program, version = "0.0.0", projectRoot = process.c
|
|
|
4167
4142
|
});
|
|
4168
4143
|
}
|
|
4169
4144
|
|
|
4170
|
-
//#endregion
|
|
4171
|
-
//#region src/persistence/migrations/001-monitor-schema.ts
|
|
4172
|
-
/**
|
|
4173
|
-
* Apply the monitor schema to the given database connection.
|
|
4174
|
-
* Idempotent — uses CREATE TABLE IF NOT EXISTS and CREATE INDEX IF NOT EXISTS.
|
|
4175
|
-
*/
|
|
4176
|
-
function applyMonitorSchema(db) {
|
|
4177
|
-
db.exec(`
|
|
4178
|
-
-- Migration versioning
|
|
4179
|
-
CREATE TABLE IF NOT EXISTS _schema_version (
|
|
4180
|
-
version_id INTEGER PRIMARY KEY,
|
|
4181
|
-
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4182
|
-
);
|
|
4183
|
-
INSERT OR IGNORE INTO _schema_version (version_id) VALUES (1);
|
|
4184
|
-
|
|
4185
|
-
-- Task-level execution metrics (AC1)
|
|
4186
|
-
CREATE TABLE IF NOT EXISTS task_metrics (
|
|
4187
|
-
task_id TEXT NOT NULL,
|
|
4188
|
-
agent TEXT NOT NULL,
|
|
4189
|
-
task_type TEXT NOT NULL,
|
|
4190
|
-
outcome TEXT NOT NULL CHECK(outcome IN ('success', 'failure')),
|
|
4191
|
-
failure_reason TEXT,
|
|
4192
|
-
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
4193
|
-
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
4194
|
-
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
4195
|
-
cost REAL NOT NULL DEFAULT 0.0,
|
|
4196
|
-
estimated_cost REAL NOT NULL DEFAULT 0.0,
|
|
4197
|
-
billing_mode TEXT NOT NULL DEFAULT 'api',
|
|
4198
|
-
retries INTEGER NOT NULL DEFAULT 0,
|
|
4199
|
-
recorded_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
4200
|
-
PRIMARY KEY (task_id, recorded_at)
|
|
4201
|
-
);
|
|
4202
|
-
CREATE INDEX IF NOT EXISTS idx_tm_agent ON task_metrics(agent);
|
|
4203
|
-
CREATE INDEX IF NOT EXISTS idx_tm_task_type ON task_metrics(task_type);
|
|
4204
|
-
CREATE INDEX IF NOT EXISTS idx_tm_recorded_at ON task_metrics(recorded_at);
|
|
4205
|
-
CREATE INDEX IF NOT EXISTS idx_tm_agent_type ON task_metrics(agent, task_type);
|
|
4206
|
-
|
|
4207
|
-
-- Aggregate performance stats per (agent, task_type) (for story 8.5)
|
|
4208
|
-
CREATE TABLE IF NOT EXISTS performance_aggregates (
|
|
4209
|
-
agent TEXT NOT NULL,
|
|
4210
|
-
task_type TEXT NOT NULL,
|
|
4211
|
-
total_tasks INTEGER NOT NULL DEFAULT 0,
|
|
4212
|
-
successful_tasks INTEGER NOT NULL DEFAULT 0,
|
|
4213
|
-
failed_tasks INTEGER NOT NULL DEFAULT 0,
|
|
4214
|
-
total_input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
4215
|
-
total_output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
4216
|
-
total_duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
4217
|
-
total_cost REAL NOT NULL DEFAULT 0.0,
|
|
4218
|
-
total_retries INTEGER NOT NULL DEFAULT 0,
|
|
4219
|
-
last_updated TEXT NOT NULL DEFAULT (datetime('now')),
|
|
4220
|
-
PRIMARY KEY (agent, task_type)
|
|
4221
|
-
);
|
|
4222
|
-
|
|
4223
|
-
-- Routing recommendations (for story 8.6)
|
|
4224
|
-
CREATE TABLE IF NOT EXISTS routing_recommendations (
|
|
4225
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
4226
|
-
task_type TEXT NOT NULL,
|
|
4227
|
-
current_agent TEXT NOT NULL,
|
|
4228
|
-
recommended_agent TEXT NOT NULL,
|
|
4229
|
-
reason TEXT,
|
|
4230
|
-
confidence REAL NOT NULL DEFAULT 0.0,
|
|
4231
|
-
supporting_data TEXT,
|
|
4232
|
-
generated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
4233
|
-
expires_at TEXT
|
|
4234
|
-
);
|
|
4235
|
-
`);
|
|
4236
|
-
}
|
|
4237
|
-
|
|
4238
4145
|
//#endregion
|
|
4239
4146
|
//#region src/persistence/monitor-database.ts
|
|
4240
4147
|
const logger$11 = createLogger("persistence:monitor-db");
|
|
4148
|
+
/**
|
|
4149
|
+
* DatabaseAdapter-backed MonitorDatabase implementation.
|
|
4150
|
+
*
|
|
4151
|
+
* All database operations are executed synchronously by calling the adapter's
|
|
4152
|
+
* sync-compatible query path. The adapter is accepted via constructor injection,
|
|
4153
|
+
* so any SyncAdapter-compatible backend (WASM SQLite, Dolt via sync wrapper) works.
|
|
4154
|
+
*
|
|
4155
|
+
* Schema is applied on construction via _applySchema().
|
|
4156
|
+
*/
|
|
4241
4157
|
var MonitorDatabaseImpl = class {
|
|
4242
|
-
|
|
4158
|
+
_adapter;
|
|
4159
|
+
_syncAdapter;
|
|
4243
4160
|
_path;
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
this.
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
this.
|
|
4257
|
-
this.
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
task_id, agent, task_type, outcome, failure_reason,
|
|
4262
|
-
input_tokens, output_tokens, duration_ms, cost, estimated_cost,
|
|
4263
|
-
billing_mode, recorded_at
|
|
4264
|
-
) VALUES (
|
|
4265
|
-
@taskId, @agent, @taskType, @outcome, @failureReason,
|
|
4266
|
-
@inputTokens, @outputTokens, @durationMs, @cost, @estimatedCost,
|
|
4267
|
-
@billingMode, @recordedAt
|
|
4161
|
+
constructor(databasePathOrAdapter) {
|
|
4162
|
+
if (typeof databasePathOrAdapter === "string") throw new Error("MonitorDatabaseImpl: string path constructor is no longer supported (Epic 29 SQLite removal). Use createWasmSqliteAdapter() and pass the adapter directly: new MonitorDatabaseImpl(await createWasmSqliteAdapter())");
|
|
4163
|
+
else {
|
|
4164
|
+
this._path = "<adapter>";
|
|
4165
|
+
this._adapter = databasePathOrAdapter;
|
|
4166
|
+
}
|
|
4167
|
+
this._syncAdapter = isSyncAdapter(this._adapter) ? this._adapter : null;
|
|
4168
|
+
if (this._syncAdapter === null) throw new Error("MonitorDatabaseImpl: adapter must implement SyncAdapter (querySync/execSync). Use createWasmSqliteAdapter() from src/persistence/wasm-sqlite-adapter.ts.");
|
|
4169
|
+
this._applySchemaSync();
|
|
4170
|
+
logger$11.info({ path: this._path }, "Monitor database ready");
|
|
4171
|
+
}
|
|
4172
|
+
_applySchemaSync() {
|
|
4173
|
+
if (this._syncAdapter === null) return;
|
|
4174
|
+
this._syncAdapter.execSync(`
|
|
4175
|
+
CREATE TABLE IF NOT EXISTS _schema_version (
|
|
4176
|
+
version_id INTEGER PRIMARY KEY,
|
|
4177
|
+
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
4268
4178
|
)
|
|
4269
4179
|
`);
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4180
|
+
const existing = this._syncAdapter.querySync("SELECT version_id FROM _schema_version WHERE version_id = 1");
|
|
4181
|
+
if (existing.length === 0) this._syncAdapter.querySync("INSERT INTO _schema_version (version_id) VALUES (1)");
|
|
4182
|
+
this._syncAdapter.execSync(`
|
|
4183
|
+
CREATE TABLE IF NOT EXISTS task_metrics (
|
|
4184
|
+
task_id VARCHAR(255) NOT NULL,
|
|
4185
|
+
agent VARCHAR(128) NOT NULL,
|
|
4186
|
+
task_type VARCHAR(128) NOT NULL,
|
|
4187
|
+
outcome VARCHAR(16) NOT NULL CHECK(outcome IN ('success', 'failure')),
|
|
4188
|
+
failure_reason TEXT,
|
|
4189
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
4190
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
4191
|
+
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
4192
|
+
cost DOUBLE NOT NULL DEFAULT 0.0,
|
|
4193
|
+
estimated_cost DOUBLE NOT NULL DEFAULT 0.0,
|
|
4194
|
+
billing_mode VARCHAR(32) NOT NULL DEFAULT 'api',
|
|
4195
|
+
retries INTEGER NOT NULL DEFAULT 0,
|
|
4196
|
+
recorded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
4197
|
+
PRIMARY KEY (task_id, recorded_at)
|
|
4198
|
+
)
|
|
4199
|
+
`);
|
|
4200
|
+
this._syncAdapter.execSync(`CREATE INDEX IF NOT EXISTS idx_tm_agent ON task_metrics(agent)`);
|
|
4201
|
+
this._syncAdapter.execSync(`CREATE INDEX IF NOT EXISTS idx_tm_task_type ON task_metrics(task_type)`);
|
|
4202
|
+
this._syncAdapter.execSync(`CREATE INDEX IF NOT EXISTS idx_tm_recorded_at ON task_metrics(recorded_at)`);
|
|
4203
|
+
this._syncAdapter.execSync(`CREATE INDEX IF NOT EXISTS idx_tm_agent_type ON task_metrics(agent, task_type)`);
|
|
4204
|
+
this._syncAdapter.execSync(`
|
|
4205
|
+
CREATE TABLE IF NOT EXISTS performance_aggregates (
|
|
4206
|
+
agent VARCHAR(255) NOT NULL,
|
|
4207
|
+
task_type VARCHAR(255) NOT NULL,
|
|
4208
|
+
total_tasks INTEGER NOT NULL DEFAULT 0,
|
|
4209
|
+
successful_tasks INTEGER NOT NULL DEFAULT 0,
|
|
4210
|
+
failed_tasks INTEGER NOT NULL DEFAULT 0,
|
|
4211
|
+
total_input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
4212
|
+
total_output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
4213
|
+
total_duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
4214
|
+
total_cost DOUBLE NOT NULL DEFAULT 0.0,
|
|
4215
|
+
total_retries INTEGER NOT NULL DEFAULT 0,
|
|
4216
|
+
last_updated DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
4217
|
+
PRIMARY KEY (agent, task_type)
|
|
4218
|
+
)
|
|
4219
|
+
`);
|
|
4220
|
+
this._syncAdapter.execSync(`
|
|
4221
|
+
CREATE TABLE IF NOT EXISTS routing_recommendations (
|
|
4222
|
+
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
|
4223
|
+
task_type VARCHAR(128) NOT NULL,
|
|
4224
|
+
current_agent VARCHAR(128) NOT NULL,
|
|
4225
|
+
recommended_agent VARCHAR(128) NOT NULL,
|
|
4226
|
+
reason TEXT,
|
|
4227
|
+
confidence DOUBLE NOT NULL DEFAULT 0.0,
|
|
4228
|
+
supporting_data TEXT,
|
|
4229
|
+
generated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
4230
|
+
expires_at TEXT
|
|
4277
4231
|
)
|
|
4278
|
-
ON CONFLICT(agent, task_type) DO UPDATE SET
|
|
4279
|
-
total_tasks = total_tasks + @totalTasks,
|
|
4280
|
-
successful_tasks = successful_tasks + @successfulTasks,
|
|
4281
|
-
failed_tasks = failed_tasks + @failedTasks,
|
|
4282
|
-
total_input_tokens = total_input_tokens + @inputTokens,
|
|
4283
|
-
total_output_tokens = total_output_tokens + @outputTokens,
|
|
4284
|
-
total_duration_ms = total_duration_ms + @durationMs,
|
|
4285
|
-
total_cost = total_cost + @cost,
|
|
4286
|
-
total_retries = total_retries + @retries,
|
|
4287
|
-
last_updated = @lastUpdated
|
|
4288
4232
|
`);
|
|
4289
|
-
logger$11.info({ path: this._path }, "Monitor database ready");
|
|
4290
4233
|
}
|
|
4291
4234
|
_assertOpen() {
|
|
4292
|
-
if (this.
|
|
4293
|
-
return this.
|
|
4235
|
+
if (this._syncAdapter === null || this._adapter === null) throw new Error("MonitorDatabase: connection is closed");
|
|
4236
|
+
return this._syncAdapter;
|
|
4237
|
+
}
|
|
4238
|
+
/**
|
|
4239
|
+
* Execute a query synchronously and return results.
|
|
4240
|
+
* Uses the SyncAdapter interface for guaranteed synchronous execution.
|
|
4241
|
+
*/
|
|
4242
|
+
_querySync(sql, params) {
|
|
4243
|
+
const adapter = this._assertOpen();
|
|
4244
|
+
return adapter.querySync(sql, params);
|
|
4245
|
+
}
|
|
4246
|
+
/**
|
|
4247
|
+
* Execute a mutation (INSERT/UPDATE/DELETE) synchronously.
|
|
4248
|
+
* Uses the SyncAdapter interface for guaranteed synchronous execution.
|
|
4249
|
+
*/
|
|
4250
|
+
_mutateSync(sql, params) {
|
|
4251
|
+
const adapter = this._assertOpen();
|
|
4252
|
+
adapter.querySync(sql, params);
|
|
4294
4253
|
}
|
|
4295
4254
|
insertTaskMetrics(row) {
|
|
4296
|
-
this.
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4255
|
+
const dup = this._querySync("SELECT task_id FROM task_metrics WHERE task_id = ? AND recorded_at = ?", [row.taskId, row.recordedAt]);
|
|
4256
|
+
if (dup.length > 0) return;
|
|
4257
|
+
this._mutateSync(`INSERT INTO task_metrics (
|
|
4258
|
+
task_id, agent, task_type, outcome, failure_reason,
|
|
4259
|
+
input_tokens, output_tokens, duration_ms, cost, estimated_cost,
|
|
4260
|
+
billing_mode, recorded_at
|
|
4261
|
+
) VALUES (
|
|
4262
|
+
?, ?, ?, ?, ?,
|
|
4263
|
+
?, ?, ?, ?, ?,
|
|
4264
|
+
?, ?
|
|
4265
|
+
)`, [
|
|
4266
|
+
row.taskId,
|
|
4267
|
+
row.agent,
|
|
4268
|
+
row.taskType,
|
|
4269
|
+
row.outcome,
|
|
4270
|
+
row.failureReason ?? null,
|
|
4271
|
+
row.inputTokens,
|
|
4272
|
+
row.outputTokens,
|
|
4273
|
+
row.durationMs,
|
|
4274
|
+
row.cost,
|
|
4275
|
+
row.estimatedCost,
|
|
4276
|
+
row.billingMode,
|
|
4277
|
+
row.recordedAt
|
|
4278
|
+
]);
|
|
4311
4279
|
}
|
|
4312
4280
|
updateAggregates(agent, taskType, delta) {
|
|
4313
|
-
|
|
4314
|
-
|
|
4281
|
+
const now = new Date().toISOString();
|
|
4282
|
+
const successfulTasks = delta.outcome === "success" ? 1 : 0;
|
|
4283
|
+
const failedTasks = delta.outcome === "failure" ? 1 : 0;
|
|
4284
|
+
const retries = delta.retries ?? 0;
|
|
4285
|
+
const existing = this._querySync(`SELECT agent FROM performance_aggregates WHERE agent = ? AND task_type = ?`, [agent, taskType]);
|
|
4286
|
+
if (existing.length === 0) this._mutateSync(`INSERT INTO performance_aggregates (
|
|
4287
|
+
agent, task_type, total_tasks, successful_tasks, failed_tasks,
|
|
4288
|
+
total_input_tokens, total_output_tokens, total_duration_ms, total_cost, total_retries, last_updated
|
|
4289
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
4315
4290
|
agent,
|
|
4316
4291
|
taskType,
|
|
4317
|
-
|
|
4318
|
-
successfulTasks
|
|
4319
|
-
failedTasks
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
retries
|
|
4325
|
-
|
|
4326
|
-
|
|
4292
|
+
1,
|
|
4293
|
+
successfulTasks,
|
|
4294
|
+
failedTasks,
|
|
4295
|
+
delta.inputTokens,
|
|
4296
|
+
delta.outputTokens,
|
|
4297
|
+
delta.durationMs,
|
|
4298
|
+
delta.cost,
|
|
4299
|
+
retries,
|
|
4300
|
+
now
|
|
4301
|
+
]);
|
|
4302
|
+
else this._mutateSync(`UPDATE performance_aggregates SET
|
|
4303
|
+
total_tasks = total_tasks + 1,
|
|
4304
|
+
successful_tasks = successful_tasks + ?,
|
|
4305
|
+
failed_tasks = failed_tasks + ?,
|
|
4306
|
+
total_input_tokens = total_input_tokens + ?,
|
|
4307
|
+
total_output_tokens = total_output_tokens + ?,
|
|
4308
|
+
total_duration_ms = total_duration_ms + ?,
|
|
4309
|
+
total_cost = total_cost + ?,
|
|
4310
|
+
total_retries = total_retries + ?,
|
|
4311
|
+
last_updated = ?
|
|
4312
|
+
WHERE agent = ? AND task_type = ?`, [
|
|
4313
|
+
successfulTasks,
|
|
4314
|
+
failedTasks,
|
|
4315
|
+
delta.inputTokens,
|
|
4316
|
+
delta.outputTokens,
|
|
4317
|
+
delta.durationMs,
|
|
4318
|
+
delta.cost,
|
|
4319
|
+
retries,
|
|
4320
|
+
now,
|
|
4321
|
+
agent,
|
|
4322
|
+
taskType
|
|
4323
|
+
]);
|
|
4327
4324
|
}
|
|
4328
4325
|
updatePerformanceAggregates(agent, taskType, delta) {
|
|
4329
4326
|
this.updateAggregates(agent, taskType, delta);
|
|
4330
4327
|
}
|
|
4331
4328
|
getAggregates(filter) {
|
|
4332
|
-
const db = this._assertOpen();
|
|
4333
4329
|
let sql = `
|
|
4334
4330
|
SELECT agent, task_type, total_tasks, successful_tasks, failed_tasks,
|
|
4335
4331
|
total_input_tokens, total_output_tokens, total_duration_ms, total_cost, last_updated
|
|
4336
4332
|
FROM performance_aggregates
|
|
4337
4333
|
`;
|
|
4338
4334
|
const conditions = [];
|
|
4339
|
-
const params =
|
|
4335
|
+
const params = [];
|
|
4340
4336
|
if (filter?.agent) {
|
|
4341
|
-
conditions.push("agent =
|
|
4342
|
-
params.
|
|
4337
|
+
conditions.push("agent = ?");
|
|
4338
|
+
params.push(filter.agent);
|
|
4343
4339
|
}
|
|
4344
4340
|
if (filter?.taskType) {
|
|
4345
|
-
conditions.push("task_type =
|
|
4346
|
-
params.
|
|
4341
|
+
conditions.push("task_type = ?");
|
|
4342
|
+
params.push(filter.taskType);
|
|
4347
4343
|
}
|
|
4348
4344
|
if (filter?.sinceDate) {
|
|
4349
|
-
conditions.push("last_updated >=
|
|
4350
|
-
params.
|
|
4345
|
+
conditions.push("last_updated >= ?");
|
|
4346
|
+
params.push(filter.sinceDate);
|
|
4351
4347
|
}
|
|
4352
4348
|
if (conditions.length > 0) sql += " WHERE " + conditions.join(" AND ");
|
|
4353
|
-
const rows =
|
|
4349
|
+
const rows = this._querySync(sql, params);
|
|
4354
4350
|
return rows.map((r) => ({
|
|
4355
4351
|
agent: r.agent,
|
|
4356
4352
|
taskType: r.task_type,
|
|
@@ -4365,9 +4361,7 @@ var MonitorDatabaseImpl = class {
|
|
|
4365
4361
|
}));
|
|
4366
4362
|
}
|
|
4367
4363
|
getAgentPerformance(agent) {
|
|
4368
|
-
const
|
|
4369
|
-
const row = db.prepare(`
|
|
4370
|
-
SELECT
|
|
4364
|
+
const rows = this._querySync(`SELECT
|
|
4371
4365
|
SUM(total_tasks) AS total_tasks,
|
|
4372
4366
|
SUM(successful_tasks) AS successful_tasks,
|
|
4373
4367
|
SUM(failed_tasks) AS failed_tasks,
|
|
@@ -4378,8 +4372,8 @@ var MonitorDatabaseImpl = class {
|
|
|
4378
4372
|
SUM(total_retries) AS total_retries,
|
|
4379
4373
|
MAX(last_updated) AS last_updated
|
|
4380
4374
|
FROM performance_aggregates
|
|
4381
|
-
WHERE agent =
|
|
4382
|
-
|
|
4375
|
+
WHERE agent = ?`, [agent]);
|
|
4376
|
+
const row = rows[0];
|
|
4383
4377
|
if (row == null || row.total_tasks == null || row.total_tasks === 0) return null;
|
|
4384
4378
|
const totalTasks = row.total_tasks;
|
|
4385
4379
|
const successfulTasks = row.successful_tasks ?? 0;
|
|
@@ -4402,9 +4396,7 @@ var MonitorDatabaseImpl = class {
|
|
|
4402
4396
|
};
|
|
4403
4397
|
}
|
|
4404
4398
|
getTaskTypeBreakdown(taskType) {
|
|
4405
|
-
const
|
|
4406
|
-
const rows = db.prepare(`
|
|
4407
|
-
SELECT
|
|
4399
|
+
const rows = this._querySync(`SELECT
|
|
4408
4400
|
agent,
|
|
4409
4401
|
total_tasks,
|
|
4410
4402
|
successful_tasks,
|
|
@@ -4415,9 +4407,8 @@ var MonitorDatabaseImpl = class {
|
|
|
4415
4407
|
total_cost,
|
|
4416
4408
|
last_updated
|
|
4417
4409
|
FROM performance_aggregates
|
|
4418
|
-
WHERE task_type =
|
|
4419
|
-
ORDER BY (CAST(successful_tasks AS
|
|
4420
|
-
`).all({ taskType });
|
|
4410
|
+
WHERE task_type = ?
|
|
4411
|
+
ORDER BY (CAST(successful_tasks AS DOUBLE) / NULLIF(total_tasks, 0)) DESC`, [taskType]);
|
|
4421
4412
|
if (rows.length === 0) return null;
|
|
4422
4413
|
return {
|
|
4423
4414
|
task_type: taskType,
|
|
@@ -4432,80 +4423,81 @@ var MonitorDatabaseImpl = class {
|
|
|
4432
4423
|
};
|
|
4433
4424
|
}
|
|
4434
4425
|
pruneOldData(retentionDays) {
|
|
4435
|
-
const db = this._assertOpen();
|
|
4436
4426
|
const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1e3).toISOString();
|
|
4437
|
-
const
|
|
4427
|
+
const countRows = this._querySync(`SELECT COUNT(*) AS cnt FROM task_metrics WHERE recorded_at < ?`, [cutoff]);
|
|
4428
|
+
const count = countRows[0]?.cnt ?? 0;
|
|
4429
|
+
this._mutateSync(`DELETE FROM task_metrics WHERE recorded_at < ?`, [cutoff]);
|
|
4438
4430
|
logger$11.info({
|
|
4439
4431
|
cutoff,
|
|
4440
|
-
deleted:
|
|
4432
|
+
deleted: count
|
|
4441
4433
|
}, "Pruned old task_metrics rows");
|
|
4442
|
-
return
|
|
4434
|
+
return count;
|
|
4443
4435
|
}
|
|
4444
4436
|
rebuildAggregates() {
|
|
4445
|
-
const
|
|
4446
|
-
db.exec("BEGIN IMMEDIATE");
|
|
4437
|
+
const adapter = this._assertOpen();
|
|
4447
4438
|
try {
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
INSERT INTO performance_aggregates (
|
|
4452
|
-
agent, task_type,
|
|
4453
|
-
total_tasks, successful_tasks, failed_tasks,
|
|
4454
|
-
total_input_tokens, total_output_tokens, total_duration_ms, total_cost, total_retries,
|
|
4455
|
-
last_updated
|
|
4456
|
-
)
|
|
4457
|
-
SELECT
|
|
4439
|
+
adapter.execSync(`BEGIN`);
|
|
4440
|
+
this._mutateSync(`DELETE FROM performance_aggregates`);
|
|
4441
|
+
const rows = this._querySync(`SELECT
|
|
4458
4442
|
agent,
|
|
4459
4443
|
task_type,
|
|
4460
|
-
COUNT(*)
|
|
4461
|
-
SUM(CASE WHEN outcome = 'success' THEN 1 ELSE 0 END)
|
|
4462
|
-
SUM(CASE WHEN outcome = 'failure' THEN 1 ELSE 0 END)
|
|
4463
|
-
SUM(input_tokens)
|
|
4464
|
-
SUM(output_tokens)
|
|
4465
|
-
SUM(duration_ms)
|
|
4466
|
-
SUM(cost)
|
|
4467
|
-
COALESCE(SUM(retries), 0)
|
|
4468
|
-
datetime('now') AS last_updated
|
|
4444
|
+
COUNT(*) AS total_tasks,
|
|
4445
|
+
SUM(CASE WHEN outcome = 'success' THEN 1 ELSE 0 END) AS successful_tasks,
|
|
4446
|
+
SUM(CASE WHEN outcome = 'failure' THEN 1 ELSE 0 END) AS failed_tasks,
|
|
4447
|
+
SUM(input_tokens) AS total_input_tokens,
|
|
4448
|
+
SUM(output_tokens) AS total_output_tokens,
|
|
4449
|
+
SUM(duration_ms) AS total_duration_ms,
|
|
4450
|
+
SUM(cost) AS total_cost,
|
|
4451
|
+
COALESCE(SUM(retries), 0) AS total_retries
|
|
4469
4452
|
FROM task_metrics
|
|
4470
|
-
GROUP BY agent, task_type;
|
|
4471
|
-
|
|
4472
|
-
|
|
4453
|
+
GROUP BY agent, task_type`);
|
|
4454
|
+
const now = new Date().toISOString();
|
|
4455
|
+
for (const r of rows) this._mutateSync(`INSERT INTO performance_aggregates (
|
|
4456
|
+
agent, task_type,
|
|
4457
|
+
total_tasks, successful_tasks, failed_tasks,
|
|
4458
|
+
total_input_tokens, total_output_tokens, total_duration_ms, total_cost, total_retries,
|
|
4459
|
+
last_updated
|
|
4460
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
4461
|
+
r.agent,
|
|
4462
|
+
r.task_type,
|
|
4463
|
+
r.total_tasks,
|
|
4464
|
+
r.successful_tasks,
|
|
4465
|
+
r.failed_tasks,
|
|
4466
|
+
r.total_input_tokens,
|
|
4467
|
+
r.total_output_tokens,
|
|
4468
|
+
r.total_duration_ms,
|
|
4469
|
+
r.total_cost,
|
|
4470
|
+
r.total_retries,
|
|
4471
|
+
now
|
|
4472
|
+
]);
|
|
4473
|
+
adapter.execSync(`COMMIT`);
|
|
4474
|
+
logger$11.info("Rebuilt performance_aggregates from task_metrics");
|
|
4473
4475
|
} catch (err) {
|
|
4474
|
-
|
|
4476
|
+
try {
|
|
4477
|
+
adapter.execSync(`ROLLBACK`);
|
|
4478
|
+
} catch {}
|
|
4475
4479
|
throw err;
|
|
4476
4480
|
}
|
|
4477
|
-
logger$11.info("Rebuilt performance_aggregates from task_metrics");
|
|
4478
4481
|
}
|
|
4479
4482
|
resetAllData() {
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
db.exec("DELETE FROM performance_aggregates");
|
|
4483
|
+
this._mutateSync(`DELETE FROM task_metrics`);
|
|
4484
|
+
this._mutateSync(`DELETE FROM performance_aggregates`);
|
|
4483
4485
|
logger$11.info({ path: this._path }, "Monitor data reset — all rows deleted");
|
|
4484
4486
|
}
|
|
4485
4487
|
getTaskMetricsDateRange() {
|
|
4486
|
-
const
|
|
4487
|
-
const row = db.prepare(`
|
|
4488
|
-
SELECT MIN(recorded_at) AS earliest, MAX(recorded_at) AS latest
|
|
4489
|
-
FROM task_metrics
|
|
4490
|
-
`).get();
|
|
4488
|
+
const rows = this._querySync(`SELECT MIN(recorded_at) AS earliest, MAX(recorded_at) AS latest FROM task_metrics`);
|
|
4491
4489
|
return {
|
|
4492
|
-
earliest:
|
|
4493
|
-
latest:
|
|
4490
|
+
earliest: rows[0]?.earliest ?? null,
|
|
4491
|
+
latest: rows[0]?.latest ?? null
|
|
4494
4492
|
};
|
|
4495
4493
|
}
|
|
4496
4494
|
close() {
|
|
4497
|
-
if (this.
|
|
4498
|
-
this.
|
|
4499
|
-
this.
|
|
4495
|
+
if (this._adapter === null) return;
|
|
4496
|
+
this._adapter.close();
|
|
4497
|
+
this._adapter = null;
|
|
4498
|
+
this._syncAdapter = null;
|
|
4500
4499
|
logger$11.info({ path: this._path }, "Monitor database closed");
|
|
4501
4500
|
}
|
|
4502
|
-
/**
|
|
4503
|
-
* Access the raw underlying database for testing purposes only.
|
|
4504
|
-
* @internal
|
|
4505
|
-
*/
|
|
4506
|
-
get rawDb() {
|
|
4507
|
-
return this._assertOpen();
|
|
4508
|
-
}
|
|
4509
4501
|
};
|
|
4510
4502
|
|
|
4511
4503
|
//#endregion
|
|
@@ -6901,22 +6893,25 @@ const logger$4 = createLogger("export-cmd");
|
|
|
6901
6893
|
*/
|
|
6902
6894
|
async function runExportAction(options) {
|
|
6903
6895
|
const { runId, outputDir, projectRoot, outputFormat } = options;
|
|
6904
|
-
let
|
|
6896
|
+
let adapter;
|
|
6905
6897
|
try {
|
|
6906
6898
|
const dbRoot = await resolveMainRepoRoot(projectRoot);
|
|
6907
6899
|
const dbPath = join$1(dbRoot, ".substrate", "substrate.db");
|
|
6908
|
-
|
|
6900
|
+
const doltDir = join$1(dbRoot, ".substrate", "state", ".dolt");
|
|
6901
|
+
if (!existsSync$1(dbPath) && !existsSync$1(doltDir)) {
|
|
6909
6902
|
const errorMsg = `Decision store not initialized. Run 'substrate init' first.`;
|
|
6910
6903
|
if (outputFormat === "json") process.stdout.write(JSON.stringify({ error: errorMsg }) + "\n");
|
|
6911
6904
|
else process.stderr.write(`Error: ${errorMsg}\n`);
|
|
6912
6905
|
return 1;
|
|
6913
6906
|
}
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6907
|
+
adapter = createDatabaseAdapter({
|
|
6908
|
+
backend: "auto",
|
|
6909
|
+
basePath: dbRoot
|
|
6910
|
+
});
|
|
6911
|
+
await initSchema(adapter);
|
|
6917
6912
|
let run;
|
|
6918
|
-
if (runId !== void 0 && runId !== "") run =
|
|
6919
|
-
else run = getLatestRun(
|
|
6913
|
+
if (runId !== void 0 && runId !== "") run = await getPipelineRunById(adapter, runId);
|
|
6914
|
+
else run = await getLatestRun(adapter);
|
|
6920
6915
|
if (run === void 0) {
|
|
6921
6916
|
const errorMsg = runId !== void 0 ? `Pipeline run '${runId}' not found.` : "No pipeline runs found. Run `substrate run` first.";
|
|
6922
6917
|
if (outputFormat === "json") process.stdout.write(JSON.stringify({ error: errorMsg }) + "\n");
|
|
@@ -6928,7 +6923,7 @@ async function runExportAction(options) {
|
|
|
6928
6923
|
if (!existsSync$1(resolvedOutputDir)) mkdirSync$1(resolvedOutputDir, { recursive: true });
|
|
6929
6924
|
const filesWritten = [];
|
|
6930
6925
|
const phasesExported = [];
|
|
6931
|
-
const analysisDecisions = getDecisionsByPhaseForRun(
|
|
6926
|
+
const analysisDecisions = await getDecisionsByPhaseForRun(adapter, activeRunId, "analysis");
|
|
6932
6927
|
if (analysisDecisions.length > 0) {
|
|
6933
6928
|
const content = renderProductBrief(analysisDecisions);
|
|
6934
6929
|
if (content !== "") {
|
|
@@ -6939,9 +6934,9 @@ async function runExportAction(options) {
|
|
|
6939
6934
|
if (outputFormat === "human") process.stdout.write(` Written: ${filePath}\n`);
|
|
6940
6935
|
}
|
|
6941
6936
|
}
|
|
6942
|
-
const planningDecisions = getDecisionsByPhaseForRun(
|
|
6937
|
+
const planningDecisions = await getDecisionsByPhaseForRun(adapter, activeRunId, "planning");
|
|
6943
6938
|
if (planningDecisions.length > 0) {
|
|
6944
|
-
const requirements = listRequirements(
|
|
6939
|
+
const requirements = (await listRequirements(adapter)).filter((r) => r.pipeline_run_id === activeRunId);
|
|
6945
6940
|
const content = renderPrd(planningDecisions, requirements);
|
|
6946
6941
|
if (content !== "") {
|
|
6947
6942
|
const filePath = join$1(resolvedOutputDir, "prd.md");
|
|
@@ -6951,7 +6946,7 @@ async function runExportAction(options) {
|
|
|
6951
6946
|
if (outputFormat === "human") process.stdout.write(` Written: ${filePath}\n`);
|
|
6952
6947
|
}
|
|
6953
6948
|
}
|
|
6954
|
-
const solutioningDecisions = getDecisionsByPhaseForRun(
|
|
6949
|
+
const solutioningDecisions = await getDecisionsByPhaseForRun(adapter, activeRunId, "solutioning");
|
|
6955
6950
|
if (solutioningDecisions.length > 0) {
|
|
6956
6951
|
const archContent = renderArchitecture(solutioningDecisions);
|
|
6957
6952
|
if (archContent !== "") {
|
|
@@ -6978,7 +6973,7 @@ async function runExportAction(options) {
|
|
|
6978
6973
|
if (outputFormat === "human") process.stdout.write(` Written: ${filePath}\n`);
|
|
6979
6974
|
}
|
|
6980
6975
|
}
|
|
6981
|
-
const operationalDecisions = getDecisionsByCategory(
|
|
6976
|
+
const operationalDecisions = await getDecisionsByCategory(adapter, OPERATIONAL_FINDING);
|
|
6982
6977
|
if (operationalDecisions.length > 0) {
|
|
6983
6978
|
const operationalContent = renderOperationalFindings(operationalDecisions);
|
|
6984
6979
|
if (operationalContent !== "") {
|
|
@@ -6989,7 +6984,7 @@ async function runExportAction(options) {
|
|
|
6989
6984
|
if (outputFormat === "human") process.stdout.write(` Written: ${filePath}\n`);
|
|
6990
6985
|
}
|
|
6991
6986
|
}
|
|
6992
|
-
const experimentDecisions = getDecisionsByCategory(
|
|
6987
|
+
const experimentDecisions = await getDecisionsByCategory(adapter, EXPERIMENT_RESULT);
|
|
6993
6988
|
if (experimentDecisions.length > 0) {
|
|
6994
6989
|
const experimentsContent = renderExperiments(experimentDecisions);
|
|
6995
6990
|
if (experimentsContent !== "") {
|
|
@@ -7024,8 +7019,8 @@ async function runExportAction(options) {
|
|
|
7024
7019
|
logger$4.error({ err }, "export action failed");
|
|
7025
7020
|
return 1;
|
|
7026
7021
|
} finally {
|
|
7027
|
-
if (
|
|
7028
|
-
|
|
7022
|
+
if (adapter !== void 0) try {
|
|
7023
|
+
await adapter.close();
|
|
7029
7024
|
} catch {}
|
|
7030
7025
|
}
|
|
7031
7026
|
}
|
|
@@ -7056,11 +7051,11 @@ function registerExportCommand(program, _version = "0.0.0", projectRoot = proces
|
|
|
7056
7051
|
* - When `runId` is omitted, the runId of the last (most recently created)
|
|
7057
7052
|
* escalation-diagnosis decision is used as the default (AC1 defaulting).
|
|
7058
7053
|
*
|
|
7059
|
-
* @param
|
|
7060
|
-
* @param runId
|
|
7054
|
+
* @param adapter The database adapter
|
|
7055
|
+
* @param runId Optional run ID to scope the query
|
|
7061
7056
|
*/
|
|
7062
|
-
function getRetryableEscalations(
|
|
7063
|
-
const decisions = getDecisionsByCategory(
|
|
7057
|
+
async function getRetryableEscalations(adapter, runId) {
|
|
7058
|
+
const decisions = await getDecisionsByCategory(adapter, ESCALATION_DIAGNOSIS);
|
|
7064
7059
|
const result = {
|
|
7065
7060
|
retryable: [],
|
|
7066
7061
|
skipped: []
|
|
@@ -7113,18 +7108,20 @@ async function runRetryEscalatedAction(options) {
|
|
|
7113
7108
|
const { runId, dryRun, outputFormat, projectRoot, concurrency, pack: packName, registry: injectedRegistry } = options;
|
|
7114
7109
|
const dbRoot = await resolveMainRepoRoot(projectRoot);
|
|
7115
7110
|
const dbPath = join(dbRoot, ".substrate", "substrate.db");
|
|
7116
|
-
|
|
7111
|
+
const doltDir = join(dbRoot, ".substrate", "state", ".dolt");
|
|
7112
|
+
if (!existsSync(dbPath) && !existsSync(doltDir)) {
|
|
7117
7113
|
const errorMsg = `Decision store not initialized. Run 'substrate init' first.`;
|
|
7118
7114
|
if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
7119
7115
|
else process.stderr.write(`Error: ${errorMsg}\n`);
|
|
7120
7116
|
return 1;
|
|
7121
7117
|
}
|
|
7122
|
-
const
|
|
7118
|
+
const adapter = createDatabaseAdapter({
|
|
7119
|
+
backend: "auto",
|
|
7120
|
+
basePath: dbRoot
|
|
7121
|
+
});
|
|
7123
7122
|
try {
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
const db = dbWrapper.db;
|
|
7127
|
-
const { retryable, skipped } = getRetryableEscalations(db, runId);
|
|
7123
|
+
await initSchema(adapter);
|
|
7124
|
+
const { retryable, skipped } = await getRetryableEscalations(adapter, runId);
|
|
7128
7125
|
if (retryable.length === 0) {
|
|
7129
7126
|
if (outputFormat === "json") process.stdout.write(formatOutput({
|
|
7130
7127
|
retryKeys: [],
|
|
@@ -7162,7 +7159,7 @@ async function runRetryEscalatedAction(options) {
|
|
|
7162
7159
|
process.stdout.write(`Retrying: ${count} ${count === 1 ? "story" : "stories"} — ${retryable.join(", ")}\n`);
|
|
7163
7160
|
for (const s of skipped) process.stdout.write(`Skipping: ${s.key} (${s.reason})\n`);
|
|
7164
7161
|
}
|
|
7165
|
-
const pipelineRun = createPipelineRun(
|
|
7162
|
+
const pipelineRun = await createPipelineRun(adapter, {
|
|
7166
7163
|
methodology: pack.manifest.name,
|
|
7167
7164
|
start_phase: "implementation",
|
|
7168
7165
|
config_json: JSON.stringify({
|
|
@@ -7172,14 +7169,14 @@ async function runRetryEscalatedAction(options) {
|
|
|
7172
7169
|
})
|
|
7173
7170
|
});
|
|
7174
7171
|
const eventBus = createEventBus();
|
|
7175
|
-
const contextCompiler = createContextCompiler({ db });
|
|
7172
|
+
const contextCompiler = createContextCompiler({ db: adapter });
|
|
7176
7173
|
if (!injectedRegistry) throw new Error("AdapterRegistry is required — must be initialized at CLI startup");
|
|
7177
7174
|
const dispatcher = createDispatcher({
|
|
7178
7175
|
eventBus,
|
|
7179
7176
|
adapterRegistry: injectedRegistry
|
|
7180
7177
|
});
|
|
7181
7178
|
const orchestrator = createImplementationOrchestrator({
|
|
7182
|
-
db,
|
|
7179
|
+
db: adapter,
|
|
7183
7180
|
pack,
|
|
7184
7181
|
contextCompiler,
|
|
7185
7182
|
dispatcher,
|
|
@@ -7197,12 +7194,14 @@ async function runRetryEscalatedAction(options) {
|
|
|
7197
7194
|
if (result?.tokenUsage !== void 0) {
|
|
7198
7195
|
const { input, output } = result.tokenUsage;
|
|
7199
7196
|
const costUsd = (input * 3 + output * 15) / 1e6;
|
|
7200
|
-
addTokenUsage(
|
|
7197
|
+
addTokenUsage(adapter, pipelineRun.id, {
|
|
7201
7198
|
phase: payload.phase,
|
|
7202
7199
|
agent: "claude-code",
|
|
7203
7200
|
input_tokens: input,
|
|
7204
7201
|
output_tokens: output,
|
|
7205
7202
|
cost_usd: costUsd
|
|
7203
|
+
}).catch((err) => {
|
|
7204
|
+
logger$3.warn({ err }, "Failed to record token usage");
|
|
7206
7205
|
});
|
|
7207
7206
|
}
|
|
7208
7207
|
} catch (err) {
|
|
@@ -7232,7 +7231,7 @@ async function runRetryEscalatedAction(options) {
|
|
|
7232
7231
|
return 1;
|
|
7233
7232
|
} finally {
|
|
7234
7233
|
try {
|
|
7235
|
-
|
|
7234
|
+
await adapter.close();
|
|
7236
7235
|
} catch {}
|
|
7237
7236
|
}
|
|
7238
7237
|
}
|