substrate-ai 0.2.30 → 0.2.31

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,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DatabaseWrapper, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, buildPipelineStatusOutput, createConfigSystem, createContextCompiler, createDispatcher, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, runAnalysisPhase, runMigrations, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-Ajt187oE.js";
2
+ import { DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DatabaseWrapper, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, buildPipelineStatusOutput, createConfigSystem, createContextCompiler, createDispatcher, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, runAnalysisPhase, runMigrations, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-BxXwD2xY.js";
3
3
  import { createLogger } from "../logger-D2fS2ccL.js";
4
4
  import { AdapterRegistry } from "../adapter-registry-PsWhP_1Q.js";
5
5
  import { CURRENT_CONFIG_FORMAT_VERSION, CURRENT_TASK_GRAPH_VERSION, PartialSubstrateConfigSchema } from "../config-migrator-DSi8KhQC.js";
6
6
  import { ConfigError, createEventBus } from "../helpers-RL22dYtn.js";
7
7
  import { addTokenUsage, createDecision, createPipelineRun, getDecisionsByCategory, getDecisionsByPhaseForRun, getLatestRun, getTokenUsageSummary, listRequirements, updatePipelineRun } from "../decisions-Dq4cAA2L.js";
8
- import { ESCALATION_DIAGNOSIS, EXPERIMENT_RESULT, OPERATIONAL_FINDING, STORY_METRICS, aggregateTokenUsageForRun, compareRunMetrics, getBaselineRunMetrics, getRunMetrics, getStoryMetricsForRun, incrementRunRestarts, listRunMetrics, tagRunAsBaseline } from "../operational-CnMlvWqc.js";
8
+ import { ESCALATION_DIAGNOSIS, EXPERIMENT_RESULT, OPERATIONAL_FINDING, STORY_METRICS, aggregateTokenUsageForRun, compareRunMetrics, getBaselineRunMetrics, getRunMetrics, getStoryMetricsForRun, incrementRunRestarts, listRunMetrics, tagRunAsBaseline } from "../operational-Bovj4fS-.js";
9
9
  import { abortMerge, createWorktree, getConflictingFiles, getMergedFiles, getOrphanedWorktrees, performMerge, removeBranch, removeWorktree, simulateMerge, verifyGitVersion } from "../git-utils-CtmrZrHS.js";
10
10
  import "../version-manager-impl-CizNmmLT.js";
11
11
  import { registerUpgradeCommand } from "../upgrade-Cvwtnwl4.js";
@@ -2571,7 +2571,7 @@ async function runSupervisorAction(options, deps = {}) {
2571
2571
  try {
2572
2572
  const { createExperimenter } = await import(
2573
2573
  /* @vite-ignore */
2574
- "../experimenter-f_Y1rreV.js"
2574
+ "../experimenter-Br1-vzYv.js"
2575
2575
  );
2576
2576
  const { getLatestRun: getLatest } = await import(
2577
2577
  /* @vite-ignore */
@@ -2585,7 +2585,7 @@ async function runSupervisorAction(options, deps = {}) {
2585
2585
  const expDb = expDbWrapper.db;
2586
2586
  const { runRunAction: runPipeline } = await import(
2587
2587
  /* @vite-ignore */
2588
- "../run-KBcR3Jpi.js"
2588
+ "../run-CMagiY2Y.js"
2589
2589
  );
2590
2590
  const runStoryFn = async (opts) => {
2591
2591
  const exitCode = await runPipeline({
@@ -1,6 +1,6 @@
1
1
  import "./logger-D2fS2ccL.js";
2
2
  import { createDecision } from "./decisions-Dq4cAA2L.js";
3
- import { EXPERIMENT_RESULT, getRunMetrics, getStoryMetricsForRun } from "./operational-CnMlvWqc.js";
3
+ import { EXPERIMENT_RESULT, getRunMetrics, getStoryMetricsForRun } from "./operational-Bovj4fS-.js";
4
4
  import { spawnGit } from "./git-utils-CtmrZrHS.js";
5
5
  import { spawn } from "node:child_process";
6
6
  import { join } from "node:path";
@@ -500,4 +500,4 @@ function createExperimenter(config, deps) {
500
500
 
501
501
  //#endregion
502
502
  export { createExperimenter };
503
- //# sourceMappingURL=experimenter-f_Y1rreV.js.map
503
+ //# sourceMappingURL=experimenter-Br1-vzYv.js.map
package/dist/index.d.ts CHANGED
@@ -185,6 +185,19 @@ interface StoryZeroDiffEscalationEvent {
185
185
  /** Always "zero-diff-on-complete" */
186
186
  reason: string;
187
187
  }
188
+ /**
189
+ * Emitted when the pre-flight build check fails before any story is dispatched (Story 25-2).
190
+ * Pipeline aborts immediately — no stories are processed.
191
+ */
192
+ interface PipelinePreFlightFailureEvent {
193
+ type: 'pipeline:pre-flight-failure';
194
+ /** ISO-8601 timestamp generated at emit time */
195
+ ts: string;
196
+ /** Exit code from the build command (-1 for timeout) */
197
+ exitCode: number;
198
+ /** Combined stdout+stderr output, truncated to 2000 chars */
199
+ output: string;
200
+ }
188
201
  /**
189
202
  * Emitted when the build verification command exits with a non-zero code
190
203
  * or times out, before code-review is dispatched (Story 24-2).
@@ -501,6 +514,26 @@ interface SupervisorExperimentErrorEvent {
501
514
  /** Error message describing why the experiment failed */
502
515
  error: string;
503
516
  }
517
+ /**
518
+ * Emitted when post-sprint contract verification finds a mismatch between
519
+ * declared export/import contracts (Story 25-6).
520
+ *
521
+ * Failures are warnings only — stories already completed. The user should
522
+ * inspect the mismatch and fix manually.
523
+ */
524
+ interface PipelineContractMismatchEvent {
525
+ type: 'pipeline:contract-mismatch';
526
+ /** ISO-8601 timestamp generated at emit time */
527
+ ts: string;
528
+ /** Story key that declared the export for this contract */
529
+ exporter: string;
530
+ /** Story key that declared the import for this contract (null if no importer found) */
531
+ importer: string | null;
532
+ /** TypeScript interface or Zod schema name (e.g., "JudgeResult") */
533
+ contractName: string;
534
+ /** Human-readable description of the mismatch (e.g., missing file, type error) */
535
+ mismatchDescription: string;
536
+ }
504
537
  /**
505
538
  * Discriminated union of all pipeline event types.
506
539
  *
@@ -513,7 +546,7 @@ interface SupervisorExperimentErrorEvent {
513
546
  * }
514
547
  * ```
515
548
  */
516
- type PipelineEvent = PipelineStartEvent | PipelineCompleteEvent | StoryPhaseEvent | StoryDoneEvent | StoryEscalationEvent | StoryWarnEvent | StoryLogEvent | PipelineHeartbeatEvent | StoryStallEvent | StoryZeroDiffEscalationEvent | StoryBuildVerificationFailedEvent | StoryBuildVerificationPassedEvent | StoryInterfaceChangeWarningEvent | StoryMetricsEvent | SupervisorPollEvent | SupervisorKillEvent | SupervisorRestartEvent | SupervisorAbortEvent | SupervisorSummaryEvent | SupervisorAnalysisCompleteEvent | SupervisorAnalysisErrorEvent | SupervisorExperimentStartEvent | SupervisorExperimentSkipEvent | SupervisorExperimentRecommendationsEvent | SupervisorExperimentCompleteEvent | SupervisorExperimentErrorEvent; //#endregion
549
+ type PipelineEvent = PipelineStartEvent | PipelineCompleteEvent | PipelinePreFlightFailureEvent | PipelineContractMismatchEvent | StoryPhaseEvent | StoryDoneEvent | StoryEscalationEvent | StoryWarnEvent | StoryLogEvent | PipelineHeartbeatEvent | StoryStallEvent | StoryZeroDiffEscalationEvent | StoryBuildVerificationFailedEvent | StoryBuildVerificationPassedEvent | StoryInterfaceChangeWarningEvent | StoryMetricsEvent | SupervisorPollEvent | SupervisorKillEvent | SupervisorRestartEvent | SupervisorAbortEvent | SupervisorSummaryEvent | SupervisorAnalysisCompleteEvent | SupervisorAnalysisErrorEvent | SupervisorExperimentStartEvent | SupervisorExperimentSkipEvent | SupervisorExperimentRecommendationsEvent | SupervisorExperimentCompleteEvent | SupervisorExperimentErrorEvent; //#endregion
517
550
  //#region src/core/errors.d.ts
518
551
 
519
552
  /**
@@ -1160,6 +1193,23 @@ interface OrchestratorEvents {
1160
1193
  affected_items: string[];
1161
1194
  }>;
1162
1195
  };
1196
+ /** Pre-flight build check failed before any stories were dispatched */
1197
+ 'pipeline:pre-flight-failure': {
1198
+ exitCode: number;
1199
+ /** Build output (stdout+stderr), truncated to 2000 chars */
1200
+ output: string;
1201
+ };
1202
+ /** Contract verification found a mismatch between declared export/import contracts */
1203
+ 'pipeline:contract-mismatch': {
1204
+ /** Story key that declared the export for this contract */
1205
+ exporter: string;
1206
+ /** Story key that declared the import for this contract (null if no importer found) */
1207
+ importer: string | null;
1208
+ /** TypeScript interface or Zod schema name (e.g., "JudgeResult") */
1209
+ contractName: string;
1210
+ /** Human-readable description of the mismatch */
1211
+ mismatchDescription: string;
1212
+ };
1163
1213
  /** Build verification command failed with non-zero exit or timeout */
1164
1214
  'story:build-verification-failed': {
1165
1215
  storyKey: string;
@@ -339,7 +339,21 @@ const TEST_EXPANSION_FINDING = "test-expansion-finding";
339
339
  * ```
340
340
  */
341
341
  const TEST_PLAN = "test-plan";
342
+ /**
343
+ * Category for advisory notes persisted when a code review returns LGTM_WITH_NOTES.
344
+ *
345
+ * Key schema: "{storyKey}:{runId}"
346
+ *
347
+ * Value shape:
348
+ * ```json
349
+ * {
350
+ * "storyKey": "25-3",
351
+ * "notes": "Consider extracting the helper into a shared module for reuse."
352
+ * }
353
+ * ```
354
+ */
355
+ const ADVISORY_NOTES = "advisory-notes";
342
356
 
343
357
  //#endregion
344
- export { ESCALATION_DIAGNOSIS, EXPERIMENT_RESULT, OPERATIONAL_FINDING, STORY_METRICS, STORY_OUTCOME, TEST_EXPANSION_FINDING, TEST_PLAN, aggregateTokenUsageForRun, aggregateTokenUsageForStory, compareRunMetrics, getBaselineRunMetrics, getRunMetrics, getStoryMetricsForRun, incrementRunRestarts, listRunMetrics, tagRunAsBaseline, writeRunMetrics, writeStoryMetrics };
345
- //# sourceMappingURL=operational-CnMlvWqc.js.map
358
+ export { ADVISORY_NOTES, ESCALATION_DIAGNOSIS, EXPERIMENT_RESULT, OPERATIONAL_FINDING, STORY_METRICS, STORY_OUTCOME, TEST_EXPANSION_FINDING, TEST_PLAN, aggregateTokenUsageForRun, aggregateTokenUsageForStory, compareRunMetrics, getBaselineRunMetrics, getRunMetrics, getStoryMetricsForRun, incrementRunRestarts, listRunMetrics, tagRunAsBaseline, writeRunMetrics, writeStoryMetrics };
359
+ //# sourceMappingURL=operational-Bovj4fS-.js.map
@@ -2,7 +2,7 @@ import { createLogger, deepMask } from "./logger-D2fS2ccL.js";
2
2
  import { CURRENT_CONFIG_FORMAT_VERSION, PartialSubstrateConfigSchema, SUPPORTED_CONFIG_FORMAT_VERSIONS, SubstrateConfigSchema, defaultConfigMigrator } from "./config-migrator-DSi8KhQC.js";
3
3
  import { ConfigError, ConfigIncompatibleFormatError, createEventBus, createTuiApp, isTuiCapable, printNonTtyWarning, sleep } from "./helpers-RL22dYtn.js";
4
4
  import { addTokenUsage, createDecision, createPipelineRun, createRequirement, getArtifactByTypeForRun, getArtifactsByRun, getDecisionsByCategory, getDecisionsByPhase, getDecisionsByPhaseForRun, getLatestRun, getPipelineRunById, getRunningPipelineRuns, getTokenUsageSummary, registerArtifact, updatePipelineRun, updatePipelineRunConfig, upsertDecision } from "./decisions-Dq4cAA2L.js";
5
- import { ESCALATION_DIAGNOSIS, OPERATIONAL_FINDING, STORY_METRICS, STORY_OUTCOME, TEST_EXPANSION_FINDING, TEST_PLAN, aggregateTokenUsageForRun, aggregateTokenUsageForStory, getStoryMetricsForRun, writeRunMetrics, writeStoryMetrics } from "./operational-CnMlvWqc.js";
5
+ import { ADVISORY_NOTES, ESCALATION_DIAGNOSIS, OPERATIONAL_FINDING, STORY_METRICS, STORY_OUTCOME, TEST_EXPANSION_FINDING, TEST_PLAN, aggregateTokenUsageForRun, aggregateTokenUsageForStory, getStoryMetricsForRun, writeRunMetrics, writeStoryMetrics } from "./operational-Bovj4fS-.js";
6
6
  import { createRequire } from "module";
7
7
  import { dirname, join, resolve } from "path";
8
8
  import { access, mkdir, readFile, readdir, stat, writeFile } from "fs/promises";
@@ -2521,6 +2521,28 @@ const PIPELINE_EVENT_METADATA = [
2521
2521
  }
2522
2522
  ]
2523
2523
  },
2524
+ {
2525
+ type: "pipeline:pre-flight-failure",
2526
+ description: "Pre-flight build check failed before any story was dispatched. Pipeline aborts immediately.",
2527
+ when: "When the build command exits with a non-zero code before the first story dispatch.",
2528
+ fields: [
2529
+ {
2530
+ name: "ts",
2531
+ type: "string",
2532
+ description: "Timestamp."
2533
+ },
2534
+ {
2535
+ name: "exitCode",
2536
+ type: "number",
2537
+ description: "Exit code from the build command (-1 for timeout)."
2538
+ },
2539
+ {
2540
+ name: "output",
2541
+ type: "string",
2542
+ description: "Combined stdout+stderr from the build command (truncated to 2000 chars)."
2543
+ }
2544
+ ]
2545
+ },
2524
2546
  {
2525
2547
  type: "story:zero-diff-escalation",
2526
2548
  description: "Dev-story reported COMPLETE but git diff shows no file changes (phantom completion).",
@@ -2652,6 +2674,38 @@ const PIPELINE_EVENT_METADATA = [
2652
2674
  description: "Dispatch count."
2653
2675
  }
2654
2676
  ]
2677
+ },
2678
+ {
2679
+ type: "pipeline:contract-mismatch",
2680
+ description: "Post-sprint contract mismatch found. Non-blocking — stories done. Manual fix required.",
2681
+ when: "After all stories complete, before pipeline:complete. When contract declarations exist and mismatch found.",
2682
+ fields: [
2683
+ {
2684
+ name: "ts",
2685
+ type: "string",
2686
+ description: "Timestamp."
2687
+ },
2688
+ {
2689
+ name: "exporter",
2690
+ type: "string",
2691
+ description: "Exporting story key."
2692
+ },
2693
+ {
2694
+ name: "importer",
2695
+ type: "string|null",
2696
+ description: "Importing story key (null if none)."
2697
+ },
2698
+ {
2699
+ name: "contractName",
2700
+ type: "string",
2701
+ description: "Contract name (e.g., \"JudgeResult\")."
2702
+ },
2703
+ {
2704
+ name: "mismatchDescription",
2705
+ type: "string",
2706
+ description: "Mismatch details (missing file, type error)."
2707
+ }
2708
+ ]
2655
2709
  }
2656
2710
  ];
2657
2711
  /**
@@ -4347,6 +4401,9 @@ const AcChecklistEntrySchema = z.object({
4347
4401
  * - Any blocker → NEEDS_MAJOR_REWORK (security, data loss, architectural breakage)
4348
4402
  * - Any major or minor issues → NEEDS_MINOR_FIXES (fixable by sonnet with guidance)
4349
4403
  * - No issues → SHIP_IT
4404
+ *
4405
+ * Note: LGTM_WITH_NOTES is handled in the transform (not here) because it
4406
+ * requires knowledge of the agent's original verdict.
4350
4407
  */
4351
4408
  function computeVerdict(issueList) {
4352
4409
  const hasBlocker = issueList.some((i) => i.severity === "blocker");
@@ -4386,7 +4443,8 @@ const CodeReviewResultSchema = z.object({
4386
4443
  verdict: z.enum([
4387
4444
  "SHIP_IT",
4388
4445
  "NEEDS_MINOR_FIXES",
4389
- "NEEDS_MAJOR_REWORK"
4446
+ "NEEDS_MAJOR_REWORK",
4447
+ "LGTM_WITH_NOTES"
4390
4448
  ]),
4391
4449
  issues: coerceNumber,
4392
4450
  issue_list: z.array(CodeReviewIssueSchema),
@@ -4403,12 +4461,14 @@ const CodeReviewResultSchema = z.object({
4403
4461
  file: ""
4404
4462
  });
4405
4463
  }
4464
+ const computedVerdict = computeVerdict(augmentedIssueList);
4465
+ const finalVerdict = data.verdict === "LGTM_WITH_NOTES" && computedVerdict === "SHIP_IT" ? "LGTM_WITH_NOTES" : computedVerdict;
4406
4466
  return {
4407
4467
  ...data,
4408
4468
  issue_list: augmentedIssueList,
4409
4469
  issues: augmentedIssueList.length,
4410
4470
  agentVerdict: data.verdict,
4411
- verdict: computeVerdict(augmentedIssueList)
4471
+ verdict: finalVerdict
4412
4472
  };
4413
4473
  });
4414
4474
  /**
@@ -4576,7 +4636,7 @@ async function runCreateStory(deps, params) {
4576
4636
  const implementationDecisions = getImplementationDecisions(deps);
4577
4637
  const epicShardContent = getEpicShard(implementationDecisions, epicId, deps.projectRoot, storyKey);
4578
4638
  const prevDevNotesContent = getPrevDevNotes(implementationDecisions, epicId);
4579
- const archConstraintsContent = getArchConstraints$2(deps);
4639
+ const archConstraintsContent = getArchConstraints$3(deps);
4580
4640
  const storyTemplateContent = await getStoryTemplate(deps);
4581
4641
  const { prompt, tokenCount, truncated } = assemblePrompt(template, [
4582
4642
  {
@@ -4823,7 +4883,7 @@ function getPrevDevNotes(decisions, epicId) {
4823
4883
  * Looks for decisions with phase='solutioning', category='architecture'.
4824
4884
  * Falls back to reading _bmad-output/architecture/architecture.md on disk if decisions are empty.
4825
4885
  */
4826
- function getArchConstraints$2(deps) {
4886
+ function getArchConstraints$3(deps) {
4827
4887
  try {
4828
4888
  const decisions = getDecisionsByPhase(deps.db, "solutioning");
4829
4889
  const constraints = decisions.filter((d) => d.category === "architecture");
@@ -5100,7 +5160,8 @@ function getProjectFindings(db) {
5100
5160
  const operational = getDecisionsByCategory(db, OPERATIONAL_FINDING);
5101
5161
  const metrics = getDecisionsByCategory(db, STORY_METRICS);
5102
5162
  const diagnoses = getDecisionsByCategory(db, ESCALATION_DIAGNOSIS);
5103
- if (outcomes.length === 0 && operational.length === 0 && metrics.length === 0 && diagnoses.length === 0) return "";
5163
+ const advisoryNotes = getDecisionsByCategory(db, ADVISORY_NOTES);
5164
+ if (outcomes.length === 0 && operational.length === 0 && metrics.length === 0 && diagnoses.length === 0 && advisoryNotes.length === 0) return "";
5104
5165
  const sections = [];
5105
5166
  if (outcomes.length > 0) {
5106
5167
  const patterns = extractRecurringPatterns(outcomes);
@@ -5135,6 +5196,16 @@ function getProjectFindings(db) {
5135
5196
  }
5136
5197
  const stalls = operational.filter((o) => o.key.startsWith("stall:"));
5137
5198
  if (stalls.length > 0) sections.push(`**Prior stalls:** ${stalls.length} stall event(s) recorded`);
5199
+ if (advisoryNotes.length > 0) {
5200
+ sections.push("**Advisory notes from prior reviews (LGTM_WITH_NOTES):**");
5201
+ for (const n of advisoryNotes.slice(-3)) try {
5202
+ const val = JSON.parse(n.value);
5203
+ const storyId = n.key.split(":")[0];
5204
+ if (typeof val.notes === "string" && val.notes.length > 0) sections.push(`- ${storyId}: ${val.notes}`);
5205
+ } catch {
5206
+ sections.push(`- ${n.key}: advisory notes available`);
5207
+ }
5208
+ }
5138
5209
  if (sections.length === 0) return "";
5139
5210
  let summary = sections.join("\n");
5140
5211
  if (summary.length > MAX_CHARS) summary = summary.slice(0, MAX_CHARS - 3) + "...";
@@ -5785,7 +5856,7 @@ async function runCodeReview(deps, params) {
5785
5856
  output: 0
5786
5857
  });
5787
5858
  }
5788
- const archConstraintsContent = getArchConstraints$1(deps);
5859
+ const archConstraintsContent = getArchConstraints$2(deps);
5789
5860
  const templateTokens = countTokens(template);
5790
5861
  const storyTokens = countTokens(storyContent);
5791
5862
  const constraintTokens = countTokens(archConstraintsContent);
@@ -5994,7 +6065,7 @@ async function runCodeReview(deps, params) {
5994
6065
  * Retrieve architecture constraints from the decision store.
5995
6066
  * Looks for decisions with phase='solutioning', category='architecture'.
5996
6067
  */
5997
- function getArchConstraints$1(deps) {
6068
+ function getArchConstraints$2(deps) {
5998
6069
  try {
5999
6070
  const decisions = getDecisionsByPhase(deps.db, "solutioning");
6000
6071
  const constraints = decisions.filter((d) => d.category === "architecture");
@@ -6061,10 +6132,15 @@ async function runTestPlan(deps, params) {
6061
6132
  }, "Failed to read story file for test planning");
6062
6133
  return makeTestPlanFailureResult(`story_file_read_error: ${error}`);
6063
6134
  }
6135
+ const archConstraintsContent = getArchConstraints$1(deps);
6064
6136
  const { prompt, tokenCount, truncated } = assemblePrompt(template, [{
6065
6137
  name: "story_content",
6066
6138
  content: storyContent,
6067
6139
  priority: "required"
6140
+ }, {
6141
+ name: "architecture_constraints",
6142
+ content: archConstraintsContent,
6143
+ priority: "optional"
6068
6144
  }], TOKEN_CEILING);
6069
6145
  logger$10.info({
6070
6146
  storyKey,
@@ -6181,6 +6257,22 @@ function makeTestPlanFailureResult(error) {
6181
6257
  }
6182
6258
  };
6183
6259
  }
6260
+ /**
6261
+ * Retrieve architecture constraints from the decision store.
6262
+ * Looks for decisions with phase='solutioning', category='architecture'.
6263
+ * Returns empty string if none found or on error (graceful degradation).
6264
+ */
6265
+ function getArchConstraints$1(deps) {
6266
+ try {
6267
+ const decisions = getDecisionsByPhase(deps.db, "solutioning");
6268
+ const constraints = decisions.filter((d) => d.category === "architecture");
6269
+ if (constraints.length === 0) return "";
6270
+ return constraints.map((d) => `${d.key}: ${d.value}`).join("\n");
6271
+ } catch (err) {
6272
+ logger$10.warn({ error: err instanceof Error ? err.message : String(err) }, "Failed to retrieve architecture constraints for test-plan — proceeding without them");
6273
+ return "";
6274
+ }
6275
+ }
6184
6276
 
6185
6277
  //#endregion
6186
6278
  //#region src/modules/compiled-workflows/test-expansion.ts
@@ -6695,6 +6787,125 @@ function detectConflictGroups(storyKeys, config) {
6695
6787
  }
6696
6788
  return Array.from(moduleToStories.values());
6697
6789
  }
6790
+ /**
6791
+ * Build a contract dependency graph from a list of contract declarations.
6792
+ *
6793
+ * Rules:
6794
+ * 1. If story A exports contract "FooSchema" and story B imports "FooSchema",
6795
+ * add a directed edge A→B (A must dispatch before B).
6796
+ * 2. If story A and story B both export the same contract (dual export),
6797
+ * add a one-way edge between them (alphabetically sorted, to avoid cycles)
6798
+ * so they are serialized and cannot produce conflicting definitions in parallel.
6799
+ *
6800
+ * @param declarations - Array of contract declarations from all stories
6801
+ * @returns List of directed dependency edges
6802
+ */
6803
+ function buildContractDependencyGraph(declarations) {
6804
+ const edges = [];
6805
+ const exportsByName = new Map();
6806
+ const importsByName = new Map();
6807
+ for (const decl of declarations) if (decl.direction === "export") {
6808
+ const arr = exportsByName.get(decl.contractName) ?? [];
6809
+ arr.push(decl.storyKey);
6810
+ exportsByName.set(decl.contractName, arr);
6811
+ } else {
6812
+ const arr = importsByName.get(decl.contractName) ?? [];
6813
+ arr.push(decl.storyKey);
6814
+ importsByName.set(decl.contractName, arr);
6815
+ }
6816
+ for (const [contractName, importerKeys] of importsByName) {
6817
+ const exporterKeys = exportsByName.get(contractName) ?? [];
6818
+ for (const from of exporterKeys) for (const to of importerKeys) {
6819
+ if (from === to) continue;
6820
+ edges.push({
6821
+ from,
6822
+ to,
6823
+ contractName,
6824
+ reason: `${from} exports ${contractName}, ${to} imports it`
6825
+ });
6826
+ }
6827
+ }
6828
+ for (const [contractName, exporterKeys] of exportsByName) {
6829
+ if (exporterKeys.length < 2) continue;
6830
+ const sorted = [...exporterKeys].sort();
6831
+ for (let i = 0; i < sorted.length - 1; i++) edges.push({
6832
+ from: sorted[i],
6833
+ to: sorted[i + 1],
6834
+ contractName,
6835
+ reason: `dual export: ${sorted[i]} and ${sorted[i + 1]} both export ${contractName} — serialized to prevent conflicting definitions`
6836
+ });
6837
+ }
6838
+ return edges;
6839
+ }
6840
+ /**
6841
+ * Detect conflict groups with contract-aware dispatch ordering.
6842
+ *
6843
+ * Combines file-based conflict grouping (via moduleMap) with semantic
6844
+ * contract dependency ordering (via ContractDeclaration[]).
6845
+ *
6846
+ * Ordering rules:
6847
+ * - If story A exports a contract that story B imports, A's conflict group
6848
+ * is placed in an earlier batch than B's conflict group.
6849
+ * - If story A and story B both export the same contract (dual export),
6850
+ * they are serialized into different batches.
6851
+ * - Stories with no contract overlap keep their original grouping
6852
+ * (no regression — all placed in the same single batch).
6853
+ *
6854
+ * Cycle detection: if contract edges form a cycle at the group level
6855
+ * (e.g., A→B and B→A), the affected groups are placed in a single
6856
+ * batch together (graceful degradation — serialization within the group
6857
+ * still applies via the file-conflict mechanism).
6858
+ *
6859
+ * @param storyKeys - Array of story key strings
6860
+ * @param config - Optional conflict detector configuration (moduleMap)
6861
+ * @param declarations - Array of contract declarations from all stories
6862
+ * @returns Ordered batches of conflict groups and the edges found
6863
+ */
6864
+ function detectConflictGroupsWithContracts(storyKeys, config, declarations) {
6865
+ const groups = detectConflictGroups(storyKeys, config);
6866
+ const edges = buildContractDependencyGraph(declarations);
6867
+ if (edges.length === 0) return {
6868
+ batches: [groups],
6869
+ edges: []
6870
+ };
6871
+ const storyToGroupIdx = new Map();
6872
+ for (let i = 0; i < groups.length; i++) for (const key of groups[i]) storyToGroupIdx.set(key, i);
6873
+ const successors = new Map();
6874
+ const inDegree = new Array(groups.length).fill(0);
6875
+ for (let i = 0; i < groups.length; i++) successors.set(i, new Set());
6876
+ for (const edge of edges) {
6877
+ const fromGroup = storyToGroupIdx.get(edge.from);
6878
+ const toGroup = storyToGroupIdx.get(edge.to);
6879
+ if (fromGroup === void 0 || toGroup === void 0) continue;
6880
+ if (fromGroup === toGroup) continue;
6881
+ if (!successors.get(fromGroup).has(toGroup)) {
6882
+ successors.get(fromGroup).add(toGroup);
6883
+ inDegree[toGroup]++;
6884
+ }
6885
+ }
6886
+ const waves = [];
6887
+ const processed = new Set();
6888
+ while (processed.size < groups.length) {
6889
+ const wave = [];
6890
+ for (let i = 0; i < groups.length; i++) if (!processed.has(i) && inDegree[i] === 0) wave.push(i);
6891
+ if (wave.length === 0) {
6892
+ const remaining = [];
6893
+ for (let i = 0; i < groups.length; i++) if (!processed.has(i)) remaining.push(i);
6894
+ waves.push(remaining);
6895
+ break;
6896
+ }
6897
+ waves.push(wave);
6898
+ for (const idx of wave) {
6899
+ processed.add(idx);
6900
+ for (const successor of successors.get(idx)) inDegree[successor]--;
6901
+ }
6902
+ }
6903
+ const batches = waves.map((wave) => wave.map((idx) => groups[idx]));
6904
+ return {
6905
+ batches,
6906
+ edges
6907
+ };
6908
+ }
6698
6909
 
6699
6910
  //#endregion
6700
6911
  //#region src/cli/commands/health.ts
@@ -7416,6 +7627,163 @@ function detectInterfaceChanges(options) {
7416
7627
  }
7417
7628
  }
7418
7629
 
7630
+ //#endregion
7631
+ //#region src/modules/compiled-workflows/interface-contracts.ts
7632
+ /**
7633
+ * Parse the `## Interface Contracts` section from a story file.
7634
+ *
7635
+ * Looks for lines matching the format:
7636
+ * - **Export**: SchemaName @ src/path/to/file.ts (optional transport)
7637
+ * - **Import**: SchemaName @ src/path/to/file.ts (optional transport)
7638
+ *
7639
+ * The section is optional — returns empty array when not found or malformed.
7640
+ * Parsing stops at the next `##` heading to avoid false positives in other sections.
7641
+ *
7642
+ * @param storyContent - Full text content of the story markdown file
7643
+ * @param storyKey - Story key to associate with each declaration (e.g., "25-4")
7644
+ * @returns Array of typed contract declarations (may be empty)
7645
+ */
7646
+ function parseInterfaceContracts(storyContent, storyKey) {
7647
+ if (!storyContent || !storyKey) return [];
7648
+ const sectionMatch = /^##\s+Interface\s+Contracts\s*$/im.exec(storyContent);
7649
+ if (!sectionMatch) return [];
7650
+ const sectionStart = sectionMatch.index + sectionMatch[0].length;
7651
+ const afterSection = storyContent.slice(sectionStart);
7652
+ const nextHeading = /^##\s+/m.exec(afterSection);
7653
+ const sectionContent = nextHeading ? afterSection.slice(0, nextHeading.index) : afterSection;
7654
+ const linePattern = /^\s*-\s+\*\*(Export|Import)\*\*:\s+(\S+)\s+@\s+(\S+)(?:\s+\(([^)]+)\))?/gim;
7655
+ const declarations = [];
7656
+ let match;
7657
+ while ((match = linePattern.exec(sectionContent)) !== null) {
7658
+ const [, directionRaw, contractName, filePath, transport] = match;
7659
+ declarations.push({
7660
+ contractName,
7661
+ direction: directionRaw.toLowerCase(),
7662
+ filePath,
7663
+ storyKey,
7664
+ ...transport !== void 0 ? { transport } : {}
7665
+ });
7666
+ }
7667
+ return declarations;
7668
+ }
7669
+
7670
+ //#endregion
7671
+ //#region src/modules/implementation-orchestrator/contract-verifier.ts
7672
+ /**
7673
+ * Verify all declared contract export/import pairs after sprint completion.
7674
+ *
7675
+ * @param declarations - All ContractDeclaration entries from the decision store
7676
+ * @param projectRoot - Absolute path to the project root for file resolution
7677
+ * @returns - Array of ContractMismatch entries (empty = all passed)
7678
+ */
7679
+ function verifyContracts(declarations, projectRoot) {
7680
+ if (declarations.length === 0) return [];
7681
+ const exports = declarations.filter((d) => d.direction === "export");
7682
+ const imports = declarations.filter((d) => d.direction === "import");
7683
+ if (exports.length === 0) return [];
7684
+ const mismatches = [];
7685
+ for (const exp of exports) {
7686
+ if (!exp.filePath) continue;
7687
+ const absPath = join$1(projectRoot, exp.filePath);
7688
+ if (!existsSync$1(absPath)) {
7689
+ const importers = imports.filter((i) => i.contractName === exp.contractName);
7690
+ if (importers.length > 0) for (const imp of importers) mismatches.push({
7691
+ exporter: exp.storyKey,
7692
+ importer: imp.storyKey,
7693
+ contractName: exp.contractName,
7694
+ mismatchDescription: `Exported file not found: ${exp.filePath}`
7695
+ });
7696
+ else mismatches.push({
7697
+ exporter: exp.storyKey,
7698
+ importer: null,
7699
+ contractName: exp.contractName,
7700
+ mismatchDescription: `Exported file not found: ${exp.filePath}`
7701
+ });
7702
+ }
7703
+ }
7704
+ const tsconfigPath = join$1(projectRoot, "tsconfig.json");
7705
+ const tscBinPath = join$1(projectRoot, "node_modules", ".bin", "tsc");
7706
+ if (existsSync$1(tsconfigPath) && existsSync$1(tscBinPath)) {
7707
+ let tscOutput = "";
7708
+ let tscFailed = false;
7709
+ try {
7710
+ execSync(`"${tscBinPath}" --noEmit`, {
7711
+ cwd: projectRoot,
7712
+ timeout: 12e4,
7713
+ encoding: "utf-8",
7714
+ stdio: [
7715
+ "pipe",
7716
+ "pipe",
7717
+ "pipe"
7718
+ ]
7719
+ });
7720
+ } catch (err) {
7721
+ tscFailed = true;
7722
+ if (err != null && typeof err === "object") {
7723
+ const e = err;
7724
+ const stdoutStr = typeof e.stdout === "string" ? e.stdout : Buffer.isBuffer(e.stdout) ? e.stdout.toString("utf-8") : "";
7725
+ const stderrStr = typeof e.stderr === "string" ? e.stderr : Buffer.isBuffer(e.stderr) ? e.stderr.toString("utf-8") : "";
7726
+ tscOutput = [stdoutStr, stderrStr].filter((s) => s.length > 0).join("\n");
7727
+ if (!tscOutput && e.message) tscOutput = e.message;
7728
+ }
7729
+ }
7730
+ if (tscFailed) {
7731
+ const truncatedOutput = tscOutput.slice(0, 1e3);
7732
+ const matchedExports = new Set();
7733
+ for (const exp of exports) {
7734
+ if (!exp.filePath) continue;
7735
+ if (tscOutput.includes(exp.filePath)) {
7736
+ matchedExports.add(exp.contractName);
7737
+ const importers = imports.filter((i) => i.contractName === exp.contractName);
7738
+ if (importers.length > 0) for (const imp of importers) mismatches.push({
7739
+ exporter: exp.storyKey,
7740
+ importer: imp.storyKey,
7741
+ contractName: exp.contractName,
7742
+ mismatchDescription: `TypeScript type-check failed for ${exp.filePath}: ${truncatedOutput}`
7743
+ });
7744
+ else mismatches.push({
7745
+ exporter: exp.storyKey,
7746
+ importer: null,
7747
+ contractName: exp.contractName,
7748
+ mismatchDescription: `TypeScript type-check failed for ${exp.filePath}: ${truncatedOutput}`
7749
+ });
7750
+ }
7751
+ }
7752
+ if (matchedExports.size === 0) {
7753
+ const reportedPairs = new Set();
7754
+ for (const exp of exports) {
7755
+ const importers = imports.filter((i) => i.contractName === exp.contractName);
7756
+ if (importers.length > 0) for (const imp of importers) {
7757
+ const pairKey = `${exp.storyKey}:${imp.storyKey}:${exp.contractName}`;
7758
+ if (!reportedPairs.has(pairKey)) {
7759
+ reportedPairs.add(pairKey);
7760
+ mismatches.push({
7761
+ exporter: exp.storyKey,
7762
+ importer: imp.storyKey,
7763
+ contractName: exp.contractName,
7764
+ mismatchDescription: `TypeScript type-check failed: ${truncatedOutput}`
7765
+ });
7766
+ }
7767
+ }
7768
+ else {
7769
+ const pairKey = `${exp.storyKey}:null:${exp.contractName}`;
7770
+ if (!reportedPairs.has(pairKey)) {
7771
+ reportedPairs.add(pairKey);
7772
+ mismatches.push({
7773
+ exporter: exp.storyKey,
7774
+ importer: null,
7775
+ contractName: exp.contractName,
7776
+ mismatchDescription: `TypeScript type-check failed: ${truncatedOutput}`
7777
+ });
7778
+ }
7779
+ }
7780
+ }
7781
+ }
7782
+ }
7783
+ }
7784
+ return mismatches;
7785
+ }
7786
+
7419
7787
  //#endregion
7420
7788
  //#region src/modules/implementation-orchestrator/orchestrator-impl.ts
7421
7789
  function createPauseGate() {
@@ -7477,6 +7845,7 @@ function createImplementationOrchestrator(deps) {
7477
7845
  const _phaseEndMs = new Map();
7478
7846
  const _storyDispatches = new Map();
7479
7847
  let _maxConcurrentActual = 0;
7848
+ let _contractMismatches;
7480
7849
  const MEMORY_PRESSURE_BACKOFF_MS = [
7481
7850
  3e4,
7482
7851
  6e4,
@@ -7673,6 +8042,7 @@ function createImplementationOrchestrator(deps) {
7673
8042
  }
7674
8043
  if (_decomposition !== void 0) status.decomposition = { ..._decomposition };
7675
8044
  if (_maxConcurrentActual > 0) status.maxConcurrentActual = _maxConcurrentActual;
8045
+ if (_contractMismatches !== void 0 && _contractMismatches.length > 0) status.contractMismatches = [..._contractMismatches];
7676
8046
  return status;
7677
8047
  }
7678
8048
  function updateStory(storyKey, updates) {
@@ -7947,6 +8317,35 @@ function createImplementationOrchestrator(deps) {
7947
8317
  persistState();
7948
8318
  return;
7949
8319
  }
8320
+ if (storyFilePath) try {
8321
+ const storyContent = await readFile$1(storyFilePath, "utf-8");
8322
+ const contracts = parseInterfaceContracts(storyContent, storyKey);
8323
+ if (contracts.length > 0) {
8324
+ for (const contract of contracts) createDecision(db, {
8325
+ pipeline_run_id: config.pipelineRunId ?? null,
8326
+ phase: "implementation",
8327
+ category: "interface-contract",
8328
+ key: `${storyKey}:${contract.contractName}`,
8329
+ value: JSON.stringify({
8330
+ direction: contract.direction,
8331
+ schemaName: contract.contractName,
8332
+ filePath: contract.filePath,
8333
+ storyKey: contract.storyKey,
8334
+ ...contract.transport !== void 0 ? { transport: contract.transport } : {}
8335
+ })
8336
+ });
8337
+ logger$23.info({
8338
+ storyKey,
8339
+ contractCount: contracts.length,
8340
+ contracts
8341
+ }, "Stored interface contract declarations in decision store");
8342
+ }
8343
+ } catch (err) {
8344
+ logger$23.warn({
8345
+ storyKey,
8346
+ error: err instanceof Error ? err.message : String(err)
8347
+ }, "Failed to parse interface contracts — continuing without contract declarations");
8348
+ }
7950
8349
  await waitIfPaused();
7951
8350
  if (_state !== "RUNNING") return;
7952
8351
  startPhase(storyKey, "test-plan");
@@ -8254,9 +8653,9 @@ function createImplementationOrchestrator(deps) {
8254
8653
  });
8255
8654
  let verdict;
8256
8655
  let issueList = [];
8656
+ let reviewResult;
8257
8657
  try {
8258
8658
  const useBatchedReview = batchFileGroups.length > 1 && previousIssueList.length === 0;
8259
- let reviewResult;
8260
8659
  if (useBatchedReview) {
8261
8660
  const allIssues = [];
8262
8661
  let worstVerdict = "SHIP_IT";
@@ -8268,6 +8667,7 @@ function createImplementationOrchestrator(deps) {
8268
8667
  let lastRawOutput;
8269
8668
  const verdictRank = {
8270
8669
  "SHIP_IT": 0,
8670
+ "LGTM_WITH_NOTES": .5,
8271
8671
  "NEEDS_MINOR_FIXES": 1,
8272
8672
  "NEEDS_MAJOR_REWORK": 2
8273
8673
  };
@@ -8334,7 +8734,7 @@ function createImplementationOrchestrator(deps) {
8334
8734
  ...previousIssueList.length > 0 ? { previousIssues: previousIssueList } : {}
8335
8735
  });
8336
8736
  }
8337
- const isPhantomReview = reviewResult.dispatchFailed === true || reviewResult.verdict !== "SHIP_IT" && (reviewResult.issue_list === void 0 || reviewResult.issue_list.length === 0) && reviewResult.error !== void 0;
8737
+ const isPhantomReview = reviewResult.dispatchFailed === true || reviewResult.verdict !== "SHIP_IT" && reviewResult.verdict !== "LGTM_WITH_NOTES" && (reviewResult.issue_list === void 0 || reviewResult.issue_list.length === 0) && reviewResult.error !== void 0;
8338
8738
  if (isPhantomReview && !timeoutRetried) {
8339
8739
  timeoutRetried = true;
8340
8740
  logger$23.warn({
@@ -8407,19 +8807,38 @@ function createImplementationOrchestrator(deps) {
8407
8807
  persistState();
8408
8808
  return;
8409
8809
  }
8410
- if (verdict === "SHIP_IT") {
8810
+ if (verdict === "SHIP_IT" || verdict === "LGTM_WITH_NOTES") {
8411
8811
  endPhase(storyKey, "code-review");
8412
8812
  updateStory(storyKey, {
8413
8813
  phase: "COMPLETE",
8414
8814
  completedAt: new Date().toISOString()
8415
8815
  });
8416
- writeStoryMetricsBestEffort(storyKey, "success", reviewCycles + 1);
8816
+ writeStoryMetricsBestEffort(storyKey, verdict, reviewCycles + 1);
8417
8817
  writeStoryOutcomeBestEffort(storyKey, "complete", reviewCycles + 1);
8418
8818
  eventBus.emit("orchestrator:story-complete", {
8419
8819
  storyKey,
8420
8820
  reviewCycles
8421
8821
  });
8422
8822
  persistState();
8823
+ if (verdict === "LGTM_WITH_NOTES" && reviewResult.notes && config.pipelineRunId) try {
8824
+ createDecision(db, {
8825
+ pipeline_run_id: config.pipelineRunId,
8826
+ phase: "implementation",
8827
+ category: ADVISORY_NOTES,
8828
+ key: `${storyKey}:${config.pipelineRunId}`,
8829
+ value: JSON.stringify({
8830
+ storyKey,
8831
+ notes: reviewResult.notes
8832
+ }),
8833
+ rationale: `Advisory notes from LGTM_WITH_NOTES review of ${storyKey}`
8834
+ });
8835
+ logger$23.info({ storyKey }, "Advisory notes persisted to decision store");
8836
+ } catch (advisoryErr) {
8837
+ logger$23.warn({
8838
+ storyKey,
8839
+ error: advisoryErr instanceof Error ? advisoryErr.message : String(advisoryErr)
8840
+ }, "Failed to persist advisory notes (best-effort)");
8841
+ }
8423
8842
  try {
8424
8843
  const expansionResult = await runTestExpansion({
8425
8844
  db,
@@ -8566,7 +8985,7 @@ function createImplementationOrchestrator(deps) {
8566
8985
  reviewCycles: finalReviewCycles,
8567
8986
  completedAt: new Date().toISOString()
8568
8987
  });
8569
- writeStoryMetricsBestEffort(storyKey, "success", finalReviewCycles);
8988
+ writeStoryMetricsBestEffort(storyKey, verdict, finalReviewCycles);
8570
8989
  writeStoryOutcomeBestEffort(storyKey, "complete", finalReviewCycles);
8571
8990
  eventBus.emit("orchestrator:story-complete", {
8572
8991
  storyKey,
@@ -8843,14 +9262,64 @@ function createImplementationOrchestrator(deps) {
8843
9262
  skippedCategories: seedResult.skippedCategories
8844
9263
  }, "Methodology context seeded from planning artifacts");
8845
9264
  }
8846
- const groups = detectConflictGroups(storyKeys, { moduleMap: pack.manifest.conflictGroups });
9265
+ const interfaceContractDecisions = getDecisionsByCategory(db, "interface-contract");
9266
+ const contractDeclarations = interfaceContractDecisions.map((d) => {
9267
+ try {
9268
+ const parsed = JSON.parse(d.value);
9269
+ const storyKey = typeof parsed.storyKey === "string" ? parsed.storyKey : "";
9270
+ const contractName = typeof parsed.schemaName === "string" ? parsed.schemaName : "";
9271
+ const direction = parsed.direction === "export" ? "export" : "import";
9272
+ const filePath = typeof parsed.filePath === "string" ? parsed.filePath : "";
9273
+ if (!storyKey || !contractName) return null;
9274
+ return {
9275
+ storyKey,
9276
+ contractName,
9277
+ direction,
9278
+ filePath,
9279
+ ...typeof parsed.transport === "string" ? { transport: parsed.transport } : {}
9280
+ };
9281
+ } catch {
9282
+ return null;
9283
+ }
9284
+ }).filter((d) => d !== null);
9285
+ const { batches, edges: contractEdges } = detectConflictGroupsWithContracts(storyKeys, { moduleMap: pack.manifest.conflictGroups }, contractDeclarations);
9286
+ if (contractEdges.length > 0) logger$23.info({
9287
+ contractEdges,
9288
+ edgeCount: contractEdges.length
9289
+ }, "Contract dependency edges detected — applying contract-aware dispatch ordering");
8847
9290
  logger$23.info("Orchestrator starting", {
8848
9291
  storyCount: storyKeys.length,
8849
- groupCount: groups.length,
9292
+ groupCount: batches.reduce((sum, b) => sum + b.length, 0),
9293
+ batchCount: batches.length,
8850
9294
  maxConcurrency: config.maxConcurrency
8851
9295
  });
9296
+ if (config.skipPreflight !== true) {
9297
+ const preFlightResult = runBuildVerification({
9298
+ verifyCommand: pack.manifest.verifyCommand,
9299
+ verifyTimeoutMs: pack.manifest.verifyTimeoutMs,
9300
+ projectRoot: projectRoot ?? process.cwd()
9301
+ });
9302
+ if (preFlightResult.status === "failed" || preFlightResult.status === "timeout") {
9303
+ stopHeartbeat();
9304
+ const truncatedOutput = (preFlightResult.output ?? "").slice(0, 2e3);
9305
+ const exitCode = preFlightResult.exitCode ?? 1;
9306
+ eventBus.emit("pipeline:pre-flight-failure", {
9307
+ exitCode,
9308
+ output: truncatedOutput
9309
+ });
9310
+ logger$23.error({
9311
+ exitCode,
9312
+ reason: preFlightResult.reason
9313
+ }, "Pre-flight build check failed — aborting pipeline before any story dispatch");
9314
+ _state = "FAILED";
9315
+ _completedAt = new Date().toISOString();
9316
+ persistState();
9317
+ return getStatus();
9318
+ }
9319
+ if (preFlightResult.status !== "skipped") logger$23.info("Pre-flight build check passed");
9320
+ }
8852
9321
  try {
8853
- await runWithConcurrency(groups, config.maxConcurrency);
9322
+ for (const batchGroups of batches) await runWithConcurrency(batchGroups, config.maxConcurrency);
8854
9323
  } catch (err) {
8855
9324
  stopHeartbeat();
8856
9325
  _state = "FAILED";
@@ -8862,6 +9331,24 @@ function createImplementationOrchestrator(deps) {
8862
9331
  stopHeartbeat();
8863
9332
  _state = "COMPLETE";
8864
9333
  _completedAt = new Date().toISOString();
9334
+ if (projectRoot !== void 0 && contractDeclarations.length > 0) try {
9335
+ const mismatches = verifyContracts(contractDeclarations, projectRoot);
9336
+ if (mismatches.length > 0) {
9337
+ _contractMismatches = mismatches;
9338
+ for (const mismatch of mismatches) eventBus.emit("pipeline:contract-mismatch", {
9339
+ exporter: mismatch.exporter,
9340
+ importer: mismatch.importer,
9341
+ contractName: mismatch.contractName,
9342
+ mismatchDescription: mismatch.mismatchDescription
9343
+ });
9344
+ logger$23.warn({
9345
+ mismatchCount: mismatches.length,
9346
+ mismatches
9347
+ }, "Post-sprint contract verification found mismatches — manual review required");
9348
+ } else logger$23.info("Post-sprint contract verification passed — all declared contracts satisfied");
9349
+ } catch (err) {
9350
+ logger$23.error({ err }, "Post-sprint contract verification threw an error — skipping");
9351
+ }
8865
9352
  let completed = 0;
8866
9353
  let escalated = 0;
8867
9354
  let failed = 0;
@@ -12927,7 +13414,7 @@ function mapInternalPhaseToEventPhase(internalPhase) {
12927
13414
  }
12928
13415
  }
12929
13416
  async function runRunAction(options) {
12930
- const { pack: packName, from: startPhase, stopAfter, concept: conceptArg, conceptFile, stories: storiesArg, concurrency, outputFormat, projectRoot, events: eventsFlag, verbose: verboseFlag, tui: tuiFlag, skipUx, research: researchFlag, skipResearch: skipResearchFlag, registry: injectedRegistry } = options;
13417
+ const { pack: packName, from: startPhase, stopAfter, concept: conceptArg, conceptFile, stories: storiesArg, concurrency, outputFormat, projectRoot, events: eventsFlag, verbose: verboseFlag, tui: tuiFlag, skipUx, research: researchFlag, skipResearch: skipResearchFlag, skipPreflight, registry: injectedRegistry } = options;
12931
13418
  if (startPhase !== void 0 && !VALID_PHASES.includes(startPhase)) {
12932
13419
  const errorMsg = `Invalid phase '${startPhase}'. Valid phases: ${VALID_PHASES.join(", ")}`;
12933
13420
  if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
@@ -13014,6 +13501,7 @@ async function runRunAction(options) {
13014
13501
  ...skipUx === true ? { skipUx: true } : {},
13015
13502
  ...researchFlag === true ? { research: true } : {},
13016
13503
  ...skipResearchFlag === true ? { skipResearch: true } : {},
13504
+ ...skipPreflight === true ? { skipPreflight: true } : {},
13017
13505
  ...injectedRegistry !== void 0 ? { registry: injectedRegistry } : {}
13018
13506
  });
13019
13507
  let storyKeys = [];
@@ -13403,6 +13891,24 @@ async function runRunAction(options) {
13403
13891
  dispatches: payload.dispatches
13404
13892
  });
13405
13893
  });
13894
+ eventBus.on("pipeline:pre-flight-failure", (payload) => {
13895
+ ndjsonEmitter.emit({
13896
+ type: "pipeline:pre-flight-failure",
13897
+ ts: new Date().toISOString(),
13898
+ exitCode: payload.exitCode,
13899
+ output: payload.output
13900
+ });
13901
+ });
13902
+ eventBus.on("pipeline:contract-mismatch", (payload) => {
13903
+ ndjsonEmitter.emit({
13904
+ type: "pipeline:contract-mismatch",
13905
+ ts: new Date().toISOString(),
13906
+ exporter: payload.exporter,
13907
+ importer: payload.importer,
13908
+ contractName: payload.contractName,
13909
+ mismatchDescription: payload.mismatchDescription
13910
+ });
13911
+ });
13406
13912
  }
13407
13913
  const orchestrator = createImplementationOrchestrator({
13408
13914
  db,
@@ -13414,7 +13920,8 @@ async function runRunAction(options) {
13414
13920
  maxConcurrency: concurrency,
13415
13921
  maxReviewCycles: 2,
13416
13922
  pipelineRunId: pipelineRun.id,
13417
- enableHeartbeat: eventsFlag === true
13923
+ enableHeartbeat: eventsFlag === true,
13924
+ skipPreflight: skipPreflight === true
13418
13925
  },
13419
13926
  projectRoot,
13420
13927
  tokenCeilings
@@ -13513,7 +14020,7 @@ async function runRunAction(options) {
13513
14020
  }
13514
14021
  }
13515
14022
  async function runFullPipeline(options) {
13516
- const { packName, packPath, dbDir, dbPath, startPhase, stopAfter, concept, concurrency, outputFormat, projectRoot, events: eventsFlag, skipUx, research: researchFlag, skipResearch: skipResearchFlag, registry: injectedRegistry, tokenCeilings } = options;
14023
+ const { packName, packPath, dbDir, dbPath, startPhase, stopAfter, concept, concurrency, outputFormat, projectRoot, events: eventsFlag, skipUx, research: researchFlag, skipResearch: skipResearchFlag, skipPreflight, registry: injectedRegistry, tokenCeilings } = options;
13517
14024
  if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
13518
14025
  const dbWrapper = new DatabaseWrapper(dbPath);
13519
14026
  try {
@@ -13718,7 +14225,8 @@ async function runFullPipeline(options) {
13718
14225
  config: {
13719
14226
  maxConcurrency: concurrency,
13720
14227
  maxReviewCycles: 2,
13721
- pipelineRunId: runId
14228
+ pipelineRunId: runId,
14229
+ skipPreflight: skipPreflight === true
13722
14230
  },
13723
14231
  projectRoot,
13724
14232
  tokenCeilings
@@ -13818,7 +14326,7 @@ async function runFullPipeline(options) {
13818
14326
  }
13819
14327
  }
13820
14328
  function registerRunCommand(program, _version = "0.0.0", projectRoot = process.cwd(), registry) {
13821
- program.command("run").description("Run the autonomous pipeline (use --from to start from a specific phase)").option("--pack <name>", "Methodology pack name", "bmad").option("--from <phase>", "Start from this phase: analysis, planning, solutioning, implementation").option("--stop-after <phase>", "Stop pipeline after this phase completes").option("--concept <text>", "Inline concept text (required when --from analysis)").option("--concept-file <path>", "Path to a file containing the concept text").option("--stories <keys>", "Comma-separated story keys (e.g., 10-1,10-2)").option("--concurrency <n>", "Maximum parallel conflict groups", (v) => parseInt(v, 10), 3).option("--project-root <path>", "Project root directory", projectRoot).option("--output-format <format>", "Output format: human (default) or json", "human").option("--events", "Emit structured NDJSON events on stdout for programmatic consumption").option("--verbose", "Show detailed pino log output").option("--help-agent", "Print a machine-optimized prompt fragment for AI agents and exit").option("--tui", "Show TUI dashboard").option("--skip-ux", "Skip the UX design phase even if enabled in the pack manifest").option("--research", "Enable the research phase even if not set in the pack manifest").option("--skip-research", "Skip the research phase even if enabled in the pack manifest").action(async (opts) => {
14329
+ program.command("run").description("Run the autonomous pipeline (use --from to start from a specific phase)").option("--pack <name>", "Methodology pack name", "bmad").option("--from <phase>", "Start from this phase: analysis, planning, solutioning, implementation").option("--stop-after <phase>", "Stop pipeline after this phase completes").option("--concept <text>", "Inline concept text (required when --from analysis)").option("--concept-file <path>", "Path to a file containing the concept text").option("--stories <keys>", "Comma-separated story keys (e.g., 10-1,10-2)").option("--concurrency <n>", "Maximum parallel conflict groups", (v) => parseInt(v, 10), 3).option("--project-root <path>", "Project root directory", projectRoot).option("--output-format <format>", "Output format: human (default) or json", "human").option("--events", "Emit structured NDJSON events on stdout for programmatic consumption").option("--verbose", "Show detailed pino log output").option("--help-agent", "Print a machine-optimized prompt fragment for AI agents and exit").option("--tui", "Show TUI dashboard").option("--skip-ux", "Skip the UX design phase even if enabled in the pack manifest").option("--research", "Enable the research phase even if not set in the pack manifest").option("--skip-research", "Skip the research phase even if enabled in the pack manifest").option("--skip-preflight", "Skip the pre-flight build check (escape hatch for known-broken projects)").action(async (opts) => {
13822
14330
  if (opts.helpAgent) {
13823
14331
  process.exitCode = await runHelpAgent();
13824
14332
  return;
@@ -13851,6 +14359,7 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
13851
14359
  skipUx: opts.skipUx,
13852
14360
  research: opts.research,
13853
14361
  skipResearch: opts.skipResearch,
14362
+ skipPreflight: opts.skipPreflight,
13854
14363
  registry
13855
14364
  });
13856
14365
  process.exitCode = exitCode;
@@ -13859,4 +14368,4 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
13859
14368
 
13860
14369
  //#endregion
13861
14370
  export { DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DatabaseWrapper, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, buildPipelineStatusOutput, createConfigSystem, createContextCompiler, createDispatcher, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, runAnalysisPhase, runMigrations, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
13862
- //# sourceMappingURL=run-Ajt187oE.js.map
14371
+ //# sourceMappingURL=run-BxXwD2xY.js.map
@@ -1,8 +1,8 @@
1
- import { registerRunCommand, runRunAction } from "./run-Ajt187oE.js";
1
+ import { registerRunCommand, runRunAction } from "./run-BxXwD2xY.js";
2
2
  import "./logger-D2fS2ccL.js";
3
3
  import "./config-migrator-DSi8KhQC.js";
4
4
  import "./helpers-RL22dYtn.js";
5
5
  import "./decisions-Dq4cAA2L.js";
6
- import "./operational-CnMlvWqc.js";
6
+ import "./operational-Bovj4fS-.js";
7
7
 
8
8
  export { runRunAction };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "substrate-ai",
3
- "version": "0.2.30",
3
+ "version": "0.2.31",
4
4
  "description": "Substrate — multi-agent orchestration daemon for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -221,6 +221,8 @@ prompts:
221
221
  refine-artifact: prompts/refine-artifact.md
222
222
  # Readiness check prompt (Story 16-6)
223
223
  readiness-check: prompts/readiness-check.md
224
+ # Test plan prompt (Story 25-7)
225
+ test-plan: prompts/test-plan.md
224
226
 
225
227
  constraints:
226
228
  create-story: constraints/create-story.yaml
@@ -100,6 +100,11 @@ ac_checklist:
100
100
  **IMPORTANT**: `ac_checklist` must contain one entry for every AC found in the story. If the story has no parseable ACs (e.g. a refactoring story), `ac_checklist` may be an empty array.
101
101
 
102
102
  **Verdict rules:**
103
- - `SHIP_IT` — zero blocker/major issues (minor issues acceptable)
104
- - `NEEDS_MINOR_FIXES` — minor issues only, or 1-2 major with no blockers
103
+ - `SHIP_IT` — zero issues of any kind
104
+ - `LGTM_WITH_NOTES` — zero correctness/logic/security issues; only advisory or style observations that do not need to be fixed before shipping. Use this when you have optional suggestions but the code is production-ready as-is. Include your suggestions in the `notes` field.
105
+ - `NEEDS_MINOR_FIXES` — one or more minor issues that should be fixed, or 1-2 major issues with no blockers
105
106
  - `NEEDS_MAJOR_REWORK` — any blocker issue, or 3+ major issues
107
+
108
+ **LGTM_WITH_NOTES vs NEEDS_MINOR_FIXES:**
109
+ - Use `LGTM_WITH_NOTES` when: all findings are purely advisory (naming preferences, optional refactors, docs suggestions) and the code ships safely without any changes
110
+ - Use `NEEDS_MINOR_FIXES` when: any finding represents a real gap that should be corrected before the story is considered done (missing error handling, incomplete AC coverage, confusing logic)
@@ -35,6 +35,26 @@ Using the context above, write a complete, implementation-ready story file for s
35
35
  - Status must be: `ready-for-dev`
36
36
  - Dev Agent Record section must be present but left blank (to be filled by dev agent)
37
37
 
38
+ ## Interface Contracts Guidance
39
+
40
+ **Identify cross-story dependencies** when the story creates or consumes shared schemas, interfaces, or message contracts.
41
+
42
+ If the story exports (creates) or imports (consumes from another story) any TypeScript interfaces, Zod schemas, message queue contracts, or API types that are shared across module boundaries, add an `## Interface Contracts` section to the story file.
43
+
44
+ Use this exact format for each item:
45
+
46
+ ```markdown
47
+ ## Interface Contracts
48
+
49
+ - **Export**: SchemaName @ src/path/to/file.ts (queue: some-queue-name)
50
+ - **Import**: SchemaName @ src/path/to/file.ts (from story 25-X)
51
+ ```
52
+
53
+ - `Export` = this story creates/defines the schema that other stories will consume
54
+ - `Import` = this story consumes a schema defined by another story
55
+ - The transport annotation `(queue: ...)` or `(api: ...)` or `(from story X-Y)` is optional but recommended when applicable
56
+ - **The `## Interface Contracts` section is optional** — omit it entirely if the story has no cross-story schema dependencies
57
+
38
58
  ## Scope Cap Guidance
39
59
 
40
60
  **Aim for 6-7 acceptance criteria and 7-8 tasks per story.**
@@ -2,24 +2,30 @@
2
2
 
3
3
  ## Mission
4
4
 
5
- Analyze the story's Acceptance Criteria and tasks. Produce a concrete test plan listing the test files to create, the test categories to cover (unit/integration/e2e), and a brief note on AC coverage.
5
+ Analyze the story's Acceptance Criteria and tasks. Produce a concrete test plan listing the test files to create, the test categories to cover (unit/integration/e2e), and comprehensive coverage notes that include: which ACs each test covers, dependencies to mock, and error conditions to assert.
6
6
 
7
7
  ## Story Content
8
8
 
9
9
  {{story_content}}
10
10
 
11
+ ## Architecture Constraints
12
+
13
+ {{architecture_constraints}}
14
+
11
15
  ## Instructions
12
16
 
13
17
  1. Read the Acceptance Criteria (AC1, AC2, etc.) and Tasks in the story above.
14
18
  2. Identify the source files that will need tests (from Dev Notes, Key File Paths, and Tasks).
15
19
  3. For each AC, determine which test file and test function will validate it.
16
- 4. Produce a concise test plan one or two test files is typical for small stories.
20
+ 4. Identify all external dependencies (modules, services, fs, db) that must be mocked or stubbed.
21
+ 5. Identify error conditions and edge cases that must be asserted (not just the happy path).
22
+ 6. Produce a concise test plan — one or two test files is typical for small stories.
17
23
 
18
24
  **Rules:**
19
25
  - List only test files that will be NEW (not existing ones you'd extend unless necessary).
20
26
  - Use the project's test path convention: `src/modules/<module>/__tests__/<file>.test.ts`
21
27
  - Test categories: `unit` for isolated function tests, `integration` for multi-module tests, `e2e` for full pipeline tests.
22
- - Keep `coverage_notes` brief one sentence per AC is sufficient.
28
+ - `coverage_notes` must include: (a) which test file covers each AC, (b) dependencies to mock (e.g., `vi.mock('node:fs/promises')`), and (c) error conditions to assert (e.g., missing file, schema validation failure, timeout).
23
29
 
24
30
  ## Output Contract
25
31
 
@@ -31,7 +37,7 @@ test_files:
31
37
  test_categories:
32
38
  - unit
33
39
  - integration
34
- coverage_notes: "AC1 covered by foo.test.ts describe('runFoo'). AC2 covered by..."
40
+ coverage_notes: "AC1: foo.test.ts describe('runFoo') covers the happy path. AC2: same file covers error path (rejects on ENOENT). Mocks needed: vi.mock('node:fs/promises'), vi.mock('../db.js'). Error conditions: file not found returns failed result, schema parse error returns failed with details, timeout triggers fallback."
35
41
 
36
42
  If you cannot produce a plan (e.g., story content is missing or unreadable), emit:
37
43