substrate-ai 0.2.39 → 0.3.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/cli/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DatabaseWrapper, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, buildPipelineStatusOutput, createConfigSystem, createContextCompiler, createDispatcher, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveStoryKeys, runAnalysisPhase, runMigrations, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-RJ0EHbfM.js";
2
+ import { DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DatabaseWrapper, DoltNotInstalled, FileStateStore, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, buildPipelineStatusOutput, checkDoltInstalled, createConfigSystem, createContextCompiler, createDispatcher, createDoltClient, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStateStore, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, initializeDolt, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveStoryKeys, runAnalysisPhase, runMigrations, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-DO9n3cwy.js";
3
3
  import { createLogger } from "../logger-D2fS2ccL.js";
4
4
  import { AdapterRegistry } from "../adapter-registry-PsWhP_1Q.js";
5
5
  import { CURRENT_CONFIG_FORMAT_VERSION, CURRENT_TASK_GRAPH_VERSION, PartialSubstrateConfigSchema } from "../config-migrator-DSi8KhQC.js";
@@ -18,11 +18,11 @@ import yaml from "js-yaml";
18
18
  import { createRequire } from "node:module";
19
19
  import * as path$1 from "node:path";
20
20
  import { isAbsolute, join as join$1 } from "node:path";
21
- import BetterSqlite3 from "better-sqlite3";
21
+ import Database from "better-sqlite3";
22
+ import { access as access$1 } from "node:fs/promises";
22
23
  import { existsSync as existsSync$1, mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1 } from "node:fs";
23
24
  import { createInterface } from "node:readline";
24
25
  import { homedir } from "os";
25
- import { access as access$1 } from "node:fs/promises";
26
26
  import { randomUUID } from "crypto";
27
27
  import { createInterface as createInterface$1 } from "readline";
28
28
 
@@ -662,13 +662,37 @@ async function runInitAction(options) {
662
662
  await scaffoldStatuslineScript(projectRoot);
663
663
  await scaffoldClaudeSettings(projectRoot);
664
664
  await scaffoldClaudeCommands(projectRoot, outputFormat);
665
+ const doltMode = options.doltMode ?? "auto";
666
+ let doltInitialized = false;
667
+ if (doltMode !== "skip") try {
668
+ if (doltMode === "auto") await checkDoltInstalled();
669
+ await initializeDolt({ projectRoot });
670
+ doltInitialized = true;
671
+ } catch (err) {
672
+ if (err instanceof DoltNotInstalled) {
673
+ if (doltMode === "force") {
674
+ process.stderr.write(`${err.message}\n`);
675
+ return INIT_EXIT_ERROR;
676
+ }
677
+ logger$16.debug("Dolt not installed, skipping auto-init");
678
+ } else {
679
+ const msg = err instanceof Error ? err.message : String(err);
680
+ if (doltMode === "force") {
681
+ process.stderr.write(`✗ Dolt initialization failed: ${msg}\n`);
682
+ return INIT_EXIT_ERROR;
683
+ }
684
+ logger$16.warn("Dolt auto-init failed (non-blocking)", { error: msg });
685
+ }
686
+ }
687
+ else logger$16.debug("Dolt step was skipped (--no-dolt)");
665
688
  const successMsg = `Pack '${packName}' and database initialized successfully at ${dbPath}`;
666
689
  if (outputFormat === "json") process.stdout.write(formatOutput({
667
690
  pack: packName,
668
691
  dbPath,
669
692
  scaffolded,
670
693
  configPath,
671
- routingPolicyPath
694
+ routingPolicyPath,
695
+ doltInitialized
672
696
  }, "json", true) + "\n");
673
697
  else {
674
698
  process.stdout.write(`\n Substrate initialized successfully!\n\n`);
@@ -683,6 +707,7 @@ async function runInitAction(options) {
683
707
  process.stdout.write(` CLAUDE.md pipeline instructions for Claude Code\n`);
684
708
  process.stdout.write(` .claude/commands/ /substrate-run, /substrate-supervisor, /substrate-metrics\n`);
685
709
  process.stdout.write(` .substrate/ config, database, routing policy\n`);
710
+ if (doltInitialized) process.stdout.write(`✓ Dolt state store initialized at .substrate/state/\n`);
686
711
  process.stdout.write("\n Next steps:\n 1. Start a Claude Code session in this project\n 2. Tell Claude: \"Run the substrate pipeline\"\n 3. Or use the /substrate-run slash command for a guided run\n");
687
712
  }
688
713
  return INIT_EXIT_SUCCESS;
@@ -695,14 +720,16 @@ async function runInitAction(options) {
695
720
  }
696
721
  }
697
722
  function registerInitCommand(program, _version, registry) {
698
- program.command("init").description("Initialize Substrate — creates config, scaffolds methodology pack, and sets up database").option("--pack <name>", "Methodology pack name", "bmad").option("--project-root <path>", "Project root directory", process.cwd()).option("-y, --yes", "Skip all interactive prompts and use defaults", false).option("--force", "Overwrite existing files and packs", false).option("--output-format <format>", "Output format: human (default) or json", "human").action(async (opts) => {
723
+ program.command("init").description("Initialize Substrate — creates config, scaffolds methodology pack, and sets up database").option("--pack <name>", "Methodology pack name", "bmad").option("--project-root <path>", "Project root directory", process.cwd()).option("-y, --yes", "Skip all interactive prompts and use defaults", false).option("--force", "Overwrite existing files and packs", false).option("--output-format <format>", "Output format: human (default) or json", "human").option("--dolt", "Initialize Dolt state database as part of init (forces Dolt bootstrapping)", false).option("--no-dolt", "Skip Dolt state store initialization even if Dolt is installed").action(async (opts) => {
699
724
  const outputFormat = opts.outputFormat === "json" ? "json" : "human";
725
+ const doltMode = opts.noDolt ? "skip" : opts.dolt ? "force" : "auto";
700
726
  const exitCode = await runInitAction({
701
727
  pack: opts.pack,
702
728
  projectRoot: opts.projectRoot,
703
729
  outputFormat,
704
730
  force: opts.force,
705
731
  yes: opts.yes,
732
+ doltMode,
706
733
  ...registry !== void 0 && { registry }
707
734
  });
708
735
  process.exitCode = exitCode;
@@ -1281,7 +1308,32 @@ function registerResumeCommand(program, _version = "0.0.0", projectRoot = proces
1281
1308
  //#region src/cli/commands/status.ts
1282
1309
  const logger$13 = createLogger("status-cmd");
1283
1310
  async function runStatusAction(options) {
1284
- const { outputFormat, runId, projectRoot } = options;
1311
+ const { outputFormat, runId, projectRoot, stateStore, history } = options;
1312
+ if (history === true) {
1313
+ if (!stateStore) {
1314
+ process.stdout.write("History not available with file backend. Use Dolt backend for state history.\n");
1315
+ return 0;
1316
+ }
1317
+ try {
1318
+ const entries = await stateStore.getHistory(20);
1319
+ if (outputFormat === "json") {
1320
+ process.stdout.write(JSON.stringify(entries, null, 2) + "\n");
1321
+ return 0;
1322
+ }
1323
+ process.stdout.write("TIMESTAMP HASH MESSAGE\n");
1324
+ for (const entry of entries) {
1325
+ const ts = (entry.timestamp ?? "").padEnd(20);
1326
+ const hash = (entry.hash ?? "").padEnd(8);
1327
+ process.stdout.write(`${ts} ${hash} ${entry.message}\n`);
1328
+ }
1329
+ return 0;
1330
+ } catch (err) {
1331
+ const msg = err instanceof Error ? err.message : String(err);
1332
+ if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, msg) + "\n");
1333
+ else process.stderr.write(`Error: ${msg}\n`);
1334
+ return 1;
1335
+ }
1336
+ }
1285
1337
  const dbRoot = await resolveMainRepoRoot(projectRoot);
1286
1338
  const dbPath = join(dbRoot, ".substrate", "substrate.db");
1287
1339
  if (!existsSync(dbPath)) {
@@ -1306,6 +1358,12 @@ async function runStatusAction(options) {
1306
1358
  const tokenSummary = getTokenUsageSummary(db, run.id);
1307
1359
  const decisionsCount = db.prepare(`SELECT COUNT(*) as cnt FROM decisions WHERE pipeline_run_id = ?`).get(run.id)?.cnt ?? 0;
1308
1360
  const storiesCount = db.prepare(`SELECT COUNT(*) as cnt FROM requirements WHERE pipeline_run_id = ? AND source = 'solutioning-phase'`).get(run.id)?.cnt ?? 0;
1361
+ let storeStories = [];
1362
+ if (stateStore) try {
1363
+ storeStories = await stateStore.queryStories({});
1364
+ } catch (err) {
1365
+ logger$13.debug("StateStore query failed, continuing without store data", { err });
1366
+ }
1309
1367
  if (outputFormat === "json") {
1310
1368
  const statusOutput = buildPipelineStatusOutput(run, tokenSummary, decisionsCount, storiesCount);
1311
1369
  const storyMetricsRows = getStoryMetricsForRun(db, run.id);
@@ -1352,7 +1410,8 @@ async function runStatusAction(options) {
1352
1410
  total_output_tokens: totalOutputTokens,
1353
1411
  stories_per_hour: storiesPerHour,
1354
1412
  cost_usd: totalCostUsd
1355
- }
1413
+ },
1414
+ story_states: storeStories
1356
1415
  };
1357
1416
  process.stdout.write(formatOutput(enhancedOutput, "json", true) + "\n");
1358
1417
  } else {
@@ -1394,6 +1453,10 @@ async function runStatusAction(options) {
1394
1453
  }
1395
1454
  }
1396
1455
  }
1456
+ if (storeStories.length > 0) {
1457
+ process.stdout.write("\nStateStore Story States:\n");
1458
+ for (const s of storeStories) process.stdout.write(` ${s.storyKey}: ${s.phase} (${s.reviewCycles} review cycles)\n`);
1459
+ }
1397
1460
  process.stdout.write("\n");
1398
1461
  process.stdout.write(formatTokenTelemetry(tokenSummary) + "\n");
1399
1462
  }
@@ -1411,14 +1474,34 @@ async function runStatusAction(options) {
1411
1474
  }
1412
1475
  }
1413
1476
  function registerStatusCommand(program, _version = "0.0.0", projectRoot = process.cwd()) {
1414
- program.command("status").description("Show status of the most recent (or specified) pipeline run").option("--run-id <id>", "Pipeline run ID to query (defaults to latest)").option("--project-root <path>", "Project root directory", projectRoot).option("--output-format <format>", "Output format: human (default) or json", "human").action(async (opts) => {
1477
+ program.command("status").description("Show status of the most recent (or specified) pipeline run").option("--run-id <id>", "Pipeline run ID to query (defaults to latest)").option("--project-root <path>", "Project root directory", projectRoot).option("--output-format <format>", "Output format: human (default) or json", "human").option("--history", "Show Dolt commit history for the state store").action(async (opts) => {
1415
1478
  const outputFormat = opts.outputFormat === "json" ? "json" : "human";
1416
- const exitCode = await runStatusAction({
1417
- outputFormat,
1418
- runId: opts.runId,
1419
- projectRoot: opts.projectRoot
1420
- });
1421
- process.exitCode = exitCode;
1479
+ const root = opts.projectRoot;
1480
+ let stateStore;
1481
+ const doltStatePath = join(root, ".substrate", "state", ".dolt");
1482
+ if (existsSync(doltStatePath)) try {
1483
+ stateStore = createStateStore({
1484
+ backend: "dolt",
1485
+ basePath: join(root, ".substrate", "state")
1486
+ });
1487
+ await stateStore.initialize();
1488
+ } catch {
1489
+ stateStore = void 0;
1490
+ }
1491
+ try {
1492
+ const exitCode = await runStatusAction({
1493
+ outputFormat,
1494
+ runId: opts.runId,
1495
+ projectRoot: root,
1496
+ stateStore,
1497
+ history: opts.history
1498
+ });
1499
+ process.exitCode = exitCode;
1500
+ } finally {
1501
+ try {
1502
+ await stateStore?.close();
1503
+ } catch {}
1504
+ }
1422
1505
  });
1423
1506
  }
1424
1507
 
@@ -2581,7 +2664,7 @@ async function runSupervisorAction(options, deps = {}) {
2581
2664
  const expDb = expDbWrapper.db;
2582
2665
  const { runRunAction: runPipeline } = await import(
2583
2666
  /* @vite-ignore */
2584
- "../run-Q-rgJf4a.js"
2667
+ "../run-DP932Mmn.js"
2585
2668
  );
2586
2669
  const runStoryFn = async (opts) => {
2587
2670
  const exitCode = await runPipeline({
@@ -2826,7 +2909,7 @@ function registerSupervisorCommand(program, _version = "0.0.0", projectRoot = pr
2826
2909
  //#region src/cli/commands/metrics.ts
2827
2910
  const logger$11 = createLogger("metrics-cmd");
2828
2911
  async function runMetricsAction(options) {
2829
- const { outputFormat, projectRoot, limit = 10, compare, tagBaseline, analysis } = options;
2912
+ const { outputFormat, projectRoot, limit = 10, compare, tagBaseline, analysis, sprint, story, taskType, since, aggregate } = options;
2830
2913
  if (analysis !== void 0) {
2831
2914
  const dbRoot$1 = await resolveMainRepoRoot(projectRoot);
2832
2915
  const reportBase = join(dbRoot$1, "_bmad-output", "supervisor-reports", `${analysis}-analysis`);
@@ -2906,6 +2989,26 @@ async function runMetricsAction(options) {
2906
2989
  return 0;
2907
2990
  }
2908
2991
  const runs = listRunMetrics(db, limit);
2992
+ let doltMetrics;
2993
+ const doltStatePath = join(dbRoot, ".substrate", "state", ".dolt");
2994
+ const hasDoltFilters = sprint !== void 0 || story !== void 0 || taskType !== void 0 || since !== void 0 || aggregate === true;
2995
+ if (existsSync(doltStatePath) && hasDoltFilters) try {
2996
+ const stateStore = createStateStore({
2997
+ backend: "dolt",
2998
+ basePath: join(dbRoot, ".substrate", "state")
2999
+ });
3000
+ await stateStore.initialize();
3001
+ const doltFilter = {};
3002
+ if (sprint !== void 0) doltFilter.sprint = sprint;
3003
+ if (story !== void 0) doltFilter.storyKey = story;
3004
+ if (taskType !== void 0) doltFilter.taskType = taskType;
3005
+ if (since !== void 0) doltFilter.since = since;
3006
+ if (aggregate !== void 0) doltFilter.aggregate = aggregate;
3007
+ doltMetrics = await stateStore.queryMetrics(doltFilter);
3008
+ await stateStore.close();
3009
+ } catch (doltErr) {
3010
+ logger$11.warn({ err: doltErr }, "StateStore query failed — falling back to SQLite metrics only");
3011
+ }
2909
3012
  const storyMetricDecisions = getDecisionsByCategory(db, STORY_METRICS);
2910
3013
  const storyMetrics = storyMetricDecisions.map((d) => {
2911
3014
  const colonIdx = d.key.indexOf(":");
@@ -2935,12 +3038,31 @@ async function runMetricsAction(options) {
2935
3038
  };
2936
3039
  }
2937
3040
  });
2938
- if (outputFormat === "json") process.stdout.write(formatOutput({
2939
- runs,
2940
- story_metrics: storyMetrics
2941
- }, "json", true) + "\n");
2942
- else {
2943
- if (runs.length === 0 && storyMetrics.length === 0) {
3041
+ if (outputFormat === "json") {
3042
+ const jsonPayload = {
3043
+ runs,
3044
+ story_metrics: storyMetrics
3045
+ };
3046
+ if (doltMetrics !== void 0) if (aggregate) {
3047
+ const aggregateResults = doltMetrics.map((m) => ({
3048
+ task_type: m.taskType,
3049
+ count: m.count ?? 0,
3050
+ avg_cost_usd: m.costUsd ?? 0,
3051
+ sum_tokens_in: m.tokensIn ?? 0,
3052
+ sum_tokens_out: m.tokensOut ?? 0
3053
+ }));
3054
+ const aggregateTotals = {
3055
+ total_count: aggregateResults.reduce((sum, r) => sum + r.count, 0),
3056
+ total_avg_cost_usd: aggregateResults.reduce((sum, r) => sum + r.avg_cost_usd, 0),
3057
+ total_tokens_in: aggregateResults.reduce((sum, r) => sum + r.sum_tokens_in, 0),
3058
+ total_tokens_out: aggregateResults.reduce((sum, r) => sum + r.sum_tokens_out, 0)
3059
+ };
3060
+ jsonPayload.aggregate_metrics = aggregateResults;
3061
+ jsonPayload.aggregate_totals = aggregateTotals;
3062
+ } else jsonPayload.dolt_metrics = doltMetrics;
3063
+ process.stdout.write(formatOutput(jsonPayload, "json", true) + "\n");
3064
+ } else {
3065
+ if (runs.length === 0 && storyMetrics.length === 0 && (doltMetrics === void 0 || doltMetrics.length === 0)) {
2944
3066
  process.stdout.write("No run metrics recorded yet. Run `substrate run` to generate metrics.\n");
2945
3067
  return 0;
2946
3068
  }
@@ -2970,6 +3092,41 @@ async function runMetricsAction(options) {
2970
3092
  process.stdout.write(` ${sm.story_key.padEnd(16)} ${runShort.padEnd(12)} ${String(sm.wall_clock_seconds).padStart(8)} ${sm.input_tokens.toLocaleString().padStart(10)} ${sm.output_tokens.toLocaleString().padStart(11)} ${String(sm.review_cycles).padStart(7)} ${stalledStr.padStart(8)}${costStr}\n`);
2971
3093
  }
2972
3094
  }
3095
+ if (doltMetrics !== void 0 && doltMetrics.length > 0) if (aggregate) {
3096
+ process.stdout.write(`\nStateStore Aggregate Metrics (by task type)\n`);
3097
+ process.stdout.write("─".repeat(80) + "\n");
3098
+ process.stdout.write(` ${"Task Type".padEnd(20)} ${"Count".padStart(8)} ${"Avg Cost".padStart(12)} ${"Sum Tokens In".padStart(14)} ${"Sum Tokens Out".padStart(15)}\n`);
3099
+ process.stdout.write(" " + "─".repeat(72) + "\n");
3100
+ let totalCount = 0;
3101
+ let totalCost = 0;
3102
+ let totalTokensIn = 0;
3103
+ let totalTokensOut = 0;
3104
+ for (const m of doltMetrics) {
3105
+ const count = m.count ?? 0;
3106
+ const avgCost = m.costUsd !== void 0 ? `$${m.costUsd.toFixed(4)}` : "-";
3107
+ const sumIn = m.tokensIn !== void 0 ? m.tokensIn.toLocaleString() : "-";
3108
+ const sumOut = m.tokensOut !== void 0 ? m.tokensOut.toLocaleString() : "-";
3109
+ totalCount += count;
3110
+ totalCost += m.costUsd ?? 0;
3111
+ totalTokensIn += m.tokensIn ?? 0;
3112
+ totalTokensOut += m.tokensOut ?? 0;
3113
+ process.stdout.write(` ${m.taskType.padEnd(20)} ${String(count).padStart(8)} ${avgCost.padStart(12)} ${sumIn.padStart(14)} ${sumOut.padStart(15)}\n`);
3114
+ }
3115
+ process.stdout.write(" " + "─".repeat(72) + "\n");
3116
+ process.stdout.write(` ${"TOTAL".padEnd(20)} ${String(totalCount).padStart(8)} ${`$${totalCost.toFixed(4)}`.padStart(12)} ${totalTokensIn.toLocaleString().padStart(14)} ${totalTokensOut.toLocaleString().padStart(15)}\n`);
3117
+ } else {
3118
+ process.stdout.write(`\nStateStore Metrics (${doltMetrics.length} records)\n`);
3119
+ process.stdout.write("─".repeat(80) + "\n");
3120
+ process.stdout.write(` ${"Story".padEnd(16)} ${"Task Type".padEnd(16)} ${"Tokens In".padStart(10)} ${"Tokens Out".padStart(11)} ${"Wall(ms)".padStart(10)} ${"Result".padEnd(12)}\n`);
3121
+ process.stdout.write(" " + "─".repeat(76) + "\n");
3122
+ for (const m of doltMetrics) {
3123
+ const tokIn = m.tokensIn !== void 0 ? m.tokensIn.toLocaleString() : "-";
3124
+ const tokOut = m.tokensOut !== void 0 ? m.tokensOut.toLocaleString() : "-";
3125
+ const wall = m.wallClockMs !== void 0 ? String(m.wallClockMs) : "-";
3126
+ const res = m.result ?? "-";
3127
+ process.stdout.write(` ${m.storyKey.padEnd(16)} ${m.taskType.padEnd(16)} ${tokIn.padStart(10)} ${tokOut.padStart(11)} ${wall.padStart(10)} ${res.padEnd(12)}\n`);
3128
+ }
3129
+ }
2973
3130
  }
2974
3131
  return 0;
2975
3132
  } catch (err) {
@@ -2985,7 +3142,7 @@ async function runMetricsAction(options) {
2985
3142
  }
2986
3143
  }
2987
3144
  function registerMetricsCommand(program, _version = "0.0.0", projectRoot = process.cwd()) {
2988
- program.command("metrics").description("Show historical pipeline run metrics and cross-run comparison").option("--project-root <path>", "Project root directory", projectRoot).option("--output-format <format>", "Output format: human (default) or json", "human").option("--limit <n>", "Number of runs to show (default: 10)", (v) => parseInt(v, 10), 10).option("--compare <run-id-a,run-id-b>", "Compare two runs side-by-side (comma-separated IDs, e.g. abc123,def456)").option("--tag-baseline <run-id>", "Mark a run as the performance baseline").option("--analysis <run-id>", "Read and output the analysis report for the specified run (AC5 of Story 17-3)").action(async (opts) => {
3145
+ program.command("metrics").description("Show historical pipeline run metrics and cross-run comparison").option("--project-root <path>", "Project root directory", projectRoot).option("--output-format <format>", "Output format: human (default) or json", "human").option("--limit <n>", "Number of runs to show (default: 10)", (v) => parseInt(v, 10), 10).option("--compare <run-id-a,run-id-b>", "Compare two runs side-by-side (comma-separated IDs, e.g. abc123,def456)").option("--tag-baseline <run-id>", "Mark a run as the performance baseline").option("--analysis <run-id>", "Read and output the analysis report for the specified run (AC5 of Story 17-3)").option("--sprint <sprint>", "Filter StateStore metrics by sprint (e.g. sprint-1)").option("--story <story-key>", "Filter StateStore metrics by story key (e.g. 26-1)").option("--task-type <type>", "Filter StateStore metrics by task type (e.g. dev-story)").option("--since <iso-date>", "Filter StateStore metrics at or after this ISO timestamp").option("--aggregate", "Aggregate StateStore metrics grouped by task_type").action(async (opts) => {
2989
3146
  const outputFormat = opts.outputFormat === "json" ? "json" : "human";
2990
3147
  let compareIds;
2991
3148
  if (opts.compare !== void 0) {
@@ -2998,12 +3155,165 @@ function registerMetricsCommand(program, _version = "0.0.0", projectRoot = proce
2998
3155
  limit: opts.limit,
2999
3156
  compare: compareIds,
3000
3157
  tagBaseline: opts.tagBaseline,
3001
- analysis: opts.analysis
3158
+ analysis: opts.analysis,
3159
+ sprint: opts.sprint,
3160
+ story: opts.story,
3161
+ taskType: opts.taskType,
3162
+ since: opts.since,
3163
+ aggregate: opts.aggregate
3002
3164
  });
3003
3165
  process.exitCode = exitCode;
3004
3166
  });
3005
3167
  }
3006
3168
 
3169
+ //#endregion
3170
+ //#region src/cli/commands/migrate.ts
3171
+ /**
3172
+ * Open the SQLite database at `dbPath` (read-only) and return a snapshot of
3173
+ * the story_metrics rows. Returns an empty snapshot if the file does not
3174
+ * exist or the table is missing.
3175
+ */
3176
+ function readSqliteSnapshot(dbPath) {
3177
+ let db = null;
3178
+ try {
3179
+ db = new Database(dbPath, { readonly: true });
3180
+ } catch {
3181
+ return { storyMetrics: [] };
3182
+ }
3183
+ try {
3184
+ const rows = db.prepare(`SELECT story_key, result, completed_at, created_at,
3185
+ wall_clock_seconds, input_tokens, output_tokens,
3186
+ cost_usd, review_cycles
3187
+ FROM story_metrics`).all();
3188
+ return { storyMetrics: rows };
3189
+ } catch (err) {
3190
+ const msg = err instanceof Error ? err.message : String(err);
3191
+ process.stderr.write(`Warning: could not read story_metrics from SQLite: ${msg}\n`);
3192
+ return { storyMetrics: [] };
3193
+ } finally {
3194
+ db.close();
3195
+ }
3196
+ }
3197
+ const BATCH_SIZE = 100;
3198
+ /**
3199
+ * Upsert story_metrics rows into the Dolt `metrics` table.
3200
+ * When `dryRun` is `true`, no queries are executed.
3201
+ */
3202
+ async function migrateDataToDolt(client, rows, dryRun) {
3203
+ let metricsWritten = 0;
3204
+ let skipped = 0;
3205
+ const valid = [];
3206
+ for (const row of rows) {
3207
+ if (!row.story_key) {
3208
+ skipped++;
3209
+ continue;
3210
+ }
3211
+ const recordedAt = row.completed_at ?? row.created_at;
3212
+ if (!recordedAt) {
3213
+ skipped++;
3214
+ continue;
3215
+ }
3216
+ valid.push(row);
3217
+ }
3218
+ if (dryRun) return {
3219
+ metricsWritten: valid.length,
3220
+ skipped
3221
+ };
3222
+ for (let i = 0; i < valid.length; i += BATCH_SIZE) {
3223
+ const batch = valid.slice(i, i + BATCH_SIZE);
3224
+ const placeholderRow = "(?,?,?,?,?,?,?,?,?,?,?,?)";
3225
+ const placeholders = batch.map(() => placeholderRow).join(", ");
3226
+ const sql = `INSERT INTO metrics (story_key, task_type, recorded_at, model, tokens_in, tokens_out, cache_read_tokens, cost_usd, wall_clock_ms, review_cycles, stall_count, result) VALUES ${placeholders} ON DUPLICATE KEY UPDATE cost_usd = VALUES(cost_usd), wall_clock_ms = VALUES(wall_clock_ms), result = VALUES(result)`;
3227
+ const params = [];
3228
+ for (const row of batch) params.push(row.story_key, "pipeline-run", row.completed_at ?? row.created_at, null, row.input_tokens ?? 0, row.output_tokens ?? 0, 0, row.cost_usd ?? 0, Math.round((row.wall_clock_seconds ?? 0) * 1e3), row.review_cycles ?? 0, 0, row.result);
3229
+ await client.query(sql, params);
3230
+ metricsWritten += batch.length;
3231
+ }
3232
+ return {
3233
+ metricsWritten,
3234
+ skipped
3235
+ };
3236
+ }
3237
+ function registerMigrateCommand(program) {
3238
+ program.command("migrate").description("Migrate historical SQLite data into Dolt").option("--dry-run", "Show counts without writing any data", false).option("--output-format <format>", "Output format: text or json", "text").option("--project-root <path>", "Project root directory (defaults to cwd)", process.cwd()).action(async (options) => {
3239
+ const projectRoot = await resolveMainRepoRoot(options.projectRoot ?? process.cwd());
3240
+ const statePath = join$1(projectRoot, ".substrate", "state");
3241
+ const doltStatePath = join$1(statePath, ".dolt");
3242
+ const doltNotInitializedMsg = "Dolt not initialized. Run 'substrate init --dolt' first.";
3243
+ try {
3244
+ await checkDoltInstalled();
3245
+ } catch (err) {
3246
+ if (err instanceof DoltNotInstalled) {
3247
+ if (options.outputFormat === "json") console.log(JSON.stringify({
3248
+ error: "ERR_DOLT_NOT_INITIALIZED",
3249
+ message: doltNotInitializedMsg
3250
+ }));
3251
+ else process.stderr.write(doltNotInitializedMsg + "\n");
3252
+ process.exitCode = 1;
3253
+ return;
3254
+ }
3255
+ process.stderr.write(`Unexpected error checking Dolt: ${err instanceof Error ? err.message : String(err)}\n`);
3256
+ process.exitCode = 2;
3257
+ return;
3258
+ }
3259
+ if (!existsSync$1(doltStatePath)) {
3260
+ if (options.outputFormat === "json") console.log(JSON.stringify({
3261
+ error: "ERR_DOLT_NOT_INITIALIZED",
3262
+ message: doltNotInitializedMsg
3263
+ }));
3264
+ else process.stderr.write(doltNotInitializedMsg + "\n");
3265
+ process.exitCode = 1;
3266
+ return;
3267
+ }
3268
+ const dbPath = join$1(projectRoot, ".substrate", "substrate.db");
3269
+ const snapshot = readSqliteSnapshot(dbPath);
3270
+ if (snapshot.storyMetrics.length === 0) {
3271
+ if (options.outputFormat === "json") console.log(JSON.stringify({
3272
+ migrated: false,
3273
+ reason: "no-sqlite-data"
3274
+ }));
3275
+ else console.log("No SQLite data found — nothing to migrate");
3276
+ return;
3277
+ }
3278
+ const repoPath = statePath;
3279
+ const client = createDoltClient({ repoPath });
3280
+ try {
3281
+ await client.connect();
3282
+ if (options.dryRun) {
3283
+ const result$1 = await migrateDataToDolt(client, snapshot.storyMetrics, true);
3284
+ if (options.outputFormat === "json") console.log(JSON.stringify({
3285
+ migrated: false,
3286
+ dryRun: true,
3287
+ counts: { metrics: result$1.metricsWritten }
3288
+ }));
3289
+ else console.log(`Would migrate ${result$1.metricsWritten} story metrics (dry run — no changes written)`);
3290
+ return;
3291
+ }
3292
+ const result = await migrateDataToDolt(client, snapshot.storyMetrics, false);
3293
+ if (result.metricsWritten > 0) try {
3294
+ await client.exec("add .");
3295
+ await client.exec("commit -m \"Migrate historical data from SQLite\"");
3296
+ } catch (execErr) {
3297
+ const msg = execErr instanceof Error ? execErr.message : String(execErr);
3298
+ process.stderr.write(`Warning: Dolt commit failed (non-fatal): ${msg}\n`);
3299
+ }
3300
+ if (result.skipped > 0) process.stderr.write(`Warning: Skipped ${result.skipped} row(s) — missing story_key or recorded_at.\n`);
3301
+ if (options.outputFormat === "json") console.log(JSON.stringify({
3302
+ migrated: true,
3303
+ counts: { metrics: result.metricsWritten },
3304
+ skipped: result.skipped
3305
+ }));
3306
+ else console.log(`Migrated ${result.metricsWritten} story metrics.`);
3307
+ } catch (err) {
3308
+ const msg = err instanceof Error ? err.message : String(err);
3309
+ process.stderr.write(`Migration failed: ${msg}\n`);
3310
+ process.exitCode = 2;
3311
+ } finally {
3312
+ await client.close();
3313
+ }
3314
+ });
3315
+ }
3316
+
3007
3317
  //#endregion
3008
3318
  //#region src/persistence/queries/cost.ts
3009
3319
  const stmtCache = new WeakMap();
@@ -3590,7 +3900,7 @@ var MonitorDatabaseImpl = class {
3590
3900
  }
3591
3901
  _open() {
3592
3902
  logger$9.info({ path: this._path }, "Opening monitor database");
3593
- this._db = new BetterSqlite3(this._path);
3903
+ this._db = new Database(this._path);
3594
3904
  const walResult = this._db.pragma("journal_mode = WAL");
3595
3905
  if (walResult?.[0]?.journal_mode !== "wal") logger$9.warn({ result: walResult?.[0]?.journal_mode }, "Monitor DB: WAL pragma did not confirm wal mode");
3596
3906
  this._db.pragma("synchronous = NORMAL");
@@ -6597,6 +6907,257 @@ function registerRetryEscalatedCommand(program, _version = "0.0.0", projectRoot
6597
6907
  });
6598
6908
  }
6599
6909
 
6910
+ //#endregion
6911
+ //#region src/cli/commands/contracts.ts
6912
+ function registerContractsCommand(program) {
6913
+ program.command("contracts").description("Show contract declarations and verification status").option("--output-format <format>", "Output format: text or json", "text").action(async (options) => {
6914
+ const dbRoot = await resolveMainRepoRoot(process.cwd());
6915
+ const statePath = join$1(dbRoot, ".substrate", "state");
6916
+ const doltStatePath = join$1(statePath, ".dolt");
6917
+ const storeConfig = existsSync$1(doltStatePath) ? {
6918
+ backend: "dolt",
6919
+ basePath: statePath
6920
+ } : {
6921
+ backend: "file",
6922
+ basePath: statePath
6923
+ };
6924
+ const store = createStateStore(storeConfig);
6925
+ try {
6926
+ await store.initialize();
6927
+ const contracts = await store.queryContracts();
6928
+ if (contracts.length === 0) {
6929
+ console.log("No contracts stored. Run a pipeline to populate contract data.");
6930
+ return;
6931
+ }
6932
+ const storyKeys = [...new Set(contracts.map((c) => c.storyKey))];
6933
+ const verificationMap = new Map();
6934
+ for (const sk of storyKeys) {
6935
+ const verifications = await store.getContractVerification(sk);
6936
+ const contractVerdicts = new Map();
6937
+ for (const v of verifications) contractVerdicts.set(v.contractName, v.verdict);
6938
+ verificationMap.set(sk, contractVerdicts);
6939
+ }
6940
+ const mergedRecords = contracts.map((c) => {
6941
+ const verdicts = verificationMap.get(c.storyKey);
6942
+ const verdict = verdicts?.get(c.contractName);
6943
+ return {
6944
+ storyKey: c.storyKey,
6945
+ contractName: c.contractName,
6946
+ direction: c.direction,
6947
+ schemaPath: c.schemaPath,
6948
+ verificationStatus: verdict === "pass" ? "✓ pass" : verdict === "fail" ? "✗ fail" : "? pending",
6949
+ verdict: verdict ?? "pending"
6950
+ };
6951
+ });
6952
+ if (options.outputFormat === "json") {
6953
+ console.log(JSON.stringify(mergedRecords, null, 2));
6954
+ return;
6955
+ }
6956
+ const headers = [
6957
+ "Story Key",
6958
+ "Contract Name",
6959
+ "Direction",
6960
+ "Schema Path",
6961
+ "Status"
6962
+ ];
6963
+ const rows = mergedRecords.map((r) => [
6964
+ r.storyKey,
6965
+ r.contractName,
6966
+ r.direction,
6967
+ r.schemaPath,
6968
+ r.verificationStatus
6969
+ ]);
6970
+ const colWidths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
6971
+ const formatRow = (cells) => cells.map((c, i) => c.padEnd(colWidths[i])).join(" ");
6972
+ console.log(formatRow(headers));
6973
+ console.log(colWidths.map((w) => "-".repeat(w)).join(" "));
6974
+ for (const row of rows) console.log(formatRow(row));
6975
+ } finally {
6976
+ await store.close();
6977
+ }
6978
+ });
6979
+ }
6980
+
6981
+ //#endregion
6982
+ //#region src/utils/degraded-mode-hint.ts
6983
+ /**
6984
+ * Determine the appropriate degraded-mode hint message based on whether Dolt
6985
+ * is installed and/or initialized at the given state path.
6986
+ *
6987
+ * @param statePath - Absolute path to the substrate state directory
6988
+ * (e.g. `/project/.substrate/state`).
6989
+ */
6990
+ async function getDegradedModeHint(statePath) {
6991
+ try {
6992
+ await checkDoltInstalled();
6993
+ if (!existsSync$1(join$1(statePath, ".dolt"))) return {
6994
+ hint: "Note: Dolt is installed but not initialized. Run `substrate init --dolt` to enable diff and history features.",
6995
+ doltInstalled: true
6996
+ };
6997
+ return {
6998
+ hint: "Note: Running on file backend. Diff and history require Dolt.",
6999
+ doltInstalled: true
7000
+ };
7001
+ } catch (err) {
7002
+ if (err instanceof DoltNotInstalled) return {
7003
+ hint: "Note: Dolt is not installed. Install it from https://docs.dolthub.com/introduction/installation, then run `substrate init --dolt` to enable diff and history features.",
7004
+ doltInstalled: false
7005
+ };
7006
+ throw err;
7007
+ }
7008
+ }
7009
+ /**
7010
+ * Emit a degraded-mode hint for the given command.
7011
+ *
7012
+ * - **Text mode**: writes the hint to `process.stderr` (not stdout).
7013
+ * - **JSON mode**: does NOT write to stderr; the caller is responsible for
7014
+ * writing the returned `hint` field to stdout as part of its JSON envelope.
7015
+ *
7016
+ * @param options - Hint options including output format, command name, and
7017
+ * the resolved state directory path.
7018
+ * @returns The hint message and a flag indicating whether Dolt is installed.
7019
+ */
7020
+ async function emitDegradedModeHint(options) {
7021
+ const { hint, doltInstalled } = await getDegradedModeHint(options.statePath);
7022
+ if (options.outputFormat !== "json") process.stderr.write(`\n${hint}\n`);
7023
+ return {
7024
+ hint,
7025
+ doltInstalled
7026
+ };
7027
+ }
7028
+
7029
+ //#endregion
7030
+ //#region src/cli/commands/diff.ts
7031
+ function registerDiffCommand(program) {
7032
+ program.command("diff [storyKey]").description("Show stat-based diff of database changes for a story or sprint").option("--sprint <sprintId>", "Diff all stories in the specified sprint").option("--output-format <format>", "Output format: text or json", "text").action(async (storyKey, options) => {
7033
+ if (storyKey === void 0 && options.sprint === void 0) {
7034
+ console.error("Error: provide a story key or --sprint <sprintId>");
7035
+ process.exitCode = 1;
7036
+ return;
7037
+ }
7038
+ const dbRoot = await resolveMainRepoRoot(process.cwd());
7039
+ const statePath = join$1(dbRoot, ".substrate", "state");
7040
+ const doltStatePath = join$1(statePath, ".dolt");
7041
+ const storeConfig = existsSync$1(doltStatePath) ? {
7042
+ backend: "dolt",
7043
+ basePath: statePath
7044
+ } : {
7045
+ backend: "file",
7046
+ basePath: statePath
7047
+ };
7048
+ const store = createStateStore(storeConfig);
7049
+ try {
7050
+ await store.initialize();
7051
+ if (store instanceof FileStateStore) {
7052
+ const result = await emitDegradedModeHint({
7053
+ outputFormat: options.outputFormat,
7054
+ command: "diff",
7055
+ statePath
7056
+ });
7057
+ if (options.outputFormat === "json") console.log(JSON.stringify({
7058
+ backend: "file",
7059
+ hint: result.hint,
7060
+ diff: null
7061
+ }));
7062
+ return;
7063
+ }
7064
+ if (storyKey !== void 0) {
7065
+ const diff = await store.diffStory(storyKey);
7066
+ if (options.outputFormat === "json") {
7067
+ console.log(JSON.stringify(diff, null, 2));
7068
+ return;
7069
+ }
7070
+ console.log(`Diff for story ${storyKey}:`);
7071
+ if (diff.tables.length === 0) console.log(" (no changes)");
7072
+ else for (const t of diff.tables) console.log(` ${t.table}: +${t.added.length} -${t.deleted.length} ~${t.modified.length}`);
7073
+ } else {
7074
+ const stories = await store.queryStories({ sprint: options.sprint });
7075
+ const tableMap = new Map();
7076
+ for (const story of stories) {
7077
+ const diff = await store.diffStory(story.storyKey);
7078
+ for (const t of diff.tables) {
7079
+ const existing = tableMap.get(t.table);
7080
+ if (existing === void 0) tableMap.set(t.table, {
7081
+ table: t.table,
7082
+ added: [...t.added],
7083
+ deleted: [...t.deleted],
7084
+ modified: [...t.modified]
7085
+ });
7086
+ else {
7087
+ existing.added = [...existing.added, ...t.added];
7088
+ existing.deleted = [...existing.deleted, ...t.deleted];
7089
+ existing.modified = [...existing.modified, ...t.modified];
7090
+ }
7091
+ }
7092
+ }
7093
+ const aggregated = Array.from(tableMap.values());
7094
+ if (options.outputFormat === "json") {
7095
+ console.log(JSON.stringify({
7096
+ sprint: options.sprint,
7097
+ tables: aggregated
7098
+ }, null, 2));
7099
+ return;
7100
+ }
7101
+ console.log(`Diff for sprint ${options.sprint}:`);
7102
+ if (aggregated.length === 0) console.log(" (no changes)");
7103
+ else for (const t of aggregated) console.log(` ${t.table}: +${t.added.length} -${t.deleted.length} ~${t.modified.length}`);
7104
+ }
7105
+ } finally {
7106
+ await store.close();
7107
+ }
7108
+ });
7109
+ }
7110
+
7111
+ //#endregion
7112
+ //#region src/cli/commands/history.ts
7113
+ function registerHistoryCommand(program) {
7114
+ program.command("history").description("Show Dolt commit history for the state repository").option("--limit <n>", "Maximum number of commits to show", "20").option("--output-format <format>", "Output format: text or json", "text").action(async (options) => {
7115
+ const limit = parseInt(options.limit, 10);
7116
+ const dbRoot = await resolveMainRepoRoot(process.cwd());
7117
+ const statePath = join$1(dbRoot, ".substrate", "state");
7118
+ const doltStatePath = join$1(statePath, ".dolt");
7119
+ const storeConfig = existsSync$1(doltStatePath) ? {
7120
+ backend: "dolt",
7121
+ basePath: statePath
7122
+ } : {
7123
+ backend: "file",
7124
+ basePath: statePath
7125
+ };
7126
+ const store = createStateStore(storeConfig);
7127
+ try {
7128
+ await store.initialize();
7129
+ if (store instanceof FileStateStore) {
7130
+ const result = await emitDegradedModeHint({
7131
+ outputFormat: options.outputFormat,
7132
+ command: "history",
7133
+ statePath
7134
+ });
7135
+ if (options.outputFormat === "json") console.log(JSON.stringify({
7136
+ backend: "file",
7137
+ hint: result.hint,
7138
+ entries: []
7139
+ }));
7140
+ return;
7141
+ }
7142
+ const entries = await store.getHistory(limit);
7143
+ if (entries.length === 0) {
7144
+ console.log("No history available.");
7145
+ return;
7146
+ }
7147
+ if (options.outputFormat === "json") {
7148
+ console.log(JSON.stringify(entries, null, 2));
7149
+ return;
7150
+ }
7151
+ for (const entry of entries) {
7152
+ const storyKeyCol = (entry.storyKey ?? "-").padEnd(8);
7153
+ console.log(`${entry.hash} ${entry.timestamp} ${storyKeyCol} ${entry.message}`);
7154
+ }
7155
+ } finally {
7156
+ await store.close();
7157
+ }
7158
+ });
7159
+ }
7160
+
6600
7161
  //#endregion
6601
7162
  //#region src/cli/index.ts
6602
7163
  process.setMaxListeners(20);
@@ -6645,6 +7206,10 @@ async function createProgram() {
6645
7206
  registerSupervisorCommand(program, version);
6646
7207
  registerMetricsCommand(program, version);
6647
7208
  registerRetryEscalatedCommand(program, version, process.cwd(), registry);
7209
+ registerContractsCommand(program);
7210
+ registerDiffCommand(program);
7211
+ registerHistoryCommand(program);
7212
+ registerMigrateCommand(program);
6648
7213
  registerCostCommand(program, version);
6649
7214
  registerMonitorCommand(program, version);
6650
7215
  registerMergeCommand(program);