substrate-ai 0.19.1 → 0.19.2

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 { FileStateStore, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, WorkGraphRepository, buildPipelineStatusOutput, createDatabaseAdapter, createStateStore, detectCycles, findPackageRoot, formatOutput, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, parseDbTimestampAsUtc, registerHealthCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot } from "../health-Cx2ZhRNT.js";
2
+ import { FileStateStore, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, WorkGraphRepository, buildPipelineStatusOutput, createDatabaseAdapter, createStateStore, findPackageRoot, formatOutput, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, parseDbTimestampAsUtc, registerHealthCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot } from "../health-Cx2ZhRNT.js";
3
3
  import { createLogger } from "../logger-KeHncl-f.js";
4
4
  import { createEventBus } from "../helpers-CElYrONe.js";
5
5
  import { AdapterRegistry, BudgetConfigSchema, CURRENT_CONFIG_FORMAT_VERSION, CURRENT_TASK_GRAPH_VERSION, ConfigError, CostTrackerConfigSchema, DEFAULT_CONFIG, DoltClient, DoltNotInstalled, EXPERIMENT_RESULT, GlobalSettingsSchema, IngestionServer, MonitorDatabaseImpl, OPERATIONAL_FINDING, PartialGlobalSettingsSchema, PartialProviderConfigSchema, ProvidersSchema, RoutingRecommender, STORY_METRICS, TelemetryConfigSchema, addTokenUsage, aggregateTokenUsageForRun, checkDoltInstalled, compareRunMetrics, createAmendmentRun, createConfigSystem, createDecision, createDoltClient, createPipelineRun, getActiveDecisions, getAllCostEntriesFiltered, getBaselineRunMetrics, getDecisionsByCategory, getDecisionsByPhaseForRun, getLatestCompletedRun, getLatestRun, getPipelineRunById, getPlanningCostTotal, getRetryableEscalations, getRunMetrics, getSessionCostSummary, getSessionCostSummaryFiltered, getStoryMetricsForRun, getTokenUsageSummary, incrementRunRestarts, initSchema, initializeDolt, listRequirements, listRunMetrics, loadParentRunDecisions, supersedeDecision, tagRunAsBaseline, updatePipelineRun } from "../dist-Bm0qSZer.js";
6
6
  import "../adapter-registry-DXLMTmfD.js";
7
- import { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-Byzy10gG.js";
7
+ import { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, EpicIngester, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-BOhSIujp.js";
8
8
  import "../errors-BSpu7pIv.js";
9
9
  import "../routing-CcBOCuC9.js";
10
10
  import "../decisions-C0pz9Clx.js";
@@ -2634,26 +2634,6 @@ function registerConfigCommand(program, _version) {
2634
2634
  });
2635
2635
  }
2636
2636
 
2637
- //#endregion
2638
- //#region src/modules/work-graph/errors.ts
2639
- /**
2640
- * Work-graph error types.
2641
- *
2642
- * Story 31-7: Cycle Detection in Work Graph
2643
- */
2644
- /**
2645
- * Thrown by `EpicIngester.ingest()` when the provided dependency list
2646
- * contains a cycle. The `cycle` field contains the path of story keys
2647
- * that form the cycle (first and last element are the same).
2648
- */
2649
- var CyclicDependencyError = class extends Error {
2650
- constructor(cycle) {
2651
- super(`Cyclic dependency detected: ${cycle.join(" → ")}`);
2652
- this.cycle = cycle;
2653
- this.name = "CyclicDependencyError";
2654
- }
2655
- };
2656
-
2657
2637
  //#endregion
2658
2638
  //#region src/cli/commands/resume.ts
2659
2639
  const logger$13 = createLogger("resume-cmd");
@@ -4542,7 +4522,7 @@ async function runSupervisorAction(options, deps = {}) {
4542
4522
  await initSchema(expAdapter);
4543
4523
  const { runRunAction: runPipeline } = await import(
4544
4524
  /* @vite-ignore */
4545
- "../run-CzEyqOom.js"
4525
+ "../run-CCHb_JMl.js"
4546
4526
  );
4547
4527
  const runStoryFn = async (opts) => {
4548
4528
  const exitCode = await runPipeline({
@@ -8620,64 +8600,6 @@ var EpicParser = class {
8620
8600
  }
8621
8601
  };
8622
8602
 
8623
- //#endregion
8624
- //#region src/modules/work-graph/epic-ingester.ts
8625
- var EpicIngester = class {
8626
- adapter;
8627
- constructor(adapter) {
8628
- this.adapter = adapter;
8629
- }
8630
- /**
8631
- * Upsert stories and sync dependencies into the database.
8632
- *
8633
- * Both operations are wrapped in a single transaction: if either fails the
8634
- * entire batch is rolled back.
8635
- *
8636
- * @param stories - Parsed story metadata from `EpicParser.parseStories()`.
8637
- * @param dependencies - Parsed dependency edges from `EpicParser.parseDependencies()`.
8638
- * @returns `IngestResult` with counts of affected rows.
8639
- */
8640
- async ingest(stories, dependencies) {
8641
- const cycle = detectCycles(dependencies);
8642
- if (cycle !== null) throw new CyclicDependencyError(cycle);
8643
- return this.adapter.transaction(async (tx) => {
8644
- let storiesUpserted = 0;
8645
- for (const story of stories) {
8646
- const existing = await tx.query("SELECT status FROM wg_stories WHERE story_key = ?", [story.story_key]);
8647
- if (existing.length > 0) await tx.query("UPDATE wg_stories SET title = ?, updated_at = ? WHERE story_key = ?", [
8648
- story.title,
8649
- new Date().toISOString(),
8650
- story.story_key
8651
- ]);
8652
- else {
8653
- const now = new Date().toISOString();
8654
- await tx.query("INSERT INTO wg_stories (story_key, epic, title, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", [
8655
- story.story_key,
8656
- String(story.epic_num),
8657
- story.title,
8658
- "planned",
8659
- now,
8660
- now
8661
- ]);
8662
- storiesUpserted++;
8663
- }
8664
- }
8665
- const epicNum = stories.length > 0 ? stories[0].epic_num : null;
8666
- if (epicNum !== null) await tx.query(`DELETE FROM story_dependencies WHERE source = 'explicit' AND story_key LIKE ?`, [`${epicNum}-%`]);
8667
- for (const dep of dependencies) await tx.query("INSERT INTO story_dependencies (story_key, depends_on, dependency_type, source) VALUES (?, ?, ?, ?)", [
8668
- dep.story_key,
8669
- dep.depends_on,
8670
- dep.dependency_type,
8671
- dep.source
8672
- ]);
8673
- return {
8674
- storiesUpserted,
8675
- dependenciesReplaced: dependencies.length
8676
- };
8677
- });
8678
- }
8679
- };
8680
-
8681
8603
  //#endregion
8682
8604
  //#region src/cli/commands/ingest-epic.ts
8683
8605
  function registerIngestEpicCommand(program) {
@@ -1,4 +1,4 @@
1
- import { BMAD_BASELINE_TOKENS_FULL, DoltMergeConflict, FileStateStore, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN, VALID_PHASES, WorkGraphRepository, __commonJS, __require, __toESM, buildPipelineStatusOutput, createDatabaseAdapter, formatOutput, formatPipelineSummary, formatTokenTelemetry, inspectProcessTree, parseDbTimestampAsUtc, resolveMainRepoRoot, validateStoryKey } from "./health-Cx2ZhRNT.js";
1
+ import { BMAD_BASELINE_TOKENS_FULL, DoltMergeConflict, FileStateStore, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN, VALID_PHASES, WorkGraphRepository, __commonJS, __require, __toESM, buildPipelineStatusOutput, createDatabaseAdapter, detectCycles, formatOutput, formatPipelineSummary, formatTokenTelemetry, inspectProcessTree, parseDbTimestampAsUtc, resolveMainRepoRoot, validateStoryKey } from "./health-Cx2ZhRNT.js";
2
2
  import { createLogger } from "./logger-KeHncl-f.js";
3
3
  import { TypedEventBusImpl, createEventBus, createTuiApp, isTuiCapable, printNonTtyWarning, sleep } from "./helpers-CElYrONe.js";
4
4
  import { ADVISORY_NOTES, Categorizer, ConsumerAnalyzer, DEFAULT_GLOBAL_SETTINGS, DispatcherImpl, DoltClient, ESCALATION_DIAGNOSIS, EfficiencyScorer, IngestionServer, LogTurnAnalyzer, OPERATIONAL_FINDING, Recommender, RoutingRecommender, RoutingResolver, RoutingTelemetry, RoutingTokenAccumulator, RoutingTuner, STORY_METRICS, STORY_OUTCOME, SubstrateConfigSchema, TEST_EXPANSION_FINDING, TEST_PLAN, TelemetryNormalizer, TelemetryPipeline, TurnAnalyzer, addTokenUsage, aggregateTokenUsageForRun, aggregateTokenUsageForStory, callLLM, createConfigSystem, createDatabaseAdapter$1, createDecision, createPipelineRun, createRequirement, detectInterfaceChanges, getArtifactByTypeForRun, getArtifactsByRun, getDecisionsByCategory, getDecisionsByPhase, getDecisionsByPhaseForRun, getPipelineRunById, getRunningPipelineRuns, getStoryMetricsForRun, getTokenUsageSummary, initSchema, loadModelRoutingConfig, registerArtifact, updatePipelineRun, updatePipelineRunConfig, upsertDecision, writeRunMetrics, writeStoryMetrics } from "./dist-Bm0qSZer.js";
@@ -6009,6 +6009,26 @@ function countFilesInLayout(content) {
6009
6009
  return count;
6010
6010
  }
6011
6011
 
6012
+ //#endregion
6013
+ //#region src/modules/work-graph/errors.ts
6014
+ /**
6015
+ * Work-graph error types.
6016
+ *
6017
+ * Story 31-7: Cycle Detection in Work Graph
6018
+ */
6019
+ /**
6020
+ * Thrown by `EpicIngester.ingest()` when the provided dependency list
6021
+ * contains a cycle. The `cycle` field contains the path of story keys
6022
+ * that form the cycle (first and last element are the same).
6023
+ */
6024
+ var CyclicDependencyError = class extends Error {
6025
+ constructor(cycle) {
6026
+ super(`Cyclic dependency detected: ${cycle.join(" → ")}`);
6027
+ this.cycle = cycle;
6028
+ this.name = "CyclicDependencyError";
6029
+ }
6030
+ };
6031
+
6012
6032
  //#endregion
6013
6033
  //#region src/modules/work-graph/spec-migrator.ts
6014
6034
  /**
@@ -9661,6 +9681,446 @@ function createTelemetryAdvisor(deps) {
9661
9681
  return new TelemetryAdvisor(deps);
9662
9682
  }
9663
9683
 
9684
+ //#endregion
9685
+ //#region src/modules/work-graph/epic-ingester.ts
9686
+ var EpicIngester = class {
9687
+ adapter;
9688
+ constructor(adapter) {
9689
+ this.adapter = adapter;
9690
+ }
9691
+ /**
9692
+ * Upsert stories and sync dependencies into the database.
9693
+ *
9694
+ * Both operations are wrapped in a single transaction: if either fails the
9695
+ * entire batch is rolled back.
9696
+ *
9697
+ * @param stories - Parsed story metadata from `EpicParser.parseStories()`.
9698
+ * @param dependencies - Parsed dependency edges from `EpicParser.parseDependencies()`.
9699
+ * @returns `IngestResult` with counts of affected rows.
9700
+ */
9701
+ async ingest(stories, dependencies) {
9702
+ const cycle = detectCycles(dependencies);
9703
+ if (cycle !== null) throw new CyclicDependencyError(cycle);
9704
+ return this.adapter.transaction(async (tx) => {
9705
+ let storiesUpserted = 0;
9706
+ for (const story of stories) {
9707
+ const existing = await tx.query("SELECT status FROM wg_stories WHERE story_key = ?", [story.story_key]);
9708
+ if (existing.length > 0) await tx.query("UPDATE wg_stories SET title = ?, updated_at = ? WHERE story_key = ?", [
9709
+ story.title,
9710
+ new Date().toISOString(),
9711
+ story.story_key
9712
+ ]);
9713
+ else {
9714
+ const now = new Date().toISOString();
9715
+ await tx.query("INSERT INTO wg_stories (story_key, epic, title, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", [
9716
+ story.story_key,
9717
+ String(story.epic_num),
9718
+ story.title,
9719
+ "planned",
9720
+ now,
9721
+ now
9722
+ ]);
9723
+ storiesUpserted++;
9724
+ }
9725
+ }
9726
+ const epicNum = stories.length > 0 ? stories[0].epic_num : null;
9727
+ if (epicNum !== null) await tx.query(`DELETE FROM story_dependencies WHERE source = 'explicit' AND story_key LIKE ?`, [`${epicNum}-%`]);
9728
+ for (const dep of dependencies) await tx.query("INSERT INTO story_dependencies (story_key, depends_on, dependency_type, source) VALUES (?, ?, ?, ?)", [
9729
+ dep.story_key,
9730
+ dep.depends_on,
9731
+ dep.dependency_type,
9732
+ dep.source
9733
+ ]);
9734
+ return {
9735
+ storiesUpserted,
9736
+ dependenciesReplaced: dependencies.length
9737
+ };
9738
+ });
9739
+ }
9740
+ };
9741
+
9742
+ //#endregion
9743
+ //#region src/modules/implementation-orchestrator/story-discovery.ts
9744
+ /**
9745
+ * Unified story key resolution with a 5-level fallback chain.
9746
+ *
9747
+ * 1. Explicit keys (from --stories flag) — returned as-is
9748
+ * 1.5. ready_stories SQL view — when work graph is populated (story 31-3)
9749
+ * 2. Decisions table (category='stories', phase='solutioning')
9750
+ * 3. Epic shard decisions (category='epic-shard') — parsed with parseStoryKeysFromEpics
9751
+ * 4. epics.md file on disk (via discoverPendingStoryKeys)
9752
+ *
9753
+ * Optionally filters out completed stories when filterCompleted is set.
9754
+ *
9755
+ * @returns Sorted, deduplicated array of story keys in "N-M" format
9756
+ */
9757
+ async function resolveStoryKeys(db, projectRoot, opts) {
9758
+ if (opts?.explicit !== void 0 && opts.explicit.length > 0) return topologicalSortByDependencies(opts.explicit, projectRoot);
9759
+ let keys = [];
9760
+ const readyKeys = await db.queryReadyStories();
9761
+ if (readyKeys.length > 0) {
9762
+ let filteredKeys = readyKeys;
9763
+ if (opts?.epicNumber !== void 0) {
9764
+ const prefix = `${opts.epicNumber}-`;
9765
+ filteredKeys = filteredKeys.filter((k) => k.startsWith(prefix));
9766
+ }
9767
+ if (opts?.filterCompleted === true && filteredKeys.length > 0) {
9768
+ const completedKeys = await getCompletedStoryKeys(db);
9769
+ filteredKeys = filteredKeys.filter((k) => !completedKeys.has(k));
9770
+ }
9771
+ const existingArtifacts = collectExistingStoryKeys(projectRoot);
9772
+ const alreadyDone = filteredKeys.filter((k) => existingArtifacts.has(k));
9773
+ if (alreadyDone.length > 0) {
9774
+ filteredKeys = filteredKeys.filter((k) => !existingArtifacts.has(k));
9775
+ for (const key of alreadyDone) db.query(`UPDATE wg_stories SET status = 'complete', completed_at = ? WHERE story_key = ? AND status <> 'complete'`, [new Date().toISOString(), key]).catch(() => {});
9776
+ }
9777
+ return sortStoryKeys([...new Set(filteredKeys)]);
9778
+ }
9779
+ try {
9780
+ const sql = opts?.pipelineRunId !== void 0 ? `SELECT key FROM decisions WHERE phase = 'solutioning' AND category = 'stories' AND pipeline_run_id = ? ORDER BY created_at ASC` : `SELECT key FROM decisions WHERE phase = 'solutioning' AND category = 'stories' ORDER BY created_at ASC`;
9781
+ const params = opts?.pipelineRunId !== void 0 ? [opts.pipelineRunId] : [];
9782
+ const rows = await db.query(sql, params);
9783
+ for (const row of rows) if (/^\d+-\d+/.test(row.key)) {
9784
+ const match$1 = /^(\d+-\d+)/.exec(row.key);
9785
+ if (match$1 !== null) keys.push(match$1[1]);
9786
+ }
9787
+ } catch {}
9788
+ if (keys.length === 0) try {
9789
+ const sql = opts?.pipelineRunId !== void 0 ? `SELECT value FROM decisions WHERE category = 'epic-shard' AND pipeline_run_id = ? ORDER BY created_at ASC` : `SELECT value FROM decisions WHERE category = 'epic-shard' ORDER BY created_at ASC`;
9790
+ const params = opts?.pipelineRunId !== void 0 ? [opts.pipelineRunId] : [];
9791
+ const shardRows = await db.query(sql, params);
9792
+ const allContent = shardRows.map((r) => r.value).join("\n");
9793
+ if (allContent.length > 0) keys = parseStoryKeysFromEpics(allContent);
9794
+ } catch {}
9795
+ if (keys.length === 0) keys = discoverPendingStoryKeys(projectRoot, opts?.epicNumber);
9796
+ if (opts?.epicNumber !== void 0 && keys.length > 0) {
9797
+ const prefix = `${opts.epicNumber}-`;
9798
+ keys = keys.filter((k) => k.startsWith(prefix));
9799
+ }
9800
+ if (opts?.filterCompleted === true && keys.length > 0) {
9801
+ const completedKeys = await getCompletedStoryKeys(db);
9802
+ keys = keys.filter((k) => !completedKeys.has(k));
9803
+ }
9804
+ if (keys.length > 0) {
9805
+ const existingArtifacts = collectExistingStoryKeys(projectRoot);
9806
+ keys = keys.filter((k) => !existingArtifacts.has(k));
9807
+ }
9808
+ return sortStoryKeys([...new Set(keys)]);
9809
+ }
9810
+ /**
9811
+ * Extract all story keys (N-M format) from epics.md content.
9812
+ *
9813
+ * Supports three extraction patterns found in real epics.md files:
9814
+ * 1. Explicit key lines: **Story key:** `7-2-human-turn-loop` → extracts "7-2"
9815
+ * 2. Story headings: ### Story 7.2: Human Turn Loop → extracts "7-2"
9816
+ * 3. File path refs: _bmad-output/implementation-artifacts/7-2-human-turn-loop.md → extracts "7-2"
9817
+ *
9818
+ * Keys are deduplicated and sorted numerically (epic number primary, story number secondary).
9819
+ *
9820
+ * @param content - Raw string content of epics.md
9821
+ * @returns Sorted, deduplicated array of story key strings in "N-M" format
9822
+ */
9823
+ function parseStoryKeysFromEpics(content) {
9824
+ if (content.length === 0) return [];
9825
+ const keys = new Set();
9826
+ const explicitKeyPattern = /\*\*Story key:\*\*\s*`?([A-Za-z0-9]+-[A-Za-z0-9]+)(?:-[^`\s]*)?`?/g;
9827
+ let match$1;
9828
+ while ((match$1 = explicitKeyPattern.exec(content)) !== null) if (match$1[1] !== void 0) keys.add(match$1[1]);
9829
+ const headingPattern = /^###\s+Story\s+([A-Za-z0-9]+)[.\-]([A-Za-z0-9]+)/gm;
9830
+ while ((match$1 = headingPattern.exec(content)) !== null) if (match$1[1] !== void 0 && match$1[2] !== void 0) keys.add(`${match$1[1]}-${match$1[2]}`);
9831
+ const inlineStoryPattern = /Story\s+([A-Za-z0-9]+)-([A-Za-z0-9]+)[:\s]/g;
9832
+ while ((match$1 = inlineStoryPattern.exec(content)) !== null) if (match$1[1] !== void 0 && match$1[2] !== void 0) keys.add(`${match$1[1]}-${match$1[2]}`);
9833
+ const filePathPattern = /_bmad-output\/implementation-artifacts\/([A-Za-z0-9]+-[A-Za-z0-9]+)-/g;
9834
+ while ((match$1 = filePathPattern.exec(content)) !== null) if (match$1[1] !== void 0) keys.add(match$1[1]);
9835
+ return sortStoryKeys(Array.from(keys));
9836
+ }
9837
+ /**
9838
+ * Discover pending story keys by diffing epics.md against existing story files.
9839
+ *
9840
+ * Algorithm:
9841
+ * 1. Read _bmad-output/planning-artifacts/epics.md (falls back to _bmad-output/epics.md)
9842
+ * 2. Extract all story keys from epics.md
9843
+ * 3. Glob _bmad-output/implementation-artifacts/ for N-M-*.md files
9844
+ * 4. Return keys from step 2 that are NOT in step 3 (pending work)
9845
+ *
9846
+ * Returns an empty array (without error) if epics.md does not exist.
9847
+ *
9848
+ * @param projectRoot - Absolute path to the project root directory
9849
+ * @returns Sorted array of pending story keys in "N-M" format
9850
+ */
9851
+ function discoverPendingStoryKeys(projectRoot, epicNumber) {
9852
+ let allKeys = [];
9853
+ if (epicNumber !== void 0) {
9854
+ const epicFiles = findEpicFiles(projectRoot);
9855
+ const targetPattern = new RegExp(`^epic-${epicNumber}[^0-9]`);
9856
+ const matched = epicFiles.filter((f$1) => targetPattern.test(f$1.split("/").pop()));
9857
+ for (const epicFile of matched) try {
9858
+ const content = readFileSync(epicFile, "utf-8");
9859
+ const keys = parseStoryKeysFromEpics(content);
9860
+ allKeys.push(...keys);
9861
+ } catch {}
9862
+ allKeys = sortStoryKeys([...new Set(allKeys)]);
9863
+ } else {
9864
+ const epicsPath = findEpicsFile(projectRoot);
9865
+ if (epicsPath !== void 0) try {
9866
+ const content = readFileSync(epicsPath, "utf-8");
9867
+ allKeys = parseStoryKeysFromEpics(content);
9868
+ } catch {}
9869
+ if (allKeys.length === 0) {
9870
+ const epicFiles = findEpicFiles(projectRoot);
9871
+ for (const epicFile of epicFiles) try {
9872
+ const content = readFileSync(epicFile, "utf-8");
9873
+ const keys = parseStoryKeysFromEpics(content);
9874
+ allKeys.push(...keys);
9875
+ } catch {}
9876
+ allKeys = sortStoryKeys([...new Set(allKeys)]);
9877
+ }
9878
+ }
9879
+ const sprintKeys = parseStoryKeysFromSprintStatus(projectRoot);
9880
+ if (sprintKeys.length > 0) {
9881
+ const merged = new Set(allKeys);
9882
+ for (const k of sprintKeys) merged.add(k);
9883
+ allKeys = sortStoryKeys([...merged]);
9884
+ }
9885
+ if (allKeys.length === 0) return [];
9886
+ const existingKeys = collectExistingStoryKeys(projectRoot);
9887
+ return allKeys.filter((k) => !existingKeys.has(k));
9888
+ }
9889
+ /**
9890
+ * Find epic files from known candidate paths relative to projectRoot.
9891
+ *
9892
+ * Checks for:
9893
+ * 1. epics.md (consolidated epic file)
9894
+ * 2. Individual epic-*.md files in planning-artifacts/
9895
+ *
9896
+ * Returns a single path for epics.md, or undefined if not found.
9897
+ * For individual epic files, use findEpicFiles() instead.
9898
+ */
9899
+ function findEpicsFile(projectRoot) {
9900
+ const candidates = ["_bmad-output/planning-artifacts/epics.md", "_bmad-output/epics.md"];
9901
+ for (const candidate of candidates) {
9902
+ const fullPath = join$1(projectRoot, candidate);
9903
+ if (existsSync(fullPath)) return fullPath;
9904
+ }
9905
+ const planningDir = join$1(projectRoot, "_bmad-output", "planning-artifacts");
9906
+ if (existsSync(planningDir)) try {
9907
+ const entries = readdirSync(planningDir, { encoding: "utf-8" });
9908
+ const match$1 = entries.filter((e) => /^epics[-.].*\.md$/i.test(e) && !/^epic-\d+/.test(e)).sort();
9909
+ if (match$1.length > 0) return join$1(planningDir, match$1[0]);
9910
+ } catch {}
9911
+ return void 0;
9912
+ }
9913
+ /**
9914
+ * Find individual epic-*.md files in the planning artifacts directory.
9915
+ * Returns paths sorted alphabetically.
9916
+ */
9917
+ function findEpicFiles(projectRoot) {
9918
+ const planningDir = join$1(projectRoot, "_bmad-output", "planning-artifacts");
9919
+ if (!existsSync(planningDir)) return [];
9920
+ try {
9921
+ const entries = readdirSync(planningDir, { encoding: "utf-8" });
9922
+ return entries.filter((e) => /^epic-\d+.*\.md$/.test(e)).sort().map((e) => join$1(planningDir, e));
9923
+ } catch {
9924
+ return [];
9925
+ }
9926
+ }
9927
+ /**
9928
+ * Collect story keys that already have implementation artifact files.
9929
+ * Scans _bmad-output/implementation-artifacts/ for files matching N-M-*.md.
9930
+ */
9931
+ function collectExistingStoryKeys(projectRoot) {
9932
+ const existing = new Set();
9933
+ const artifactsDir = join$1(projectRoot, "_bmad-output", "implementation-artifacts");
9934
+ if (!existsSync(artifactsDir)) return existing;
9935
+ let entries;
9936
+ try {
9937
+ entries = readdirSync(artifactsDir, { encoding: "utf-8" });
9938
+ } catch {
9939
+ return existing;
9940
+ }
9941
+ const filePattern = /^([A-Za-z0-9]+-[A-Za-z0-9]+)-/;
9942
+ for (const entry of entries) {
9943
+ if (!entry.endsWith(".md")) continue;
9944
+ const m = filePattern.exec(entry);
9945
+ if (m !== null && m[1] !== void 0) existing.add(m[1]);
9946
+ }
9947
+ return existing;
9948
+ }
9949
+ /**
9950
+ * Parse story keys from sprint-status.yaml.
9951
+ * Reads the development_status map and extracts keys that match the
9952
+ * alphanumeric story key pattern (e.g., 1-1a, NEW-26, E5-accessibility).
9953
+ * Filters out epic status entries (epic-N) and retrospective entries.
9954
+ */
9955
+ function parseStoryKeysFromSprintStatus(projectRoot) {
9956
+ const candidates = [join$1(projectRoot, "_bmad-output", "implementation-artifacts", "sprint-status.yaml"), join$1(projectRoot, "_bmad-output", "sprint-status.yaml")];
9957
+ const statusPath = candidates.find((p) => existsSync(p));
9958
+ if (!statusPath) return [];
9959
+ try {
9960
+ const content = readFileSync(statusPath, "utf-8");
9961
+ const keys = [];
9962
+ const linePattern = /^\s{2}([A-Za-z0-9]+-[A-Za-z0-9]+(?:-[A-Za-z0-9-]*)?)\s*:/gm;
9963
+ let match$1;
9964
+ while ((match$1 = linePattern.exec(content)) !== null) {
9965
+ const fullKey = match$1[1];
9966
+ if (/^epic-\d+$/.test(fullKey)) continue;
9967
+ if (fullKey.includes("retrospective")) continue;
9968
+ const segments = fullKey.split("-");
9969
+ if (segments.length >= 2) keys.push(`${segments[0]}-${segments[1]}`);
9970
+ }
9971
+ return [...new Set(keys)];
9972
+ } catch {
9973
+ return [];
9974
+ }
9975
+ }
9976
+ /**
9977
+ * Collect story keys already completed in previous pipeline runs.
9978
+ * Scans pipeline_runs with status='completed' and extracts story keys
9979
+ * with phase='COMPLETE' from their token_usage_json state.
9980
+ */
9981
+ async function getCompletedStoryKeys(db) {
9982
+ const completed = new Set();
9983
+ try {
9984
+ const rows = await db.query(`SELECT token_usage_json FROM pipeline_runs WHERE status = 'completed' AND token_usage_json IS NOT NULL`);
9985
+ for (const row of rows) try {
9986
+ const state = JSON.parse(row.token_usage_json);
9987
+ if (state.stories !== void 0) {
9988
+ for (const [key, s$1] of Object.entries(state.stories)) if (s$1.phase === "COMPLETE") completed.add(key);
9989
+ }
9990
+ } catch {}
9991
+ } catch {}
9992
+ return completed;
9993
+ }
9994
+ /**
9995
+ * Sort story keys: numeric keys first (by epic then story number),
9996
+ * then alphabetic-prefix keys (NEW-*, E-*) sorted lexicographically.
9997
+ * E.g. ["10-1", "1-2a", "1-2", "NEW-26", "E5-acc"] → ["1-2", "1-2a", "10-1", "E5-acc", "NEW-26"]
9998
+ */
9999
+ function sortStoryKeys(keys) {
10000
+ return keys.slice().sort((a, b) => {
10001
+ const aParts = a.split("-");
10002
+ const bParts = b.split("-");
10003
+ const aNum = Number(aParts[0]);
10004
+ const bNum = Number(bParts[0]);
10005
+ if (!isNaN(aNum) && !isNaN(bNum)) {
10006
+ if (aNum !== bNum) return aNum - bNum;
10007
+ const aStory = Number(aParts[1]);
10008
+ const bStory = Number(bParts[1]);
10009
+ if (!isNaN(aStory) && !isNaN(bStory) && aStory !== bStory) return aStory - bStory;
10010
+ return (aParts[1] ?? "").localeCompare(bParts[1] ?? "");
10011
+ }
10012
+ if (!isNaN(aNum)) return -1;
10013
+ if (!isNaN(bNum)) return 1;
10014
+ return a.localeCompare(b);
10015
+ });
10016
+ }
10017
+ /**
10018
+ * Parse inter-story dependencies from the consolidated epics document.
10019
+ *
10020
+ * Scans for patterns like:
10021
+ * ### Story 50-2: Title
10022
+ * **Dependencies:** 50-1
10023
+ *
10024
+ * Returns a Map where key=storyKey, value=Set of dependency keys.
10025
+ * Only returns dependencies that are within the provided storyKeys set
10026
+ * (external dependencies to other epics are ignored for ordering purposes).
10027
+ */
10028
+ function parseEpicsDependencies(projectRoot, storyKeys) {
10029
+ const deps = new Map();
10030
+ const epicsPath = findEpicsFile(projectRoot);
10031
+ if (epicsPath === void 0) return deps;
10032
+ let content;
10033
+ try {
10034
+ content = readFileSync(epicsPath, "utf-8");
10035
+ } catch {
10036
+ return deps;
10037
+ }
10038
+ const storyPattern = /^###\s+Story\s+(\d+)-(\d+)[:\s]/gm;
10039
+ const depPattern = /^\*\*Dependencies:\*\*\s*(.+)$/gm;
10040
+ const storyPositions = [];
10041
+ let match$1;
10042
+ while ((match$1 = storyPattern.exec(content)) !== null) storyPositions.push({
10043
+ key: `${match$1[1]}-${match$1[2]}`,
10044
+ pos: match$1.index
10045
+ });
10046
+ for (let i = 0; i < storyPositions.length; i++) {
10047
+ const story = storyPositions[i];
10048
+ const nextStoryPos = i + 1 < storyPositions.length ? storyPositions[i + 1].pos : content.length;
10049
+ const section = content.slice(story.pos, nextStoryPos);
10050
+ depPattern.lastIndex = 0;
10051
+ const depMatch = depPattern.exec(section);
10052
+ if (depMatch === null || /^none$/i.test(depMatch[1].trim())) continue;
10053
+ const depText = depMatch[1];
10054
+ const storyDeps = new Set();
10055
+ const rangeMatch = /(\d+)-(\d+)\s+through\s+\1-(\d+)/i.exec(depText);
10056
+ if (rangeMatch !== null) {
10057
+ const epic = rangeMatch[1];
10058
+ const start = Number(rangeMatch[2]);
10059
+ const end = Number(rangeMatch[3]);
10060
+ for (let n$1 = start; n$1 <= end; n$1++) {
10061
+ const depKey = `${epic}-${n$1}`;
10062
+ if (storyKeys.has(depKey)) storyDeps.add(depKey);
10063
+ }
10064
+ } else {
10065
+ const keyPattern = /(\d+-\d+[a-z]?)/g;
10066
+ let km;
10067
+ while ((km = keyPattern.exec(depText)) !== null) {
10068
+ const depKey = km[1];
10069
+ if (storyKeys.has(depKey)) storyDeps.add(depKey);
10070
+ }
10071
+ }
10072
+ if (storyDeps.size > 0) deps.set(story.key, storyDeps);
10073
+ }
10074
+ return deps;
10075
+ }
10076
+ /**
10077
+ * Topologically sort explicit story keys by inter-story dependencies.
10078
+ *
10079
+ * Parses the consolidated epics document for dependency metadata, builds
10080
+ * a DAG, and returns keys in dependency-first order using Kahn's algorithm.
10081
+ * Stories with no dependencies come first; stories that depend on others
10082
+ * are placed after their prerequisites.
10083
+ *
10084
+ * Falls back to numeric sort if no epics document exists or no
10085
+ * dependencies are found among the provided keys.
10086
+ */
10087
+ function topologicalSortByDependencies(keys, projectRoot) {
10088
+ if (keys.length <= 1) return keys;
10089
+ const keySet = new Set(keys);
10090
+ const deps = parseEpicsDependencies(projectRoot, keySet);
10091
+ if (deps.size === 0) return sortStoryKeys(keys);
10092
+ const inDegree = new Map();
10093
+ const successors = new Map();
10094
+ for (const key of keys) {
10095
+ inDegree.set(key, 0);
10096
+ successors.set(key, new Set());
10097
+ }
10098
+ for (const [dependent, depSet] of deps) {
10099
+ if (!keySet.has(dependent)) continue;
10100
+ for (const dep of depSet) {
10101
+ if (!keySet.has(dep)) continue;
10102
+ successors.get(dep).add(dependent);
10103
+ inDegree.set(dependent, (inDegree.get(dependent) ?? 0) + 1);
10104
+ }
10105
+ }
10106
+ const result = [];
10107
+ const processed = new Set();
10108
+ while (processed.size < keys.length) {
10109
+ const wave = [];
10110
+ for (const key of keys) if (!processed.has(key) && (inDegree.get(key) ?? 0) === 0) wave.push(key);
10111
+ if (wave.length === 0) {
10112
+ for (const key of sortStoryKeys(keys)) if (!processed.has(key)) result.push(key);
10113
+ break;
10114
+ }
10115
+ for (const key of sortStoryKeys(wave)) {
10116
+ result.push(key);
10117
+ processed.add(key);
10118
+ for (const succ of successors.get(key) ?? []) inDegree.set(succ, (inDegree.get(succ) ?? 0) - 1);
10119
+ }
10120
+ }
10121
+ return result;
10122
+ }
10123
+
9664
10124
  //#endregion
9665
10125
  //#region src/modules/implementation-orchestrator/orchestrator-impl.ts
9666
10126
  function estimateDispatchCost(input, output) {
@@ -9805,6 +10265,88 @@ function isImplicitlyCovered(storyKey, projectRoot) {
9805
10265
  const existCount = expectedNewFiles.filter((f$1) => existsSync(join$1(projectRoot, f$1))).length;
9806
10266
  return existCount === expectedNewFiles.length;
9807
10267
  }
10268
+ /**
10269
+ * Auto-ingest stories and inter-story dependencies from the consolidated
10270
+ * epics document into the work graph (`wg_stories` + `story_dependencies`).
10271
+ *
10272
+ * This bridges the gap between Level 4 discovery (file-based, no dependency
10273
+ * gating) and Level 1.5 discovery (`ready_stories` view, dependency-aware).
10274
+ *
10275
+ * Idempotent: existing stories preserve their status; dependencies are
10276
+ * replaced per-epic (EpicIngester's delete-and-reinsert pattern).
10277
+ */
10278
+ async function autoIngestEpicsDependencies(db, projectRoot) {
10279
+ const epicsPath = findEpicsFile(projectRoot);
10280
+ if (!epicsPath) return {
10281
+ storiesIngested: 0,
10282
+ dependenciesIngested: 0
10283
+ };
10284
+ let content;
10285
+ try {
10286
+ content = readFileSync(epicsPath, "utf-8");
10287
+ } catch {
10288
+ return {
10289
+ storiesIngested: 0,
10290
+ dependenciesIngested: 0
10291
+ };
10292
+ }
10293
+ const storyPattern = /^###\s+Story\s+(\d+)-(\d+):\s+(.+)$/gm;
10294
+ const stories = [];
10295
+ let match$1;
10296
+ while ((match$1 = storyPattern.exec(content)) !== null) {
10297
+ const epicNum = parseInt(match$1[1], 10);
10298
+ const storyNum = parseInt(match$1[2], 10);
10299
+ stories.push({
10300
+ story_key: `${epicNum}-${storyNum}`,
10301
+ epic_num: epicNum,
10302
+ story_num: storyNum,
10303
+ title: match$1[3].trim(),
10304
+ priority: "P0",
10305
+ size: "Medium",
10306
+ sprint: 0
10307
+ });
10308
+ }
10309
+ if (stories.length === 0) return {
10310
+ storiesIngested: 0,
10311
+ dependenciesIngested: 0
10312
+ };
10313
+ const allKeys = new Set(stories.map((s$1) => s$1.story_key));
10314
+ const depMap = parseEpicsDependencies(projectRoot, allKeys);
10315
+ const dependencies = [];
10316
+ for (const [dependent, depSet] of depMap) for (const dep of depSet) dependencies.push({
10317
+ story_key: dependent,
10318
+ depends_on: dep,
10319
+ dependency_type: "blocks",
10320
+ source: "explicit"
10321
+ });
10322
+ try {
10323
+ await db.query(`CREATE TABLE IF NOT EXISTS wg_stories (
10324
+ story_key VARCHAR(20) NOT NULL,
10325
+ epic VARCHAR(20) NOT NULL,
10326
+ title VARCHAR(255),
10327
+ status VARCHAR(30) NOT NULL DEFAULT 'planned',
10328
+ spec_path VARCHAR(500),
10329
+ created_at DATETIME,
10330
+ updated_at DATETIME,
10331
+ completed_at DATETIME,
10332
+ PRIMARY KEY (story_key)
10333
+ )`);
10334
+ await db.query(`CREATE TABLE IF NOT EXISTS story_dependencies (
10335
+ story_key VARCHAR(50) NOT NULL,
10336
+ depends_on VARCHAR(50) NOT NULL,
10337
+ dependency_type VARCHAR(50) NOT NULL DEFAULT 'blocks',
10338
+ source VARCHAR(50) NOT NULL DEFAULT 'explicit',
10339
+ created_at DATETIME,
10340
+ PRIMARY KEY (story_key, depends_on)
10341
+ )`);
10342
+ } catch {}
10343
+ const ingester = new EpicIngester(db);
10344
+ const result = await ingester.ingest(stories, dependencies);
10345
+ return {
10346
+ storiesIngested: result.storiesUpserted,
10347
+ dependenciesIngested: result.dependenciesReplaced
10348
+ };
10349
+ }
9808
10350
  const TITLE_OVERLAP_WARNING_THRESHOLD = .3;
9809
10351
  /**
9810
10352
  * Map a StoryPhase to the corresponding WgStoryStatus for wg_stories writes.
@@ -12075,6 +12617,18 @@ function createImplementationOrchestrator(deps) {
12075
12617
  durationMs: _startupTimings.seedMethodologyMs
12076
12618
  }, "Methodology context seeded from planning artifacts");
12077
12619
  }
12620
+ if (projectRoot !== void 0) {
12621
+ const ingestStart = Date.now();
12622
+ try {
12623
+ const ingestResult = await autoIngestEpicsDependencies(db, projectRoot);
12624
+ if (ingestResult.storiesIngested > 0 || ingestResult.dependenciesIngested > 0) logger$21.info({
12625
+ ...ingestResult,
12626
+ durationMs: Date.now() - ingestStart
12627
+ }, "Auto-ingested stories and dependencies from epics document");
12628
+ } catch (err) {
12629
+ logger$21.debug({ err }, "Auto-ingest from epics document skipped — work graph may be unavailable");
12630
+ }
12631
+ }
12078
12632
  try {
12079
12633
  if (stateStore !== void 0) {
12080
12634
  const stateStoreInitStart = Date.now();
@@ -12352,388 +12906,6 @@ function createImplementationOrchestrator(deps) {
12352
12906
  };
12353
12907
  }
12354
12908
 
12355
- //#endregion
12356
- //#region src/modules/implementation-orchestrator/story-discovery.ts
12357
- /**
12358
- * Unified story key resolution with a 5-level fallback chain.
12359
- *
12360
- * 1. Explicit keys (from --stories flag) — returned as-is
12361
- * 1.5. ready_stories SQL view — when work graph is populated (story 31-3)
12362
- * 2. Decisions table (category='stories', phase='solutioning')
12363
- * 3. Epic shard decisions (category='epic-shard') — parsed with parseStoryKeysFromEpics
12364
- * 4. epics.md file on disk (via discoverPendingStoryKeys)
12365
- *
12366
- * Optionally filters out completed stories when filterCompleted is set.
12367
- *
12368
- * @returns Sorted, deduplicated array of story keys in "N-M" format
12369
- */
12370
- async function resolveStoryKeys(db, projectRoot, opts) {
12371
- if (opts?.explicit !== void 0 && opts.explicit.length > 0) return topologicalSortByDependencies(opts.explicit, projectRoot);
12372
- let keys = [];
12373
- const readyKeys = await db.queryReadyStories();
12374
- if (readyKeys.length > 0) {
12375
- let filteredKeys = readyKeys;
12376
- if (opts?.epicNumber !== void 0) {
12377
- const prefix = `${opts.epicNumber}-`;
12378
- filteredKeys = filteredKeys.filter((k) => k.startsWith(prefix));
12379
- }
12380
- if (opts?.filterCompleted === true && filteredKeys.length > 0) {
12381
- const completedKeys = await getCompletedStoryKeys(db);
12382
- filteredKeys = filteredKeys.filter((k) => !completedKeys.has(k));
12383
- }
12384
- const existingArtifacts = collectExistingStoryKeys(projectRoot);
12385
- const alreadyDone = filteredKeys.filter((k) => existingArtifacts.has(k));
12386
- if (alreadyDone.length > 0) {
12387
- filteredKeys = filteredKeys.filter((k) => !existingArtifacts.has(k));
12388
- for (const key of alreadyDone) db.query(`UPDATE wg_stories SET status = 'complete', completed_at = ? WHERE story_key = ? AND status <> 'complete'`, [new Date().toISOString(), key]).catch(() => {});
12389
- }
12390
- return sortStoryKeys([...new Set(filteredKeys)]);
12391
- }
12392
- try {
12393
- const sql = opts?.pipelineRunId !== void 0 ? `SELECT key FROM decisions WHERE phase = 'solutioning' AND category = 'stories' AND pipeline_run_id = ? ORDER BY created_at ASC` : `SELECT key FROM decisions WHERE phase = 'solutioning' AND category = 'stories' ORDER BY created_at ASC`;
12394
- const params = opts?.pipelineRunId !== void 0 ? [opts.pipelineRunId] : [];
12395
- const rows = await db.query(sql, params);
12396
- for (const row of rows) if (/^\d+-\d+/.test(row.key)) {
12397
- const match$1 = /^(\d+-\d+)/.exec(row.key);
12398
- if (match$1 !== null) keys.push(match$1[1]);
12399
- }
12400
- } catch {}
12401
- if (keys.length === 0) try {
12402
- const sql = opts?.pipelineRunId !== void 0 ? `SELECT value FROM decisions WHERE category = 'epic-shard' AND pipeline_run_id = ? ORDER BY created_at ASC` : `SELECT value FROM decisions WHERE category = 'epic-shard' ORDER BY created_at ASC`;
12403
- const params = opts?.pipelineRunId !== void 0 ? [opts.pipelineRunId] : [];
12404
- const shardRows = await db.query(sql, params);
12405
- const allContent = shardRows.map((r) => r.value).join("\n");
12406
- if (allContent.length > 0) keys = parseStoryKeysFromEpics(allContent);
12407
- } catch {}
12408
- if (keys.length === 0) keys = discoverPendingStoryKeys(projectRoot, opts?.epicNumber);
12409
- if (opts?.epicNumber !== void 0 && keys.length > 0) {
12410
- const prefix = `${opts.epicNumber}-`;
12411
- keys = keys.filter((k) => k.startsWith(prefix));
12412
- }
12413
- if (opts?.filterCompleted === true && keys.length > 0) {
12414
- const completedKeys = await getCompletedStoryKeys(db);
12415
- keys = keys.filter((k) => !completedKeys.has(k));
12416
- }
12417
- if (keys.length > 0) {
12418
- const existingArtifacts = collectExistingStoryKeys(projectRoot);
12419
- keys = keys.filter((k) => !existingArtifacts.has(k));
12420
- }
12421
- return sortStoryKeys([...new Set(keys)]);
12422
- }
12423
- /**
12424
- * Extract all story keys (N-M format) from epics.md content.
12425
- *
12426
- * Supports three extraction patterns found in real epics.md files:
12427
- * 1. Explicit key lines: **Story key:** `7-2-human-turn-loop` → extracts "7-2"
12428
- * 2. Story headings: ### Story 7.2: Human Turn Loop → extracts "7-2"
12429
- * 3. File path refs: _bmad-output/implementation-artifacts/7-2-human-turn-loop.md → extracts "7-2"
12430
- *
12431
- * Keys are deduplicated and sorted numerically (epic number primary, story number secondary).
12432
- *
12433
- * @param content - Raw string content of epics.md
12434
- * @returns Sorted, deduplicated array of story key strings in "N-M" format
12435
- */
12436
- function parseStoryKeysFromEpics(content) {
12437
- if (content.length === 0) return [];
12438
- const keys = new Set();
12439
- const explicitKeyPattern = /\*\*Story key:\*\*\s*`?([A-Za-z0-9]+-[A-Za-z0-9]+)(?:-[^`\s]*)?`?/g;
12440
- let match$1;
12441
- while ((match$1 = explicitKeyPattern.exec(content)) !== null) if (match$1[1] !== void 0) keys.add(match$1[1]);
12442
- const headingPattern = /^###\s+Story\s+([A-Za-z0-9]+)[.\-]([A-Za-z0-9]+)/gm;
12443
- while ((match$1 = headingPattern.exec(content)) !== null) if (match$1[1] !== void 0 && match$1[2] !== void 0) keys.add(`${match$1[1]}-${match$1[2]}`);
12444
- const inlineStoryPattern = /Story\s+([A-Za-z0-9]+)-([A-Za-z0-9]+)[:\s]/g;
12445
- while ((match$1 = inlineStoryPattern.exec(content)) !== null) if (match$1[1] !== void 0 && match$1[2] !== void 0) keys.add(`${match$1[1]}-${match$1[2]}`);
12446
- const filePathPattern = /_bmad-output\/implementation-artifacts\/([A-Za-z0-9]+-[A-Za-z0-9]+)-/g;
12447
- while ((match$1 = filePathPattern.exec(content)) !== null) if (match$1[1] !== void 0) keys.add(match$1[1]);
12448
- return sortStoryKeys(Array.from(keys));
12449
- }
12450
- /**
12451
- * Discover pending story keys by diffing epics.md against existing story files.
12452
- *
12453
- * Algorithm:
12454
- * 1. Read _bmad-output/planning-artifacts/epics.md (falls back to _bmad-output/epics.md)
12455
- * 2. Extract all story keys from epics.md
12456
- * 3. Glob _bmad-output/implementation-artifacts/ for N-M-*.md files
12457
- * 4. Return keys from step 2 that are NOT in step 3 (pending work)
12458
- *
12459
- * Returns an empty array (without error) if epics.md does not exist.
12460
- *
12461
- * @param projectRoot - Absolute path to the project root directory
12462
- * @returns Sorted array of pending story keys in "N-M" format
12463
- */
12464
- function discoverPendingStoryKeys(projectRoot, epicNumber) {
12465
- let allKeys = [];
12466
- if (epicNumber !== void 0) {
12467
- const epicFiles = findEpicFiles(projectRoot);
12468
- const targetPattern = new RegExp(`^epic-${epicNumber}[^0-9]`);
12469
- const matched = epicFiles.filter((f$1) => targetPattern.test(f$1.split("/").pop()));
12470
- for (const epicFile of matched) try {
12471
- const content = readFileSync(epicFile, "utf-8");
12472
- const keys = parseStoryKeysFromEpics(content);
12473
- allKeys.push(...keys);
12474
- } catch {}
12475
- allKeys = sortStoryKeys([...new Set(allKeys)]);
12476
- } else {
12477
- const epicsPath = findEpicsFile(projectRoot);
12478
- if (epicsPath !== void 0) try {
12479
- const content = readFileSync(epicsPath, "utf-8");
12480
- allKeys = parseStoryKeysFromEpics(content);
12481
- } catch {}
12482
- if (allKeys.length === 0) {
12483
- const epicFiles = findEpicFiles(projectRoot);
12484
- for (const epicFile of epicFiles) try {
12485
- const content = readFileSync(epicFile, "utf-8");
12486
- const keys = parseStoryKeysFromEpics(content);
12487
- allKeys.push(...keys);
12488
- } catch {}
12489
- allKeys = sortStoryKeys([...new Set(allKeys)]);
12490
- }
12491
- }
12492
- const sprintKeys = parseStoryKeysFromSprintStatus(projectRoot);
12493
- if (sprintKeys.length > 0) {
12494
- const merged = new Set(allKeys);
12495
- for (const k of sprintKeys) merged.add(k);
12496
- allKeys = sortStoryKeys([...merged]);
12497
- }
12498
- if (allKeys.length === 0) return [];
12499
- const existingKeys = collectExistingStoryKeys(projectRoot);
12500
- return allKeys.filter((k) => !existingKeys.has(k));
12501
- }
12502
- /**
12503
- * Find epic files from known candidate paths relative to projectRoot.
12504
- *
12505
- * Checks for:
12506
- * 1. epics.md (consolidated epic file)
12507
- * 2. Individual epic-*.md files in planning-artifacts/
12508
- *
12509
- * Returns a single path for epics.md, or undefined if not found.
12510
- * For individual epic files, use findEpicFiles() instead.
12511
- */
12512
- function findEpicsFile(projectRoot) {
12513
- const candidates = ["_bmad-output/planning-artifacts/epics.md", "_bmad-output/epics.md"];
12514
- for (const candidate of candidates) {
12515
- const fullPath = join$1(projectRoot, candidate);
12516
- if (existsSync(fullPath)) return fullPath;
12517
- }
12518
- const planningDir = join$1(projectRoot, "_bmad-output", "planning-artifacts");
12519
- if (existsSync(planningDir)) try {
12520
- const entries = readdirSync(planningDir, { encoding: "utf-8" });
12521
- const match$1 = entries.filter((e) => /^epics[-.].*\.md$/i.test(e) && !/^epic-\d+/.test(e)).sort();
12522
- if (match$1.length > 0) return join$1(planningDir, match$1[0]);
12523
- } catch {}
12524
- return void 0;
12525
- }
12526
- /**
12527
- * Find individual epic-*.md files in the planning artifacts directory.
12528
- * Returns paths sorted alphabetically.
12529
- */
12530
- function findEpicFiles(projectRoot) {
12531
- const planningDir = join$1(projectRoot, "_bmad-output", "planning-artifacts");
12532
- if (!existsSync(planningDir)) return [];
12533
- try {
12534
- const entries = readdirSync(planningDir, { encoding: "utf-8" });
12535
- return entries.filter((e) => /^epic-\d+.*\.md$/.test(e)).sort().map((e) => join$1(planningDir, e));
12536
- } catch {
12537
- return [];
12538
- }
12539
- }
12540
- /**
12541
- * Collect story keys that already have implementation artifact files.
12542
- * Scans _bmad-output/implementation-artifacts/ for files matching N-M-*.md.
12543
- */
12544
- function collectExistingStoryKeys(projectRoot) {
12545
- const existing = new Set();
12546
- const artifactsDir = join$1(projectRoot, "_bmad-output", "implementation-artifacts");
12547
- if (!existsSync(artifactsDir)) return existing;
12548
- let entries;
12549
- try {
12550
- entries = readdirSync(artifactsDir, { encoding: "utf-8" });
12551
- } catch {
12552
- return existing;
12553
- }
12554
- const filePattern = /^([A-Za-z0-9]+-[A-Za-z0-9]+)-/;
12555
- for (const entry of entries) {
12556
- if (!entry.endsWith(".md")) continue;
12557
- const m = filePattern.exec(entry);
12558
- if (m !== null && m[1] !== void 0) existing.add(m[1]);
12559
- }
12560
- return existing;
12561
- }
12562
- /**
12563
- * Parse story keys from sprint-status.yaml.
12564
- * Reads the development_status map and extracts keys that match the
12565
- * alphanumeric story key pattern (e.g., 1-1a, NEW-26, E5-accessibility).
12566
- * Filters out epic status entries (epic-N) and retrospective entries.
12567
- */
12568
- function parseStoryKeysFromSprintStatus(projectRoot) {
12569
- const candidates = [join$1(projectRoot, "_bmad-output", "implementation-artifacts", "sprint-status.yaml"), join$1(projectRoot, "_bmad-output", "sprint-status.yaml")];
12570
- const statusPath = candidates.find((p) => existsSync(p));
12571
- if (!statusPath) return [];
12572
- try {
12573
- const content = readFileSync(statusPath, "utf-8");
12574
- const keys = [];
12575
- const linePattern = /^\s{2}([A-Za-z0-9]+-[A-Za-z0-9]+(?:-[A-Za-z0-9-]*)?)\s*:/gm;
12576
- let match$1;
12577
- while ((match$1 = linePattern.exec(content)) !== null) {
12578
- const fullKey = match$1[1];
12579
- if (/^epic-\d+$/.test(fullKey)) continue;
12580
- if (fullKey.includes("retrospective")) continue;
12581
- const segments = fullKey.split("-");
12582
- if (segments.length >= 2) keys.push(`${segments[0]}-${segments[1]}`);
12583
- }
12584
- return [...new Set(keys)];
12585
- } catch {
12586
- return [];
12587
- }
12588
- }
12589
- /**
12590
- * Collect story keys already completed in previous pipeline runs.
12591
- * Scans pipeline_runs with status='completed' and extracts story keys
12592
- * with phase='COMPLETE' from their token_usage_json state.
12593
- */
12594
- async function getCompletedStoryKeys(db) {
12595
- const completed = new Set();
12596
- try {
12597
- const rows = await db.query(`SELECT token_usage_json FROM pipeline_runs WHERE status = 'completed' AND token_usage_json IS NOT NULL`);
12598
- for (const row of rows) try {
12599
- const state = JSON.parse(row.token_usage_json);
12600
- if (state.stories !== void 0) {
12601
- for (const [key, s$1] of Object.entries(state.stories)) if (s$1.phase === "COMPLETE") completed.add(key);
12602
- }
12603
- } catch {}
12604
- } catch {}
12605
- return completed;
12606
- }
12607
- /**
12608
- * Sort story keys: numeric keys first (by epic then story number),
12609
- * then alphabetic-prefix keys (NEW-*, E-*) sorted lexicographically.
12610
- * E.g. ["10-1", "1-2a", "1-2", "NEW-26", "E5-acc"] → ["1-2", "1-2a", "10-1", "E5-acc", "NEW-26"]
12611
- */
12612
- function sortStoryKeys(keys) {
12613
- return keys.slice().sort((a, b) => {
12614
- const aParts = a.split("-");
12615
- const bParts = b.split("-");
12616
- const aNum = Number(aParts[0]);
12617
- const bNum = Number(bParts[0]);
12618
- if (!isNaN(aNum) && !isNaN(bNum)) {
12619
- if (aNum !== bNum) return aNum - bNum;
12620
- const aStory = Number(aParts[1]);
12621
- const bStory = Number(bParts[1]);
12622
- if (!isNaN(aStory) && !isNaN(bStory) && aStory !== bStory) return aStory - bStory;
12623
- return (aParts[1] ?? "").localeCompare(bParts[1] ?? "");
12624
- }
12625
- if (!isNaN(aNum)) return -1;
12626
- if (!isNaN(bNum)) return 1;
12627
- return a.localeCompare(b);
12628
- });
12629
- }
12630
- /**
12631
- * Parse inter-story dependencies from the consolidated epics document.
12632
- *
12633
- * Scans for patterns like:
12634
- * ### Story 50-2: Title
12635
- * **Dependencies:** 50-1
12636
- *
12637
- * Returns a Map where key=storyKey, value=Set of dependency keys.
12638
- * Only returns dependencies that are within the provided storyKeys set
12639
- * (external dependencies to other epics are ignored for ordering purposes).
12640
- */
12641
- function parseEpicsDependencies(projectRoot, storyKeys) {
12642
- const deps = new Map();
12643
- const epicsPath = findEpicsFile(projectRoot);
12644
- if (epicsPath === void 0) return deps;
12645
- let content;
12646
- try {
12647
- content = readFileSync(epicsPath, "utf-8");
12648
- } catch {
12649
- return deps;
12650
- }
12651
- const storyPattern = /^###\s+Story\s+(\d+)-(\d+)[:\s]/gm;
12652
- const depPattern = /^\*\*Dependencies:\*\*\s*(.+)$/gm;
12653
- const storyPositions = [];
12654
- let match$1;
12655
- while ((match$1 = storyPattern.exec(content)) !== null) storyPositions.push({
12656
- key: `${match$1[1]}-${match$1[2]}`,
12657
- pos: match$1.index
12658
- });
12659
- for (let i = 0; i < storyPositions.length; i++) {
12660
- const story = storyPositions[i];
12661
- const nextStoryPos = i + 1 < storyPositions.length ? storyPositions[i + 1].pos : content.length;
12662
- const section = content.slice(story.pos, nextStoryPos);
12663
- depPattern.lastIndex = 0;
12664
- const depMatch = depPattern.exec(section);
12665
- if (depMatch === null || /^none$/i.test(depMatch[1].trim())) continue;
12666
- const depText = depMatch[1];
12667
- const storyDeps = new Set();
12668
- const rangeMatch = /(\d+)-(\d+)\s+through\s+\1-(\d+)/i.exec(depText);
12669
- if (rangeMatch !== null) {
12670
- const epic = rangeMatch[1];
12671
- const start = Number(rangeMatch[2]);
12672
- const end = Number(rangeMatch[3]);
12673
- for (let n$1 = start; n$1 <= end; n$1++) {
12674
- const depKey = `${epic}-${n$1}`;
12675
- if (storyKeys.has(depKey)) storyDeps.add(depKey);
12676
- }
12677
- } else {
12678
- const keyPattern = /(\d+-\d+[a-z]?)/g;
12679
- let km;
12680
- while ((km = keyPattern.exec(depText)) !== null) {
12681
- const depKey = km[1];
12682
- if (storyKeys.has(depKey)) storyDeps.add(depKey);
12683
- }
12684
- }
12685
- if (storyDeps.size > 0) deps.set(story.key, storyDeps);
12686
- }
12687
- return deps;
12688
- }
12689
- /**
12690
- * Topologically sort explicit story keys by inter-story dependencies.
12691
- *
12692
- * Parses the consolidated epics document for dependency metadata, builds
12693
- * a DAG, and returns keys in dependency-first order using Kahn's algorithm.
12694
- * Stories with no dependencies come first; stories that depend on others
12695
- * are placed after their prerequisites.
12696
- *
12697
- * Falls back to numeric sort if no epics document exists or no
12698
- * dependencies are found among the provided keys.
12699
- */
12700
- function topologicalSortByDependencies(keys, projectRoot) {
12701
- if (keys.length <= 1) return keys;
12702
- const keySet = new Set(keys);
12703
- const deps = parseEpicsDependencies(projectRoot, keySet);
12704
- if (deps.size === 0) return sortStoryKeys(keys);
12705
- const inDegree = new Map();
12706
- const successors = new Map();
12707
- for (const key of keys) {
12708
- inDegree.set(key, 0);
12709
- successors.set(key, new Set());
12710
- }
12711
- for (const [dependent, depSet] of deps) {
12712
- if (!keySet.has(dependent)) continue;
12713
- for (const dep of depSet) {
12714
- if (!keySet.has(dep)) continue;
12715
- successors.get(dep).add(dependent);
12716
- inDegree.set(dependent, (inDegree.get(dependent) ?? 0) + 1);
12717
- }
12718
- }
12719
- const result = [];
12720
- const processed = new Set();
12721
- while (processed.size < keys.length) {
12722
- const wave = [];
12723
- for (const key of keys) if (!processed.has(key) && (inDegree.get(key) ?? 0) === 0) wave.push(key);
12724
- if (wave.length === 0) {
12725
- for (const key of sortStoryKeys(keys)) if (!processed.has(key)) result.push(key);
12726
- break;
12727
- }
12728
- for (const key of sortStoryKeys(wave)) {
12729
- result.push(key);
12730
- processed.add(key);
12731
- for (const succ of successors.get(key) ?? []) inDegree.set(succ, (inDegree.get(succ) ?? 0) - 1);
12732
- }
12733
- }
12734
- return result;
12735
- }
12736
-
12737
12909
  //#endregion
12738
12910
  //#region src/modules/phase-orchestrator/phase-detection.ts
12739
12911
  const PHASE_ARTIFACTS = [
@@ -40614,5 +40786,5 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
40614
40786
  }
40615
40787
 
40616
40788
  //#endregion
40617
- export { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, normalizeGraphSummaryToStatus, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
40618
- //# sourceMappingURL=run-Byzy10gG.js.map
40789
+ export { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, EpicIngester, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, normalizeGraphSummaryToStatus, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
40790
+ //# sourceMappingURL=run-BOhSIujp.js.map
@@ -2,7 +2,7 @@ import "./health-Cx2ZhRNT.js";
2
2
  import "./logger-KeHncl-f.js";
3
3
  import "./helpers-CElYrONe.js";
4
4
  import "./dist-Bm0qSZer.js";
5
- import { normalizeGraphSummaryToStatus, registerRunCommand, runRunAction } from "./run-Byzy10gG.js";
5
+ import { normalizeGraphSummaryToStatus, registerRunCommand, runRunAction } from "./run-BOhSIujp.js";
6
6
  import "./routing-CcBOCuC9.js";
7
7
  import "./decisions-C0pz9Clx.js";
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "substrate-ai",
3
- "version": "0.19.1",
3
+ "version": "0.19.2",
4
4
  "description": "Substrate — multi-agent orchestration daemon for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",