substrate-ai 0.4.7 → 0.4.9

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,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DatabaseWrapper, DoltClient, DoltNotInstalled, DoltRepoMapMetaRepository, DoltSymbolRepository, FileStateStore, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SUBSTRATE_OWNED_SETTINGS_KEYS, SymbolParser, TelemetryPersistence, 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-fWZd8vvq.js";
2
+ import { AppError, DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DatabaseWrapper, DoltClient, DoltNotInstalled, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, FileStateStore, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SUBSTRATE_OWNED_SETTINGS_KEYS, SymbolParser, TelemetryPersistence, 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-Dx7bxebF.js";
3
3
  import { createLogger } from "../logger-D2fS2ccL.js";
4
4
  import { AdapterRegistry } from "../adapter-registry-Cd-7lG5v.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
- import { RoutingRecommender } from "../routing-CZfJB3y9.js";
7
+ import { RoutingRecommender } from "../routing-BUE9pIxW.js";
8
8
  import { addTokenUsage, createDecision, createPipelineRun, getDecisionsByCategory, getDecisionsByPhaseForRun, getLatestRun, getTokenUsageSummary, listRequirements, updatePipelineRun } from "../decisions-Db8GTbH2.js";
9
9
  import { ESCALATION_DIAGNOSIS, EXPERIMENT_RESULT, OPERATIONAL_FINDING, STORY_METRICS, aggregateTokenUsageForRun, compareRunMetrics, getBaselineRunMetrics, getRunMetrics, getStoryMetricsForRun, incrementRunRestarts, listRunMetrics, tagRunAsBaseline } from "../operational-C0_y8DAs.js";
10
10
  import { abortMerge, createWorktree, getConflictingFiles, getMergedFiles, getOrphanedWorktrees, performMerge, removeBranch, removeWorktree, simulateMerge, verifyGitVersion } from "../git-utils-C-fdrHF_.js";
@@ -2709,7 +2709,7 @@ async function runSupervisorAction(options, deps = {}) {
2709
2709
  const expDb = expDbWrapper.db;
2710
2710
  const { runRunAction: runPipeline } = await import(
2711
2711
  /* @vite-ignore */
2712
- "../run-RerrMpM1.js"
2712
+ "../run-c8_Yj6xH.js"
2713
2713
  );
2714
2714
  const runStoryFn = async (opts) => {
2715
2715
  const exitCode = await runPipeline({
@@ -3194,7 +3194,7 @@ async function runMetricsAction(options) {
3194
3194
  const routingConfigPath = join(dbDir, "routing.yml");
3195
3195
  let routingConfig = null;
3196
3196
  if (existsSync(routingConfigPath)) try {
3197
- const { loadModelRoutingConfig } = await import("../routing-DWCBjrt7.js");
3197
+ const { loadModelRoutingConfig } = await import("../routing-DbR9FPmj.js");
3198
3198
  routingConfig = loadModelRoutingConfig(routingConfigPath);
3199
3199
  } catch {}
3200
3200
  if (routingConfig === null) routingConfig = {
@@ -7540,6 +7540,15 @@ function registerRepoMapCommand(program) {
7540
7540
  return;
7541
7541
  }
7542
7542
  const doltClient = new DoltClient({ repoPath: statePath });
7543
+ try {
7544
+ const colRows = await doltClient.query(`SHOW COLUMNS FROM repo_map_symbols LIKE 'dependencies'`);
7545
+ if (colRows.length === 0) {
7546
+ await doltClient.query(`ALTER TABLE repo_map_symbols ADD COLUMN dependencies JSON`);
7547
+ logger$2.info("Applied migration: added dependencies column to repo_map_symbols");
7548
+ }
7549
+ } catch {
7550
+ logger$2.debug("Skipping repo_map_symbols migration: table not yet created");
7551
+ }
7543
7552
  const symbolRepo = new DoltSymbolRepository(doltClient, logger$2);
7544
7553
  const metaRepo = new DoltRepoMapMetaRepository(doltClient);
7545
7554
  const repoMapModule = new RepoMapModule(metaRepo, logger$2);
@@ -7572,19 +7581,41 @@ function registerRepoMapCommand(program) {
7572
7581
  logger$2.info("repo-map --update: triggering incremental update");
7573
7582
  const gitClient = new GitClient(logger$2);
7574
7583
  const grammarLoader = new GrammarLoader(logger$2);
7584
+ if (grammarLoader.getGrammar(".ts") === null) {
7585
+ const msg = "tree-sitter grammars not installed. Run `npm install tree-sitter tree-sitter-typescript tree-sitter-javascript tree-sitter-python` in the substrate installation directory.";
7586
+ if (options.outputFormat === "json") console.log(JSON.stringify({
7587
+ result: "error",
7588
+ error: msg
7589
+ }));
7590
+ else process.stderr.write(`Error: ${msg}\n`);
7591
+ process.exitCode = 1;
7592
+ return;
7593
+ }
7575
7594
  const parser = new SymbolParser(grammarLoader, logger$2);
7576
7595
  const storage = new RepoMapStorage(symbolRepo, metaRepo, gitClient, logger$2);
7577
- await storage.incrementalUpdate(dbRoot, parser);
7596
+ let updateWarning;
7597
+ try {
7598
+ await storage.incrementalUpdate(dbRoot, parser);
7599
+ } catch (err) {
7600
+ if (err instanceof AppError && err.code === ERR_REPO_MAP_STORAGE_WRITE) {
7601
+ updateWarning = err.message;
7602
+ logger$2.warn({ err }, "repo-map --update: storage write error (partial update)");
7603
+ } else throw err;
7604
+ }
7578
7605
  const meta = await metaRepo.getMeta();
7579
7606
  const symbolCount = (await symbolRepo.getSymbols()).length;
7580
7607
  if (options.outputFormat === "json") console.log(JSON.stringify({
7581
- result: "updated",
7608
+ result: updateWarning ? "partial" : "updated",
7582
7609
  symbolCount,
7583
7610
  fileCount: meta?.fileCount ?? 0,
7584
7611
  commitSha: meta?.commitSha ?? null,
7585
- updatedAt: meta?.updatedAt?.toISOString() ?? null
7612
+ updatedAt: meta?.updatedAt?.toISOString() ?? null,
7613
+ ...updateWarning ? { warning: updateWarning } : {}
7586
7614
  }));
7587
- else console.log(`Repo-map updated: ${symbolCount} symbols across ${meta?.fileCount ?? 0} files`);
7615
+ else if (updateWarning) {
7616
+ console.log(`Repo-map partially updated: ${symbolCount} symbols across ${meta?.fileCount ?? 0} files`);
7617
+ console.log(`Warning: ${updateWarning}`);
7618
+ } else console.log(`Repo-map updated: ${symbolCount} symbols across ${meta?.fileCount ?? 0} files`);
7588
7619
  return;
7589
7620
  }
7590
7621
  if (options.query !== void 0) {
package/dist/index.d.ts CHANGED
@@ -551,6 +551,24 @@ interface PipelineContractVerificationSummaryEvent {
551
551
  /** 'pass' if zero mismatches, 'fail' otherwise */
552
552
  verdict: 'pass' | 'fail';
553
553
  }
554
+ /**
555
+ * Emitted when the RoutingResolver selects a model for a dispatch.
556
+ */
557
+ interface RoutingModelSelectedEvent {
558
+ type: 'routing:model-selected';
559
+ /** ISO-8601 timestamp generated at emit time */
560
+ ts: string;
561
+ /** Unique dispatch ID */
562
+ dispatch_id: string;
563
+ /** Task type (e.g. 'dev-story', 'test-plan', 'code-review') */
564
+ task_type: string;
565
+ /** Routing phase that matched (e.g. 'generate', 'explore', 'review') */
566
+ phase: string;
567
+ /** Selected model ID */
568
+ model: string;
569
+ /** How the model was selected: 'phase', 'baseline', 'override' */
570
+ source: string;
571
+ }
554
572
  /**
555
573
  * Discriminated union of all pipeline event types.
556
574
  *
@@ -563,7 +581,7 @@ interface PipelineContractVerificationSummaryEvent {
563
581
  * }
564
582
  * ```
565
583
  */
566
- type PipelineEvent = PipelineStartEvent | PipelineCompleteEvent | PipelinePreFlightFailureEvent | PipelineContractMismatchEvent | PipelineContractVerificationSummaryEvent | StoryPhaseEvent | StoryDoneEvent | StoryEscalationEvent | StoryWarnEvent | StoryLogEvent | PipelineHeartbeatEvent | StoryStallEvent | StoryZeroDiffEscalationEvent | StoryBuildVerificationFailedEvent | StoryBuildVerificationPassedEvent | StoryInterfaceChangeWarningEvent | StoryMetricsEvent | SupervisorPollEvent | SupervisorKillEvent | SupervisorRestartEvent | SupervisorAbortEvent | SupervisorSummaryEvent | SupervisorAnalysisCompleteEvent | SupervisorAnalysisErrorEvent | SupervisorExperimentStartEvent | SupervisorExperimentSkipEvent | SupervisorExperimentRecommendationsEvent | SupervisorExperimentCompleteEvent | SupervisorExperimentErrorEvent; //#endregion
584
+ type PipelineEvent = PipelineStartEvent | PipelineCompleteEvent | PipelinePreFlightFailureEvent | PipelineContractMismatchEvent | PipelineContractVerificationSummaryEvent | StoryPhaseEvent | StoryDoneEvent | StoryEscalationEvent | StoryWarnEvent | StoryLogEvent | PipelineHeartbeatEvent | StoryStallEvent | StoryZeroDiffEscalationEvent | StoryBuildVerificationFailedEvent | StoryBuildVerificationPassedEvent | StoryInterfaceChangeWarningEvent | StoryMetricsEvent | SupervisorPollEvent | SupervisorKillEvent | SupervisorRestartEvent | SupervisorAbortEvent | SupervisorSummaryEvent | SupervisorAnalysisCompleteEvent | SupervisorAnalysisErrorEvent | SupervisorExperimentStartEvent | SupervisorExperimentSkipEvent | SupervisorExperimentRecommendationsEvent | SupervisorExperimentCompleteEvent | SupervisorExperimentErrorEvent | RoutingModelSelectedEvent; //#endregion
567
585
  //#region src/core/errors.d.ts
568
586
 
569
587
  /**
@@ -1,5 +1,5 @@
1
1
  import { createLogger } from "./logger-D2fS2ccL.js";
2
- import { load } from "js-yaml";
2
+ import { dump, load } from "js-yaml";
3
3
  import { z } from "zod";
4
4
  import { readFileSync, writeFileSync } from "node:fs";
5
5
 
@@ -280,13 +280,142 @@ var RoutingResolver = class RoutingResolver {
280
280
  }
281
281
  };
282
282
 
283
+ //#endregion
284
+ //#region src/modules/routing/routing-token-accumulator.ts
285
+ /**
286
+ * Accumulates per-dispatch routing decisions and agent token usage, then
287
+ * flushes an aggregated `PhaseTokenBreakdown` to the StateStore at run end.
288
+ *
289
+ * Thread-safety: all methods are synchronous accumulators; `flush` is async
290
+ * but should only be called once per run after all dispatches settle.
291
+ */
292
+ var RoutingTokenAccumulator = class {
293
+ _config;
294
+ _stateStore;
295
+ _logger;
296
+ /** Maps dispatchId → { phase, model } registered from routing:model-selected events */
297
+ _dispatchMap = new Map();
298
+ /**
299
+ * Bucket key = `"${phase}::${model}"`.
300
+ * Separate entries per (phase, model) combination so mixed-model runs
301
+ * produce distinct rows in the breakdown.
302
+ */
303
+ _buckets = new Map();
304
+ constructor(config, stateStore, logger$1) {
305
+ this._config = config;
306
+ this._stateStore = stateStore;
307
+ this._logger = logger$1;
308
+ }
309
+ /**
310
+ * Register the routing decision for a dispatch.
311
+ * A second event for the same `dispatchId` overwrites the prior entry (last-writer-wins).
312
+ *
313
+ * @param event - payload from `routing:model-selected`
314
+ */
315
+ onRoutingSelected(event) {
316
+ this._dispatchMap.set(event.dispatchId, {
317
+ phase: event.phase,
318
+ model: event.model
319
+ });
320
+ this._logger.debug({
321
+ dispatchId: event.dispatchId,
322
+ phase: event.phase,
323
+ model: event.model
324
+ }, "routing:model-selected registered");
325
+ }
326
+ /**
327
+ * Attribute token usage to the phase bucket for this dispatch.
328
+ * Unknown `dispatchId` values are attributed to `phase: 'default', model: 'unknown'`.
329
+ *
330
+ * @param event - payload from `agent:completed` (must include inputTokens / outputTokens)
331
+ */
332
+ onAgentCompleted(event) {
333
+ const mapping = this._dispatchMap.get(event.dispatchId);
334
+ const phase = mapping?.phase ?? "default";
335
+ const model = mapping?.model ?? "unknown";
336
+ this._upsertBucket(phase, model, event.inputTokens, event.outputTokens);
337
+ this._logger.debug({
338
+ dispatchId: event.dispatchId,
339
+ phase,
340
+ model,
341
+ inputTokens: event.inputTokens
342
+ }, "agent:completed attributed");
343
+ }
344
+ /**
345
+ * Construct the `PhaseTokenBreakdown` from the accumulated buckets and
346
+ * persist it to the StateStore via `setMetric`.
347
+ * Clears all in-memory state afterwards so a second call writes an empty entry.
348
+ *
349
+ * @param runId - the pipeline run ID used to scope the metric key
350
+ */
351
+ async flush(runId) {
352
+ const entries = Array.from(this._buckets.values());
353
+ const breakdown = {
354
+ entries,
355
+ baselineModel: this._config.baseline_model,
356
+ runId
357
+ };
358
+ await this._stateStore.setMetric(runId, "phase_token_breakdown", breakdown);
359
+ this._logger.debug({
360
+ runId,
361
+ entryCount: entries.length
362
+ }, "Phase token breakdown flushed to StateStore");
363
+ this._dispatchMap.clear();
364
+ this._buckets.clear();
365
+ }
366
+ _upsertBucket(phase, model, inputTokens, outputTokens) {
367
+ const key = `${phase}::${model}`;
368
+ const existing = this._buckets.get(key);
369
+ if (existing) {
370
+ existing.inputTokens += inputTokens;
371
+ existing.outputTokens += outputTokens;
372
+ existing.dispatchCount += 1;
373
+ } else this._buckets.set(key, {
374
+ phase,
375
+ model,
376
+ inputTokens,
377
+ outputTokens,
378
+ dispatchCount: 1
379
+ });
380
+ }
381
+ };
382
+
383
+ //#endregion
384
+ //#region src/modules/routing/routing-telemetry.ts
385
+ /**
386
+ * Emits `routing.model_resolved` OTEL spans via a TelemetryPersistence instance.
387
+ *
388
+ * Injected into the run command alongside RoutingResolver. When telemetry is
389
+ * not configured, pass null to the run command; no spans are emitted.
390
+ */
391
+ var RoutingTelemetry = class {
392
+ _telemetry;
393
+ _logger;
394
+ constructor(telemetry, logger$1) {
395
+ this._telemetry = telemetry;
396
+ this._logger = logger$1;
397
+ }
398
+ /**
399
+ * Emit a `routing.model_resolved` span for a single routing decision.
400
+ *
401
+ * @param attrs - span attributes including dispatchId, taskType, phase, model, source, latencyMs
402
+ */
403
+ recordModelResolved(attrs) {
404
+ this._telemetry.recordSpan({
405
+ name: "routing.model_resolved",
406
+ attributes: attrs
407
+ });
408
+ this._logger.debug(attrs, "routing.model_resolved span emitted");
409
+ }
410
+ };
411
+
283
412
  //#endregion
284
413
  //#region src/modules/routing/routing-recommender.ts
285
414
  /**
286
415
  * Ordered tier list: index 0 = cheapest / smallest, index N = most expensive / largest.
287
416
  * Tiers are determined by substring matching — e.g. 'claude-haiku-4-5' → tier 1.
288
417
  */
289
- const TIER_KEYWORDS = [
418
+ const TIER_KEYWORDS$1 = [
290
419
  {
291
420
  keyword: "haiku",
292
421
  tier: 1
@@ -330,7 +459,7 @@ var RoutingRecommender = class {
330
459
  */
331
460
  _getTier(model) {
332
461
  const lower = model.toLowerCase();
333
- for (const { keyword, tier } of TIER_KEYWORDS) if (lower.includes(keyword)) return tier;
462
+ for (const { keyword, tier } of TIER_KEYWORDS$1) if (lower.includes(keyword)) return tier;
334
463
  return 2;
335
464
  }
336
465
  /**
@@ -473,5 +602,231 @@ var RoutingRecommender = class {
473
602
  };
474
603
 
475
604
  //#endregion
476
- export { ModelRoutingConfigSchema, ProviderPolicySchema, RoutingConfigError, RoutingRecommender, RoutingResolver, TASK_TYPE_PHASE_MAP, loadModelRoutingConfig };
477
- //# sourceMappingURL=routing-CZfJB3y9.js.map
605
+ //#region src/modules/routing/model-tier.ts
606
+ /**
607
+ * Shared model tier resolution utility.
608
+ *
609
+ * Determines whether a model string belongs to the haiku (1), sonnet (2),
610
+ * or opus (3) tier based on substring matching against well-known keywords.
611
+ *
612
+ * Used by both RoutingRecommender and RoutingTuner to ensure consistent
613
+ * tier comparisons — in particular the one-step guard in RoutingTuner.
614
+ */
615
+ /** Ordered tier keywords: index 0 = cheapest, index N = most expensive. */
616
+ const TIER_KEYWORDS = [
617
+ {
618
+ keyword: "haiku",
619
+ tier: 1
620
+ },
621
+ {
622
+ keyword: "sonnet",
623
+ tier: 2
624
+ },
625
+ {
626
+ keyword: "opus",
627
+ tier: 3
628
+ }
629
+ ];
630
+ /**
631
+ * Get the model tier for a given model name string.
632
+ *
633
+ * Returns:
634
+ * - 1 for haiku-tier models
635
+ * - 2 for sonnet-tier models (also the default when unrecognized)
636
+ * - 3 for opus-tier models
637
+ *
638
+ * Matching is case-insensitive substring search.
639
+ */
640
+ function getModelTier(model) {
641
+ const lower = model.toLowerCase();
642
+ for (const { keyword, tier } of TIER_KEYWORDS) if (lower.includes(keyword)) return tier;
643
+ return 2;
644
+ }
645
+
646
+ //#endregion
647
+ //#region src/modules/routing/routing-tuner.ts
648
+ /** Minimum number of breakdowns required before auto-tuning is attempted. */
649
+ const MIN_BREAKDOWNS_FOR_TUNING = 5;
650
+ /** Key used to store the list of known run IDs in the StateStore. */
651
+ const RUN_INDEX_KEY = "phase_token_breakdown_runs";
652
+ /** Key used to store the tune log in the StateStore. */
653
+ const TUNE_LOG_KEY = "routing_tune_log";
654
+ /**
655
+ * Auto-applies a single conservative model downgrade per invocation when
656
+ * `config.auto_tune` is `true` and sufficient historical data is available.
657
+ *
658
+ * The tuner reads the current routing YAML config, applies the change in memory,
659
+ * and writes it back to disk synchronously. It also appends a `TuneLogEntry`
660
+ * to the StateStore for audit purposes, and emits a `routing:auto-tuned` event.
661
+ */
662
+ var RoutingTuner = class {
663
+ _stateStore;
664
+ _recommender;
665
+ _eventEmitter;
666
+ _configPath;
667
+ _logger;
668
+ constructor(stateStore, recommender, eventEmitter, configPath, logger$1) {
669
+ this._stateStore = stateStore;
670
+ this._recommender = recommender;
671
+ this._eventEmitter = eventEmitter;
672
+ this._configPath = configPath;
673
+ this._logger = logger$1;
674
+ }
675
+ /**
676
+ * Called at the end of a pipeline run. When auto_tune is enabled and sufficient
677
+ * historical data exists, applies a single conservative model downgrade to the
678
+ * routing config YAML file.
679
+ *
680
+ * @param runId - ID of the just-completed pipeline run
681
+ * @param config - Current model routing config (already loaded from disk)
682
+ */
683
+ async maybeAutoTune(runId, config) {
684
+ if (config.auto_tune !== true) {
685
+ this._logger.debug({ runId }, "auto_tune_disabled — skipping RoutingTuner");
686
+ return;
687
+ }
688
+ await this._registerRunId(runId);
689
+ const breakdowns = await this._loadRecentBreakdowns(10);
690
+ if (breakdowns.length < MIN_BREAKDOWNS_FOR_TUNING) {
691
+ this._logger.debug({
692
+ runId,
693
+ available: breakdowns.length,
694
+ required: MIN_BREAKDOWNS_FOR_TUNING
695
+ }, "insufficient_data — not enough breakdowns for auto-tuning");
696
+ return;
697
+ }
698
+ const analysis = this._recommender.analyze(breakdowns, config);
699
+ if (analysis.insufficientData) {
700
+ this._logger.debug({ runId }, "Recommender returned insufficientData");
701
+ return;
702
+ }
703
+ const downgradeCandidates = analysis.recommendations.filter((rec) => {
704
+ if (rec.direction !== "downgrade") return false;
705
+ const tierDiff = Math.abs(getModelTier(rec.currentModel) - getModelTier(rec.suggestedModel));
706
+ return tierDiff === 1;
707
+ });
708
+ if (downgradeCandidates.length === 0) {
709
+ this._logger.debug({ runId }, "no_safe_recommendation");
710
+ return;
711
+ }
712
+ const topRec = downgradeCandidates.sort((a, b) => b.confidence - a.confidence)[0];
713
+ let rawContent;
714
+ try {
715
+ rawContent = readFileSync(this._configPath, "utf-8");
716
+ } catch (err) {
717
+ const msg = err instanceof Error ? err.message : String(err);
718
+ this._logger.warn({
719
+ err: msg,
720
+ configPath: this._configPath
721
+ }, "Failed to read routing config for auto-tune");
722
+ return;
723
+ }
724
+ let rawObject;
725
+ try {
726
+ rawObject = load(rawContent);
727
+ } catch (err) {
728
+ const msg = err instanceof Error ? err.message : String(err);
729
+ this._logger.warn({ err: msg }, "Failed to parse routing config YAML for auto-tune");
730
+ return;
731
+ }
732
+ const configObj = rawObject;
733
+ if (configObj.phases === void 0) configObj.phases = {};
734
+ const existingPhase = configObj.phases[topRec.phase];
735
+ if (existingPhase !== void 0) existingPhase.model = topRec.suggestedModel;
736
+ else configObj.phases[topRec.phase] = { model: topRec.suggestedModel };
737
+ try {
738
+ writeFileSync(this._configPath, dump(rawObject, { lineWidth: 120 }), "utf-8");
739
+ } catch (err) {
740
+ const msg = err instanceof Error ? err.message : String(err);
741
+ this._logger.warn({
742
+ err: msg,
743
+ configPath: this._configPath
744
+ }, "Failed to write updated routing config");
745
+ return;
746
+ }
747
+ const tuneEntry = {
748
+ id: crypto.randomUUID(),
749
+ runId,
750
+ phase: topRec.phase,
751
+ oldModel: topRec.currentModel,
752
+ newModel: topRec.suggestedModel,
753
+ estimatedSavingsPct: topRec.estimatedSavingsPct,
754
+ appliedAt: new Date().toISOString()
755
+ };
756
+ await this._appendTuneLog(tuneEntry);
757
+ this._eventEmitter.emit("routing:auto-tuned", {
758
+ runId,
759
+ phase: topRec.phase,
760
+ oldModel: topRec.currentModel,
761
+ newModel: topRec.suggestedModel,
762
+ estimatedSavingsPct: topRec.estimatedSavingsPct
763
+ });
764
+ this._logger.info({
765
+ runId,
766
+ phase: topRec.phase,
767
+ oldModel: topRec.currentModel,
768
+ newModel: topRec.suggestedModel
769
+ }, "Auto-tuned routing config — applied downgrade");
770
+ }
771
+ /**
772
+ * Register a run ID in the stored run index so future calls can discover
773
+ * all historical breakdowns without a separate run listing endpoint.
774
+ */
775
+ async _registerRunId(runId) {
776
+ const existing = await this._stateStore.getMetric("__global__", RUN_INDEX_KEY);
777
+ const runIds = Array.isArray(existing) ? existing : [];
778
+ if (!runIds.includes(runId)) {
779
+ runIds.push(runId);
780
+ await this._stateStore.setMetric("__global__", RUN_INDEX_KEY, runIds);
781
+ }
782
+ }
783
+ /**
784
+ * Load the most recent `lookback` PhaseTokenBreakdown records from the StateStore.
785
+ *
786
+ * Each breakdown is stored by RoutingTokenAccumulator under the key
787
+ * `'phase_token_breakdown'` scoped to the run ID. The run IDs themselves are
788
+ * tracked in a global index stored under `('__global__', RUN_INDEX_KEY)`.
789
+ *
790
+ * @param lookback - Maximum number of recent runs to inspect
791
+ */
792
+ async _loadRecentBreakdowns(lookback) {
793
+ const existing = await this._stateStore.getMetric("__global__", RUN_INDEX_KEY);
794
+ const allRunIds = Array.isArray(existing) ? existing : [];
795
+ const recentRunIds = allRunIds.slice(-lookback);
796
+ const breakdowns = [];
797
+ for (const runId of recentRunIds) try {
798
+ const raw = await this._stateStore.getMetric(runId, "phase_token_breakdown");
799
+ if (raw !== void 0 && raw !== null) {
800
+ const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
801
+ breakdowns.push(parsed);
802
+ }
803
+ } catch (err) {
804
+ const msg = err instanceof Error ? err.message : String(err);
805
+ this._logger.debug({
806
+ runId,
807
+ err: msg
808
+ }, "Failed to load breakdown for run — skipping");
809
+ }
810
+ return breakdowns;
811
+ }
812
+ /**
813
+ * Append a TuneLogEntry to the persisted tune log in the StateStore.
814
+ *
815
+ * NOTE: This uses `'__global__'` as the scope key (codebase convention) rather
816
+ * than the literal `'global'` mentioned in AC6. The tune log is stored as a raw
817
+ * array (not a JSON-stringified string) for internal consistency with how other
818
+ * array values are stored in this StateStore. Story 28-9's
819
+ * `substrate routing --history` command MUST use the same `'__global__'` scope
820
+ * key and `'routing_tune_log'` metric key when reading this log.
821
+ */
822
+ async _appendTuneLog(entry) {
823
+ const existing = await this._stateStore.getMetric("__global__", TUNE_LOG_KEY);
824
+ const log = Array.isArray(existing) ? existing : [];
825
+ log.push(entry);
826
+ await this._stateStore.setMetric("__global__", TUNE_LOG_KEY, log);
827
+ }
828
+ };
829
+
830
+ //#endregion
831
+ export { ModelRoutingConfigSchema, ProviderPolicySchema, RoutingConfigError, RoutingRecommender, RoutingResolver, RoutingTelemetry, RoutingTokenAccumulator, RoutingTuner, TASK_TYPE_PHASE_MAP, getModelTier, loadModelRoutingConfig };
832
+ //# sourceMappingURL=routing-BUE9pIxW.js.map
@@ -0,0 +1,4 @@
1
+ import "./logger-D2fS2ccL.js";
2
+ import { ModelRoutingConfigSchema, ProviderPolicySchema, RoutingConfigError, RoutingRecommender, RoutingResolver, RoutingTelemetry, RoutingTokenAccumulator, RoutingTuner, TASK_TYPE_PHASE_MAP, getModelTier, loadModelRoutingConfig } from "./routing-BUE9pIxW.js";
3
+
4
+ export { loadModelRoutingConfig };
@@ -1,7 +1,7 @@
1
1
  import { createLogger, deepMask } from "./logger-D2fS2ccL.js";
2
2
  import { CURRENT_CONFIG_FORMAT_VERSION, PartialSubstrateConfigSchema, SUPPORTED_CONFIG_FORMAT_VERSIONS, SubstrateConfigSchema, defaultConfigMigrator } from "./config-migrator-DtZW1maj.js";
3
3
  import { ConfigError, ConfigIncompatibleFormatError, createEventBus, createTuiApp, isTuiCapable, printNonTtyWarning, sleep } from "./helpers-BihqWgVe.js";
4
- import { RoutingResolver } from "./routing-CZfJB3y9.js";
4
+ import { RoutingRecommender, RoutingResolver, RoutingTelemetry, RoutingTokenAccumulator, RoutingTuner, loadModelRoutingConfig } from "./routing-BUE9pIxW.js";
5
5
  import { addTokenUsage, createDecision, createPipelineRun, createRequirement, getArtifactByTypeForRun, getArtifactsByRun, getDecisionsByCategory, getDecisionsByPhase, getDecisionsByPhaseForRun, getLatestRun, getPipelineRunById, getRunningPipelineRuns, getTokenUsageSummary, registerArtifact, updatePipelineRun, updatePipelineRunConfig, upsertDecision } from "./decisions-Db8GTbH2.js";
6
6
  import { ADVISORY_NOTES, ESCALATION_DIAGNOSIS, OPERATIONAL_FINDING, STORY_METRICS, STORY_OUTCOME, TEST_EXPANSION_FINDING, TEST_PLAN, aggregateTokenUsageForRun, aggregateTokenUsageForStory, getStoryMetricsForRun, writeRunMetrics, writeStoryMetrics } from "./operational-C0_y8DAs.js";
7
7
  import { createRequire } from "module";
@@ -2810,6 +2810,43 @@ const PIPELINE_EVENT_METADATA = [
2810
2810
  }
2811
2811
  ]
2812
2812
  },
2813
+ {
2814
+ type: "routing:model-selected",
2815
+ description: "Model routing resolver selected a model for a dispatch.",
2816
+ when: "When a story dispatch uses model routing and the resolver returns a non-null model.",
2817
+ fields: [
2818
+ {
2819
+ name: "ts",
2820
+ type: "string",
2821
+ description: "Timestamp."
2822
+ },
2823
+ {
2824
+ name: "dispatch_id",
2825
+ type: "string",
2826
+ description: "Unique dispatch ID."
2827
+ },
2828
+ {
2829
+ name: "task_type",
2830
+ type: "string",
2831
+ description: "Task type (dev-story, test-plan, code-review)."
2832
+ },
2833
+ {
2834
+ name: "phase",
2835
+ type: "string",
2836
+ description: "Routing phase that matched (generate, explore, review)."
2837
+ },
2838
+ {
2839
+ name: "model",
2840
+ type: "string",
2841
+ description: "Selected model ID."
2842
+ },
2843
+ {
2844
+ name: "source",
2845
+ type: "string",
2846
+ description: "How selected: phase, baseline, or override."
2847
+ }
2848
+ ]
2849
+ },
2813
2850
  {
2814
2851
  type: "pipeline:pre-flight-failure",
2815
2852
  description: "Pre-flight build check failed before any story was dispatched. Pipeline aborts immediately.",
@@ -3273,6 +3310,9 @@ Patterns for \`substrate supervisor --output-format json\` events:
3273
3310
 
3274
3311
  ### On \`supervisor:experiment:error\`
3275
3312
  - Report error. Suggest running without \`--experiment\` for a clean run.
3313
+
3314
+ ### On \`routing:model-selected\`
3315
+ - Informational. Log which model was selected for the dispatch and why (phase config, baseline, or override).
3276
3316
  `;
3277
3317
  }
3278
3318
  /**
@@ -3832,10 +3872,12 @@ var DoltSymbolRepository = class {
3832
3872
  this._logger.debug({ filePath }, "upsertFileSymbols: cleared symbols for deleted/empty file");
3833
3873
  return;
3834
3874
  }
3835
- const placeholders = symbols.map(() => "(?, ?, ?, ?, ?, ?, ?)").join(", ");
3875
+ const deps = symbols.filter((s) => s.kind === "import").map((s) => s.name);
3876
+ const depsJson = JSON.stringify(deps);
3877
+ const placeholders = symbols.map(() => "(?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
3836
3878
  const params = [];
3837
- for (const sym of symbols) params.push(filePath, sym.name, sym.kind, sym.signature ?? "", sym.lineNumber, sym.exported ? 1 : 0, fileHash);
3838
- await this._client.query(`INSERT INTO repo_map_symbols (file_path, symbol_name, symbol_kind, signature, line_number, exported, file_hash) VALUES ${placeholders}`, params);
3879
+ for (const sym of symbols) params.push(filePath, sym.name, sym.kind, sym.signature ?? "", sym.lineNumber, sym.exported ? 1 : 0, fileHash, depsJson);
3880
+ await this._client.query(`INSERT INTO repo_map_symbols (file_path, symbol_name, symbol_kind, signature, line_number, exported, file_hash, dependencies) VALUES ${placeholders}`, params);
3839
3881
  this._logger.debug({
3840
3882
  filePath,
3841
3883
  count: symbols.length
@@ -3899,7 +3941,7 @@ var DoltSymbolRepository = class {
3899
3941
  if (filePaths.length === 0) return [];
3900
3942
  try {
3901
3943
  const placeholders = filePaths.map(() => "?").join(", ");
3902
- const rows = await this._client.query(`SELECT file_path, symbol_name, symbol_kind, signature, line_number, exported, file_hash FROM repo_map_symbols WHERE file_path IN (${placeholders})`, filePaths);
3944
+ const rows = await this._client.query(`SELECT file_path, symbol_name, symbol_kind, signature, line_number, exported, file_hash, dependencies FROM repo_map_symbols WHERE file_path IN (${placeholders})`, filePaths);
3903
3945
  return rows.map((r) => this._rowToRepoMapSymbol(r));
3904
3946
  } catch (err) {
3905
3947
  const detail = err instanceof Error ? err.message : String(err);
@@ -3910,7 +3952,7 @@ var DoltSymbolRepository = class {
3910
3952
  if (names.length === 0) return [];
3911
3953
  try {
3912
3954
  const placeholders = names.map(() => "?").join(", ");
3913
- const rows = await this._client.query(`SELECT file_path, symbol_name, symbol_kind, signature, line_number, exported, file_hash FROM repo_map_symbols WHERE symbol_name IN (${placeholders})`, names);
3955
+ const rows = await this._client.query(`SELECT file_path, symbol_name, symbol_kind, signature, line_number, exported, file_hash, dependencies FROM repo_map_symbols WHERE symbol_name IN (${placeholders})`, names);
3914
3956
  return rows.map((r) => this._rowToRepoMapSymbol(r));
3915
3957
  } catch (err) {
3916
3958
  const detail = err instanceof Error ? err.message : String(err);
@@ -3921,7 +3963,7 @@ var DoltSymbolRepository = class {
3921
3963
  if (types$1.length === 0) return [];
3922
3964
  try {
3923
3965
  const placeholders = types$1.map(() => "?").join(", ");
3924
- const rows = await this._client.query(`SELECT file_path, symbol_name, symbol_kind, signature, line_number, exported, file_hash FROM repo_map_symbols WHERE symbol_kind IN (${placeholders})`, types$1);
3966
+ const rows = await this._client.query(`SELECT file_path, symbol_name, symbol_kind, signature, line_number, exported, file_hash, dependencies FROM repo_map_symbols WHERE symbol_kind IN (${placeholders})`, types$1);
3925
3967
  return rows.map((r) => this._rowToRepoMapSymbol(r));
3926
3968
  } catch (err) {
3927
3969
  const detail = err instanceof Error ? err.message : String(err);
@@ -3929,16 +3971,20 @@ var DoltSymbolRepository = class {
3929
3971
  }
3930
3972
  }
3931
3973
  /**
3932
- * Returns symbols from files whose dependencies contain the given symbol name.
3933
- * Currently returns [] — the repo_map_symbols schema does not yet include a
3934
- * `dependencies` JSON column. A future migration will add it.
3974
+ * Returns symbols from files whose dependencies array contains symbolName.
3935
3975
  */
3936
- async findByDependedBy(_symbolName) {
3937
- return [];
3976
+ async findByDependedBy(symbolName) {
3977
+ try {
3978
+ const rows = await this._client.query(`SELECT file_path, symbol_name, symbol_kind, signature, line_number, exported, file_hash, dependencies FROM repo_map_symbols WHERE JSON_CONTAINS(dependencies, JSON_QUOTE(?), '$')`, [symbolName]);
3979
+ return rows.map((r) => this._rowToRepoMapSymbol(r));
3980
+ } catch (err) {
3981
+ const detail = err instanceof Error ? err.message : String(err);
3982
+ throw new AppError(ERR_REPO_MAP_STORAGE_READ, 2, `findByDependedBy failed: ${detail}`);
3983
+ }
3938
3984
  }
3939
3985
  async findAll() {
3940
3986
  try {
3941
- const rows = await this._client.query("SELECT file_path, symbol_name, symbol_kind, signature, line_number, exported, file_hash FROM repo_map_symbols");
3987
+ const rows = await this._client.query("SELECT file_path, symbol_name, symbol_kind, signature, line_number, exported, file_hash, dependencies FROM repo_map_symbols");
3942
3988
  return rows.map((r) => this._rowToRepoMapSymbol(r));
3943
3989
  } catch (err) {
3944
3990
  const detail = err instanceof Error ? err.message : String(err);
@@ -3946,13 +3992,18 @@ var DoltSymbolRepository = class {
3946
3992
  }
3947
3993
  }
3948
3994
  _rowToRepoMapSymbol(row) {
3995
+ let deps = [];
3996
+ if (row.dependencies) try {
3997
+ const parsed = typeof row.dependencies === "string" ? JSON.parse(row.dependencies) : row.dependencies;
3998
+ if (Array.isArray(parsed)) deps = parsed;
3999
+ } catch {}
3949
4000
  return {
3950
4001
  filePath: row.file_path,
3951
4002
  symbolName: row.symbol_name,
3952
4003
  symbolType: row.symbol_kind,
3953
4004
  signature: row.signature ?? void 0,
3954
4005
  lineNumber: row.line_number,
3955
- dependencies: [],
4006
+ dependencies: deps,
3956
4007
  fileHash: row.file_hash
3957
4008
  };
3958
4009
  }
@@ -3978,7 +4029,7 @@ var DoltRepoMapMetaRepository = class {
3978
4029
  updated_at = VALUES(updated_at),
3979
4030
  file_count = VALUES(file_count)`, [
3980
4031
  meta.commitSha,
3981
- meta.updatedAt,
4032
+ meta.updatedAt.toISOString(),
3982
4033
  meta.fileCount
3983
4034
  ]);
3984
4035
  } catch (err) {
@@ -4050,15 +4101,18 @@ var RepoMapStorage = class {
4050
4101
  const changedFiles = await this._gitClient.getChangedFiles(projectRoot, meta.commitSha);
4051
4102
  this._logger.debug({ count: changedFiles.length }, "incrementalUpdate: changed files");
4052
4103
  const supported = changedFiles.filter((f) => SUPPORTED_EXTENSIONS.has(extname(f)));
4104
+ let parsedCount = 0;
4053
4105
  for (const filePath of supported) try {
4054
4106
  const exists = await fileExists(filePath);
4055
4107
  if (!exists) {
4056
4108
  await this._symbolRepo.upsertFileSymbols(filePath, [], "");
4109
+ parsedCount++;
4057
4110
  continue;
4058
4111
  }
4059
4112
  const symbols = await parser.parseFile(filePath);
4060
4113
  const hash = await computeFileHash(filePath);
4061
4114
  await this._symbolRepo.upsertFileSymbols(filePath, symbols, hash);
4115
+ parsedCount++;
4062
4116
  } catch (err) {
4063
4117
  this._logger.warn({
4064
4118
  filePath,
@@ -4069,7 +4123,7 @@ var RepoMapStorage = class {
4069
4123
  await this._metaRepo.updateMeta({
4070
4124
  commitSha: currentSha,
4071
4125
  updatedAt: new Date(),
4072
- fileCount: supported.length
4126
+ fileCount: parsedCount
4073
4127
  });
4074
4128
  }
4075
4129
  /**
@@ -5699,6 +5753,36 @@ var RepoMapFormatter = class {
5699
5753
  }
5700
5754
  };
5701
5755
 
5756
+ //#endregion
5757
+ //#region src/modules/repo-map/repo-map-telemetry.ts
5758
+ /**
5759
+ * Emits `repo_map.query` OTEL spans via a TelemetryPersistence instance.
5760
+ *
5761
+ * Constructed with an `ITelemetryPersistence` and a logger. Pass an instance
5762
+ * to `RepoMapQueryEngine` constructor to enable query telemetry; omit it to
5763
+ * skip telemetry without changing existing query behaviour.
5764
+ */
5765
+ var RepoMapTelemetry = class {
5766
+ _telemetry;
5767
+ _logger;
5768
+ constructor(telemetry, logger$27) {
5769
+ this._telemetry = telemetry;
5770
+ this._logger = logger$27;
5771
+ }
5772
+ /**
5773
+ * Emit a `repo_map.query` span.
5774
+ *
5775
+ * @param attrs - query telemetry attributes
5776
+ */
5777
+ recordQuery(attrs) {
5778
+ this._telemetry.recordSpan({
5779
+ name: "repo_map.query",
5780
+ attributes: attrs
5781
+ });
5782
+ this._logger.debug(attrs, "repo_map.query span emitted");
5783
+ }
5784
+ };
5785
+
5702
5786
  //#endregion
5703
5787
  //#region src/modules/repo-map/RepoMapModule.ts
5704
5788
  /**
@@ -7061,6 +7145,21 @@ var DoltStateStore = class DoltStateStore {
7061
7145
  )`
7062
7146
  ];
7063
7147
  for (const sql of ddl) await this._client.query(sql);
7148
+ try {
7149
+ const colRows = await this._client.query(`SHOW COLUMNS FROM repo_map_symbols LIKE 'dependencies'`);
7150
+ if (colRows.length === 0) {
7151
+ await this._client.query(`ALTER TABLE repo_map_symbols ADD COLUMN dependencies JSON`);
7152
+ await this._client.query(`INSERT IGNORE INTO _schema_version (version, description) VALUES (6, 'Add dependencies JSON column to repo_map_symbols (Epic 28-3)')`);
7153
+ log$1.info({
7154
+ component: "dolt-state",
7155
+ migration: "v5-to-v6",
7156
+ column: "dependencies",
7157
+ table: "repo_map_symbols"
7158
+ }, "Applied migration v5-to-v6: added dependencies column to repo_map_symbols");
7159
+ }
7160
+ } catch {
7161
+ log$1.debug("Skipping repo_map_symbols migration: table not yet created");
7162
+ }
7064
7163
  log$1.debug("Schema migrations applied");
7065
7164
  }
7066
7165
  /**
@@ -20478,6 +20577,7 @@ async function runRunAction(options) {
20478
20577
  return 1;
20479
20578
  }
20480
20579
  const db = dbWrapper.db;
20580
+ const telemetryPersistence = telemetryEnabled ? new TelemetryPersistence(db) : void 0;
20481
20581
  const packLoader = createPackLoader();
20482
20582
  let pack;
20483
20583
  try {
@@ -20547,6 +20647,33 @@ async function runRunAction(options) {
20547
20647
  if (!injectedRegistry) throw new Error("AdapterRegistry is required — must be initialized at CLI startup");
20548
20648
  const routingConfigPath = join(projectRoot, "substrate.routing.yml");
20549
20649
  const routingResolver = RoutingResolver.createWithFallback(routingConfigPath, logger);
20650
+ let routingTokenAccumulator;
20651
+ let routingConfig;
20652
+ try {
20653
+ routingConfig = loadModelRoutingConfig(routingConfigPath);
20654
+ } catch {
20655
+ logger.debug("Routing config not loadable — RoutingTokenAccumulator skipped");
20656
+ }
20657
+ let routingTuner;
20658
+ if (routingConfig !== void 0) {
20659
+ const kvStateStore = new FileStateStore({ basePath: join(dbRoot, ".substrate") });
20660
+ routingTokenAccumulator = new RoutingTokenAccumulator(routingConfig, kvStateStore, logger);
20661
+ eventBus.on("routing:model-selected", (payload) => {
20662
+ routingTokenAccumulator.onRoutingSelected({
20663
+ dispatchId: payload.dispatchId,
20664
+ phase: payload.phase,
20665
+ model: payload.model
20666
+ });
20667
+ });
20668
+ eventBus.on("agent:completed", (payload) => {
20669
+ routingTokenAccumulator.onAgentCompleted({
20670
+ dispatchId: payload.dispatchId,
20671
+ inputTokens: payload.inputTokens ?? 0,
20672
+ outputTokens: payload.outputTokens ?? 0
20673
+ });
20674
+ });
20675
+ if (routingConfig.auto_tune === true) routingTuner = new RoutingTuner(kvStateStore, new RoutingRecommender(logger), eventBus, routingConfigPath, logger);
20676
+ }
20550
20677
  const statePath = join(dbRoot, ".substrate", "state");
20551
20678
  const isDoltAvailable = existsSync(join(statePath, ".dolt"));
20552
20679
  let repoMapInjector;
@@ -20556,7 +20683,8 @@ async function runRunAction(options) {
20556
20683
  const doltClient = new DoltClient({ repoPath: statePath });
20557
20684
  const symbolRepo = new DoltSymbolRepository(doltClient, logger);
20558
20685
  const metaRepo = new DoltRepoMapMetaRepository(doltClient);
20559
- const queryEngine = new RepoMapQueryEngine(symbolRepo, logger);
20686
+ const repoMapTelemetry = telemetryPersistence !== void 0 ? new RepoMapTelemetry(telemetryPersistence, logger) : void 0;
20687
+ const queryEngine = new RepoMapQueryEngine(symbolRepo, logger, repoMapTelemetry);
20560
20688
  repoMapInjector = new RepoMapInjector(queryEngine, logger);
20561
20689
  repoMapModule = new RepoMapModule(metaRepo, logger);
20562
20690
  logger.debug("repo-map injector constructed (Dolt backend detected)");
@@ -20810,6 +20938,17 @@ async function runRunAction(options) {
20810
20938
  });
20811
20939
  }
20812
20940
  });
20941
+ eventBus.on("routing:model-selected", (payload) => {
20942
+ ndjsonEmitter.emit({
20943
+ type: "routing:model-selected",
20944
+ ts: new Date().toISOString(),
20945
+ dispatch_id: payload.dispatchId,
20946
+ task_type: payload.taskType,
20947
+ phase: payload.phase,
20948
+ model: payload.model,
20949
+ source: payload.source
20950
+ });
20951
+ });
20813
20952
  eventBus.on("orchestrator:story-complete", (payload) => {
20814
20953
  ndjsonEmitter.emit({
20815
20954
  type: "story:done",
@@ -20944,7 +21083,19 @@ async function runRunAction(options) {
20944
21083
  });
20945
21084
  }
20946
21085
  const ingestionServer = telemetryEnabled ? new IngestionServer({ port: telemetryPort }) : void 0;
20947
- const telemetryPersistence = telemetryEnabled ? new TelemetryPersistence(db) : void 0;
21086
+ if (telemetryPersistence !== void 0) {
21087
+ const routingTelemetry = new RoutingTelemetry(telemetryPersistence, logger);
21088
+ eventBus.on("routing:model-selected", (payload) => {
21089
+ routingTelemetry.recordModelResolved({
21090
+ dispatchId: payload.dispatchId,
21091
+ taskType: payload.taskType,
21092
+ phase: payload.phase,
21093
+ model: payload.model,
21094
+ source: payload.source,
21095
+ latencyMs: 0
21096
+ });
21097
+ });
21098
+ }
20948
21099
  if (repoMapModule !== void 0) try {
20949
21100
  const stale = await repoMapModule.checkStaleness();
20950
21101
  if (stale !== null) {
@@ -20982,6 +21133,17 @@ async function runRunAction(options) {
20982
21133
  process.stdout.write(`Stories: ${storyKeys.join(", ")}\n`);
20983
21134
  }
20984
21135
  const status = await orchestrator.run(storyKeys);
21136
+ if (routingTokenAccumulator !== void 0) try {
21137
+ await routingTokenAccumulator.flush(pipelineRun.id);
21138
+ logger.debug({ runId: pipelineRun.id }, "Phase token breakdown flushed");
21139
+ } catch (flushErr) {
21140
+ logger.warn({ err: flushErr }, "Failed to flush phase token breakdown (best-effort)");
21141
+ }
21142
+ if (routingTuner !== void 0 && routingConfig !== void 0) try {
21143
+ await routingTuner.maybeAutoTune(pipelineRun.id, routingConfig);
21144
+ } catch (tuneErr) {
21145
+ logger.warn({ err: tuneErr }, "RoutingTuner.maybeAutoTune failed (best-effort)");
21146
+ }
20985
21147
  const succeededKeys = [];
20986
21148
  const failedKeys = [];
20987
21149
  const escalatedKeys = [];
@@ -21425,5 +21587,5 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
21425
21587
  }
21426
21588
 
21427
21589
  //#endregion
21428
- export { DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DatabaseWrapper, DoltClient, DoltNotInstalled, DoltRepoMapMetaRepository, DoltSymbolRepository, FileStateStore, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SUBSTRATE_OWNED_SETTINGS_KEYS, SymbolParser, TelemetryPersistence, 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, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
21429
- //# sourceMappingURL=run-fWZd8vvq.js.map
21590
+ export { AppError, DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DatabaseWrapper, DoltClient, DoltNotInstalled, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, FileStateStore, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SUBSTRATE_OWNED_SETTINGS_KEYS, SymbolParser, TelemetryPersistence, 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, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
21591
+ //# sourceMappingURL=run-Dx7bxebF.js.map
@@ -1,8 +1,8 @@
1
- import { registerRunCommand, runRunAction } from "./run-fWZd8vvq.js";
1
+ import { registerRunCommand, runRunAction } from "./run-Dx7bxebF.js";
2
2
  import "./logger-D2fS2ccL.js";
3
3
  import "./config-migrator-DtZW1maj.js";
4
4
  import "./helpers-BihqWgVe.js";
5
- import "./routing-CZfJB3y9.js";
5
+ import "./routing-BUE9pIxW.js";
6
6
  import "./decisions-Db8GTbH2.js";
7
7
  import "./operational-C0_y8DAs.js";
8
8
 
package/dist/schema.sql CHANGED
@@ -215,7 +215,8 @@ CREATE TABLE IF NOT EXISTS repo_map_symbols (
215
215
  signature TEXT,
216
216
  line_number INT NOT NULL DEFAULT 0,
217
217
  exported TINYINT(1) NOT NULL DEFAULT 0,
218
- file_hash VARCHAR(64) NOT NULL,
218
+ file_hash VARCHAR(64) NOT NULL,
219
+ dependencies JSON,
219
220
  PRIMARY KEY (id)
220
221
  );
221
222
 
@@ -234,3 +235,4 @@ CREATE TABLE IF NOT EXISTS repo_map_meta (
234
235
  );
235
236
 
236
237
  INSERT IGNORE INTO _schema_version (version, description) VALUES (5, 'Add repo_map_symbols and repo_map_meta tables (Epic 28-2)');
238
+ INSERT IGNORE INTO _schema_version (version, description) VALUES (6, 'Add dependencies JSON column to repo_map_symbols (Epic 28-3)');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "substrate-ai",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "Substrate — multi-agent orchestration daemon for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -75,9 +75,9 @@
75
75
  },
76
76
  "optionalDependencies": {
77
77
  "tree-sitter": "^0.21.1",
78
- "tree-sitter-typescript": "^0.21.2",
79
78
  "tree-sitter-javascript": "^0.21.4",
80
- "tree-sitter-python": "^0.21.0"
79
+ "tree-sitter-python": "^0.21.0",
80
+ "tree-sitter-typescript": "^0.21.2"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@eslint/js": "^9.17.0",
@@ -1,4 +0,0 @@
1
- import "./logger-D2fS2ccL.js";
2
- import { ModelRoutingConfigSchema, ProviderPolicySchema, RoutingConfigError, RoutingRecommender, RoutingResolver, TASK_TYPE_PHASE_MAP, loadModelRoutingConfig } from "./routing-CZfJB3y9.js";
3
-
4
- export { loadModelRoutingConfig };