substrate-ai 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { AdapterTelemetryPersistence, AppError, DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DoltClient, DoltNotInstalled, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, FileStateStore, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SUBSTRATE_OWNED_SETTINGS_KEYS, SymbolParser, VALID_PHASES, buildPipelineStatusOutput, checkDoltInstalled, createConfigSystem, createContextCompiler, createDatabaseAdapter, createDispatcher, createDoltClient, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStateStore, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, initSchema, initializeDolt, isSyncAdapter, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-GqmIa5YW.js";
2
+ import { AdapterTelemetryPersistence, AppError, DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DoltClient, DoltNotInstalled, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, FileStateStore, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SUBSTRATE_OWNED_SETTINGS_KEYS, SymbolParser, VALID_PHASES, buildPipelineStatusOutput, checkDoltInstalled, createConfigSystem, createContextCompiler, createDatabaseAdapter, createDispatcher, createDoltClient, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStateStore, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, initSchema, initializeDolt, isSyncAdapter, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-BD0Ugp7F.js";
3
3
  import { createLogger } from "../logger-D2fS2ccL.js";
4
4
  import { AdapterRegistry } from "../adapter-registry-BkUvZSKJ.js";
5
5
  import { CURRENT_CONFIG_FORMAT_VERSION, CURRENT_TASK_GRAPH_VERSION, PartialSubstrateConfigSchema } from "../config-migrator-DtZW1maj.js";
@@ -19,7 +19,7 @@ import yaml from "js-yaml";
19
19
  import { createRequire } from "node:module";
20
20
  import * as path$1 from "node:path";
21
21
  import { isAbsolute, join as join$1 } from "node:path";
22
- import { existsSync as existsSync$1, mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1 } from "node:fs";
22
+ import { existsSync as existsSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, writeFileSync as writeFileSync$1 } from "node:fs";
23
23
  import { access as access$1, readFile as readFile$1 } from "node:fs/promises";
24
24
  import { createInterface } from "node:readline";
25
25
  import { homedir } from "os";
@@ -574,8 +574,8 @@ async function directoryExists(path$2) {
574
574
  */
575
575
  async function runInitAction(options) {
576
576
  const { pack: packName, projectRoot, outputFormat, force = false, yes: nonInteractive = false } = options;
577
- const packPath = join(projectRoot, "packs", packName);
578
577
  const dbRoot = await resolveMainRepoRoot(projectRoot);
578
+ const packPath = join(dbRoot, "packs", packName);
579
579
  const substrateDir = join(dbRoot, ".substrate");
580
580
  const dbPath = join(substrateDir, "substrate.db");
581
581
  const configPath = join(substrateDir, "config.yaml");
@@ -1007,16 +1007,30 @@ function registerConfigCommand(program, _version) {
1007
1007
  //#endregion
1008
1008
  //#region src/cli/commands/resume.ts
1009
1009
  const logger$16 = createLogger("resume-cmd");
1010
+ /**
1011
+ * Map internal orchestrator phase names to pipeline event protocol phase names.
1012
+ */
1013
+ function mapInternalPhaseToEventPhase(internalPhase) {
1014
+ switch (internalPhase) {
1015
+ case "IN_STORY_CREATION": return "create-story";
1016
+ case "IN_DEV": return "dev-story";
1017
+ case "IN_REVIEW": return "code-review";
1018
+ case "IN_MINOR_FIX":
1019
+ case "IN_MAJOR_FIX": return "fix";
1020
+ case "IN_TEST_PLANNING": return "test-planning";
1021
+ default: return null;
1022
+ }
1023
+ }
1010
1024
  async function runResumeAction(options) {
1011
- const { runId: specifiedRunId, stopAfter, outputFormat, projectRoot, concurrency, pack: packName, registry } = options;
1025
+ const { runId: specifiedRunId, stopAfter, outputFormat, projectRoot, concurrency, pack: packName, events: eventsFlag, registry } = options;
1012
1026
  if (stopAfter !== void 0 && !VALID_PHASES.includes(stopAfter)) {
1013
1027
  const errorMsg = `Invalid phase: "${stopAfter}". Valid phases: ${VALID_PHASES.join(", ")}`;
1014
1028
  if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
1015
1029
  else process.stderr.write(`Error: ${errorMsg}\n`);
1016
1030
  return 1;
1017
1031
  }
1018
- const packPath = join(projectRoot, "packs", packName);
1019
1032
  const dbRoot = await resolveMainRepoRoot(projectRoot);
1033
+ const packPath = join(dbRoot, "packs", packName);
1020
1034
  const dbPath = join(dbRoot, ".substrate", "substrate.db");
1021
1035
  const doltDir = join(dbRoot, ".substrate", "state", ".dolt");
1022
1036
  if (!existsSync(dbPath) && !existsSync(doltDir)) {
@@ -1086,6 +1100,7 @@ async function runResumeAction(options) {
1086
1100
  concept,
1087
1101
  concurrency,
1088
1102
  outputFormat,
1103
+ events: eventsFlag,
1089
1104
  existingRunId: runId,
1090
1105
  projectRoot,
1091
1106
  registry
@@ -1103,7 +1118,7 @@ async function runResumeAction(options) {
1103
1118
  }
1104
1119
  }
1105
1120
  async function runFullPipelineFromPhase(options) {
1106
- const { packName, packPath, dbDir, dbPath, startPhase, stopAfter, concept, concurrency, outputFormat, existingRunId, projectRoot, registry: injectedRegistry } = options;
1121
+ const { packName, packPath, dbDir, dbPath, startPhase, stopAfter, concept, concurrency, outputFormat, events: eventsFlag, existingRunId, projectRoot, registry: injectedRegistry } = options;
1107
1122
  if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
1108
1123
  const adapter = createDatabaseAdapter({
1109
1124
  backend: "auto",
@@ -1143,6 +1158,8 @@ async function runFullPipelineFromPhase(options) {
1143
1158
  let runId;
1144
1159
  if (existingRunId !== void 0) runId = existingRunId;
1145
1160
  else runId = await phaseOrchestrator.startRun(concept, startPhase);
1161
+ let ndjsonEmitter;
1162
+ if (eventsFlag === true) ndjsonEmitter = createEventEmitter(process.stdout);
1146
1163
  const phaseOrder = [
1147
1164
  "analysis",
1148
1165
  "planning",
@@ -1239,12 +1256,87 @@ async function runFullPipelineFromPhase(options) {
1239
1256
  config: {
1240
1257
  maxConcurrency: concurrency,
1241
1258
  maxReviewCycles: 2,
1242
- pipelineRunId: runId
1259
+ pipelineRunId: runId,
1260
+ enableHeartbeat: eventsFlag === true
1243
1261
  },
1244
1262
  projectRoot,
1245
1263
  ...ingestionServer !== void 0 ? { ingestionServer } : {},
1246
1264
  ...telemetryPersistence !== void 0 ? { telemetryPersistence } : {}
1247
1265
  });
1266
+ if (ndjsonEmitter !== void 0) {
1267
+ const resolvedKeys = await resolveStoryKeys(adapter, projectRoot, { pipelineRunId: runId });
1268
+ ndjsonEmitter.emit({
1269
+ type: "pipeline:start",
1270
+ ts: new Date().toISOString(),
1271
+ run_id: runId,
1272
+ stories: resolvedKeys,
1273
+ concurrency
1274
+ });
1275
+ eventBus.on("orchestrator:story-phase-start", (payload) => {
1276
+ const phase = mapInternalPhaseToEventPhase(payload.phase);
1277
+ if (phase !== null) ndjsonEmitter.emit({
1278
+ type: "story:phase",
1279
+ ts: new Date().toISOString(),
1280
+ key: payload.storyKey,
1281
+ phase,
1282
+ status: "in_progress"
1283
+ });
1284
+ });
1285
+ eventBus.on("orchestrator:story-phase-complete", (payload) => {
1286
+ const phase = mapInternalPhaseToEventPhase(payload.phase);
1287
+ if (phase !== null) {
1288
+ const result = payload.result;
1289
+ ndjsonEmitter.emit({
1290
+ type: "story:phase",
1291
+ ts: new Date().toISOString(),
1292
+ key: payload.storyKey,
1293
+ phase,
1294
+ status: "complete",
1295
+ ...phase === "code-review" && result?.verdict !== void 0 ? { verdict: result.verdict } : {},
1296
+ ...phase === "create-story" && result?.story_file !== void 0 ? { file: result.story_file } : {}
1297
+ });
1298
+ }
1299
+ });
1300
+ eventBus.on("orchestrator:story-complete", (payload) => {
1301
+ ndjsonEmitter.emit({
1302
+ type: "story:done",
1303
+ ts: new Date().toISOString(),
1304
+ key: payload.storyKey,
1305
+ result: "success",
1306
+ review_cycles: payload.reviewCycles
1307
+ });
1308
+ });
1309
+ eventBus.on("orchestrator:story-escalated", (payload) => {
1310
+ const rawIssues = Array.isArray(payload.issues) ? payload.issues : [];
1311
+ const issues = rawIssues.map((issue) => {
1312
+ const iss = issue;
1313
+ return {
1314
+ severity: iss.severity ?? "unknown",
1315
+ file: iss.file ?? "",
1316
+ desc: iss.desc ?? iss.description ?? ""
1317
+ };
1318
+ });
1319
+ ndjsonEmitter.emit({
1320
+ type: "story:escalation",
1321
+ ts: new Date().toISOString(),
1322
+ key: payload.storyKey,
1323
+ reason: payload.lastVerdict ?? "escalated",
1324
+ cycles: payload.reviewCycles ?? 0,
1325
+ issues,
1326
+ ...payload.diagnosis !== void 0 ? { diagnosis: payload.diagnosis } : {}
1327
+ });
1328
+ });
1329
+ eventBus.on("orchestrator:heartbeat", (payload) => {
1330
+ ndjsonEmitter.emit({
1331
+ type: "pipeline:heartbeat",
1332
+ ts: new Date().toISOString(),
1333
+ run_id: payload.runId,
1334
+ active_dispatches: payload.activeDispatches,
1335
+ completed_dispatches: payload.completedDispatches,
1336
+ queued_dispatches: payload.queuedDispatches
1337
+ });
1338
+ });
1339
+ }
1248
1340
  eventBus.on("orchestrator:story-phase-complete", (payload) => {
1249
1341
  try {
1250
1342
  const result = payload.result;
@@ -1268,6 +1360,13 @@ async function runFullPipelineFromPhase(options) {
1268
1360
  const storyKeys = await resolveStoryKeys(adapter, projectRoot, { pipelineRunId: runId });
1269
1361
  if (storyKeys.length === 0 && outputFormat === "human") process.stdout.write("[IMPLEMENTATION] No stories found for this run. Check solutioning phase output.\n");
1270
1362
  await orchestrator.run(storyKeys);
1363
+ if (ndjsonEmitter !== void 0) ndjsonEmitter.emit({
1364
+ type: "pipeline:complete",
1365
+ ts: new Date().toISOString(),
1366
+ succeeded: storyKeys,
1367
+ failed: [],
1368
+ escalated: []
1369
+ });
1271
1370
  if (outputFormat === "human") process.stdout.write("[IMPLEMENTATION] Complete\n");
1272
1371
  }
1273
1372
  if (stopAfter !== void 0 && currentPhase === stopAfter) {
@@ -1330,7 +1429,7 @@ async function runFullPipelineFromPhase(options) {
1330
1429
  }
1331
1430
  }
1332
1431
  function registerResumeCommand(program, _version = "0.0.0", projectRoot = process.cwd(), registry) {
1333
- program.command("resume").description("Resume a previously interrupted pipeline run").option("--run-id <id>", "Pipeline run ID to resume (defaults to latest)").option("--pack <name>", "Methodology pack name", "bmad").option("--stop-after <phase>", "Stop pipeline after this phase completes (overrides saved state)").option("--concurrency <n>", "Maximum parallel conflict groups", (v) => parseInt(v, 10), 3).option("--project-root <path>", "Project root directory", projectRoot).option("--output-format <format>", "Output format: human (default) or json", "human").action(async (opts) => {
1432
+ program.command("resume").description("Resume a previously interrupted pipeline run").option("--run-id <id>", "Pipeline run ID to resume (defaults to latest)").option("--pack <name>", "Methodology pack name", "bmad").option("--stop-after <phase>", "Stop pipeline after this phase completes (overrides saved state)").option("--concurrency <n>", "Maximum parallel conflict groups", (v) => parseInt(v, 10), 3).option("--project-root <path>", "Project root directory", projectRoot).option("--output-format <format>", "Output format: human (default) or json", "human").option("--events", "Emit structured NDJSON events on stdout for programmatic consumption").action(async (opts) => {
1334
1433
  const outputFormat = opts.outputFormat === "json" ? "json" : "human";
1335
1434
  const exitCode = await runResumeAction({
1336
1435
  runId: opts.runId,
@@ -1339,6 +1438,7 @@ function registerResumeCommand(program, _version = "0.0.0", projectRoot = proces
1339
1438
  projectRoot: opts.projectRoot,
1340
1439
  concurrency: opts.concurrency,
1341
1440
  pack: opts.pack,
1441
+ events: opts.events,
1342
1442
  registry
1343
1443
  });
1344
1444
  process.exitCode = exitCode;
@@ -2031,7 +2131,7 @@ async function runAmendAction(options) {
2031
2131
  const dbRoot = await resolveMainRepoRoot(projectRoot);
2032
2132
  const dbDir = join(dbRoot, ".substrate");
2033
2133
  const dbPath = join(dbDir, "substrate.db");
2034
- const packPath = join(projectRoot, "packs", packName);
2134
+ const packPath = join(dbRoot, "packs", packName);
2035
2135
  const doltDir = join(dbRoot, ".substrate", "state", ".dolt");
2036
2136
  if (!existsSync(dbPath) && !existsSync(doltDir)) {
2037
2137
  process.stderr.write(`Error: Decision store not initialized. Run 'substrate init' first.\n`);
@@ -2749,7 +2849,7 @@ async function runSupervisorAction(options, deps = {}) {
2749
2849
  await initSchema(expAdapter);
2750
2850
  const { runRunAction: runPipeline } = await import(
2751
2851
  /* @vite-ignore */
2752
- "../run-C-yCMYlt.js"
2852
+ "../run-B-TUWMCv.js"
2753
2853
  );
2754
2854
  const runStoryFn = async (opts) => {
2755
2855
  const exitCode = await runPipeline({
@@ -7142,7 +7242,7 @@ async function runRetryEscalatedAction(options) {
7142
7242
  }
7143
7243
  return 0;
7144
7244
  }
7145
- const packPath = join(projectRoot, "packs", packName);
7245
+ const packPath = join(dbRoot, "packs", packName);
7146
7246
  const packLoader = createPackLoader();
7147
7247
  let pack;
7148
7248
  try {
@@ -7701,6 +7801,261 @@ function registerRoutingCommand(program) {
7701
7801
  });
7702
7802
  }
7703
7803
 
7804
+ //#endregion
7805
+ //#region src/modules/work-graph/schema.ts
7806
+ /**
7807
+ * Work-graph schema DDL constants.
7808
+ *
7809
+ * Story 31-1 placeholder — defines the `stories`, `story_dependencies`, and
7810
+ * `ready_stories` DDL used by the EpicIngester and downstream consumers.
7811
+ *
7812
+ * NOTE: This file is a minimal placeholder created by story 31-2 because story
7813
+ * 31-1 (schema creation) had not yet run. If story 31-1 produces a richer
7814
+ * schema, merge carefully and remove this note.
7815
+ */
7816
+ const CREATE_STORIES_TABLE = `
7817
+ CREATE TABLE IF NOT EXISTS stories (
7818
+ story_key VARCHAR(50) NOT NULL,
7819
+ epic_num INT NOT NULL,
7820
+ story_num INT NOT NULL,
7821
+ title VARCHAR(500) NOT NULL,
7822
+ priority VARCHAR(10) NOT NULL,
7823
+ size VARCHAR(50) NOT NULL,
7824
+ sprint INT NOT NULL,
7825
+ status VARCHAR(50) NOT NULL DEFAULT 'planned',
7826
+ PRIMARY KEY (story_key)
7827
+ )
7828
+ `.trim();
7829
+ const CREATE_STORY_DEPENDENCIES_TABLE = `
7830
+ CREATE TABLE IF NOT EXISTS story_dependencies (
7831
+ story_key VARCHAR(50) NOT NULL,
7832
+ depends_on VARCHAR(50) NOT NULL,
7833
+ dependency_type VARCHAR(50) NOT NULL DEFAULT 'blocks',
7834
+ source VARCHAR(50) NOT NULL DEFAULT 'explicit',
7835
+ PRIMARY KEY (story_key, depends_on)
7836
+ )
7837
+ `.trim();
7838
+ const CREATE_READY_STORIES_VIEW = `
7839
+ CREATE VIEW IF NOT EXISTS ready_stories AS
7840
+ SELECT s.*
7841
+ FROM stories s
7842
+ WHERE s.status = 'planned'
7843
+ AND NOT EXISTS (
7844
+ SELECT 1 FROM story_dependencies sd
7845
+ JOIN stories blocking ON sd.depends_on = blocking.story_key
7846
+ WHERE sd.story_key = s.story_key
7847
+ AND blocking.status != 'done'
7848
+ )
7849
+ `.trim();
7850
+
7851
+ //#endregion
7852
+ //#region src/modules/work-graph/epic-parser.ts
7853
+ /** Regex for sprint header lines: `**Sprint 1 —` (em dash or hyphen) */
7854
+ const SPRINT_HEADER_RE = /^\*\*Sprint\s+(\d+)\s*[—–-]/i;
7855
+ /**
7856
+ * Regex for story lines: `- 31-2: Epic doc ingestion (P0, Medium)`
7857
+ * Captures: epicNum, storyNum, title, priority, size
7858
+ */
7859
+ const STORY_LINE_RE = /^-\s+(\d+)-(\d+):\s+(.+?)\s+\((P\d+),\s+([\w-]+)\)\s*$/;
7860
+ /** Regex to find the story map section heading */
7861
+ const STORY_MAP_HEADING_RE = /^#{1,6}\s+.*Story\s+Map/im;
7862
+ /** Regex to find the dependency chain line */
7863
+ const DEPENDENCY_CHAIN_RE = /\*\*Dependency\s+chain\*\*:\s*(.+)/i;
7864
+ /** Regex for "also gates" clauses: `31-3 also gates 31-6, 31-7` */
7865
+ const ALSO_GATES_RE = /^([\d]+-[\d]+)\s+also\s+gates\s+(.+)$/i;
7866
+ var EpicParser = class {
7867
+ /**
7868
+ * Parse story metadata from an epic planning document.
7869
+ *
7870
+ * @param content - Full text of the epic markdown document.
7871
+ * @returns Array of `ParsedStory` objects, one per story line found.
7872
+ * @throws {Error} If the story map section is absent or no stories can be parsed.
7873
+ */
7874
+ parseStories(content) {
7875
+ const headingMatch = STORY_MAP_HEADING_RE.exec(content);
7876
+ if (!headingMatch) throw new Error("No story map section found in document");
7877
+ const afterHeading = content.slice(headingMatch.index + headingMatch[0].length);
7878
+ const stories = [];
7879
+ let currentSprint = 0;
7880
+ for (const rawLine of afterHeading.split("\n")) {
7881
+ const line = rawLine.trim();
7882
+ const sprintMatch = SPRINT_HEADER_RE.exec(line);
7883
+ if (sprintMatch) {
7884
+ currentSprint = parseInt(sprintMatch[1], 10);
7885
+ continue;
7886
+ }
7887
+ const storyMatch = STORY_LINE_RE.exec(line);
7888
+ if (storyMatch) {
7889
+ const epicNum = parseInt(storyMatch[1], 10);
7890
+ const storyNum = parseInt(storyMatch[2], 10);
7891
+ stories.push({
7892
+ story_key: `${epicNum}-${storyNum}`,
7893
+ epic_num: epicNum,
7894
+ story_num: storyNum,
7895
+ title: storyMatch[3].trim(),
7896
+ priority: storyMatch[4],
7897
+ size: storyMatch[5],
7898
+ sprint: currentSprint
7899
+ });
7900
+ }
7901
+ }
7902
+ if (stories.length === 0) throw new Error("Story map section found but contained no parseable story lines");
7903
+ return stories;
7904
+ }
7905
+ /**
7906
+ * Parse dependency relationships from an epic planning document.
7907
+ *
7908
+ * If the `**Dependency chain**:` line is absent, returns an empty array
7909
+ * (not all epics declare dependencies).
7910
+ *
7911
+ * @param content - Full text of the epic markdown document.
7912
+ * @returns Array of `ParsedDependency` objects.
7913
+ */
7914
+ parseDependencies(content) {
7915
+ const chainLineMatch = DEPENDENCY_CHAIN_RE.exec(content);
7916
+ if (!chainLineMatch) return [];
7917
+ const chainStr = chainLineMatch[1].trim();
7918
+ const dependencies = [];
7919
+ const clauses = chainStr.split(";").map((c) => c.trim()).filter(Boolean);
7920
+ for (const clause of clauses) {
7921
+ const alsoGatesMatch = ALSO_GATES_RE.exec(clause);
7922
+ if (alsoGatesMatch) {
7923
+ const gater = alsoGatesMatch[1].trim();
7924
+ const gatedList = alsoGatesMatch[2].split(",").map((t) => t.trim()).filter(Boolean);
7925
+ for (const gated of gatedList) dependencies.push({
7926
+ story_key: gated,
7927
+ depends_on: gater,
7928
+ dependency_type: "blocks",
7929
+ source: "explicit"
7930
+ });
7931
+ continue;
7932
+ }
7933
+ const parts = clause.split("→").map((p) => p.trim()).filter(Boolean);
7934
+ for (let i = 0; i < parts.length - 1; i++) {
7935
+ const upstream = parts[i];
7936
+ const downstream = parts[i + 1];
7937
+ dependencies.push({
7938
+ story_key: downstream,
7939
+ depends_on: upstream,
7940
+ dependency_type: "blocks",
7941
+ source: "explicit"
7942
+ });
7943
+ }
7944
+ }
7945
+ return dependencies;
7946
+ }
7947
+ };
7948
+
7949
+ //#endregion
7950
+ //#region src/modules/work-graph/epic-ingester.ts
7951
+ var EpicIngester = class {
7952
+ adapter;
7953
+ constructor(adapter) {
7954
+ this.adapter = adapter;
7955
+ }
7956
+ /**
7957
+ * Upsert stories and sync dependencies into the database.
7958
+ *
7959
+ * Both operations are wrapped in a single transaction: if either fails the
7960
+ * entire batch is rolled back.
7961
+ *
7962
+ * @param stories - Parsed story metadata from `EpicParser.parseStories()`.
7963
+ * @param dependencies - Parsed dependency edges from `EpicParser.parseDependencies()`.
7964
+ * @returns `IngestResult` with counts of affected rows.
7965
+ */
7966
+ async ingest(stories, dependencies) {
7967
+ return this.adapter.transaction(async (tx) => {
7968
+ let storiesUpserted = 0;
7969
+ for (const story of stories) {
7970
+ const existing = await tx.query("SELECT status FROM stories WHERE story_key = ?", [story.story_key]);
7971
+ if (existing.length > 0) await tx.query("UPDATE stories SET title = ?, priority = ?, size = ?, sprint = ? WHERE story_key = ?", [
7972
+ story.title,
7973
+ story.priority,
7974
+ story.size,
7975
+ story.sprint,
7976
+ story.story_key
7977
+ ]);
7978
+ else {
7979
+ await tx.query("INSERT INTO stories (story_key, epic_num, story_num, title, priority, size, sprint, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
7980
+ story.story_key,
7981
+ story.epic_num,
7982
+ story.story_num,
7983
+ story.title,
7984
+ story.priority,
7985
+ story.size,
7986
+ story.sprint,
7987
+ "planned"
7988
+ ]);
7989
+ storiesUpserted++;
7990
+ }
7991
+ }
7992
+ const epicNum = stories.length > 0 ? stories[0].epic_num : null;
7993
+ if (epicNum !== null) await tx.query(`DELETE FROM story_dependencies WHERE source = 'explicit' AND story_key LIKE ?`, [`${epicNum}-%`]);
7994
+ for (const dep of dependencies) await tx.query("INSERT INTO story_dependencies (story_key, depends_on, dependency_type, source) VALUES (?, ?, ?, ?)", [
7995
+ dep.story_key,
7996
+ dep.depends_on,
7997
+ dep.dependency_type,
7998
+ dep.source
7999
+ ]);
8000
+ return {
8001
+ storiesUpserted,
8002
+ dependenciesReplaced: dependencies.length
8003
+ };
8004
+ });
8005
+ }
8006
+ };
8007
+
8008
+ //#endregion
8009
+ //#region src/cli/commands/ingest-epic.ts
8010
+ function registerIngestEpicCommand(program) {
8011
+ program.command("ingest-epic <epic-doc-path>").description("Parse an epic planning doc and upsert story metadata into the work-graph").action(async (epicDocPath) => {
8012
+ if (!existsSync$1(epicDocPath)) {
8013
+ process.stderr.write(`Error: File not found: ${epicDocPath}\n`);
8014
+ process.exitCode = 1;
8015
+ return;
8016
+ }
8017
+ let content;
8018
+ try {
8019
+ content = readFileSync$1(epicDocPath, "utf-8");
8020
+ } catch (err) {
8021
+ const msg = err instanceof Error ? err.message : String(err);
8022
+ process.stderr.write(`Error: Cannot read file ${epicDocPath}: ${msg}\n`);
8023
+ process.exitCode = 1;
8024
+ return;
8025
+ }
8026
+ const parser = new EpicParser();
8027
+ let stories;
8028
+ let dependencies;
8029
+ try {
8030
+ stories = parser.parseStories(content);
8031
+ dependencies = parser.parseDependencies(content);
8032
+ } catch (err) {
8033
+ const msg = err instanceof Error ? err.message : String(err);
8034
+ process.stderr.write(`Error: ${msg}\n`);
8035
+ process.exitCode = 1;
8036
+ return;
8037
+ }
8038
+ const adapter = createDatabaseAdapter({
8039
+ backend: "auto",
8040
+ basePath: process.cwd()
8041
+ });
8042
+ try {
8043
+ await adapter.exec(CREATE_STORIES_TABLE);
8044
+ await adapter.exec(CREATE_STORY_DEPENDENCIES_TABLE);
8045
+ const ingester = new EpicIngester(adapter);
8046
+ const result = await ingester.ingest(stories, dependencies);
8047
+ const epicNum = stories[0].epic_num;
8048
+ process.stdout.write(`Ingested ${result.storiesUpserted} stories and ${result.dependenciesReplaced} dependencies from epic ${epicNum}\n`);
8049
+ } catch (err) {
8050
+ const msg = err instanceof Error ? err.message : String(err);
8051
+ process.stderr.write(`Error: ${msg}\n`);
8052
+ process.exitCode = 1;
8053
+ } finally {
8054
+ await adapter.close();
8055
+ }
8056
+ });
8057
+ }
8058
+
7704
8059
  //#endregion
7705
8060
  //#region src/cli/index.ts
7706
8061
  process.setMaxListeners(20);
@@ -7761,6 +8116,7 @@ async function createProgram() {
7761
8116
  registerWorktreesCommand(program, version);
7762
8117
  registerBrainstormCommand(program, version);
7763
8118
  registerExportCommand(program, version);
8119
+ registerIngestEpicCommand(program);
7764
8120
  registerUpgradeCommand(program);
7765
8121
  return program;
7766
8122
  }
@@ -1,4 +1,4 @@
1
- import { registerRunCommand, runRunAction } from "./run-GqmIa5YW.js";
1
+ import { registerRunCommand, runRunAction } from "./run-BD0Ugp7F.js";
2
2
  import "./logger-D2fS2ccL.js";
3
3
  import "./config-migrator-DtZW1maj.js";
4
4
  import "./helpers-BihqWgVe.js";
@@ -247,7 +247,8 @@ var InMemoryDatabaseAdapter = class {
247
247
  const upper = resolved.trimStart().toUpperCase();
248
248
  if (/^CREATE\s+TABLE/i.test(upper)) return this._createTable(resolved);
249
249
  if (/^DROP\s+TABLE/i.test(upper)) return this._dropTable(resolved);
250
- if (/^INSERT\s+INTO/i.test(upper)) return this._insert(resolved);
250
+ if (/^CREATE\s+(?:OR\s+REPLACE\s+)?VIEW/i.test(upper)) return [];
251
+ if (/^INSERT\s+(?:IGNORE\s+)?INTO/i.test(upper)) return this._insert(resolved, /^INSERT\s+IGNORE\s+INTO/i.test(upper));
251
252
  if (/^SELECT/i.test(upper)) return this._select(resolved);
252
253
  if (/^UPDATE/i.test(upper)) return this._update(resolved);
253
254
  if (/^DELETE\s+FROM/i.test(upper)) return this._delete(resolved);
@@ -539,6 +540,8 @@ var DoltClient = class {
539
540
  _pool = null;
540
541
  _useCliMode = false;
541
542
  _connected = false;
543
+ /** Promise-chain mutex that serializes all CLI operations to prevent concurrent noms manifest access */
544
+ _cliMutex = Promise.resolve();
542
545
  constructor(options) {
543
546
  this.repoPath = options.repoPath;
544
547
  this.socketPath = options.socketPath ?? `${options.repoPath}/.dolt/dolt.sock`;
@@ -586,6 +589,20 @@ var DoltClient = class {
586
589
  throw new DoltQueryError(sql, detail);
587
590
  }
588
591
  }
592
+ /**
593
+ * Acquire an exclusive CLI lock. Dolt CLI takes an exclusive lock on the noms
594
+ * manifest, so concurrent `dolt sql -q` / `dolt <subcommand>` processes
595
+ * produce "cannot update manifest: database is read only" errors.
596
+ * Serialize all CLI operations through a single promise chain.
597
+ */
598
+ _withCliLock(fn) {
599
+ const prev = this._cliMutex;
600
+ let release;
601
+ this._cliMutex = new Promise((resolve$2) => {
602
+ release = resolve$2;
603
+ });
604
+ return prev.then(fn).finally(() => release());
605
+ }
589
606
  async _queryCli(sql, params, branch) {
590
607
  let resolvedSql = sql;
591
608
  if (params && params.length > 0) {
@@ -597,24 +614,27 @@ var DoltClient = class {
597
614
  return `'${String(val).replace(/'/g, "''")}'`;
598
615
  });
599
616
  }
600
- try {
601
- const branchPrefix = branch ? `CALL DOLT_CHECKOUT('${branch.replace(/'/g, "''")}'); ` : "";
602
- const args = [
603
- "sql",
604
- "-q",
605
- branchPrefix + resolvedSql,
606
- "--result-format",
607
- "json"
608
- ];
609
- const { stdout } = await runExecFile("dolt", args, { cwd: this.repoPath });
610
- const lines = (stdout || "").trim().split("\n").filter(Boolean);
611
- const lastLine = lines.length > 0 ? lines[lines.length - 1] : "{\"rows\":[]}";
612
- const parsed = JSON.parse(lastLine);
613
- return parsed.rows ?? [];
614
- } catch (err) {
615
- const detail = err instanceof Error ? err.message : String(err);
616
- throw new DoltQueryError(resolvedSql, detail);
617
- }
617
+ const finalSql = resolvedSql;
618
+ return this._withCliLock(async () => {
619
+ try {
620
+ const branchPrefix = branch ? `CALL DOLT_CHECKOUT('${branch.replace(/'/g, "''")}'); ` : "";
621
+ const args = [
622
+ "sql",
623
+ "-q",
624
+ branchPrefix + finalSql,
625
+ "--result-format",
626
+ "json"
627
+ ];
628
+ const { stdout } = await runExecFile("dolt", args, { cwd: this.repoPath });
629
+ const lines = (stdout || "").trim().split("\n").filter(Boolean);
630
+ const lastLine = lines.length > 0 ? lines[lines.length - 1] : "{\"rows\":[]}";
631
+ const parsed = JSON.parse(lastLine);
632
+ return parsed.rows ?? [];
633
+ } catch (err) {
634
+ const detail = err instanceof Error ? err.message : String(err);
635
+ throw new DoltQueryError(finalSql, detail);
636
+ }
637
+ });
618
638
  }
619
639
  /**
620
640
  * Execute a raw Dolt CLI command (e.g. `dolt diff main...story/26-1 --stat`)
@@ -635,13 +655,15 @@ var DoltClient = class {
635
655
  * messages) to avoid whitespace-splitting issues.
636
656
  */
637
657
  async execArgs(args) {
638
- try {
639
- const { stdout } = await runExecFile("dolt", args, { cwd: this.repoPath });
640
- return stdout;
641
- } catch (err) {
642
- const detail = err instanceof Error ? err.message : String(err);
643
- throw new DoltQueryError(args.join(" "), detail);
644
- }
658
+ return this._withCliLock(async () => {
659
+ try {
660
+ const { stdout } = await runExecFile("dolt", args, { cwd: this.repoPath });
661
+ return stdout;
662
+ } catch (err) {
663
+ const detail = err instanceof Error ? err.message : String(err);
664
+ throw new DoltQueryError(args.join(" "), detail);
665
+ }
666
+ });
645
667
  }
646
668
  async close() {
647
669
  if (this._pool) {
@@ -17004,7 +17026,12 @@ var PhaseOrchestratorImpl = class {
17004
17026
  if (result.passed) lastCompletedPhaseIdx = i;
17005
17027
  else break;
17006
17028
  }
17007
- const resumePhaseIdx = lastCompletedPhaseIdx + 1;
17029
+ const gateDerivedResumeIdx = lastCompletedPhaseIdx + 1;
17030
+ let resumePhaseIdx = gateDerivedResumeIdx;
17031
+ if (run.current_phase) {
17032
+ const dbPhaseIdx = this._phases.findIndex((p) => p.name === run.current_phase);
17033
+ if (dbPhaseIdx >= 0 && dbPhaseIdx > gateDerivedResumeIdx) resumePhaseIdx = dbPhaseIdx;
17034
+ }
17008
17035
  if (resumePhaseIdx >= this._phases.length) {
17009
17036
  await updatePipelineRun(this._db, runId, { status: "completed" });
17010
17037
  return this.getRunStatus(runId);
@@ -20588,8 +20615,8 @@ async function runRunAction(options) {
20588
20615
  return 1;
20589
20616
  }
20590
20617
  }
20591
- const packPath = join(projectRoot, "packs", packName);
20592
20618
  const dbRoot = await resolveMainRepoRoot(projectRoot);
20619
+ const packPath = join(dbRoot, "packs", packName);
20593
20620
  const dbDir = join(dbRoot, ".substrate");
20594
20621
  const dbPath = join(dbDir, "substrate.db");
20595
20622
  mkdirSync(dbDir, { recursive: true });
@@ -21723,5 +21750,5 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
21723
21750
  }
21724
21751
 
21725
21752
  //#endregion
21726
- export { AdapterTelemetryPersistence, AppError, DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DoltClient, DoltNotInstalled, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, FileStateStore, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SUBSTRATE_OWNED_SETTINGS_KEYS, SymbolParser, VALID_PHASES, buildPipelineStatusOutput, checkDoltInstalled, createConfigSystem, createContextCompiler, createDatabaseAdapter, createDispatcher, createDoltClient, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStateStore, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, initSchema, initializeDolt, isSyncAdapter, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
21727
- //# sourceMappingURL=run-GqmIa5YW.js.map
21753
+ export { AdapterTelemetryPersistence, AppError, DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DoltClient, DoltNotInstalled, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, FileStateStore, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SUBSTRATE_OWNED_SETTINGS_KEYS, SymbolParser, VALID_PHASES, buildPipelineStatusOutput, checkDoltInstalled, createConfigSystem, createContextCompiler, createDatabaseAdapter, createDispatcher, createDoltClient, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStateStore, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, initSchema, initializeDolt, isSyncAdapter, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
21754
+ //# sourceMappingURL=run-BD0Ugp7F.js.map
package/dist/schema.sql CHANGED
@@ -236,3 +236,48 @@ CREATE TABLE IF NOT EXISTS repo_map_meta (
236
236
 
237
237
  INSERT IGNORE INTO _schema_version (version, description) VALUES (5, 'Add repo_map_symbols and repo_map_meta tables (Epic 28-2)');
238
238
  INSERT IGNORE INTO _schema_version (version, description) VALUES (6, 'Add dependencies JSON column to repo_map_symbols (Epic 28-3)');
239
+
240
+ -- ---------------------------------------------------------------------------
241
+ -- wg_stories (Epic 31-1) — planning-level work graph story nodes
242
+ -- ---------------------------------------------------------------------------
243
+ CREATE TABLE IF NOT EXISTS wg_stories (
244
+ story_key VARCHAR(20) NOT NULL,
245
+ epic VARCHAR(20) NOT NULL,
246
+ title VARCHAR(255),
247
+ status VARCHAR(30) NOT NULL DEFAULT 'planned',
248
+ spec_path VARCHAR(500),
249
+ created_at DATETIME,
250
+ updated_at DATETIME,
251
+ completed_at DATETIME,
252
+ PRIMARY KEY (story_key)
253
+ );
254
+
255
+ CREATE INDEX IF NOT EXISTS idx_wg_stories_epic ON wg_stories (epic);
256
+
257
+ -- ---------------------------------------------------------------------------
258
+ -- story_dependencies (Epic 31-1) — directed dependency edges
259
+ -- ---------------------------------------------------------------------------
260
+ CREATE TABLE IF NOT EXISTS story_dependencies (
261
+ story_key VARCHAR(20) NOT NULL,
262
+ depends_on VARCHAR(20) NOT NULL,
263
+ dep_type VARCHAR(20) NOT NULL,
264
+ source VARCHAR(20) NOT NULL,
265
+ created_at DATETIME,
266
+ PRIMARY KEY (story_key, depends_on)
267
+ );
268
+
269
+ -- ---------------------------------------------------------------------------
270
+ -- ready_stories view (Epic 31-1)
271
+ -- ---------------------------------------------------------------------------
272
+ CREATE OR REPLACE VIEW ready_stories AS
273
+ SELECT s.* FROM wg_stories s
274
+ WHERE s.status IN ('planned', 'ready')
275
+ AND NOT EXISTS (
276
+ SELECT 1 FROM story_dependencies d
277
+ JOIN wg_stories dep ON dep.story_key = d.depends_on
278
+ WHERE d.story_key = s.story_key
279
+ AND d.dep_type = 'blocks'
280
+ AND dep.status <> 'complete'
281
+ );
282
+
283
+ INSERT IGNORE INTO _schema_version (version, description) VALUES (7, 'Add wg_stories, story_dependencies tables and ready_stories view (Epic 31-1)');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "substrate-ai",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Substrate — multi-agent orchestration daemon for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",