substrate-ai 0.20.12 → 0.20.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { FileStateStore, RunManifest, SUBSTRATE_OWNED_SETTINGS_KEYS, SupervisorLock, VALID_PHASES, WorkGraphRepository, ZERO_FINDING_COUNTS, buildPipelineStatusOutput, createDatabaseAdapter, createStateStore, findPackageRoot, formatOutput, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, inspectProcessTree, parseDbTimestampAsUtc, registerHealthCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveRunManifest, rollupFindingCounts } from "../health-0jqPFBEL.js";
2
+ import { FileStateStore, RunManifest, SUBSTRATE_OWNED_SETTINGS_KEYS, SupervisorLock, VALID_PHASES, WorkGraphRepository, ZERO_FINDING_COUNTS, buildPipelineStatusOutput, createDatabaseAdapter, createStateStore, findPackageRoot, formatOutput, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, inspectProcessTree, parseDbTimestampAsUtc, registerHealthCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveRunManifest, rollupFindingCounts } from "../health-Cx8rztu5.js";
3
3
  import { createLogger } from "../logger-KeHncl-f.js";
4
4
  import { createEventBus } from "../helpers-CElYrONe.js";
5
5
  import { AdapterRegistry, BudgetConfigSchema, CURRENT_CONFIG_FORMAT_VERSION, CURRENT_TASK_GRAPH_VERSION, ConfigError, CostTrackerConfigSchema, DEFAULT_CONFIG, DoltClient, DoltNotInstalled, 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-B6qbsy-F.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-i-5COg2l.js";
8
8
  import "../errors-1uLGqnvr.js";
9
9
  import "../routing-CcBOCuC9.js";
10
10
  import "../decisions-C0pz9Clx.js";
@@ -3667,7 +3667,7 @@ async function runStatusAction(options) {
3667
3667
  logger$12.debug({ err }, "Work graph query failed, continuing without work graph data");
3668
3668
  }
3669
3669
  if (run === void 0) {
3670
- const { inspectProcessTree: inspectProcessTree$1 } = await import("../health-C401hYzi.js");
3670
+ const { inspectProcessTree: inspectProcessTree$1 } = await import("../health-C1vOBDbS.js");
3671
3671
  const substrateDirPath = join(projectRoot, ".substrate");
3672
3672
  const processInfo = inspectProcessTree$1({
3673
3673
  projectRoot,
@@ -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-DEyd1p7U.js"
5201
+ "../run-Dj5UQs5e.js"
5202
5202
  );
5203
5203
  const runStoryFn = async (opts) => {
5204
5204
  const exitCode = await runPipeline({
@@ -1,4 +1,4 @@
1
- import { DEFAULT_STALL_THRESHOLD_SECONDS, getAllDescendantPids, getAutoHealthData, inspectProcessTree, isOrchestratorProcessLine, registerHealthCommand, runHealthAction } from "./health-0jqPFBEL.js";
1
+ import { DEFAULT_STALL_THRESHOLD_SECONDS, getAllDescendantPids, getAutoHealthData, inspectProcessTree, isOrchestratorProcessLine, registerHealthCommand, runHealthAction } from "./health-Cx8rztu5.js";
2
2
  import "./logger-KeHncl-f.js";
3
3
  import "./dist-CqtWS9wF.js";
4
4
  import "./decisions-C0pz9Clx.js";
@@ -3549,9 +3549,10 @@ function parseRuntimeProbes(storyContent) {
3549
3549
  error: `YAML parse error: ${detail}`
3550
3550
  };
3551
3551
  }
3552
+ if (!Array.isArray(parsed) && typeof parsed === "object" && parsed !== null && Array.isArray(parsed.probes)) parsed = parsed.probes;
3552
3553
  if (!Array.isArray(parsed)) return {
3553
3554
  kind: "invalid",
3554
- error: `probe block root must be a YAML list; got ${typeof parsed}`
3555
+ error: `probe block root must be a YAML list or a \`probes:\` mapping; got ${typeof parsed}`
3555
3556
  };
3556
3557
  const validation = RuntimeProbeListSchema.safeParse(parsed);
3557
3558
  if (!validation.success) {
@@ -3848,7 +3849,7 @@ var SourceAcFidelityCheck = class {
3848
3849
  const truncated = clause.text.length > 120 ? clause.text.slice(0, 120) : clause.text;
3849
3850
  findings.push({
3850
3851
  category: "source-ac-drift",
3851
- severity: "error",
3852
+ severity: "warn",
3852
3853
  message: `runtime-probes-section: "${truncated}" present in epics source but absent in story artifact`
3853
3854
  });
3854
3855
  }
@@ -3856,7 +3857,7 @@ var SourceAcFidelityCheck = class {
3856
3857
  const truncated = clause.text.length > 120 ? clause.text.slice(0, 120) : clause.text;
3857
3858
  findings.push({
3858
3859
  category: "source-ac-drift",
3859
- severity: "error",
3860
+ severity: "warn",
3860
3861
  message: `${clause.type}: "${truncated}" present in epics source but absent in story artifact`
3861
3862
  });
3862
3863
  }
@@ -4210,6 +4211,12 @@ const RunManifestSchema = z.object({
4210
4211
  run_id: z.string(),
4211
4212
  cli_flags: CliFlagsSchema.transform((v) => v),
4212
4213
  story_scope: z.array(z.string()),
4214
+ run_status: z.enum([
4215
+ "running",
4216
+ "completed",
4217
+ "failed",
4218
+ "stopped"
4219
+ ]).optional(),
4213
4220
  supervisor_pid: z.number().nullable(),
4214
4221
  supervisor_session_id: z.string().nullable(),
4215
4222
  per_story_state: z.record(z.string(), PerStoryStateSchema),
@@ -4219,6 +4226,8 @@ const RunManifestSchema = z.object({
4219
4226
  run_total: 0
4220
4227
  }),
4221
4228
  pending_proposals: z.array(ProposalSchema),
4229
+ stopped_reason: z.string().optional(),
4230
+ stopped_at: z.string().optional(),
4222
4231
  generation: z.number().int().nonnegative(),
4223
4232
  created_at: z.string(),
4224
4233
  updated_at: z.string()
@@ -4560,6 +4569,55 @@ var RunManifest = class RunManifest {
4560
4569
  /**
4561
4570
  * Raw implementation — must only be called from within `_enqueue`.
4562
4571
  */
4572
+ async _patchRunStatusImpl(updates) {
4573
+ let existingData;
4574
+ try {
4575
+ const read = await RunManifest.read(this.runId, this.baseDir, this.doltAdapter);
4576
+ const { generation: _gen, updated_at: _ts,...rest } = read;
4577
+ existingData = rest;
4578
+ } catch {
4579
+ const now = new Date().toISOString();
4580
+ existingData = {
4581
+ run_id: this.runId,
4582
+ cli_flags: {},
4583
+ story_scope: [],
4584
+ supervisor_pid: null,
4585
+ supervisor_session_id: null,
4586
+ per_story_state: {},
4587
+ recovery_history: [],
4588
+ cost_accumulation: {
4589
+ per_story: {},
4590
+ run_total: 0
4591
+ },
4592
+ pending_proposals: [],
4593
+ created_at: now
4594
+ };
4595
+ }
4596
+ const merged = { ...existingData };
4597
+ if (updates.run_status !== void 0) merged.run_status = updates.run_status;
4598
+ if (updates.stopped_reason !== void 0) merged.stopped_reason = updates.stopped_reason;
4599
+ if (updates.stopped_at !== void 0) merged.stopped_at = updates.stopped_at;
4600
+ await this._writeImpl(merged);
4601
+ }
4602
+ /**
4603
+ * Atomically update the run-level status fields in the manifest.
4604
+ *
4605
+ * Reads the current manifest (or creates a minimal default if absent),
4606
+ * merges the provided status updates at the top level, and writes the
4607
+ * result atomically via a single `write()` call.
4608
+ *
4609
+ * Enqueues the operation via `_enqueue` so concurrent calls are serialized
4610
+ * (preserves the single-writer guarantee from Epic 57-1).
4611
+ * Non-fatal: callers MUST wrap in `.catch((err) => logger.warn(...))`.
4612
+ *
4613
+ * @param updates - Top-level status fields to merge (run_status, stopped_reason, stopped_at)
4614
+ */
4615
+ async patchRunStatus(updates) {
4616
+ return this._enqueue(() => this._patchRunStatusImpl(updates));
4617
+ }
4618
+ /**
4619
+ * Raw implementation — must only be called from within `_enqueue`.
4620
+ */
4563
4621
  async _appendRecoveryEntryImpl(entry) {
4564
4622
  let existingData;
4565
4623
  try {
@@ -5477,4 +5535,4 @@ function registerHealthCommand(program, _version = "0.0.0", projectRoot = proces
5477
5535
 
5478
5536
  //#endregion
5479
5537
  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 };
5480
- //# sourceMappingURL=health-0jqPFBEL.js.map
5538
+ //# sourceMappingURL=health-Cx8rztu5.js.map
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-0jqPFBEL.js";
1
+ import "./health-Cx8rztu5.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-B6qbsy-F.js";
5
+ import { normalizeGraphSummaryToStatus, registerRunCommand, resolveMaxReviewCycles, runRunAction, wireNdjsonEmitter } from "./run-i-5COg2l.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-0jqPFBEL.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-Cx8rztu5.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,
@@ -11366,6 +11386,12 @@ function createImplementationOrchestrator(deps) {
11366
11386
  const _costChecker = new CostGovernanceChecker();
11367
11387
  let _costWarningEmitted = false;
11368
11388
  let _budgetExhausted = false;
11389
+ let _shutdownRequested = false;
11390
+ let _inFlightCount = 0;
11391
+ let _drainResolve = null;
11392
+ const _drainPromise = new Promise((resolve$6) => {
11393
+ _drainResolve = resolve$6;
11394
+ });
11369
11395
  let _otlpEndpoint;
11370
11396
  const verificationStore = new VerificationStore();
11371
11397
  const verificationPipeline = createDefaultVerificationPipeline(toSdlcEventBus(eventBus));
@@ -12004,6 +12030,7 @@ function createImplementationOrchestrator(deps) {
12004
12030
  startedAt: new Date().toISOString()
12005
12031
  });
12006
12032
  let storyFilePath;
12033
+ let sourceAcHash;
12007
12034
  const artifactsDir = projectRoot ? join$1(projectRoot, "_bmad-output", "implementation-artifacts") : void 0;
12008
12035
  if (artifactsDir && existsSync(artifactsDir)) try {
12009
12036
  const files = readdirSync(artifactsDir);
@@ -12017,22 +12044,52 @@ function createImplementationOrchestrator(deps) {
12017
12044
  reason: validation.reason
12018
12045
  }, `Existing story file for ${storyKey} is invalid (${validation.reason}) — re-creating`);
12019
12046
  else {
12020
- storyFilePath = candidatePath;
12021
- logger$24.info({
12022
- storyKey,
12023
- storyFilePath
12024
- }, "Found existing story file — skipping create-story");
12025
- endPhase(storyKey, "create-story");
12026
- eventBus.emit("orchestrator:story-phase-complete", {
12027
- storyKey,
12028
- phase: "IN_STORY_CREATION",
12029
- result: {
12030
- result: "success",
12031
- story_file: storyFilePath,
12032
- story_key: storyKey
12047
+ let isDrift = false;
12048
+ try {
12049
+ const epicsPath = projectRoot ? findEpicsFile(projectRoot) : void 0;
12050
+ if (epicsPath !== void 0) {
12051
+ const epicContent = readFileSync(epicsPath, "utf-8");
12052
+ const sourceSection = extractStorySection(epicContent, storyKey);
12053
+ if (sourceSection != null) {
12054
+ const currentHash = hashSourceAcSection(sourceSection);
12055
+ sourceAcHash = currentHash;
12056
+ const artifactContent = await readFile$1(candidatePath, "utf-8");
12057
+ const hashMatch = /<!--\s*source-ac-hash:\s*([0-9a-f]{64})\s*-->/.exec(artifactContent);
12058
+ const storedHash = hashMatch?.[1] ?? null;
12059
+ if (storedHash !== currentHash) {
12060
+ isDrift = true;
12061
+ eventBus.emit("story:ac-source-drift", {
12062
+ storyKey,
12063
+ storedHash,
12064
+ currentHash
12065
+ });
12066
+ logger$24.info({
12067
+ storyKey,
12068
+ storedHash,
12069
+ currentHash
12070
+ }, `[orchestrator] story ${storyKey}: source AC hash mismatch, regenerating story artifact`);
12071
+ }
12072
+ }
12033
12073
  }
12034
- });
12035
- await persistState();
12074
+ } catch {}
12075
+ if (!isDrift) {
12076
+ storyFilePath = candidatePath;
12077
+ logger$24.info({
12078
+ storyKey,
12079
+ storyFilePath
12080
+ }, "Found existing story file — skipping create-story");
12081
+ endPhase(storyKey, "create-story");
12082
+ eventBus.emit("orchestrator:story-phase-complete", {
12083
+ storyKey,
12084
+ phase: "IN_STORY_CREATION",
12085
+ result: {
12086
+ result: "success",
12087
+ story_file: storyFilePath,
12088
+ story_key: storyKey
12089
+ }
12090
+ });
12091
+ await persistState();
12092
+ }
12036
12093
  }
12037
12094
  }
12038
12095
  } catch {}
@@ -12069,7 +12126,8 @@ function createImplementationOrchestrator(deps) {
12069
12126
  }, {
12070
12127
  epicId: storyKey.split("-")[0] ?? storyKey,
12071
12128
  storyKey,
12072
- pipelineRunId: config.pipelineRunId
12129
+ pipelineRunId: config.pipelineRunId,
12130
+ source_ac_hash: sourceAcHash
12073
12131
  });
12074
12132
  endPhase(storyKey, "create-story");
12075
12133
  eventBus.emit("orchestrator:story-phase-complete", {
@@ -13898,6 +13956,10 @@ function createImplementationOrchestrator(deps) {
13898
13956
  async function processConflictGroup(group) {
13899
13957
  const completedStoryKeys = [];
13900
13958
  for (const storyKey of group) {
13959
+ if (_shutdownRequested) {
13960
+ logger$24.info({ storyKey }, "shutdown requested — skipping dispatch");
13961
+ return;
13962
+ }
13901
13963
  if (runManifest !== null && runManifest !== void 0) try {
13902
13964
  const manifestData = await runManifest.read();
13903
13965
  const ceiling = manifestData.cli_flags.cost_ceiling;
@@ -13937,7 +13999,13 @@ function createImplementationOrchestrator(deps) {
13937
13999
  storyKey
13938
14000
  }, "Failed to fetch optimization directives — proceeding without");
13939
14001
  }
13940
- await processStory(storyKey, { optimizationDirectives });
14002
+ _inFlightCount++;
14003
+ try {
14004
+ await processStory(storyKey, { optimizationDirectives });
14005
+ } finally {
14006
+ _inFlightCount--;
14007
+ if (_inFlightCount === 0 && _shutdownRequested) _drainResolve?.();
14008
+ }
13941
14009
  completedStoryKeys.push(storyKey);
13942
14010
  globalThis.gc?.();
13943
14011
  const gcPauseMs = config.gcPauseMs ?? 2e3;
@@ -13956,11 +14024,12 @@ function createImplementationOrchestrator(deps) {
13956
14024
  const running = new Set();
13957
14025
  function enqueue() {
13958
14026
  if (_budgetExhausted) return;
14027
+ if (_shutdownRequested) return;
13959
14028
  const group = queue.shift();
13960
14029
  if (group === void 0) return;
13961
14030
  const p = processConflictGroup(group).finally(() => {
13962
14031
  running.delete(p);
13963
- while (running.size < maxConcurrency && queue.length > 0) enqueue();
14032
+ while (running.size < maxConcurrency && queue.length > 0 && !_budgetExhausted && !_shutdownRequested) enqueue();
13964
14033
  });
13965
14034
  running.add(p);
13966
14035
  if (running.size > _maxConcurrentActual) _maxConcurrentActual = running.size;
@@ -13969,6 +14038,46 @@ function createImplementationOrchestrator(deps) {
13969
14038
  for (let i = 0; i < initial; i++) enqueue();
13970
14039
  while (running.size > 0) await Promise.race(running);
13971
14040
  }
14041
+ /**
14042
+ * Graceful shutdown handler for SIGTERM and SIGINT (Story 58-7, AC2).
14043
+ *
14044
+ * Idempotent: subsequent calls during the same run are no-ops.
14045
+ * Sets `_shutdownRequested` to stop new dispatches, waits for in-flight
14046
+ * dispatches to drain (up to `shutdownGracePeriodMs`), persists stopped state
14047
+ * to the run manifest and Dolt (best-effort), then calls process.exit.
14048
+ */
14049
+ async function shutdownGracefully(reason, signal) {
14050
+ if (_shutdownRequested) return;
14051
+ _shutdownRequested = true;
14052
+ logger$24.info({
14053
+ reason,
14054
+ signal
14055
+ }, "Graceful shutdown initiated — stopping new dispatches");
14056
+ const gracePeriod = config.shutdownGracePeriodMs ?? 5e3;
14057
+ if (_inFlightCount > 0) await Promise.race([_drainPromise, new Promise((r) => setTimeout(r, gracePeriod))]);
14058
+ if (runManifest !== null) 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) await updatePipelineRun(db, config.pipelineRunId, { status: "stopped" }).catch((err) => logger$24.warn({ err }, "updatePipelineRun(stopped) failed during shutdown (best-effort)"));
14064
+ const activePhases = [
14065
+ "PENDING",
14066
+ "IN_STORY_CREATION",
14067
+ "IN_TEST_PLANNING",
14068
+ "IN_DEV",
14069
+ "IN_REVIEW",
14070
+ "NEEDS_FIXES",
14071
+ "CHECKPOINT"
14072
+ ];
14073
+ const cancellations = [];
14074
+ for (const [storyKey, state] of _stories.entries()) if (activePhases.includes(state.phase)) cancellations.push(wgRepo.updateStoryStatus(storyKey, "cancelled").catch((err) => logger$24.warn({
14075
+ err,
14076
+ storyKey
14077
+ }, "wg_stories → cancelled failed during shutdown (best-effort)")));
14078
+ if (cancellations.length > 0) await Promise.allSettled(cancellations);
14079
+ process.exit(signal === "SIGINT" ? 130 : 143);
14080
+ }
13972
14081
  async function run(storyKeys) {
13973
14082
  if (_state === "RUNNING" || _state === "PAUSED") {
13974
14083
  logger$24.warn({ state: _state }, "run() called while orchestrator is already running or paused — ignoring");
@@ -14017,6 +14126,14 @@ function createImplementationOrchestrator(deps) {
14017
14126
  logger$24.debug({ err }, "Auto-ingest from epics document skipped — work graph may be unavailable");
14018
14127
  }
14019
14128
  }
14129
+ const sigtermHandler = () => {
14130
+ shutdownGracefully("killed_by_user", "SIGTERM");
14131
+ };
14132
+ const sigintHandler = () => {
14133
+ shutdownGracefully("killed_by_user", "SIGINT");
14134
+ };
14135
+ process.on("SIGTERM", sigtermHandler);
14136
+ process.on("SIGINT", sigintHandler);
14020
14137
  try {
14021
14138
  if (stateStore !== void 0) {
14022
14139
  const stateStoreInitStart = Date.now();
@@ -14264,6 +14381,8 @@ function createImplementationOrchestrator(deps) {
14264
14381
  await persistState();
14265
14382
  return getStatus();
14266
14383
  } finally {
14384
+ process.off("SIGTERM", sigtermHandler);
14385
+ process.off("SIGINT", sigintHandler);
14267
14386
  if (stateStore !== void 0) await stateStore.close().catch((err) => logger$24.warn({ err }, "StateStore.close() failed (best-effort)"));
14268
14387
  if (ingestionServer !== void 0) await ingestionServer.stop().catch((err) => logger$24.warn({ err }, "IngestionServer.stop() failed (best-effort)"));
14269
14388
  }
@@ -43858,4 +43977,4 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
43858
43977
 
43859
43978
  //#endregion
43860
43979
  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 };
43861
- //# sourceMappingURL=run-B6qbsy-F.js.map
43980
+ //# sourceMappingURL=run-i-5COg2l.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "substrate-ai",
3
- "version": "0.20.12",
3
+ "version": "0.20.14",
4
4
  "description": "Substrate — multi-agent orchestration daemon for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,12 +32,24 @@ a different story from the epic scope. The story key, title, and core scope are
32
32
  ## Instructions
33
33
 
34
34
  1. **Use the Story Definition as your primary input** — it specifies exactly what this story builds. The epic scope provides surrounding context only.
35
- 2. **Acceptance-criteria text from the Story Definition is read-only input.** Any AC clause from the source that contains `MUST`, `MUST NOT`, `SHALL`, `SHALL NOT`, enumerated file or directory paths, or explicit technology / storage / data-format choices (e.g., "plain JSON file", "LanceDB table", "systemd unit") must appear in the rendered story artifact's Acceptance Criteria section **verbatim**. Never soften, abstract, or paraphrase a hard clause reshaping "MUST remove X" into "consider deprecating X" silently strips the requirement and ships code that violates its own spec. When in doubt, copy the source clause literally and add any clarifying BDD phrasing alongside it, not in place of it.
35
+ 2. **Source AC is read-only input; default rendering is a verbatim copy.** Under the rendered `## Acceptance Criteria` heading, begin with an exact copy of the source AC text (all sub-sections, heading hierarchy, file lists, storage choices, probes). Any restructuring or BDD rephrasing goes in a separate `### Create-story reformulation (optional)` subsection BELOW the verbatim copy never in place of it.
36
+
37
+ Categories that MUST appear verbatim (substring-identical). Never soften, abstract, or paraphrase:
38
+
39
+ - **Directive keywords**: `MUST`, `MUST NOT`, `SHALL`, `SHALL NOT`, `SHOULD NOT` lines with surrounding context.
40
+ - **Named filenames**: backtick-wrapped paths appear as-is. Do not rename (`adjacency-store.ts` → `wikilink-queries.ts`), drop, pluralize, or recase.
41
+ - **Named directories**: same rule. `packages/memory/src/graph/` stays, not `packages/memory/src/wikilink/`.
42
+ - **Explicit technology / storage / data-format choices**: `plain JSON file`, `LanceDB table`, `SQLite database`, `systemd unit`, `Docker Compose`, `Podman Quadlet` etc. appear exactly. A source "JSON adjacency list" MUST NOT become "LanceDB table" — flipping the storage backend is a correctness change, not a clarification.
43
+ - **Named probe identifiers**: probe filenames (`real-jarvis-status-probe.mjs`) and declared probe `name:` fields are part of the contract. Renaming breaks operator dashboards and downstream references.
44
+ - **Full `## Runtime Probes` sections**: transfer entire section verbatim (heading, prose, YAML fence) per Runtime Verification Guidance below.
45
+
46
+ Reshaping "MUST remove X" → "consider deprecating X" silently strips the requirement. Renaming `adjacency-store.ts` silently drops a MUST-exist file. Flipping `plain JSON file` → `LanceDB table` silently swaps the storage architecture. Each of these has shipped code that violated its own spec in recorded substrate sessions — the verbatim-first rule exists because the agent's judgment about "what the author really meant" has been systematically wrong on these dimensions.
36
47
  3. **Apply architecture constraints** — every constraint listed above is mandatory (file paths, import style, test framework, etc.)
37
48
  4. **Use previous dev notes** as guardrails — don't repeat mistakes, build on patterns that worked
38
49
  5. **Fill out the story template** with:
39
50
  - A clear user story (As a / I want / So that)
40
51
  - Acceptance criteria: preserve the hard-clause text from the Story Definition verbatim. BDD Given/When/Then phrasing is **optional** — permitted for behavior-oriented criteria where it adds clarity, not mandatory. Never let BDD reshape a MUST / MUST NOT / SHALL clause; copy those clauses literally and, if BDD adds clarity, append the Given/When/Then alongside the original clause rather than replacing it. Aim for the same number of ACs as the source — do not condense clauses into fewer items to hit a target count.
52
+ - Immediately after the `## Acceptance Criteria` heading in the rendered story file, emit the line `<!-- source-ac-hash: {{source_ac_hash}} -->` on its own line. When the hash value is empty or blank (the `{{source_ac_hash}}` placeholder resolved to nothing), omit the comment entirely — do not write `<!-- source-ac-hash: -->` or any comment with an empty or missing hash value.
41
53
  - Concrete tasks broken into 2–4 hour subtasks, each tied to specific ACs
42
54
  - Dev Notes with file paths, import patterns, testing requirements
43
55
  6. **Apply the scope cap** — see Scope Cap Guidance below