substrate-ai 0.20.19 → 0.20.20

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
@@ -4,7 +4,7 @@ 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, 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, getRunningPipelineRuns, getSessionCostSummary, getSessionCostSummaryFiltered, getStoryMetricsForRun, getTokenUsageSummary, incrementRunRestarts, initSchema, initializeDolt, listRunMetrics, loadParentRunDecisions, supersedeDecision, tagRunAsBaseline, updatePipelineRun } from "../dist-CqtWS9wF.js";
6
6
  import "../adapter-registry-DXLMTmfD.js";
7
- import { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, EpicIngester, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, registerExportCommand, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-2nI3qh0-.js";
7
+ import { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, EpicIngester, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, registerExportCommand, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-BLObh_Ou.js";
8
8
  import "../errors-1uLGqnvr.js";
9
9
  import "../routing-CcBOCuC9.js";
10
10
  import "../decisions-C0pz9Clx.js";
@@ -5198,7 +5198,7 @@ async function runSupervisorAction(options, deps = {}) {
5198
5198
  await initSchema(expAdapter);
5199
5199
  const { runRunAction: runPipeline } = await import(
5200
5200
  /* @vite-ignore */
5201
- "../run-BNCfGm5V.js"
5201
+ "../run-DPhVpqrX.js"
5202
5202
  );
5203
5203
  const runStoryFn = async (opts) => {
5204
5204
  const exitCode = await runPipeline({
@@ -5958,6 +5958,70 @@ function extractStorySection(shardContent, storyKey) {
5958
5958
  return section.length > 0 ? section : null;
5959
5959
  }
5960
5960
  /**
5961
+ * Story 59-3: extract the list of named filesystem paths and filenames from
5962
+ * source AC text. Looks for backtick-wrapped strings that contain a path
5963
+ * separator OR end with a recognized source-file extension. The result is
5964
+ * a deduped list of names the source AC declares as part of the story's
5965
+ * artifact contract.
5966
+ *
5967
+ * Used by the orchestrator's pre-dev fidelity gate to detect create-story
5968
+ * output drift: if the agent produced a story file that doesn't reference
5969
+ * the source AC's named files (`adjacency-store.ts`, `wikilink-parser.ts`,
5970
+ * etc.) the gate fires and the orchestrator renames-to-stale + retries.
5971
+ *
5972
+ * Heuristic chosen over LLM-based fidelity check because deterministic,
5973
+ * fast, and the LLM-based source-ac-fidelity check still runs at
5974
+ * verification phase as a backstop for nuanced drift this gate can't see.
5975
+ */
5976
+ function extractNamedPathsFromSource(shardContent) {
5977
+ const paths = new Set();
5978
+ const backtickPattern = /`([^`\n]+)`/g;
5979
+ const extensionPattern = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|md|json|sql|sh|toml|yaml|yml|html|css|scss|svelte)$/i;
5980
+ let match$2;
5981
+ while ((match$2 = backtickPattern.exec(shardContent)) !== null) {
5982
+ const candidate = match$2[1]?.trim();
5983
+ if (candidate === void 0 || candidate.length === 0) continue;
5984
+ if (candidate.includes(" ")) continue;
5985
+ if (!candidate.includes("/") && !extensionPattern.test(candidate)) continue;
5986
+ paths.add(candidate);
5987
+ }
5988
+ return Array.from(paths);
5989
+ }
5990
+ /**
5991
+ * Story 59-3: compute fidelity between a generated story file and the
5992
+ * named paths in the source AC. Drift is the fraction of source-AC paths
5993
+ * that do NOT appear anywhere in the story file content.
5994
+ *
5995
+ * Substring-match (not exact) — a source AC path
5996
+ * `packages/memory/src/graph/adjacency-store.ts` matches a story-file
5997
+ * mention of just the basename `adjacency-store.ts`. This is intentional:
5998
+ * stories sometimes shorten paths in prose while still preserving the
5999
+ * named file. Full-path or basename — both count as present. The drift
6000
+ * case we're catching (Run 9's WikilinkResolver substituting for
6001
+ * adjacency-store) has neither full nor basename present.
6002
+ *
6003
+ * Empty namedPaths list returns drift=0 (cannot drift from nothing).
6004
+ */
6005
+ function computeStoryFileFidelity(storyFileContent, namedPaths) {
6006
+ if (namedPaths.length === 0) return {
6007
+ missing: [],
6008
+ present: [],
6009
+ drift: 0
6010
+ };
6011
+ const missing = [];
6012
+ const present = [];
6013
+ for (const path$3 of namedPaths) {
6014
+ const basename$2 = path$3.includes("/") ? path$3.slice(path$3.lastIndexOf("/") + 1) : path$3;
6015
+ if (storyFileContent.includes(path$3) || storyFileContent.includes(basename$2)) present.push(path$3);
6016
+ else missing.push(path$3);
6017
+ }
6018
+ return {
6019
+ missing,
6020
+ present,
6021
+ drift: missing.length / namedPaths.length
6022
+ };
6023
+ }
6024
+ /**
5961
6025
  * Retrieve the epic shard from the pre-fetched implementation decisions.
5962
6026
  *
5963
6027
  * Lookup order (post-37-0 schema):
@@ -8915,9 +8979,9 @@ function parseEpicShards(content) {
8915
8979
  * Parse an epic section's content into per-story subsections.
8916
8980
  *
8917
8981
  * Matches story headings using three patterns:
8918
- * - Markdown headings: #{2,6} Story \d+[-._ ]\d+ (e.g., ### Story 37-1: Title or ### Story 1.1)
8919
- * - Bold: **Story \d+[-._ ]\d+** (e.g., **Story 37-1**)
8920
- * - Bare key: \d+[-._ ]\d+:\s (e.g., 37-1: Title — must start at line start)
8982
+ * - Markdown headings: #{2,6} Story \d+[-._ ]\d+[a-z]* (e.g., ### Story 37-1, ### Story 1.1, ### Story 1.11a)
8983
+ * - Bold: **Story \d+[-._ ]\d+[a-z]*** (e.g., **Story 37-1**)
8984
+ * - Bare key: \d+[-._ ]\d+[a-z]*:\s (e.g., 37-1a: Title)
8921
8985
  *
8922
8986
  * Each subsection spans from its heading to the next matching heading or EOF.
8923
8987
  *
@@ -8934,16 +8998,28 @@ function parseEpicShards(content) {
8934
8998
  * Epic 58-5 already made `extractStorySection` separator-agnostic for the
8935
8999
  * same reason; this matches that precedent at the seed-time parser.
8936
9000
  *
8937
- * Captured storyKey is normalized to canonical dash-form (`1.1` `1-1`) so
8938
- * decision keys are consistent regardless of the source heading style a
8939
- * `--stories 1-9` CLI invocation finds the shard whether the epic used dot,
8940
- * dash, underscore, or space separators.
9001
+ * Story 59-2: alpha-suffix capture. BMAD-template projects subdivide stories
9002
+ * with letter suffixes (`### Story 1.11a`, `### Story 1.11b`, …) when an
9003
+ * original 1.11 ships broken and gets split for clarity. Strata's epics.md
9004
+ * uses this convention with 5 substories (1.11, 1.11a-d). 58-17's regex
9005
+ * `\d+[-._ ]\d+` truncated capture at the digit boundary, normalizing all
9006
+ * 5 to canonical key `1-11` and inserting 5 duplicate-key rows in the
9007
+ * decisions store (no UNIQUE constraint enforces uniqueness — strata
9008
+ * obs_2026-04-25_010). Trailing `[a-z]*` captures alpha suffixes of any
9009
+ * length; the `i` flag on the storyPattern (needed for `Story` heading
9010
+ * case variation) means uppercase is also accepted, so a stray `1.11A`
9011
+ * heading captures cleanly to key `1-11A` rather than truncating.
9012
+ *
9013
+ * Captured storyKey is normalized to canonical dash-form (`1.1` → `1-1`,
9014
+ * `1.11a` → `1-11a`) so decision keys are consistent regardless of the
9015
+ * source heading style. The separator-replace `[._ ] → -` does not touch
9016
+ * `[a-z]`, so trailing alpha is preserved through normalization.
8941
9017
  *
8942
9018
  * AC3: If no story headings are found, returns a single per-epic fallback entry
8943
9019
  * keyed by epicId — preserving backward-compatible behaviour for unstructured epics.
8944
9020
  */
8945
9021
  function parseStorySubsections(epicId, epicContent) {
8946
- const storyPattern = /(?:^#{2,6}\s+Story\s+(\d+[-._ ]\d+)|^\*\*Story\s+(\d+[-._ ]\d+)\*\*|^(\d+[-._ ]\d+):\s)/gim;
9022
+ const storyPattern = /(?:^#{2,6}\s+Story\s+(\d+[-._ ]\d+[a-z]*)|^\*\*Story\s+(\d+[-._ ]\d+[a-z]*)\*\*|^(\d+[-._ ]\d+[a-z]*):\s)/gim;
8947
9023
  const matches = [];
8948
9024
  let match$2;
8949
9025
  while ((match$2 = storyPattern.exec(epicContent)) !== null) {
@@ -11307,6 +11383,9 @@ async function autoIngestEpicsDependencies(db, projectRoot) {
11307
11383
  };
11308
11384
  }
11309
11385
  const TITLE_OVERLAP_WARNING_THRESHOLD = .3;
11386
+ const FIDELITY_DRIFT_THRESHOLD = .5;
11387
+ const MIN_NAMED_PATHS_FOR_FIDELITY_GATE = 3;
11388
+ const MAX_FIDELITY_RETRIES = 2;
11310
11389
  /**
11311
11390
  * Map a terminal StoryPhase to the corresponding PerStoryStatus for run-manifest writes.
11312
11391
  * Returns 'dispatched' for in-progress phases (used as a safe default).
@@ -12166,66 +12245,233 @@ function createImplementationOrchestrator(deps) {
12166
12245
  await persistState();
12167
12246
  return;
12168
12247
  }
12169
- if (storyFilePath === void 0) try {
12170
- incrementDispatches(storyKey);
12171
- const dispatchStartMs = Date.now();
12172
- const createResult = await runCreateStory({
12173
- db,
12174
- pack,
12175
- contextCompiler,
12176
- dispatcher,
12177
- projectRoot,
12178
- tokenCeilings,
12179
- otlpEndpoint: _otlpEndpoint,
12180
- agentId
12181
- }, {
12182
- epicId: storyKey.split("-")[0] ?? storyKey,
12183
- storyKey,
12184
- pipelineRunId: config.pipelineRunId,
12185
- source_ac_hash: sourceAcHash
12186
- });
12187
- endPhase(storyKey, "create-story");
12188
- eventBus.emit("orchestrator:story-phase-complete", {
12189
- storyKey,
12190
- phase: "IN_STORY_CREATION",
12191
- result: createResult
12192
- });
12193
- if (config.pipelineRunId !== void 0 && createResult.tokenUsage !== void 0) Promise.resolve().then(() => addTokenUsage(db, config.pipelineRunId, {
12194
- phase: "create-story",
12195
- agent: "create-story",
12196
- input_tokens: createResult.tokenUsage.input,
12197
- output_tokens: createResult.tokenUsage.output,
12198
- cost_usd: estimateDispatchCost(createResult.tokenUsage.input, createResult.tokenUsage.output),
12199
- metadata: JSON.stringify({ storyKey })
12200
- })).catch((tokenErr) => logger$24.warn({
12201
- storyKey,
12202
- err: tokenErr
12203
- }, "Failed to record create-story token usage"));
12204
- await persistState();
12205
- if (createResult.result === "failed") {
12206
- const errMsg = createResult.error ?? "create-story failed";
12207
- const stderrSnippet = errMsg.includes("--- stderr ---") ? errMsg.slice(errMsg.indexOf("--- stderr ---") + 15, errMsg.indexOf("--- stderr ---") + 515) : errMsg.slice(0, 500);
12208
- logger$24.error({
12248
+ let fidelityRetries = 0;
12249
+ while (storyFilePath === void 0) {
12250
+ try {
12251
+ incrementDispatches(storyKey);
12252
+ const dispatchStartMs = Date.now();
12253
+ const createResult = await runCreateStory({
12254
+ db,
12255
+ pack,
12256
+ contextCompiler,
12257
+ dispatcher,
12258
+ projectRoot,
12259
+ tokenCeilings,
12260
+ otlpEndpoint: _otlpEndpoint,
12261
+ agentId
12262
+ }, {
12263
+ epicId: storyKey.split("-")[0] ?? storyKey,
12209
12264
  storyKey,
12210
- stderrSnippet
12211
- }, `Create-story failed: ${stderrSnippet.split("\n")[0]}`);
12212
- updateStory(storyKey, {
12213
- phase: "ESCALATED",
12214
- error: errMsg,
12215
- completedAt: new Date().toISOString()
12265
+ pipelineRunId: config.pipelineRunId,
12266
+ source_ac_hash: sourceAcHash
12216
12267
  });
12217
- await writeStoryMetricsBestEffort(storyKey, "failed", 0);
12218
- await emitEscalation({
12268
+ endPhase(storyKey, "create-story");
12269
+ eventBus.emit("orchestrator:story-phase-complete", {
12219
12270
  storyKey,
12220
- lastVerdict: "create-story-failed",
12221
- reviewCycles: 0,
12222
- issues: [errMsg]
12271
+ phase: "IN_STORY_CREATION",
12272
+ result: createResult
12223
12273
  });
12274
+ if (config.pipelineRunId !== void 0 && createResult.tokenUsage !== void 0) Promise.resolve().then(() => addTokenUsage(db, config.pipelineRunId, {
12275
+ phase: "create-story",
12276
+ agent: "create-story",
12277
+ input_tokens: createResult.tokenUsage.input,
12278
+ output_tokens: createResult.tokenUsage.output,
12279
+ cost_usd: estimateDispatchCost(createResult.tokenUsage.input, createResult.tokenUsage.output),
12280
+ metadata: JSON.stringify({ storyKey })
12281
+ })).catch((tokenErr) => logger$24.warn({
12282
+ storyKey,
12283
+ err: tokenErr
12284
+ }, "Failed to record create-story token usage"));
12224
12285
  await persistState();
12225
- return;
12226
- }
12227
- if (createResult.story_file === void 0 || createResult.story_file === "") {
12228
- const errMsg = "create-story succeeded but returned no story_file path";
12286
+ if (createResult.result === "failed") {
12287
+ const errMsg = createResult.error ?? "create-story failed";
12288
+ const stderrSnippet = errMsg.includes("--- stderr ---") ? errMsg.slice(errMsg.indexOf("--- stderr ---") + 15, errMsg.indexOf("--- stderr ---") + 515) : errMsg.slice(0, 500);
12289
+ logger$24.error({
12290
+ storyKey,
12291
+ stderrSnippet
12292
+ }, `Create-story failed: ${stderrSnippet.split("\n")[0]}`);
12293
+ updateStory(storyKey, {
12294
+ phase: "ESCALATED",
12295
+ error: errMsg,
12296
+ completedAt: new Date().toISOString()
12297
+ });
12298
+ await writeStoryMetricsBestEffort(storyKey, "failed", 0);
12299
+ await emitEscalation({
12300
+ storyKey,
12301
+ lastVerdict: "create-story-failed",
12302
+ reviewCycles: 0,
12303
+ issues: [errMsg]
12304
+ });
12305
+ await persistState();
12306
+ return;
12307
+ }
12308
+ if (createResult.story_file === void 0 || createResult.story_file === "") {
12309
+ const errMsg = "create-story succeeded but returned no story_file path";
12310
+ updateStory(storyKey, {
12311
+ phase: "ESCALATED",
12312
+ error: errMsg,
12313
+ completedAt: new Date().toISOString()
12314
+ });
12315
+ await writeStoryMetricsBestEffort(storyKey, "failed", 0);
12316
+ await emitEscalation({
12317
+ storyKey,
12318
+ lastVerdict: "create-story-no-file",
12319
+ reviewCycles: 0,
12320
+ issues: [errMsg]
12321
+ });
12322
+ await persistState();
12323
+ return;
12324
+ }
12325
+ if (projectRoot !== void 0) {
12326
+ const expectedArtifactsDir = join$1(projectRoot, "_bmad-output", "implementation-artifacts");
12327
+ const escapedExpectedDir = expectedArtifactsDir.replace("/_bmad-output/", "/\\_bmad-output/");
12328
+ let claimedPath = createResult.story_file;
12329
+ if (claimedPath.startsWith(escapedExpectedDir)) {
12330
+ claimedPath = claimedPath.replace("/\\_bmad-output/", "/_bmad-output/");
12331
+ logger$24.warn({
12332
+ storyKey,
12333
+ originalClaim: createResult.story_file,
12334
+ normalizedClaim: claimedPath
12335
+ }, "create-story claimed path contains backslash-escaped underscore; normalizing");
12336
+ eventBus.emit("orchestrator:story-warn", {
12337
+ storyKey,
12338
+ msg: `create-story claim path was backslash-escaped; normalized to ${claimedPath}`
12339
+ });
12340
+ }
12341
+ if (claimedPath.startsWith(expectedArtifactsDir)) try {
12342
+ let actualPath = null;
12343
+ if (existsSync(claimedPath)) actualPath = claimedPath;
12344
+ else {
12345
+ const escapedVariant = claimedPath.replace("/_bmad-output/", "/\\_bmad-output/");
12346
+ if (escapedVariant !== claimedPath && existsSync(escapedVariant)) try {
12347
+ renameSync(escapedVariant, claimedPath);
12348
+ actualPath = claimedPath;
12349
+ logger$24.warn({
12350
+ storyKey,
12351
+ escapedVariant,
12352
+ canonicalPath: claimedPath
12353
+ }, "create-story wrote artifact to backslash-escaped path; moved to canonical location");
12354
+ eventBus.emit("orchestrator:story-warn", {
12355
+ storyKey,
12356
+ msg: `create-story wrote to backslash-escaped path ${escapedVariant}; corrected to ${claimedPath}`
12357
+ });
12358
+ } catch (renameErr) {
12359
+ actualPath = escapedVariant;
12360
+ logger$24.warn({
12361
+ storyKey,
12362
+ escapedVariant,
12363
+ canonicalPath: claimedPath,
12364
+ err: renameErr
12365
+ }, "create-story wrote to backslash-escaped path; rename to canonical failed; treating as success at escaped location");
12366
+ eventBus.emit("orchestrator:story-warn", {
12367
+ storyKey,
12368
+ msg: `create-story wrote to backslash-escaped path ${escapedVariant}; rename to canonical failed`
12369
+ });
12370
+ }
12371
+ }
12372
+ if (actualPath === null) {
12373
+ const outputTokens = createResult.tokenUsage?.output ?? 0;
12374
+ const errMsg = `create-story claimed success (story_file: ${createResult.story_file}) but the file does not exist on disk (output tokens: ${outputTokens})`;
12375
+ logger$24.error({
12376
+ storyKey,
12377
+ claimedPath: createResult.story_file,
12378
+ outputTokens
12379
+ }, errMsg);
12380
+ updateStory(storyKey, {
12381
+ phase: "ESCALATED",
12382
+ error: errMsg,
12383
+ completedAt: new Date().toISOString()
12384
+ });
12385
+ await writeStoryMetricsBestEffort(storyKey, "failed", 0);
12386
+ await emitEscalation({
12387
+ storyKey,
12388
+ lastVerdict: "create-story-fraud-success",
12389
+ reviewCycles: 0,
12390
+ issues: [errMsg]
12391
+ });
12392
+ await persistState();
12393
+ return;
12394
+ }
12395
+ if (actualPath !== createResult.story_file) createResult.story_file = actualPath;
12396
+ const claimedStat = statSync(actualPath);
12397
+ if (claimedStat.mtimeMs < dispatchStartMs) {
12398
+ const outputTokens = createResult.tokenUsage?.output ?? 0;
12399
+ const mtimeISO = new Date(claimedStat.mtimeMs).toISOString();
12400
+ const dispatchStartISO = new Date(dispatchStartMs).toISOString();
12401
+ const errMsg = `create-story claimed success but did not rewrite ${actualPath} during this dispatch (file mtime ${mtimeISO} predates dispatch start ${dispatchStartISO}; output tokens: ${outputTokens})`;
12402
+ logger$24.error({
12403
+ storyKey,
12404
+ claimedPath: actualPath,
12405
+ mtimeISO,
12406
+ dispatchStartISO,
12407
+ outputTokens
12408
+ }, errMsg);
12409
+ updateStory(storyKey, {
12410
+ phase: "ESCALATED",
12411
+ error: errMsg,
12412
+ completedAt: new Date().toISOString()
12413
+ });
12414
+ await writeStoryMetricsBestEffort(storyKey, "failed", 0);
12415
+ await emitEscalation({
12416
+ storyKey,
12417
+ lastVerdict: "create-story-fraud-success",
12418
+ reviewCycles: 0,
12419
+ issues: [errMsg]
12420
+ });
12421
+ await persistState();
12422
+ return;
12423
+ }
12424
+ } catch (verifyErr) {
12425
+ logger$24.warn({
12426
+ storyKey,
12427
+ err: verifyErr
12428
+ }, "create-story post-dispatch file verification threw; proceeding with claimed path");
12429
+ }
12430
+ }
12431
+ storyFilePath = createResult.story_file;
12432
+ if (createResult.story_title) try {
12433
+ const epicId = storyKey.split("-")[0] ?? storyKey;
12434
+ const implDecisions = await getDecisionsByPhase(db, "implementation");
12435
+ let shardContent;
12436
+ const perStoryShard = implDecisions.find((d) => d.category === "epic-shard" && d.key === storyKey);
12437
+ if (perStoryShard?.value) shardContent = perStoryShard.value;
12438
+ else {
12439
+ const epicShard = implDecisions.find((d) => d.category === "epic-shard" && d.key === epicId);
12440
+ if (epicShard?.value) shardContent = extractStorySection(epicShard.value, storyKey) ?? epicShard.value;
12441
+ }
12442
+ if (shardContent) {
12443
+ const expectedTitle = extractExpectedStoryTitle(shardContent, storyKey);
12444
+ if (expectedTitle) {
12445
+ const overlap = computeTitleOverlap(expectedTitle, createResult.story_title);
12446
+ if (overlap < TITLE_OVERLAP_WARNING_THRESHOLD) {
12447
+ const msg = `Story title mismatch: expected "${expectedTitle}" but got "${createResult.story_title}" (word overlap: ${Math.round(overlap * 100)}%). This may indicate the create-story agent received truncated context.`;
12448
+ logger$24.warn({
12449
+ storyKey,
12450
+ expectedTitle,
12451
+ generatedTitle: createResult.story_title,
12452
+ overlap
12453
+ }, msg);
12454
+ eventBus.emit("orchestrator:story-warn", {
12455
+ storyKey,
12456
+ msg
12457
+ });
12458
+ } else logger$24.debug({
12459
+ storyKey,
12460
+ expectedTitle,
12461
+ generatedTitle: createResult.story_title,
12462
+ overlap
12463
+ }, "Story title validation passed");
12464
+ }
12465
+ }
12466
+ } catch (titleValidationErr) {
12467
+ logger$24.debug({
12468
+ storyKey,
12469
+ err: titleValidationErr
12470
+ }, "Story title validation skipped due to error");
12471
+ }
12472
+ } catch (err) {
12473
+ const errMsg = err instanceof Error ? err.message : String(err);
12474
+ endPhase(storyKey, "create-story");
12229
12475
  updateStory(storyKey, {
12230
12476
  phase: "ESCALATED",
12231
12477
  error: errMsg,
@@ -12234,133 +12480,95 @@ function createImplementationOrchestrator(deps) {
12234
12480
  await writeStoryMetricsBestEffort(storyKey, "failed", 0);
12235
12481
  await emitEscalation({
12236
12482
  storyKey,
12237
- lastVerdict: "create-story-no-file",
12483
+ lastVerdict: "create-story-exception",
12238
12484
  reviewCycles: 0,
12239
12485
  issues: [errMsg]
12240
12486
  });
12241
12487
  await persistState();
12242
12488
  return;
12243
12489
  }
12244
- if (projectRoot !== void 0) {
12245
- const expectedArtifactsDir = join$1(projectRoot, "_bmad-output", "implementation-artifacts");
12246
- const claimedPath = createResult.story_file;
12247
- if (claimedPath.startsWith(expectedArtifactsDir)) try {
12248
- if (!existsSync(claimedPath)) {
12249
- const outputTokens = createResult.tokenUsage?.output ?? 0;
12250
- const errMsg = `create-story claimed success (story_file: ${claimedPath}) but the file does not exist on disk (output tokens: ${outputTokens})`;
12251
- logger$24.error({
12252
- storyKey,
12253
- claimedPath,
12254
- outputTokens
12255
- }, errMsg);
12256
- updateStory(storyKey, {
12257
- phase: "ESCALATED",
12258
- error: errMsg,
12259
- completedAt: new Date().toISOString()
12260
- });
12261
- await writeStoryMetricsBestEffort(storyKey, "failed", 0);
12262
- await emitEscalation({
12263
- storyKey,
12264
- lastVerdict: "create-story-fraud-success",
12265
- reviewCycles: 0,
12266
- issues: [errMsg]
12267
- });
12268
- await persistState();
12269
- return;
12270
- }
12271
- const claimedStat = statSync(claimedPath);
12272
- if (claimedStat.mtimeMs < dispatchStartMs) {
12273
- const outputTokens = createResult.tokenUsage?.output ?? 0;
12274
- const mtimeISO = new Date(claimedStat.mtimeMs).toISOString();
12275
- const dispatchStartISO = new Date(dispatchStartMs).toISOString();
12276
- const errMsg = `create-story claimed success but did not rewrite ${claimedPath} during this dispatch (file mtime ${mtimeISO} predates dispatch start ${dispatchStartISO}; output tokens: ${outputTokens})`;
12277
- logger$24.error({
12278
- storyKey,
12279
- claimedPath,
12280
- mtimeISO,
12281
- dispatchStartISO,
12282
- outputTokens
12283
- }, errMsg);
12284
- updateStory(storyKey, {
12285
- phase: "ESCALATED",
12286
- error: errMsg,
12287
- completedAt: new Date().toISOString()
12288
- });
12289
- await writeStoryMetricsBestEffort(storyKey, "failed", 0);
12290
- await emitEscalation({
12291
- storyKey,
12292
- lastVerdict: "create-story-fraud-success",
12293
- reviewCycles: 0,
12294
- issues: [errMsg]
12295
- });
12296
- await persistState();
12297
- return;
12298
- }
12299
- } catch (verifyErr) {
12300
- logger$24.warn({
12301
- storyKey,
12302
- err: verifyErr
12303
- }, "create-story post-dispatch file verification threw; proceeding with claimed path");
12304
- }
12305
- }
12306
- storyFilePath = createResult.story_file;
12307
- if (createResult.story_title) try {
12490
+ if (storyFilePath !== void 0 && projectRoot !== void 0) try {
12308
12491
  const epicId = storyKey.split("-")[0] ?? storyKey;
12309
- const implDecisions = await getDecisionsByPhase(db, "implementation");
12310
- let shardContent;
12311
- const perStoryShard = implDecisions.find((d) => d.category === "epic-shard" && d.key === storyKey);
12312
- if (perStoryShard?.value) shardContent = perStoryShard.value;
12492
+ const fidelityImplDecisions = await getDecisionsByPhase(db, "implementation");
12493
+ let fidelitySourceContent;
12494
+ const fidelityPerStoryShard = fidelityImplDecisions.find((d) => d.category === "epic-shard" && d.key === storyKey);
12495
+ if (fidelityPerStoryShard?.value) fidelitySourceContent = fidelityPerStoryShard.value;
12313
12496
  else {
12314
- const epicShard = implDecisions.find((d) => d.category === "epic-shard" && d.key === epicId);
12315
- if (epicShard?.value) shardContent = extractStorySection(epicShard.value, storyKey) ?? epicShard.value;
12316
- }
12317
- if (shardContent) {
12318
- const expectedTitle = extractExpectedStoryTitle(shardContent, storyKey);
12319
- if (expectedTitle) {
12320
- const overlap = computeTitleOverlap(expectedTitle, createResult.story_title);
12321
- if (overlap < TITLE_OVERLAP_WARNING_THRESHOLD) {
12322
- const msg = `Story title mismatch: expected "${expectedTitle}" but got "${createResult.story_title}" (word overlap: ${Math.round(overlap * 100)}%). This may indicate the create-story agent received truncated context.`;
12323
- logger$24.warn({
12324
- storyKey,
12325
- expectedTitle,
12326
- generatedTitle: createResult.story_title,
12327
- overlap
12328
- }, msg);
12329
- eventBus.emit("orchestrator:story-warn", {
12330
- storyKey,
12331
- msg
12332
- });
12333
- } else logger$24.debug({
12497
+ const epicShardForFidelity = fidelityImplDecisions.find((d) => d.category === "epic-shard" && d.key === epicId);
12498
+ if (epicShardForFidelity?.value) fidelitySourceContent = extractStorySection(epicShardForFidelity.value, storyKey) ?? void 0;
12499
+ }
12500
+ if (fidelitySourceContent !== void 0) {
12501
+ const namedPaths = extractNamedPathsFromSource(fidelitySourceContent);
12502
+ if (namedPaths.length >= MIN_NAMED_PATHS_FOR_FIDELITY_GATE) {
12503
+ const storyContentForFidelity = await readFile$1(storyFilePath, "utf-8");
12504
+ const fidelity = computeStoryFileFidelity(storyContentForFidelity, namedPaths);
12505
+ logger$24.debug({
12334
12506
  storyKey,
12335
- expectedTitle,
12336
- generatedTitle: createResult.story_title,
12337
- overlap
12338
- }, "Story title validation passed");
12507
+ drift: fidelity.drift,
12508
+ missing: fidelity.missing,
12509
+ presentCount: fidelity.present.length,
12510
+ namedPathsCount: namedPaths.length
12511
+ }, "create-story output fidelity check");
12512
+ if (fidelity.drift > FIDELITY_DRIFT_THRESHOLD) {
12513
+ fidelityRetries++;
12514
+ if (fidelityRetries <= MAX_FIDELITY_RETRIES) {
12515
+ const stalePath = storyFilePath.replace(/\.md$/, `.stale-${Date.now()}.md`);
12516
+ try {
12517
+ renameSync(storyFilePath, stalePath);
12518
+ const driftPct = Math.round(fidelity.drift * 100);
12519
+ logger$24.warn({
12520
+ storyKey,
12521
+ drift: fidelity.drift,
12522
+ missing: fidelity.missing,
12523
+ retries: fidelityRetries,
12524
+ stalePath
12525
+ }, `create-story output drifted from source AC (${driftPct}% of ${namedPaths.length} named paths missing); renamed to ${stalePath} and retrying (${fidelityRetries}/${MAX_FIDELITY_RETRIES})`);
12526
+ eventBus.emit("orchestrator:story-warn", {
12527
+ storyKey,
12528
+ msg: `create-story drift detected (${fidelity.missing.length}/${namedPaths.length} named paths missing); retry ${fidelityRetries}/${MAX_FIDELITY_RETRIES}`
12529
+ });
12530
+ storyFilePath = void 0;
12531
+ continue;
12532
+ } catch (renameErr) {
12533
+ logger$24.warn({
12534
+ storyKey,
12535
+ err: renameErr,
12536
+ stalePath
12537
+ }, "failed to rename drifting artifact for retry; proceeding with current artifact");
12538
+ }
12539
+ } else {
12540
+ const errMsg = `create-story output drifted from source AC after ${MAX_FIDELITY_RETRIES} retries; ${fidelity.missing.length} of ${namedPaths.length} named paths missing: ` + fidelity.missing.join(", ");
12541
+ logger$24.error({
12542
+ storyKey,
12543
+ drift: fidelity.drift,
12544
+ missing: fidelity.missing,
12545
+ namedPaths
12546
+ }, errMsg);
12547
+ endPhase(storyKey, "create-story");
12548
+ updateStory(storyKey, {
12549
+ phase: "ESCALATED",
12550
+ error: errMsg,
12551
+ completedAt: new Date().toISOString()
12552
+ });
12553
+ await writeStoryMetricsBestEffort(storyKey, "failed", 0);
12554
+ await emitEscalation({
12555
+ storyKey,
12556
+ lastVerdict: "create-story-source-ac-drift",
12557
+ reviewCycles: 0,
12558
+ issues: [errMsg]
12559
+ });
12560
+ await persistState();
12561
+ return;
12562
+ }
12563
+ }
12339
12564
  }
12340
12565
  }
12341
- } catch (titleValidationErr) {
12342
- logger$24.debug({
12566
+ } catch (fidelityErr) {
12567
+ logger$24.warn({
12343
12568
  storyKey,
12344
- err: titleValidationErr
12345
- }, "Story title validation skipped due to error");
12569
+ err: fidelityErr
12570
+ }, "fidelity gate threw; proceeding without retry");
12346
12571
  }
12347
- } catch (err) {
12348
- const errMsg = err instanceof Error ? err.message : String(err);
12349
- endPhase(storyKey, "create-story");
12350
- updateStory(storyKey, {
12351
- phase: "ESCALATED",
12352
- error: errMsg,
12353
- completedAt: new Date().toISOString()
12354
- });
12355
- await writeStoryMetricsBestEffort(storyKey, "failed", 0);
12356
- await emitEscalation({
12357
- storyKey,
12358
- lastVerdict: "create-story-exception",
12359
- reviewCycles: 0,
12360
- issues: [errMsg]
12361
- });
12362
- await persistState();
12363
- return;
12364
12572
  }
12365
12573
  if (storyFilePath) try {
12366
12574
  const storyContent = await readFile$1(storyFilePath, "utf-8");
@@ -44091,4 +44299,4 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
44091
44299
 
44092
44300
  //#endregion
44093
44301
  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, registerExportCommand, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveMaxReviewCycles, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict, wireNdjsonEmitter };
44094
- //# sourceMappingURL=run-2nI3qh0-.js.map
44302
+ //# sourceMappingURL=run-BLObh_Ou.js.map
@@ -2,7 +2,7 @@ import "./health-ZGa9E0D2.js";
2
2
  import "./logger-KeHncl-f.js";
3
3
  import "./helpers-CElYrONe.js";
4
4
  import "./dist-CqtWS9wF.js";
5
- import { normalizeGraphSummaryToStatus, registerRunCommand, resolveMaxReviewCycles, runRunAction, wireNdjsonEmitter } from "./run-2nI3qh0-.js";
5
+ import { normalizeGraphSummaryToStatus, registerRunCommand, resolveMaxReviewCycles, runRunAction, wireNdjsonEmitter } from "./run-BLObh_Ou.js";
6
6
  import "./routing-CcBOCuC9.js";
7
7
  import "./decisions-C0pz9Clx.js";
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "substrate-ai",
3
- "version": "0.20.19",
3
+ "version": "0.20.20",
4
4
  "description": "Substrate — multi-agent orchestration daemon for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -72,6 +72,7 @@ Do NOT write a partial story file. Do NOT paraphrase surrounding context. Do NOT
72
72
  - Dev Notes with file paths, import patterns, testing requirements
73
73
  6. **Apply the scope cap** — see Scope Cap Guidance below
74
74
  7. **Write the story file** to: `_bmad-output/implementation-artifacts/{{story_key}}-<kebab-title>.md`
75
+ - Pass this path to your file-writing tool **literally as written** — do NOT markdown-escape the underscore as `\_bmad-output`. The leading underscore is part of the directory name, not a markdown italic delimiter.
75
76
  - Do NOT add a `Status:` field to the story file — story status is managed exclusively by the Dolt work graph (`wg_stories` table)
76
77
  - Dev Agent Record section must be present but left blank (to be filled by dev agent)
77
78