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,
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
5
|
+
import { normalizeGraphSummaryToStatus, registerRunCommand, runRunAction } from "./run-BOhSIujp.js";
|
|
6
6
|
import "./routing-CcBOCuC9.js";
|
|
7
7
|
import "./decisions-C0pz9Clx.js";
|
|
8
8
|
|