substrate-ai 0.2.21 → 0.2.24

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/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { childLogger, createLogger, logger } from "./logger-D2fS2ccL.js";
2
- import { AdapterRegistry, ClaudeCodeAdapter, CodexCLIAdapter, GeminiCLIAdapter, createEventBus, createTuiApp, isTuiCapable, printNonTtyWarning } from "./event-bus-BMxhfxfT.js";
3
- import { AdtError, BudgetExceededError, ConfigError, ConfigIncompatibleFormatError, GitError, RecoveryError, TaskConfigError, TaskGraphCycleError, TaskGraphError, TaskGraphIncompatibleFormatError, WorkerError, WorkerNotFoundError } from "./errors-BPqtzQ4U.js";
2
+ import { AdapterRegistry, AdtError, BudgetExceededError, ClaudeCodeAdapter, CodexCLIAdapter, ConfigError, ConfigIncompatibleFormatError, GeminiCLIAdapter, GitError, RecoveryError, TaskConfigError, TaskGraphCycleError, TaskGraphError, TaskGraphIncompatibleFormatError, WorkerError, WorkerNotFoundError } from "./errors-CswS7Mzg.js";
3
+ import { createEventBus, createTuiApp, isTuiCapable, printNonTtyWarning } from "./event-bus-CAvDMst7.js";
4
4
  import { randomUUID } from "crypto";
5
5
 
6
6
  //#region src/utils/helpers.ts
@@ -1,5 +1,5 @@
1
1
  import { createLogger } from "./logger-D2fS2ccL.js";
2
- import { AdapterRegistry, createEventBus, createTuiApp, isTuiCapable, printNonTtyWarning } from "./event-bus-BMxhfxfT.js";
2
+ import { createEventBus, createTuiApp, isTuiCapable, printNonTtyWarning } from "./event-bus-CAvDMst7.js";
3
3
  import { addTokenUsage, createDecision, createPipelineRun, createRequirement, getArtifactByTypeForRun, getArtifactsByRun, getDecisionsByCategory, getDecisionsByPhase, getDecisionsByPhaseForRun, getLatestRun, getPipelineRunById, getRunningPipelineRuns, getTokenUsageSummary, registerArtifact, updatePipelineRun, updatePipelineRunConfig, upsertDecision } from "./decisions-Dq4cAA2L.js";
4
4
  import { ESCALATION_DIAGNOSIS, OPERATIONAL_FINDING, STORY_METRICS, STORY_OUTCOME, TEST_EXPANSION_FINDING, TEST_PLAN, aggregateTokenUsageForRun, aggregateTokenUsageForStory, getStoryMetricsForRun, writeRunMetrics, writeStoryMetrics } from "./operational-CnMlvWqc.js";
5
5
  import { createRequire } from "module";
@@ -15,7 +15,7 @@ import BetterSqlite3 from "better-sqlite3";
15
15
  import { fileURLToPath } from "node:url";
16
16
  import { existsSync as existsSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1 } from "node:fs";
17
17
  import { freemem, platform } from "node:os";
18
- import { randomUUID } from "node:crypto";
18
+ import { createHash, randomUUID } from "node:crypto";
19
19
  import { readFile as readFile$1, stat as stat$1 } from "node:fs/promises";
20
20
 
21
21
  //#region rolldown:runtime
@@ -676,7 +676,8 @@ const PackManifestSchema = z.object({
676
676
  phases: z.array(PhaseDefinitionSchema),
677
677
  prompts: z.record(z.string(), z.string()),
678
678
  constraints: z.record(z.string(), z.string()),
679
- templates: z.record(z.string(), z.string())
679
+ templates: z.record(z.string(), z.string()),
680
+ conflictGroups: z.record(z.string(), z.string()).optional()
680
681
  });
681
682
  const ConstraintSeveritySchema = z.enum([
682
683
  "required",
@@ -3868,7 +3869,7 @@ async function runCreateStory(deps, params) {
3868
3869
  };
3869
3870
  }
3870
3871
  const implementationDecisions = getImplementationDecisions(deps);
3871
- const epicShardContent = getEpicShard(implementationDecisions, epicId, deps.projectRoot);
3872
+ const epicShardContent = getEpicShard(implementationDecisions, epicId, deps.projectRoot, storyKey);
3872
3873
  const prevDevNotesContent = getPrevDevNotes(implementationDecisions, epicId);
3873
3874
  const archConstraintsContent = getArchConstraints$2(deps);
3874
3875
  const storyTemplateContent = await getStoryTemplate(deps);
@@ -4018,18 +4019,71 @@ function getImplementationDecisions(deps) {
4018
4019
  }
4019
4020
  }
4020
4021
  /**
4022
+ * Extract the section for a specific story key from a full epic shard.
4023
+ *
4024
+ * Matches patterns like:
4025
+ * - "Story 23-1:" / "### Story 23-1" / "#### Story 23-1"
4026
+ * - "23-1:" / "**23-1**"
4027
+ *
4028
+ * Returns the matched section content (from heading to next story heading or end),
4029
+ * or null if no matching section is found (caller falls back to full shard).
4030
+ */
4031
+ function extractStorySection(shardContent, storyKey) {
4032
+ if (!shardContent || !storyKey) return null;
4033
+ const escaped = storyKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4034
+ const headingPattern = new RegExp(`(?:^#{2,4}\\s+Story\\s+${escaped}\\b|^Story\\s+${escaped}:|^\\*\\*${escaped}\\*\\*|^${escaped}:)`, "mi");
4035
+ const match = headingPattern.exec(shardContent);
4036
+ if (!match) return null;
4037
+ const startIdx = match.index;
4038
+ const rest = shardContent.slice(startIdx + match[0].length);
4039
+ const nextStoryPattern = new RegExp(`(?:^#{2,4}\\s+Story\\s+[\\d]|^Story\\s+[\\d][\\d-]*:|^\\*\\*[\\d][\\d-]*\\*\\*|^[\\d][\\d-]*:)`, "mi");
4040
+ const nextMatch = nextStoryPattern.exec(rest);
4041
+ const endIdx = nextMatch !== null ? startIdx + match[0].length + nextMatch.index : shardContent.length;
4042
+ const section = shardContent.slice(startIdx, endIdx).trim();
4043
+ return section.length > 0 ? section : null;
4044
+ }
4045
+ /**
4021
4046
  * Retrieve the epic shard from the pre-fetched implementation decisions.
4022
4047
  * Looks for decisions with category='epic-shard', key=epicId.
4023
4048
  * Falls back to reading _bmad-output/epics.md on disk if decisions are empty.
4049
+ *
4050
+ * When storyKey is provided, extracts only the section for that story (AC3).
4024
4051
  */
4025
- function getEpicShard(decisions, epicId, projectRoot) {
4052
+ function getEpicShard(decisions, epicId, projectRoot, storyKey) {
4026
4053
  try {
4027
4054
  const epicShard = decisions.find((d) => d.category === "epic-shard" && d.key === epicId);
4028
- if (epicShard?.value) return epicShard.value;
4055
+ const shardContent = epicShard?.value;
4056
+ if (shardContent) {
4057
+ if (storyKey) {
4058
+ const storySection = extractStorySection(shardContent, storyKey);
4059
+ if (storySection) {
4060
+ logger$14.debug({
4061
+ epicId,
4062
+ storyKey
4063
+ }, "Extracted per-story section from epic shard");
4064
+ return storySection;
4065
+ }
4066
+ logger$14.debug({
4067
+ epicId,
4068
+ storyKey
4069
+ }, "No matching story section found — using full epic shard");
4070
+ }
4071
+ return shardContent;
4072
+ }
4029
4073
  if (projectRoot) {
4030
4074
  const fallback = readEpicShardFromFile(projectRoot, epicId);
4031
4075
  if (fallback) {
4032
4076
  logger$14.info({ epicId }, "Using file-based fallback for epic shard (decisions table empty)");
4077
+ if (storyKey) {
4078
+ const storySection = extractStorySection(fallback, storyKey);
4079
+ if (storySection) {
4080
+ logger$14.debug({
4081
+ epicId,
4082
+ storyKey
4083
+ }, "Extracted per-story section from file-based epic shard");
4084
+ return storySection;
4085
+ }
4086
+ }
4033
4087
  return fallback;
4034
4088
  }
4035
4089
  }
@@ -4094,7 +4148,7 @@ function readEpicShardFromFile(projectRoot, epicId) {
4094
4148
  if (!epicsPath) return "";
4095
4149
  const content = readFileSync$1(epicsPath, "utf-8");
4096
4150
  const epicNum = epicId.replace(/^epic-/i, "");
4097
- const pattern = new RegExp(`^## (?:Epic\\s+)?${epicNum}[.:\\s].*?(?=\\n## |$)`, "ms");
4151
+ const pattern = new RegExp(`^#{2,4}\\s+(?:Epic\\s+)?${epicNum}[.:\\s].*?(?=\\n#{2,4}\\s|$)`, "ms");
4098
4152
  const match = pattern.exec(content);
4099
4153
  return match ? match[0].trim() : "";
4100
4154
  } catch (err) {
@@ -4138,6 +4192,36 @@ async function getStoryTemplate(deps) {
4138
4192
  return "";
4139
4193
  }
4140
4194
  }
4195
+ /**
4196
+ * Validate that an existing story file is non-empty and structurally valid.
4197
+ *
4198
+ * A valid story file must:
4199
+ * 1. Be non-empty (> 0 bytes after trim)
4200
+ * 2. Contain at least one heading (`#`) AND either "Acceptance Criteria" or "AC1"
4201
+ *
4202
+ * @returns `{ valid: true }` or `{ valid: false, reason: 'empty' | 'missing_structure' }`
4203
+ */
4204
+ async function isValidStoryFile(filePath) {
4205
+ try {
4206
+ const content = await readFile$1(filePath, "utf-8");
4207
+ if (content.trim().length === 0) return {
4208
+ valid: false,
4209
+ reason: "empty"
4210
+ };
4211
+ const hasHeading = content.includes("#");
4212
+ const hasAC = /acceptance criteria|AC1/i.test(content);
4213
+ if (!hasHeading || !hasAC) return {
4214
+ valid: false,
4215
+ reason: "missing_structure"
4216
+ };
4217
+ return { valid: true };
4218
+ } catch {
4219
+ return {
4220
+ valid: false,
4221
+ reason: "empty"
4222
+ };
4223
+ }
4224
+ }
4141
4225
 
4142
4226
  //#endregion
4143
4227
  //#region src/modules/compiled-workflows/git-helpers.ts
@@ -4235,11 +4319,17 @@ async function getGitChangedFiles(workingDirectory = process.cwd()) {
4235
4319
  */
4236
4320
  async function stageIntentToAdd(files, workingDirectory) {
4237
4321
  if (files.length === 0) return;
4322
+ const existing = files.filter((f) => {
4323
+ const exists = existsSync$1(f);
4324
+ if (!exists) logger$13.debug({ file: f }, "Skipping nonexistent file in stageIntentToAdd");
4325
+ return exists;
4326
+ });
4327
+ if (existing.length === 0) return;
4238
4328
  await runGitCommand([
4239
4329
  "add",
4240
4330
  "-N",
4241
4331
  "--",
4242
- ...files
4332
+ ...existing
4243
4333
  ], workingDirectory, "git-add-intent");
4244
4334
  }
4245
4335
  /**
@@ -4816,6 +4906,7 @@ function defaultFailResult(error, tokenUsage) {
4816
4906
  issues: 0,
4817
4907
  issue_list: [],
4818
4908
  error,
4909
+ dispatchFailed: true,
4819
4910
  tokenUsage
4820
4911
  };
4821
4912
  }
@@ -4908,6 +4999,19 @@ async function runCodeReview(deps, params) {
4908
4999
  gitDiffContent = await getGitDiffStatSummary(cwd);
4909
5000
  }
4910
5001
  }
5002
+ if (gitDiffContent.trim().length === 0) {
5003
+ logger$10.info({ storyKey }, "Empty git diff — skipping review with SHIP_IT");
5004
+ return {
5005
+ verdict: "SHIP_IT",
5006
+ issues: 0,
5007
+ issue_list: [],
5008
+ notes: "no_changes_to_review",
5009
+ tokenUsage: {
5010
+ input: 0,
5011
+ output: 0
5012
+ }
5013
+ };
5014
+ }
4911
5015
  let previousFindingsContent = "";
4912
5016
  if (previousIssues !== void 0 && previousIssues.length > 0) previousFindingsContent = [
4913
5017
  "The previous code review found these issues. A fix agent has attempted to resolve them.",
@@ -5703,33 +5807,6 @@ function makeBatch(batchIndex, tasks) {
5703
5807
  //#endregion
5704
5808
  //#region src/modules/implementation-orchestrator/conflict-detector.ts
5705
5809
  /**
5706
- * Maps story key prefix patterns to module directory names.
5707
- *
5708
- * The heuristic: stories whose numeric prefix belongs to the same epic
5709
- * (same first digit) and whose known mapping points to the same module are
5710
- * considered conflicting. Unknown prefixes default to the story key itself
5711
- * (each unknown story is in its own group).
5712
- *
5713
- * Format: prefix (e.g. "10-") → module name
5714
- */
5715
- const STORY_PREFIX_TO_MODULE = {
5716
- "1-": "core",
5717
- "2-": "core",
5718
- "3-": "core",
5719
- "4-": "core",
5720
- "5-": "core",
5721
- "6-": "task-graph",
5722
- "7-": "worker-pool",
5723
- "8-": "monitor",
5724
- "9-": "bmad-context-engine",
5725
- "10-1": "compiled-workflows",
5726
- "10-2": "compiled-workflows",
5727
- "10-3": "compiled-workflows",
5728
- "10-4": "implementation-orchestrator",
5729
- "10-5": "cli",
5730
- "11-": "pipeline-phases"
5731
- };
5732
- /**
5733
5810
  * Determine the module prefix for a story key.
5734
5811
  *
5735
5812
  * Checks most-specific prefix first (e.g., "10-1" before "10-"), then falls
@@ -5737,10 +5814,11 @@ const STORY_PREFIX_TO_MODULE = {
5737
5814
  * itself for unknown stories so each gets its own group.
5738
5815
  *
5739
5816
  * @param storyKey - e.g. "10-1", "10-2-dev-story", "5-3-something"
5740
- * @param effectiveMap - The resolved prefix-to-module map (built-in + any extras)
5817
+ * @param effectiveMap - The resolved prefix-to-module map
5741
5818
  * @returns module name string used for conflict grouping
5742
5819
  */
5743
5820
  function resolveModulePrefix(storyKey, effectiveMap) {
5821
+ if (Object.keys(effectiveMap).length === 0) return storyKey;
5744
5822
  const sortedKeys = Object.keys(effectiveMap).sort((a, b) => b.length - a.length);
5745
5823
  for (const prefix of sortedKeys) if (storyKey.startsWith(prefix)) return effectiveMap[prefix];
5746
5824
  return storyKey;
@@ -5752,26 +5830,33 @@ function resolveModulePrefix(storyKey, effectiveMap) {
5752
5830
  * group and will be serialized. Stories with different prefixes can run in
5753
5831
  * parallel.
5754
5832
  *
5833
+ * When no `moduleMap` is configured, every story key is placed in its own
5834
+ * conflict group (maximum parallelism). This is the default for cross-project
5835
+ * runs where the story key prefixes are not known in advance.
5836
+ *
5755
5837
  * @param storyKeys - Array of story key strings
5756
- * @param config - Optional configuration; supply `moduleMap` to extend the
5757
- * built-in prefix-to-module mappings (additional entries are
5758
- * merged on top of the defaults, allowing overrides).
5838
+ * @param config - Optional configuration; supply `moduleMap` to define
5839
+ * prefix-to-module mappings. Without this config, each story
5840
+ * gets its own group (maximum parallelism).
5759
5841
  * @returns Array of conflict groups; each inner array is a list of story keys
5760
5842
  * that must be processed sequentially
5761
5843
  *
5762
5844
  * @example
5763
- * detectConflictGroups(['10-1', '10-2', '10-4', '10-5'])
5764
- * // => [['10-1', '10-2'], ['10-4'], ['10-5']]
5845
+ * // Without a moduleMap, all stories run in parallel
5846
+ * detectConflictGroups(['4-1', '4-2', '4-3'])
5847
+ * // => [['4-1'], ['4-2'], ['4-3']]
5848
+ *
5849
+ * @example
5850
+ * // With a moduleMap, matching stories are serialized
5851
+ * detectConflictGroups(['10-1', '10-2', '10-4'], { moduleMap: { '10-1': 'compiled-workflows', '10-2': 'compiled-workflows', '10-4': 'implementation-orchestrator' } })
5852
+ * // => [['10-1', '10-2'], ['10-4']]
5765
5853
  *
5766
5854
  * @example
5767
5855
  * detectConflictGroups(['12-1', '12-2'], { moduleMap: { '12-': 'my-module' } })
5768
5856
  * // => [['12-1', '12-2']]
5769
5857
  */
5770
5858
  function detectConflictGroups(storyKeys, config) {
5771
- const effectiveMap = {
5772
- ...STORY_PREFIX_TO_MODULE,
5773
- ...config?.moduleMap ?? {}
5774
- };
5859
+ const effectiveMap = { ...config?.moduleMap ?? {} };
5775
5860
  const moduleToStories = new Map();
5776
5861
  for (const key of storyKeys) {
5777
5862
  const module = resolveModulePrefix(key, effectiveMap);
@@ -5787,8 +5872,8 @@ function detectConflictGroups(storyKeys, config) {
5787
5872
  const logger$7 = createLogger("implementation-orchestrator:seed");
5788
5873
  /** Max chars for the architecture summary seeded into decisions */
5789
5874
  const MAX_ARCH_CHARS = 6e3;
5790
- /** Max chars per epic shard */
5791
- const MAX_EPIC_SHARD_CHARS = 4e3;
5875
+ /** Max chars per epic shard (fallback when per-story extraction returns null) */
5876
+ const MAX_EPIC_SHARD_CHARS = 12e3;
5792
5877
  /** Max chars for test patterns */
5793
5878
  const MAX_TEST_PATTERNS_CHARS = 2e3;
5794
5879
  /**
@@ -5872,16 +5957,35 @@ function seedArchitecture(db, projectRoot) {
5872
5957
  }
5873
5958
  /**
5874
5959
  * Seed epic shards from epics.md.
5875
- * Parses each "## Epic N" section and creates an implementation/epic-shard decision.
5876
- * Returns number of decisions created, or -1 if skipped (already seeded).
5960
+ * Parses each epic section and creates an implementation/epic-shard decision.
5961
+ *
5962
+ * Uses content-hash comparison (AC1, AC2, AC6):
5963
+ * - Computes SHA-256 of the epics file and compares to the stored `epic-shard-hash` decision.
5964
+ * - If hashes match: skip re-seeding (unchanged file).
5965
+ * - If hash differs or no hash stored: delete existing epic-shard decisions and re-seed.
5966
+ *
5967
+ * Returns number of decisions created, or -1 if skipped (hash unchanged).
5877
5968
  */
5878
5969
  function seedEpicShards(db, projectRoot) {
5879
- const existing = getDecisionsByPhase(db, "implementation");
5880
- if (existing.some((d) => d.category === "epic-shard")) return -1;
5881
5970
  const epicsPath = findArtifact(projectRoot, ["_bmad-output/planning-artifacts/epics.md", "_bmad-output/epics.md"]);
5882
5971
  if (epicsPath === void 0) return 0;
5883
5972
  const content = readFileSync$1(epicsPath, "utf-8");
5884
5973
  if (content.length === 0) return 0;
5974
+ const currentHash = createHash("sha256").update(content).digest("hex");
5975
+ const implementationDecisions = getDecisionsByPhase(db, "implementation");
5976
+ const storedHashDecision = implementationDecisions.find((d) => d.category === "epic-shard-hash" && d.key === "epics-file");
5977
+ const storedHash = storedHashDecision?.value;
5978
+ if (storedHash === currentHash) {
5979
+ logger$7.debug({ hash: currentHash }, "Epic shards up-to-date (hash unchanged) — skipping re-seed");
5980
+ return -1;
5981
+ }
5982
+ if (implementationDecisions.some((d) => d.category === "epic-shard")) {
5983
+ logger$7.debug({
5984
+ storedHash,
5985
+ currentHash
5986
+ }, "Epics file changed — deleting stale epic-shard decisions");
5987
+ db.prepare("DELETE FROM decisions WHERE phase = 'implementation' AND category = 'epic-shard'").run();
5988
+ }
5885
5989
  const shards = parseEpicShards(content);
5886
5990
  let count = 0;
5887
5991
  for (const shard of shards) {
@@ -5895,7 +5999,19 @@ function seedEpicShards(db, projectRoot) {
5895
5999
  });
5896
6000
  count++;
5897
6001
  }
5898
- logger$7.debug({ count }, "Seeded epic shard decisions");
6002
+ db.prepare("DELETE FROM decisions WHERE phase = 'implementation' AND category = 'epic-shard-hash' AND key = 'epics-file'").run();
6003
+ createDecision(db, {
6004
+ pipeline_run_id: null,
6005
+ phase: "implementation",
6006
+ category: "epic-shard-hash",
6007
+ key: "epics-file",
6008
+ value: currentHash,
6009
+ rationale: "SHA-256 hash of epics file content for change detection"
6010
+ });
6011
+ logger$7.debug({
6012
+ count,
6013
+ hash: currentHash
6014
+ }, "Seeded epic shard decisions");
5899
6015
  return count;
5900
6016
  }
5901
6017
  /**
@@ -5962,11 +6078,11 @@ function extractSection(content, headingPattern) {
5962
6078
  }
5963
6079
  /**
5964
6080
  * Parse epics.md into individual epic shards.
5965
- * Matches "## Epic N" or "## N." or "## N:" section headings.
6081
+ * Matches "## Epic N", "### Epic N", "#### Epic N", or depth-2 to depth-4 numeric headings.
5966
6082
  */
5967
6083
  function parseEpicShards(content) {
5968
6084
  const shards = [];
5969
- const epicPattern = /^## (?:Epic\s+)?(\d+)[.:\s]/gm;
6085
+ const epicPattern = /^#{2,4}\s+(?:Epic\s+)?(\d+)[.:\s]/gm;
5970
6086
  let match;
5971
6087
  const matches = [];
5972
6088
  while ((match = epicPattern.exec(content)) !== null) {
@@ -6384,22 +6500,31 @@ function createImplementationOrchestrator(deps) {
6384
6500
  const files = readdirSync$1(artifactsDir);
6385
6501
  const match = files.find((f) => f.startsWith(`${storyKey}-`) && f.endsWith(".md"));
6386
6502
  if (match) {
6387
- storyFilePath = join$1(artifactsDir, match);
6388
- logger$20.info({
6503
+ const candidatePath = join$1(artifactsDir, match);
6504
+ const validation = await isValidStoryFile(candidatePath);
6505
+ if (!validation.valid) logger$20.warn({
6389
6506
  storyKey,
6390
- storyFilePath
6391
- }, "Found existing story file — skipping create-story");
6392
- endPhase(storyKey, "create-story");
6393
- eventBus.emit("orchestrator:story-phase-complete", {
6394
- storyKey,
6395
- phase: "IN_STORY_CREATION",
6396
- result: {
6397
- result: "success",
6398
- story_file: storyFilePath,
6399
- story_key: storyKey
6400
- }
6401
- });
6402
- persistState();
6507
+ storyFilePath: candidatePath,
6508
+ reason: validation.reason
6509
+ }, `Existing story file for ${storyKey} is invalid (${validation.reason}) — re-creating`);
6510
+ else {
6511
+ storyFilePath = candidatePath;
6512
+ logger$20.info({
6513
+ storyKey,
6514
+ storyFilePath
6515
+ }, "Found existing story file — skipping create-story");
6516
+ endPhase(storyKey, "create-story");
6517
+ eventBus.emit("orchestrator:story-phase-complete", {
6518
+ storyKey,
6519
+ phase: "IN_STORY_CREATION",
6520
+ result: {
6521
+ result: "success",
6522
+ story_file: storyFilePath,
6523
+ story_key: storyKey
6524
+ }
6525
+ });
6526
+ persistState();
6527
+ }
6403
6528
  }
6404
6529
  } catch {}
6405
6530
  if (storyFilePath === void 0) try {
@@ -6768,7 +6893,7 @@ function createImplementationOrchestrator(deps) {
6768
6893
  ...previousIssueList.length > 0 ? { previousIssues: previousIssueList } : {}
6769
6894
  });
6770
6895
  }
6771
- const isPhantomReview = reviewResult.verdict !== "SHIP_IT" && (reviewResult.issue_list === void 0 || reviewResult.issue_list.length === 0) && reviewResult.error !== void 0;
6896
+ const isPhantomReview = reviewResult.dispatchFailed === true || reviewResult.verdict !== "SHIP_IT" && (reviewResult.issue_list === void 0 || reviewResult.issue_list.length === 0) && reviewResult.error !== void 0;
6772
6897
  if (isPhantomReview && !timeoutRetried) {
6773
6898
  timeoutRetried = true;
6774
6899
  logger$20.warn({
@@ -7005,31 +7130,61 @@ function createImplementationOrchestrator(deps) {
7005
7130
  const fixModel = taskType === "major-rework" ? "claude-opus-4-6" : void 0;
7006
7131
  try {
7007
7132
  let fixPrompt;
7133
+ const isMajorRework = taskType === "major-rework";
7134
+ const templateName = isMajorRework ? "rework-story" : "fix-story";
7008
7135
  try {
7009
- const fixTemplate = await pack.getPrompt("fix-story");
7136
+ const fixTemplate = await pack.getPrompt(templateName);
7010
7137
  const storyContent = await readFile$1(storyFilePath ?? "", "utf-8");
7011
7138
  let reviewFeedback;
7012
- if (issueList.length === 0) reviewFeedback = [
7139
+ if (issueList.length === 0) reviewFeedback = isMajorRework ? [
7140
+ `Verdict: ${verdict}`,
7141
+ "Issues: The reviewer flagged fundamental issues but did not provide specifics.",
7142
+ "Instructions: Re-read the story file carefully, re-implement from scratch addressing all acceptance criteria."
7143
+ ].join("\n") : [
7013
7144
  `Verdict: ${verdict}`,
7014
7145
  "Issues: The reviewer flagged this as needing work but did not provide specific issues.",
7015
7146
  "Instructions: Re-read the story file carefully, compare each acceptance criterion against the current implementation, and fix any gaps you find.",
7016
7147
  "Focus on: unimplemented ACs, missing tests, incorrect behavior, and incomplete task checkboxes."
7017
7148
  ].join("\n");
7018
- else reviewFeedback = [
7019
- `Verdict: ${verdict}`,
7020
- `Issues (${issueList.length}):`,
7021
- ...issueList.map((issue, i) => {
7022
- const iss = issue;
7023
- return ` ${i + 1}. [${iss.severity ?? "unknown"}] ${iss.description ?? "no description"}${iss.file ? ` (${iss.file}${iss.line ? `:${iss.line}` : ""})` : ""}`;
7024
- })
7025
- ].join("\n");
7149
+ else {
7150
+ const issueHeader = isMajorRework ? "Issues from previous review that MUST be addressed" : "Issues";
7151
+ reviewFeedback = [
7152
+ `Verdict: ${verdict}`,
7153
+ `${issueHeader} (${issueList.length}):`,
7154
+ ...issueList.map((issue, i) => {
7155
+ const iss = issue;
7156
+ return ` ${i + 1}. [${iss.severity ?? "unknown"}] ${iss.description ?? "no description"}${iss.file ? ` (${iss.file}${iss.line ? `:${iss.line}` : ""})` : ""}`;
7157
+ })
7158
+ ].join("\n");
7159
+ }
7026
7160
  let archConstraints = "";
7027
7161
  try {
7028
7162
  const decisions = getDecisionsByPhase(db, "solutioning");
7029
7163
  const constraints = decisions.filter((d) => d.category === "architecture");
7030
7164
  archConstraints = constraints.map((d) => `${d.key}: ${d.value}`).join("\n");
7031
7165
  } catch {}
7032
- const sections = [
7166
+ const sections = isMajorRework ? [
7167
+ {
7168
+ name: "story_content",
7169
+ content: storyContent,
7170
+ priority: "required"
7171
+ },
7172
+ {
7173
+ name: "review_findings",
7174
+ content: reviewFeedback,
7175
+ priority: "required"
7176
+ },
7177
+ {
7178
+ name: "arch_constraints",
7179
+ content: archConstraints,
7180
+ priority: "optional"
7181
+ },
7182
+ {
7183
+ name: "git_diff",
7184
+ content: "",
7185
+ priority: "optional"
7186
+ }
7187
+ ] : [
7033
7188
  {
7034
7189
  name: "story_content",
7035
7190
  content: storyContent,
@@ -7056,12 +7211,19 @@ function createImplementationOrchestrator(deps) {
7056
7211
  });
7057
7212
  }
7058
7213
  incrementDispatches(storyKey);
7059
- const handle = dispatcher.dispatch({
7214
+ const handle = isMajorRework ? dispatcher.dispatch({
7215
+ prompt: fixPrompt,
7216
+ agent: "claude-code",
7217
+ taskType,
7218
+ ...fixModel !== void 0 ? { model: fixModel } : {},
7219
+ outputSchema: DevStoryResultSchema,
7220
+ ...projectRoot !== void 0 ? { workingDirectory: projectRoot } : {}
7221
+ }) : dispatcher.dispatch({
7060
7222
  prompt: fixPrompt,
7061
7223
  agent: "claude-code",
7062
7224
  taskType,
7063
7225
  ...fixModel !== void 0 ? { model: fixModel } : {},
7064
- workingDirectory: projectRoot
7226
+ ...projectRoot !== void 0 ? { workingDirectory: projectRoot } : {}
7065
7227
  });
7066
7228
  const fixResult = await handle.result;
7067
7229
  eventBus.emit("orchestrator:story-phase-complete", {
@@ -7093,11 +7255,34 @@ function createImplementationOrchestrator(deps) {
7093
7255
  persistState();
7094
7256
  return;
7095
7257
  }
7096
- if (fixResult.status === "failed") logger$20.warn("Fix dispatch failed", {
7097
- storyKey,
7098
- taskType,
7099
- exitCode: fixResult.exitCode
7100
- });
7258
+ if (fixResult.status === "failed") {
7259
+ if (isMajorRework) {
7260
+ logger$20.warn("Major rework dispatch failed — escalating story", {
7261
+ storyKey,
7262
+ exitCode: fixResult.exitCode
7263
+ });
7264
+ endPhase(storyKey, "code-review");
7265
+ updateStory(storyKey, {
7266
+ phase: "ESCALATED",
7267
+ error: "major-rework-dispatch-failed",
7268
+ completedAt: new Date().toISOString()
7269
+ });
7270
+ writeStoryMetricsBestEffort(storyKey, "escalated", reviewCycles + 1);
7271
+ emitEscalation({
7272
+ storyKey,
7273
+ lastVerdict: verdict,
7274
+ reviewCycles: reviewCycles + 1,
7275
+ issues: issueList
7276
+ });
7277
+ persistState();
7278
+ return;
7279
+ }
7280
+ logger$20.warn("Fix dispatch failed", {
7281
+ storyKey,
7282
+ taskType,
7283
+ exitCode: fixResult.exitCode
7284
+ });
7285
+ }
7101
7286
  } catch (err) {
7102
7287
  logger$20.warn("Fix dispatch failed, continuing to next review", {
7103
7288
  storyKey,
@@ -7180,7 +7365,7 @@ function createImplementationOrchestrator(deps) {
7180
7365
  skippedCategories: seedResult.skippedCategories
7181
7366
  }, "Methodology context seeded from planning artifacts");
7182
7367
  }
7183
- const groups = detectConflictGroups(storyKeys);
7368
+ const groups = detectConflictGroups(storyKeys, { moduleMap: pack.manifest.conflictGroups });
7184
7369
  logger$20.info("Orchestrator starting", {
7185
7370
  storyCount: storyKeys.length,
7186
7371
  groupCount: groups.length,
@@ -11674,11 +11859,10 @@ async function runRunAction(options) {
11674
11859
  });
11675
11860
  const eventBus = createEventBus();
11676
11861
  const contextCompiler = createContextCompiler({ db });
11677
- const adapterRegistry = injectedRegistry ?? new AdapterRegistry();
11678
- if (injectedRegistry === void 0) await adapterRegistry.discoverAndRegister();
11862
+ if (!injectedRegistry) throw new Error("AdapterRegistry is required — must be initialized at CLI startup");
11679
11863
  const dispatcher = createDispatcher({
11680
11864
  eventBus,
11681
- adapterRegistry
11865
+ adapterRegistry: injectedRegistry
11682
11866
  });
11683
11867
  eventBus.on("orchestrator:story-phase-complete", (payload) => {
11684
11868
  try {
@@ -12066,11 +12250,10 @@ async function runFullPipeline(options) {
12066
12250
  }
12067
12251
  const eventBus = createEventBus();
12068
12252
  const contextCompiler = createContextCompiler({ db });
12069
- const adapterRegistry = injectedRegistry ?? new AdapterRegistry();
12070
- if (injectedRegistry === void 0) await adapterRegistry.discoverAndRegister();
12253
+ if (!injectedRegistry) throw new Error("AdapterRegistry is required — must be initialized at CLI startup");
12071
12254
  const dispatcher = createDispatcher({
12072
12255
  eventBus,
12073
- adapterRegistry
12256
+ adapterRegistry: injectedRegistry
12074
12257
  });
12075
12258
  const phaseDeps = {
12076
12259
  db,
@@ -12384,4 +12567,4 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
12384
12567
 
12385
12568
  //#endregion
12386
12569
  export { DatabaseWrapper, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, buildPipelineStatusOutput, createContextCompiler, createDispatcher, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, runAnalysisPhase, runMigrations, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
12387
- //# sourceMappingURL=run-BLIgARum.js.map
12570
+ //# sourceMappingURL=run-CT8B9gG9.js.map
@@ -0,0 +1,7 @@
1
+ import { registerRunCommand, runRunAction } from "./run-CT8B9gG9.js";
2
+ import "./logger-D2fS2ccL.js";
3
+ import "./event-bus-CAvDMst7.js";
4
+ import "./decisions-Dq4cAA2L.js";
5
+ import "./operational-CnMlvWqc.js";
6
+
7
+ export { runRunAction };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "substrate-ai",
3
- "version": "0.2.21",
3
+ "version": "0.2.24",
4
4
  "description": "Substrate — multi-agent orchestration daemon for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -191,6 +191,7 @@ prompts:
191
191
  dev-story: prompts/dev-story.md
192
192
  code-review: prompts/code-review.md
193
193
  fix-story: prompts/fix-story.md
194
+ rework-story: prompts/rework-story.md
194
195
  # Multi-step phase decomposition prompts
195
196
  analysis-step-1-vision: prompts/analysis-step-1-vision.md
196
197
  analysis-step-2-scope: prompts/analysis-step-2-scope.md
@@ -228,3 +229,25 @@ constraints:
228
229
 
229
230
  templates:
230
231
  story: templates/story.md
232
+
233
+ # Conflict group mappings for the implementation orchestrator.
234
+ # Stories whose prefixes map to the same module name are serialized within
235
+ # a conflict group. This map covers substrate's own epic numbering so that
236
+ # self-pipeline runs preserve correct serialization (e.g., epics 1-5 share
237
+ # 'core', stories 10-1/10-2/10-3 share 'compiled-workflows').
238
+ conflictGroups:
239
+ '1-': core
240
+ '2-': core
241
+ '3-': core
242
+ '4-': core
243
+ '5-': core
244
+ '6-': task-graph
245
+ '7-': worker-pool
246
+ '8-': monitor
247
+ '9-': bmad-context-engine
248
+ '10-1': compiled-workflows
249
+ '10-2': compiled-workflows
250
+ '10-3': compiled-workflows
251
+ '10-4': implementation-orchestrator
252
+ '10-5': cli
253
+ '11-': pipeline-phases