substrate-ai 0.20.10 → 0.20.13

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.
@@ -3442,21 +3442,52 @@ const RuntimeProbeListSchema = z.array(RuntimeProbeSchema);
3442
3442
  //#endregion
3443
3443
  //#region packages/sdlc/dist/verification/probes/parser.js
3444
3444
  const SECTION_HEADING = /^##\s+Runtime\s+Probes\s*$/i;
3445
+ const FENCE_DELIMITER = /^\s*```/;
3445
3446
  /**
3446
3447
  * Return the raw text of the story's `## Runtime Probes` section (excluding
3447
3448
  * the heading line itself), or `undefined` if the section is not present.
3448
3449
  *
3449
3450
  * The section ends at the next `##` heading or end-of-file. Sub-headings
3450
3451
  * (`###`, `####`) remain part of the section body.
3452
+ *
3453
+ * Story 58-4: the scan tracks code-fence depth so a `## Runtime Probes`
3454
+ * heading that appears *inside* an outer ``` block is ignored. Stories that
3455
+ * DOCUMENT probes in prose — regression fixtures, how-to-author docs, the
3456
+ * Epic 58 e2e test spec — contain illustrative `## Runtime Probes` examples
3457
+ * inside outer fences. Without fence-awareness the parser matches those
3458
+ * illustrations as the story's own section, fails to find a terminated
3459
+ * yaml block (the inner fences are typically escaped), and emits a spurious
3460
+ * `runtime-probe-parse-error`. Hit live during the Epic 58 substrate
3461
+ * dispatch on 58-3's artifact.
3451
3462
  */
3452
3463
  function extractRuntimeProbesSection(storyContent) {
3453
3464
  const lines = storyContent.split(/\r?\n/);
3454
- const start = lines.findIndex((line) => SECTION_HEADING.test(line.trim()));
3465
+ let inCodeFence = false;
3466
+ let start = -1;
3467
+ for (let i = 0; i < lines.length; i += 1) {
3468
+ const line = lines[i] ?? "";
3469
+ if (FENCE_DELIMITER.test(line)) {
3470
+ inCodeFence = !inCodeFence;
3471
+ continue;
3472
+ }
3473
+ if (!inCodeFence && SECTION_HEADING.test(line.trim())) {
3474
+ start = i;
3475
+ break;
3476
+ }
3477
+ }
3455
3478
  if (start === -1) return void 0;
3456
3479
  let end = lines.length;
3457
- for (let i = start + 1; i < lines.length; i += 1) if (/^##\s+\S/.test(lines[i] ?? "")) {
3458
- end = i;
3459
- break;
3480
+ inCodeFence = false;
3481
+ for (let i = start + 1; i < lines.length; i += 1) {
3482
+ const line = lines[i] ?? "";
3483
+ if (FENCE_DELIMITER.test(line)) {
3484
+ inCodeFence = !inCodeFence;
3485
+ continue;
3486
+ }
3487
+ if (!inCodeFence && /^##\s+\S/.test(line)) {
3488
+ end = i;
3489
+ break;
3490
+ }
3460
3491
  }
3461
3492
  return lines.slice(start + 1, end).join("\n");
3462
3493
  }
@@ -3732,6 +3763,113 @@ var RuntimeProbeCheck = class {
3732
3763
  }
3733
3764
  };
3734
3765
 
3766
+ //#endregion
3767
+ //#region packages/sdlc/dist/verification/source-ac-fidelity-check.js
3768
+ /**
3769
+ * Extract the story's section from the full epic content.
3770
+ *
3771
+ * Uses the same heading pattern as `isImplicitlyCovered` in the monolith:
3772
+ * `### Story <storyKey>:` or `### Story <storyKey> ` or `### Story <storyKey>\n`
3773
+ *
3774
+ * Returns the extracted section text (from the heading match through to the
3775
+ * next `### Story` heading or end of file), or the full content if no
3776
+ * matching heading is found.
3777
+ */
3778
+ function extractStorySection(epicContent, storyKey) {
3779
+ const escapedKey = storyKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3780
+ const headingPattern = new RegExp(`^###\\s+Story\\s+${escapedKey}[:\\s]`, "m");
3781
+ const match = headingPattern.exec(epicContent);
3782
+ if (!match) return epicContent;
3783
+ const start = match.index;
3784
+ const nextHeading = /\n### Story /m.exec(epicContent.slice(start + 1));
3785
+ if (nextHeading) return epicContent.slice(start, start + 1 + nextHeading.index);
3786
+ return epicContent.slice(start);
3787
+ }
3788
+ /**
3789
+ * Extract hard clauses from a story section of an epic file.
3790
+ *
3791
+ * Hard clauses:
3792
+ * 1. Lines containing MUST NOT / MUST / SHALL NOT / SHALL as standalone keywords (case-sensitive)
3793
+ * 2. Backtick-wrapped paths with at least one `/` (excludes bare filenames)
3794
+ * 3. The presence of `## Runtime Probes` heading followed by a fenced yaml block
3795
+ * (represented as a single "runtime-probes-section" clause)
3796
+ */
3797
+ function extractHardClauses(sectionContent) {
3798
+ const clauses = [];
3799
+ const mustPattern = /\b(MUST NOT|MUST|SHALL NOT|SHALL)\b/;
3800
+ const lines = sectionContent.split("\n");
3801
+ for (const line of lines) {
3802
+ const match = mustPattern.exec(line);
3803
+ if (match) {
3804
+ const keyword = match[1];
3805
+ clauses.push({
3806
+ type: keyword,
3807
+ text: line.trim()
3808
+ });
3809
+ }
3810
+ }
3811
+ const pathPattern = /`([a-zA-Z0-9_./-]+\/[a-zA-Z0-9_./-]+)`/g;
3812
+ let pathMatch;
3813
+ while ((pathMatch = pathPattern.exec(sectionContent)) !== null) clauses.push({
3814
+ type: "path",
3815
+ text: `\`${pathMatch[1]}\``
3816
+ });
3817
+ const probesPattern = /^##\s+Runtime Probes[\s\S]*?```yaml/m;
3818
+ if (probesPattern.test(sectionContent)) clauses.push({
3819
+ type: "runtime-probes-section",
3820
+ text: "## Runtime Probes"
3821
+ });
3822
+ return clauses;
3823
+ }
3824
+ var SourceAcFidelityCheck = class {
3825
+ name = "source-ac-fidelity";
3826
+ tier = "A";
3827
+ async run(context) {
3828
+ const start = Date.now();
3829
+ if (!context.sourceEpicContent) {
3830
+ const findings$1 = [{
3831
+ category: "source-ac-source-unavailable",
3832
+ severity: "warn",
3833
+ message: "source epic content unavailable — skipping fidelity check"
3834
+ }];
3835
+ return {
3836
+ status: "pass",
3837
+ details: renderFindings(findings$1),
3838
+ duration_ms: Date.now() - start,
3839
+ findings: findings$1
3840
+ };
3841
+ }
3842
+ const storySection = extractStorySection(context.sourceEpicContent, context.storyKey);
3843
+ const hardClauses = extractHardClauses(storySection);
3844
+ const findings = [];
3845
+ const storyContent = context.storyContent ?? "";
3846
+ for (const clause of hardClauses) if (clause.type === "runtime-probes-section") {
3847
+ if (!storyContent.includes("## Runtime Probes")) {
3848
+ const truncated = clause.text.length > 120 ? clause.text.slice(0, 120) : clause.text;
3849
+ findings.push({
3850
+ category: "source-ac-drift",
3851
+ severity: "error",
3852
+ message: `runtime-probes-section: "${truncated}" present in epics source but absent in story artifact`
3853
+ });
3854
+ }
3855
+ } else if (!storyContent.includes(clause.text)) {
3856
+ const truncated = clause.text.length > 120 ? clause.text.slice(0, 120) : clause.text;
3857
+ findings.push({
3858
+ category: "source-ac-drift",
3859
+ severity: "error",
3860
+ message: `${clause.type}: "${truncated}" present in epics source but absent in story artifact`
3861
+ });
3862
+ }
3863
+ const status = findings.some((f) => f.severity === "error") ? "fail" : "pass";
3864
+ return {
3865
+ status,
3866
+ details: findings.length > 0 ? renderFindings(findings) : `source-ac-fidelity: ${hardClauses.length} hard clause(s) verified — all present`,
3867
+ duration_ms: Date.now() - start,
3868
+ findings
3869
+ };
3870
+ }
3871
+ };
3872
+
3735
3873
  //#endregion
3736
3874
  //#region packages/sdlc/dist/verification/verification-pipeline.js
3737
3875
  /**
@@ -3839,12 +3977,14 @@ var VerificationPipeline = class {
3839
3977
  * Create a VerificationPipeline pre-loaded with the canonical check set.
3840
3978
  *
3841
3979
  * Canonical Tier A check order:
3842
- * 1. PhantomReviewCheck — story 51-2 (runs first: unreviewed stories skipped)
3843
- * 2. TrivialOutputCheck — story 51-3
3980
+ * 1. PhantomReviewCheck — story 51-2 (runs first: unreviewed stories skipped)
3981
+ * 2. TrivialOutputCheck — story 51-3
3844
3982
  * 3. AcceptanceCriteriaEvidenceCheck
3845
- * 4. BuildCheck — story 51-4
3846
- * 5. RuntimeProbeCheck — Epic 55 Phase 2: runtime behavior gate; runs last
3847
- * in Tier A because probes may depend on built artifacts
3983
+ * 4. BuildCheck — story 51-4
3984
+ * 5. RuntimeProbeCheck — Epic 55 Phase 2: runtime behavior gate; runs last
3985
+ * in Tier A because probes may depend on built artifacts
3986
+ * 6. SourceAcFidelityCheck — Story 58-2: cross-references rendered story artifact
3987
+ * against the source epic's hard clauses (MUST/SHALL/paths)
3848
3988
  *
3849
3989
  * @param bus Typed event bus for verification events.
3850
3990
  * @param config Optional config (used to forward threshold to TrivialOutputCheck).
@@ -3855,7 +3995,8 @@ function createDefaultVerificationPipeline(bus, config) {
3855
3995
  new TrivialOutputCheck(config),
3856
3996
  new AcceptanceCriteriaEvidenceCheck(),
3857
3997
  new BuildCheck(),
3858
- new RuntimeProbeCheck()
3998
+ new RuntimeProbeCheck(),
3999
+ new SourceAcFidelityCheck()
3859
4000
  ];
3860
4001
  return new VerificationPipeline(bus, checks);
3861
4002
  }
@@ -4069,6 +4210,12 @@ const RunManifestSchema = z.object({
4069
4210
  run_id: z.string(),
4070
4211
  cli_flags: CliFlagsSchema.transform((v) => v),
4071
4212
  story_scope: z.array(z.string()),
4213
+ run_status: z.enum([
4214
+ "running",
4215
+ "completed",
4216
+ "failed",
4217
+ "stopped"
4218
+ ]).optional(),
4072
4219
  supervisor_pid: z.number().nullable(),
4073
4220
  supervisor_session_id: z.string().nullable(),
4074
4221
  per_story_state: z.record(z.string(), PerStoryStateSchema),
@@ -4078,6 +4225,8 @@ const RunManifestSchema = z.object({
4078
4225
  run_total: 0
4079
4226
  }),
4080
4227
  pending_proposals: z.array(ProposalSchema),
4228
+ stopped_reason: z.string().optional(),
4229
+ stopped_at: z.string().optional(),
4081
4230
  generation: z.number().int().nonnegative(),
4082
4231
  created_at: z.string(),
4083
4232
  updated_at: z.string()
@@ -4419,6 +4568,55 @@ var RunManifest = class RunManifest {
4419
4568
  /**
4420
4569
  * Raw implementation — must only be called from within `_enqueue`.
4421
4570
  */
4571
+ async _patchRunStatusImpl(updates) {
4572
+ let existingData;
4573
+ try {
4574
+ const read = await RunManifest.read(this.runId, this.baseDir, this.doltAdapter);
4575
+ const { generation: _gen, updated_at: _ts,...rest } = read;
4576
+ existingData = rest;
4577
+ } catch {
4578
+ const now = new Date().toISOString();
4579
+ existingData = {
4580
+ run_id: this.runId,
4581
+ cli_flags: {},
4582
+ story_scope: [],
4583
+ supervisor_pid: null,
4584
+ supervisor_session_id: null,
4585
+ per_story_state: {},
4586
+ recovery_history: [],
4587
+ cost_accumulation: {
4588
+ per_story: {},
4589
+ run_total: 0
4590
+ },
4591
+ pending_proposals: [],
4592
+ created_at: now
4593
+ };
4594
+ }
4595
+ const merged = { ...existingData };
4596
+ if (updates.run_status !== void 0) merged.run_status = updates.run_status;
4597
+ if (updates.stopped_reason !== void 0) merged.stopped_reason = updates.stopped_reason;
4598
+ if (updates.stopped_at !== void 0) merged.stopped_at = updates.stopped_at;
4599
+ await this._writeImpl(merged);
4600
+ }
4601
+ /**
4602
+ * Atomically update the run-level status fields in the manifest.
4603
+ *
4604
+ * Reads the current manifest (or creates a minimal default if absent),
4605
+ * merges the provided status updates at the top level, and writes the
4606
+ * result atomically via a single `write()` call.
4607
+ *
4608
+ * Enqueues the operation via `_enqueue` so concurrent calls are serialized
4609
+ * (preserves the single-writer guarantee from Epic 57-1).
4610
+ * Non-fatal: callers MUST wrap in `.catch((err) => logger.warn(...))`.
4611
+ *
4612
+ * @param updates - Top-level status fields to merge (run_status, stopped_reason, stopped_at)
4613
+ */
4614
+ async patchRunStatus(updates) {
4615
+ return this._enqueue(() => this._patchRunStatusImpl(updates));
4616
+ }
4617
+ /**
4618
+ * Raw implementation — must only be called from within `_enqueue`.
4619
+ */
4422
4620
  async _appendRecoveryEntryImpl(entry) {
4423
4621
  let existingData;
4424
4622
  try {
@@ -5336,4 +5534,4 @@ function registerHealthCommand(program, _version = "0.0.0", projectRoot = proces
5336
5534
 
5337
5535
  //#endregion
5338
5536
  export { BMAD_BASELINE_TOKENS_FULL, DEFAULT_STALL_THRESHOLD_SECONDS, DoltMergeConflict, FileStateStore, FindingsInjector, RunManifest, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN$1 as STORY_KEY_PATTERN, SUBSTRATE_OWNED_SETTINGS_KEYS, SupervisorLock, VALID_PHASES, WorkGraphRepository, ZERO_FINDING_COUNTS, __commonJS, __require, __toESM, applyConfigToGraph, buildPipelineStatusOutput, createDatabaseAdapter$1 as createDatabaseAdapter, createDefaultVerificationPipeline, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, createStateStore, detectCycles, extractTargetFilesFromStoryContent, findPackageRoot, formatOutput, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, inspectProcessTree, isOrchestratorProcessLine, parseDbTimestampAsUtc, registerHealthCommand, renderFindings, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveGraphPath, resolveMainRepoRoot, resolveRunManifest, rollupFindingCounts, runHealthAction, validateStoryKey };
5339
- //# sourceMappingURL=health-C6Up5GCr.js.map
5537
+ //# sourceMappingURL=health-CxBbduMn.js.map
@@ -1,4 +1,4 @@
1
- import { DEFAULT_STALL_THRESHOLD_SECONDS, getAllDescendantPids, getAutoHealthData, inspectProcessTree, isOrchestratorProcessLine, registerHealthCommand, runHealthAction } from "./health-C6Up5GCr.js";
1
+ import { DEFAULT_STALL_THRESHOLD_SECONDS, getAllDescendantPids, getAutoHealthData, inspectProcessTree, isOrchestratorProcessLine, registerHealthCommand, runHealthAction } from "./health-CxBbduMn.js";
2
2
  import "./logger-KeHncl-f.js";
3
3
  import "./dist-CqtWS9wF.js";
4
4
  import "./decisions-C0pz9Clx.js";
package/dist/index.d.ts CHANGED
@@ -2286,6 +2286,20 @@ interface OrchestratorEvents {
2286
2286
  /** Retry attempt number (always 2 — first retry after initial timeout) */
2287
2287
  attempt: number;
2288
2288
  };
2289
+ /**
2290
+ * Emitted when an existing story artifact's stored source-AC hash differs
2291
+ * from the current source epic's AC hash, or when the artifact carries no
2292
+ * hash at all (legacy artifact). Causes the orchestrator to re-run
2293
+ * create-story instead of reusing the stale artifact.
2294
+ */
2295
+ 'story:ac-source-drift': {
2296
+ /** Story key whose artifact is stale */
2297
+ storyKey: string;
2298
+ /** Hash stored in the artifact's HTML comment (null if absent — legacy artifact) */
2299
+ storedHash: string | null;
2300
+ /** Hash computed from the current source epic's AC section */
2301
+ currentHash: string;
2302
+ };
2289
2303
  }
2290
2304
 
2291
2305
  //#endregion
@@ -1,8 +1,8 @@
1
- import "./health-C6Up5GCr.js";
1
+ import "./health-CxBbduMn.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-BcwaSYTg.js";
5
+ import { normalizeGraphSummaryToStatus, registerRunCommand, resolveMaxReviewCycles, runRunAction, wireNdjsonEmitter } from "./run-D5VAkItq.js";
6
6
  import "./routing-CcBOCuC9.js";
7
7
  import "./decisions-C0pz9Clx.js";
8
8
 
@@ -1,4 +1,4 @@
1
- import { BMAD_BASELINE_TOKENS_FULL, DoltMergeConflict, FileStateStore, FindingsInjector, RunManifest, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN, VALID_PHASES, WorkGraphRepository, __commonJS, __require, __toESM, applyConfigToGraph, buildPipelineStatusOutput, createDatabaseAdapter, createDefaultVerificationPipeline, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, detectCycles, extractTargetFilesFromStoryContent, formatOutput, formatPipelineSummary, formatTokenTelemetry, inspectProcessTree, parseDbTimestampAsUtc, renderFindings, resolveGraphPath, resolveMainRepoRoot, validateStoryKey } from "./health-C6Up5GCr.js";
1
+ import { BMAD_BASELINE_TOKENS_FULL, DoltMergeConflict, FileStateStore, FindingsInjector, RunManifest, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN, VALID_PHASES, WorkGraphRepository, __commonJS, __require, __toESM, applyConfigToGraph, buildPipelineStatusOutput, createDatabaseAdapter, createDefaultVerificationPipeline, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, detectCycles, extractTargetFilesFromStoryContent, formatOutput, formatPipelineSummary, formatTokenTelemetry, inspectProcessTree, parseDbTimestampAsUtc, renderFindings, resolveGraphPath, resolveMainRepoRoot, validateStoryKey } from "./health-CxBbduMn.js";
2
2
  import { createLogger } from "./logger-KeHncl-f.js";
3
3
  import { TypedEventBusImpl, createEventBus, createTuiApp, isTuiCapable, printNonTtyWarning, sleep } from "./helpers-CElYrONe.js";
4
4
  import { ADVISORY_NOTES, Categorizer, ConsumerAnalyzer, DEFAULT_GLOBAL_SETTINGS, DispatcherImpl, DoltClient, ESCALATION_DIAGNOSIS, EXPERIMENT_RESULT, EfficiencyScorer, IngestionServer, LogTurnAnalyzer, OPERATIONAL_FINDING, Recommender, RoutingRecommender, RoutingResolver, RoutingTelemetry, RoutingTokenAccumulator, RoutingTuner, STORY_METRICS, STORY_OUTCOME, SubstrateConfigSchema, TEST_EXPANSION_FINDING, TEST_PLAN, TelemetryNormalizer, TelemetryPipeline, TurnAnalyzer, addTokenUsage, aggregateTokenUsageForRun, aggregateTokenUsageForStory, callLLM, createConfigSystem, createDatabaseAdapter$1, createDecision, createPipelineRun, createRequirement, detectInterfaceChanges, getArtifactByTypeForRun, getArtifactsByRun, getDecisionsByCategory, getDecisionsByPhase, getDecisionsByPhaseForRun, getLatestRun, getPipelineRunById, getRunMetrics, getRunningPipelineRuns, getStoryMetricsForRun, getTokenUsageSummary, initSchema, listRequirements, loadModelRoutingConfig, registerArtifact, updatePipelineRun, updatePipelineRunConfig, upsertDecision, writeRunMetrics, writeStoryMetrics } from "./dist-CqtWS9wF.js";
@@ -5676,6 +5676,21 @@ function getTokenCeiling(workflowType, tokenCeilings) {
5676
5676
  //#region src/modules/compiled-workflows/create-story.ts
5677
5677
  const logger$18 = createLogger("compiled-workflows:create-story");
5678
5678
  /**
5679
+ * Compute a hex SHA-256 of the normalized source AC section text.
5680
+ *
5681
+ * Normalization (minimal — avoids spurious regen from editor whitespace noise):
5682
+ * 1. Split on `\n`
5683
+ * 2. Strip trailing whitespace from each line (`.trimEnd()`)
5684
+ * 3. Rejoin with `\n`
5685
+ * 4. Trim the whole result (`.trim()`)
5686
+ *
5687
+ * Pure function: no I/O, no side effects. Safe to call from tests with zero setup.
5688
+ */
5689
+ function hashSourceAcSection(section) {
5690
+ const normalized = section.split("\n").map((line) => line.trimEnd()).join("\n").trim();
5691
+ return createHash("sha256").update(normalized, "utf8").digest("hex");
5692
+ }
5693
+ /**
5679
5694
  * Execute the compiled create-story workflow.
5680
5695
  *
5681
5696
  * Steps:
@@ -5693,7 +5708,7 @@ const logger$18 = createLogger("compiled-workflows:create-story");
5693
5708
  * @returns Promise resolving to CreateStoryResult
5694
5709
  */
5695
5710
  async function runCreateStory(deps, params) {
5696
- const { epicId, storyKey, pipelineRunId } = params;
5711
+ const { epicId, storyKey, pipelineRunId, source_ac_hash } = params;
5697
5712
  logger$18.debug({
5698
5713
  epicId,
5699
5714
  storyKey,
@@ -5764,7 +5779,12 @@ async function runCreateStory(deps, params) {
5764
5779
  name: "story_template",
5765
5780
  content: storyTemplateContent,
5766
5781
  priority: "important"
5767
- }
5782
+ },
5783
+ ...source_ac_hash !== void 0 ? [{
5784
+ name: "source_ac_hash",
5785
+ content: source_ac_hash,
5786
+ priority: "required"
5787
+ }] : []
5768
5788
  ], TOKEN_CEILING);
5769
5789
  logger$18.debug({
5770
5790
  tokenCount,
@@ -5911,13 +5931,14 @@ async function getImplementationDecisions(deps, pipelineRunId) {
5911
5931
  */
5912
5932
  function extractStorySection(shardContent, storyKey) {
5913
5933
  if (!shardContent || !storyKey) return null;
5914
- const escaped = storyKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5915
- const headingPattern = new RegExp(`(?:^#{2,4}\\s+Story\\s+${escaped}\\b|^Story\\s+${escaped}:|^\\*\\*${escaped}\\*\\*|^${escaped}:)`, "mi");
5934
+ const parts = storyKey.split(/[-._ ]/);
5935
+ const normalized = parts.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[-._ ]");
5936
+ const headingPattern = new RegExp(`(?:^#{2,4}\\s+Story\\s+${normalized}\\b|^Story\\s+${normalized}:|^\\*\\*${normalized}\\*\\*|^${normalized}:)`, "mi");
5916
5937
  const match$2 = headingPattern.exec(shardContent);
5917
5938
  if (!match$2) return null;
5918
5939
  const startIdx = match$2.index;
5919
5940
  const rest = shardContent.slice(startIdx + match$2[0].length);
5920
- const nextStoryPattern = new RegExp(`(?:^#{2,4}\\s+Story\\s+[\\d]|^Story\\s+[\\d][\\d-]*:|^\\*\\*[\\d][\\d-]*\\*\\*|^[\\d][\\d-]*:)`, "mi");
5941
+ const nextStoryPattern = new RegExp(`(?:^#{2,4}\\s+Story\\s+[\\d]|^Story\\s+[\\d][\\d.\\-_a-z]*:|^\\*\\*[\\d][\\d.\\-_a-z]*\\*\\*|^[\\d][\\d.\\-_a-z]*:)`, "mi");
5921
5942
  const nextMatch = nextStoryPattern.exec(rest);
5922
5943
  const endIdx = nextMatch !== null ? startIdx + match$2[0].length + nextMatch.index : shardContent.length;
5923
5944
  const section = shardContent.slice(startIdx, endIdx).trim();
@@ -10429,7 +10450,8 @@ function assembleVerificationContext(opts) {
10429
10450
  reviewResult: opts.reviewResult,
10430
10451
  storyContent: opts.storyContent,
10431
10452
  devStoryResult: opts.devStoryResult,
10432
- outputTokenCount: opts.outputTokenCount
10453
+ outputTokenCount: opts.outputTokenCount,
10454
+ sourceEpicContent: opts.sourceEpicContent
10433
10455
  };
10434
10456
  }
10435
10457
  /**
@@ -11365,6 +11387,10 @@ function createImplementationOrchestrator(deps) {
11365
11387
  let _costWarningEmitted = false;
11366
11388
  let _budgetExhausted = false;
11367
11389
  let _otlpEndpoint;
11390
+ let _shutdownRequested = false;
11391
+ let _inFlightCount = 0;
11392
+ let _drainResolve = null;
11393
+ let _drainPromise = Promise.resolve();
11368
11394
  const verificationStore = new VerificationStore();
11369
11395
  const verificationPipeline = createDefaultVerificationPipeline(toSdlcEventBus(eventBus));
11370
11396
  const _stateStoreCache = new Map();
@@ -12002,6 +12028,7 @@ function createImplementationOrchestrator(deps) {
12002
12028
  startedAt: new Date().toISOString()
12003
12029
  });
12004
12030
  let storyFilePath;
12031
+ let sourceAcHash;
12005
12032
  const artifactsDir = projectRoot ? join$1(projectRoot, "_bmad-output", "implementation-artifacts") : void 0;
12006
12033
  if (artifactsDir && existsSync(artifactsDir)) try {
12007
12034
  const files = readdirSync(artifactsDir);
@@ -12015,22 +12042,52 @@ function createImplementationOrchestrator(deps) {
12015
12042
  reason: validation.reason
12016
12043
  }, `Existing story file for ${storyKey} is invalid (${validation.reason}) — re-creating`);
12017
12044
  else {
12018
- storyFilePath = candidatePath;
12019
- logger$24.info({
12020
- storyKey,
12021
- storyFilePath
12022
- }, "Found existing story file — skipping create-story");
12023
- endPhase(storyKey, "create-story");
12024
- eventBus.emit("orchestrator:story-phase-complete", {
12025
- storyKey,
12026
- phase: "IN_STORY_CREATION",
12027
- result: {
12028
- result: "success",
12029
- story_file: storyFilePath,
12030
- story_key: storyKey
12045
+ let isDrift = false;
12046
+ try {
12047
+ const epicsPath = projectRoot ? findEpicsFile(projectRoot) : void 0;
12048
+ if (epicsPath !== void 0) {
12049
+ const epicContent = readFileSync(epicsPath, "utf-8");
12050
+ const sourceSection = extractStorySection(epicContent, storyKey);
12051
+ if (sourceSection != null) {
12052
+ const currentHash = hashSourceAcSection(sourceSection);
12053
+ sourceAcHash = currentHash;
12054
+ const artifactContent = await readFile$1(candidatePath, "utf-8");
12055
+ const hashMatch = /<!--\s*source-ac-hash:\s*([0-9a-f]{64})\s*-->/.exec(artifactContent);
12056
+ const storedHash = hashMatch?.[1] ?? null;
12057
+ if (storedHash !== currentHash) {
12058
+ isDrift = true;
12059
+ eventBus.emit("story:ac-source-drift", {
12060
+ storyKey,
12061
+ storedHash,
12062
+ currentHash
12063
+ });
12064
+ logger$24.info({
12065
+ storyKey,
12066
+ storedHash,
12067
+ currentHash
12068
+ }, `[orchestrator] story ${storyKey}: source AC hash mismatch, regenerating story artifact`);
12069
+ }
12070
+ }
12031
12071
  }
12032
- });
12033
- await persistState();
12072
+ } catch {}
12073
+ if (!isDrift) {
12074
+ storyFilePath = candidatePath;
12075
+ logger$24.info({
12076
+ storyKey,
12077
+ storyFilePath
12078
+ }, "Found existing story file — skipping create-story");
12079
+ endPhase(storyKey, "create-story");
12080
+ eventBus.emit("orchestrator:story-phase-complete", {
12081
+ storyKey,
12082
+ phase: "IN_STORY_CREATION",
12083
+ result: {
12084
+ result: "success",
12085
+ story_file: storyFilePath,
12086
+ story_key: storyKey
12087
+ }
12088
+ });
12089
+ await persistState();
12090
+ }
12034
12091
  }
12035
12092
  }
12036
12093
  } catch {}
@@ -12067,7 +12124,8 @@ function createImplementationOrchestrator(deps) {
12067
12124
  }, {
12068
12125
  epicId: storyKey.split("-")[0] ?? storyKey,
12069
12126
  storyKey,
12070
- pipelineRunId: config.pipelineRunId
12127
+ pipelineRunId: config.pipelineRunId,
12128
+ source_ac_hash: sourceAcHash
12071
12129
  });
12072
12130
  endPhase(storyKey, "create-story");
12073
12131
  eventBus.emit("orchestrator:story-phase-complete", {
@@ -13258,13 +13316,21 @@ function createImplementationOrchestrator(deps) {
13258
13316
  error: reviewResult.error,
13259
13317
  rawOutput: reviewResult.rawOutput
13260
13318
  } : void 0;
13319
+ let sourceEpicContent;
13320
+ const epicsPath1 = findEpicsFile(projectRoot ?? process.cwd());
13321
+ if (epicsPath1) try {
13322
+ const epicFull = readFileSync(epicsPath1, "utf-8");
13323
+ const section = extractStorySection(epicFull, storyKey);
13324
+ if (section) sourceEpicContent = section;
13325
+ } catch {}
13261
13326
  const verifContext = assembleVerificationContext({
13262
13327
  storyKey,
13263
13328
  workingDir: projectRoot ?? process.cwd(),
13264
13329
  reviewResult: latestReviewSignals,
13265
13330
  storyContent: storyContentForVerification,
13266
13331
  devStoryResult: devStorySignals,
13267
- outputTokenCount: devOutputTokenCount
13332
+ outputTokenCount: devOutputTokenCount,
13333
+ sourceEpicContent
13268
13334
  });
13269
13335
  const verifSummary = await verificationPipeline.run(verifContext, "A");
13270
13336
  verificationStore.set(storyKey, verifSummary);
@@ -13522,13 +13588,21 @@ function createImplementationOrchestrator(deps) {
13522
13588
  error: reviewResult.error,
13523
13589
  rawOutput: reviewResult.rawOutput
13524
13590
  } : void 0;
13591
+ let sourceEpicContent2;
13592
+ const epicsPath2 = findEpicsFile(projectRoot ?? process.cwd());
13593
+ if (epicsPath2) try {
13594
+ const epicFull2 = readFileSync(epicsPath2, "utf-8");
13595
+ const section2 = extractStorySection(epicFull2, storyKey);
13596
+ if (section2) sourceEpicContent2 = section2;
13597
+ } catch {}
13525
13598
  const verifContext = assembleVerificationContext({
13526
13599
  storyKey,
13527
13600
  workingDir: projectRoot ?? process.cwd(),
13528
13601
  reviewResult: latestReviewSignals,
13529
13602
  storyContent: storyContentForVerification,
13530
13603
  devStoryResult: devStorySignals,
13531
- outputTokenCount: devOutputTokenCount
13604
+ outputTokenCount: devOutputTokenCount,
13605
+ sourceEpicContent: sourceEpicContent2
13532
13606
  });
13533
13607
  const verifSummary = await verificationPipeline.run(verifContext, "A");
13534
13608
  verificationStore.set(storyKey, verifSummary);
@@ -13880,6 +13954,10 @@ function createImplementationOrchestrator(deps) {
13880
13954
  async function processConflictGroup(group) {
13881
13955
  const completedStoryKeys = [];
13882
13956
  for (const storyKey of group) {
13957
+ if (_shutdownRequested) {
13958
+ logger$24.info({ storyKey }, "shutdown requested — skipping dispatch");
13959
+ return;
13960
+ }
13883
13961
  if (runManifest !== null && runManifest !== void 0) try {
13884
13962
  const manifestData = await runManifest.read();
13885
13963
  const ceiling = manifestData.cli_flags.cost_ceiling;
@@ -13938,11 +14016,15 @@ function createImplementationOrchestrator(deps) {
13938
14016
  const running = new Set();
13939
14017
  function enqueue() {
13940
14018
  if (_budgetExhausted) return;
14019
+ if (_shutdownRequested) return;
13941
14020
  const group = queue.shift();
13942
14021
  if (group === void 0) return;
14022
+ _inFlightCount++;
13943
14023
  const p = processConflictGroup(group).finally(() => {
13944
14024
  running.delete(p);
13945
- while (running.size < maxConcurrency && queue.length > 0) enqueue();
14025
+ _inFlightCount--;
14026
+ if (_inFlightCount === 0 && _shutdownRequested) _drainResolve?.();
14027
+ while (running.size < maxConcurrency && queue.length > 0 && !_shutdownRequested && !_budgetExhausted) enqueue();
13946
14028
  });
13947
14029
  running.add(p);
13948
14030
  if (running.size > _maxConcurrentActual) _maxConcurrentActual = running.size;
@@ -13951,6 +14033,47 @@ function createImplementationOrchestrator(deps) {
13951
14033
  for (let i = 0; i < initial; i++) enqueue();
13952
14034
  while (running.size > 0) await Promise.race(running);
13953
14035
  }
14036
+ /**
14037
+ * Gracefully shut down the pipeline in response to a POSIX signal.
14038
+ *
14039
+ * 1. Sets the in-memory `_shutdownRequested` flag so the dispatch loop stops
14040
+ * scheduling new stories.
14041
+ * 2. Awaits any in-flight dispatches for up to `config.shutdownGracePeriodMs` ms
14042
+ * (default 5000).
14043
+ * 3. Writes `run_status: 'stopped'` and `stopped_reason` to the run manifest.
14044
+ * 4. Best-effort: updates `pipeline_runs.status = 'stopped'` in Dolt and
14045
+ * transitions active wg_stories to 'cancelled'.
14046
+ * 5. Calls `process.exit(130)` for SIGINT or `process.exit(143)` for SIGTERM.
14047
+ */
14048
+ async function shutdownGracefully(reason, signal) {
14049
+ if (_shutdownRequested) return;
14050
+ _shutdownRequested = true;
14051
+ logger$24.info({
14052
+ reason,
14053
+ signal
14054
+ }, "Graceful shutdown initiated");
14055
+ const gracePeriod = config.shutdownGracePeriodMs ?? 5e3;
14056
+ if (_inFlightCount === 0) _drainResolve?.();
14057
+ await Promise.race([_drainPromise, new Promise((resolve$6) => setTimeout(resolve$6, gracePeriod))]);
14058
+ if (runManifest !== null && runManifest !== void 0) await runManifest.patchRunStatus({
14059
+ run_status: "stopped",
14060
+ stopped_reason: reason,
14061
+ stopped_at: new Date().toISOString()
14062
+ }).catch((err) => logger$24.warn({ err }, "patchRunStatus failed during shutdown (best-effort)"));
14063
+ if (config.pipelineRunId !== void 0) try {
14064
+ await updatePipelineRun(db, config.pipelineRunId, { status: "stopped" });
14065
+ } catch (err) {
14066
+ logger$24.warn({ err }, "updatePipelineRun(stopped) failed during shutdown (best-effort)");
14067
+ }
14068
+ for (const [storyKey, storyState] of _stories) {
14069
+ const isActive = storyState.phase === "PENDING" || storyState.phase === "IN_STORY_CREATION" || storyState.phase === "IN_TEST_PLANNING" || storyState.phase === "IN_DEV" || storyState.phase === "IN_REVIEW" || storyState.phase === "NEEDS_FIXES" || storyState.phase === "CHECKPOINT";
14070
+ if (isActive) wgRepo.updateStoryStatus(storyKey, "cancelled").catch((err) => logger$24.warn({
14071
+ err,
14072
+ storyKey
14073
+ }, "updateStoryStatus(cancelled) failed during shutdown (best-effort)"));
14074
+ }
14075
+ process.exit(signal === "SIGINT" ? 130 : 143);
14076
+ }
13954
14077
  async function run(storyKeys) {
13955
14078
  if (_state === "RUNNING" || _state === "PAUSED") {
13956
14079
  logger$24.warn({ state: _state }, "run() called while orchestrator is already running or paused — ignoring");
@@ -13962,6 +14085,20 @@ function createImplementationOrchestrator(deps) {
13962
14085
  }
13963
14086
  _state = "RUNNING";
13964
14087
  _startedAt = new Date().toISOString();
14088
+ _shutdownRequested = false;
14089
+ _inFlightCount = 0;
14090
+ _drainResolve = null;
14091
+ _drainPromise = new Promise((resolve$6) => {
14092
+ _drainResolve = resolve$6;
14093
+ });
14094
+ const sigtermHandler = () => {
14095
+ shutdownGracefully("killed_by_user", "SIGTERM");
14096
+ };
14097
+ const sigintHandler = () => {
14098
+ shutdownGracefully("killed_by_user", "SIGINT");
14099
+ };
14100
+ process.on("SIGTERM", sigtermHandler);
14101
+ process.on("SIGINT", sigintHandler);
13965
14102
  for (const key of storyKeys) {
13966
14103
  const pendingState = {
13967
14104
  phase: "PENDING",
@@ -14246,6 +14383,8 @@ function createImplementationOrchestrator(deps) {
14246
14383
  await persistState();
14247
14384
  return getStatus();
14248
14385
  } finally {
14386
+ process.off("SIGTERM", sigtermHandler);
14387
+ process.off("SIGINT", sigintHandler);
14249
14388
  if (stateStore !== void 0) await stateStore.close().catch((err) => logger$24.warn({ err }, "StateStore.close() failed (best-effort)"));
14250
14389
  if (ingestionServer !== void 0) await ingestionServer.stop().catch((err) => logger$24.warn({ err }, "IngestionServer.stop() failed (best-effort)"));
14251
14390
  }
@@ -43840,4 +43979,4 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
43840
43979
 
43841
43980
  //#endregion
43842
43981
  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 };
43843
- //# sourceMappingURL=run-BcwaSYTg.js.map
43982
+ //# sourceMappingURL=run-D5VAkItq.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "substrate-ai",
3
- "version": "0.20.10",
3
+ "version": "0.20.13",
4
4
  "description": "Substrate — multi-agent orchestration daemon for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",