substrate-ai 0.7.0 → 0.8.0

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.
@@ -1,4 +1,4 @@
1
- import { BMAD_BASELINE_TOKENS_FULL, DoltClient, DoltMergeConflict, FileStateStore, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN, VALID_PHASES, WorkGraphRepository, __commonJS, __require, __toESM, buildPipelineStatusOutput, createDatabaseAdapter, formatOutput, formatPipelineSummary, formatTokenTelemetry, initSchema, inspectProcessTree, parseDbTimestampAsUtc, resolveMainRepoRoot, validateStoryKey } from "./health-Dnx-FGva.js";
1
+ import { BMAD_BASELINE_TOKENS_FULL, DoltClient, DoltMergeConflict, FileStateStore, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN, VALID_PHASES, WorkGraphRepository, __commonJS, __require, __toESM, buildPipelineStatusOutput, createDatabaseAdapter, formatOutput, formatPipelineSummary, formatTokenTelemetry, initSchema, inspectProcessTree, parseDbTimestampAsUtc, resolveMainRepoRoot, validateStoryKey } from "./health-C-VRJruD.js";
2
2
  import { createLogger, deepMask } from "./logger-D2fS2ccL.js";
3
3
  import { CURRENT_CONFIG_FORMAT_VERSION, PartialSubstrateConfigSchema, SUPPORTED_CONFIG_FORMAT_VERSIONS, SubstrateConfigSchema, defaultConfigMigrator } from "./config-migrator-CtGelIsG.js";
4
4
  import { ConfigError, ConfigIncompatibleFormatError, createEventBus, createTuiApp, isTuiCapable, printNonTtyWarning, sleep } from "./helpers-CpMs8VZX.js";
@@ -7,7 +7,7 @@ import { addTokenUsage, createDecision, createPipelineRun, createRequirement, ge
7
7
  import { ADVISORY_NOTES, ESCALATION_DIAGNOSIS, OPERATIONAL_FINDING, STORY_METRICS, STORY_OUTCOME, TEST_EXPANSION_FINDING, TEST_PLAN, aggregateTokenUsageForRun, aggregateTokenUsageForStory, getStoryMetricsForRun, writeRunMetrics, writeStoryMetrics } from "./operational-BdcdmDqS.js";
8
8
  import { dirname, join, resolve } from "path";
9
9
  import { access, mkdir, readFile, readdir, stat, writeFile } from "fs/promises";
10
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
10
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "fs";
11
11
  import yaml from "js-yaml";
12
12
  import { z } from "zod";
13
13
  import { execFile, execSync, spawn } from "node:child_process";
@@ -4665,7 +4665,7 @@ const DEFAULT_TIMEOUTS = {
4665
4665
  "create-story": 6e5,
4666
4666
  "dev-story": 18e5,
4667
4667
  "code-review": 9e5,
4668
- "minor-fixes": 6e5,
4668
+ "minor-fixes": 12e5,
4669
4669
  "major-rework": 9e5,
4670
4670
  "readiness-check": 6e5,
4671
4671
  "elicitation": 9e5,
@@ -5669,7 +5669,22 @@ function runBuildVerification(options) {
5669
5669
  */
5670
5670
  function checkGitDiffFiles(workingDir = process.cwd()) {
5671
5671
  const results = new Set();
5672
+ let repoHasCommits = true;
5672
5673
  try {
5674
+ execSync("git rev-parse --verify HEAD", {
5675
+ cwd: workingDir,
5676
+ stdio: [
5677
+ "ignore",
5678
+ "pipe",
5679
+ "pipe"
5680
+ ],
5681
+ timeout: 3e3
5682
+ });
5683
+ } catch {
5684
+ repoHasCommits = false;
5685
+ }
5686
+ try {
5687
+ if (!repoHasCommits) throw new Error("no commits — skip HEAD diff");
5673
5688
  const unstaged = execSync("git diff --name-only HEAD", {
5674
5689
  cwd: workingDir,
5675
5690
  encoding: "utf-8",
@@ -6600,6 +6615,27 @@ async function isValidStoryFile(filePath) {
6600
6615
  //#region src/modules/compiled-workflows/git-helpers.ts
6601
6616
  const logger$19 = createLogger("compiled-workflows:git-helpers");
6602
6617
  /**
6618
+ * Check whether the repo at `cwd` has at least one commit (HEAD resolves).
6619
+ * Returns false for fresh repos with no commits, avoiding `fatal: bad revision 'HEAD'`.
6620
+ * Synchronous (execSync) to keep it simple — this is a fast local check.
6621
+ */
6622
+ function hasCommits(cwd) {
6623
+ try {
6624
+ execSync("git rev-parse --verify HEAD", {
6625
+ cwd,
6626
+ stdio: [
6627
+ "ignore",
6628
+ "pipe",
6629
+ "pipe"
6630
+ ],
6631
+ timeout: 3e3
6632
+ });
6633
+ return true;
6634
+ } catch {
6635
+ return false;
6636
+ }
6637
+ }
6638
+ /**
6603
6639
  * Capture the full git diff for HEAD (working tree vs current commit).
6604
6640
  *
6605
6641
  * Runs `git diff HEAD` in the specified working directory and returns
@@ -6613,6 +6649,10 @@ const logger$19 = createLogger("compiled-workflows:git-helpers");
6613
6649
  * @returns The diff output string, or '' on error
6614
6650
  */
6615
6651
  async function getGitDiffSummary(workingDirectory = process.cwd()) {
6652
+ if (!hasCommits(workingDirectory)) {
6653
+ logger$19.debug({ cwd: workingDirectory }, "No commits in repo — returning empty diff");
6654
+ return "";
6655
+ }
6616
6656
  return runGitCommand(["diff", "HEAD"], workingDirectory, "git-diff-summary");
6617
6657
  }
6618
6658
  /**
@@ -6626,6 +6666,10 @@ async function getGitDiffSummary(workingDirectory = process.cwd()) {
6626
6666
  * @returns The stat summary string, or '' on error
6627
6667
  */
6628
6668
  async function getGitDiffStatSummary(workingDirectory = process.cwd()) {
6669
+ if (!hasCommits(workingDirectory)) {
6670
+ logger$19.debug({ cwd: workingDirectory }, "No commits in repo — returning empty stat");
6671
+ return "";
6672
+ }
6629
6673
  return runGitCommand([
6630
6674
  "diff",
6631
6675
  "--stat",
@@ -6648,6 +6692,10 @@ async function getGitDiffStatSummary(workingDirectory = process.cwd()) {
6648
6692
  */
6649
6693
  async function getGitDiffForFiles(files, workingDirectory = process.cwd()) {
6650
6694
  if (files.length === 0) return "";
6695
+ if (!hasCommits(workingDirectory)) {
6696
+ logger$19.debug({ cwd: workingDirectory }, "No commits in repo — returning empty diff for files");
6697
+ return "";
6698
+ }
6651
6699
  await stageIntentToAdd(files, workingDirectory);
6652
6700
  return runGitCommand([
6653
6701
  "diff",
@@ -6671,6 +6719,10 @@ async function getGitDiffForFiles(files, workingDirectory = process.cwd()) {
6671
6719
  */
6672
6720
  async function getGitDiffStatForFiles(files, workingDirectory = process.cwd()) {
6673
6721
  if (files.length === 0) return "";
6722
+ if (!hasCommits(workingDirectory)) {
6723
+ logger$19.debug({ cwd: workingDirectory }, "No commits in repo — returning empty stat for files");
6724
+ return "";
6725
+ }
6674
6726
  return runGitCommand([
6675
6727
  "diff",
6676
6728
  "--stat",
@@ -10706,22 +10758,49 @@ var IngestionServer = class {
10706
10758
  /**
10707
10759
  * Start the HTTP ingestion server.
10708
10760
  * Resolves when the server is listening and ready to accept connections.
10761
+ * On EADDRINUSE, retries up to 10 consecutive ports before failing.
10709
10762
  */
10710
10763
  async start() {
10711
10764
  if (this._server !== null) {
10712
10765
  logger$8.warn("IngestionServer.start() called while already started — ignoring");
10713
10766
  return;
10714
10767
  }
10768
+ const maxRetries = 10;
10769
+ let lastErr;
10770
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
10771
+ const port = this._port + attempt;
10772
+ try {
10773
+ await this._tryListen(port);
10774
+ return;
10775
+ } catch (err) {
10776
+ const nodeErr = err;
10777
+ if (nodeErr.code === "EADDRINUSE" && attempt < maxRetries) {
10778
+ logger$8.warn({
10779
+ port,
10780
+ attempt: attempt + 1
10781
+ }, `Port ${port} in use — trying ${port + 1}`);
10782
+ lastErr = nodeErr;
10783
+ continue;
10784
+ }
10785
+ throw err;
10786
+ }
10787
+ }
10788
+ throw lastErr ?? new Error("IngestionServer: exhausted port retry attempts");
10789
+ }
10790
+ _tryListen(port) {
10715
10791
  return new Promise((resolve$2, reject) => {
10716
10792
  const server = createServer(this._handleRequest.bind(this));
10717
10793
  server.on("error", (err) => {
10718
- logger$8.error({ err }, "IngestionServer failed to start");
10794
+ server.close();
10719
10795
  reject(err);
10720
10796
  });
10721
- server.listen(this._port, "127.0.0.1", () => {
10797
+ server.listen(port, "127.0.0.1", () => {
10722
10798
  this._server = server;
10723
10799
  const addr = server.address();
10724
- logger$8.info({ port: addr.port }, "IngestionServer listening");
10800
+ logger$8.info({
10801
+ port: addr.port,
10802
+ requestedPort: this._port
10803
+ }, "IngestionServer listening");
10725
10804
  this._buffer?.start();
10726
10805
  resolve$2();
10727
10806
  });
@@ -13190,7 +13269,8 @@ function wgStatusForPhase(phase) {
13190
13269
  case "IN_TEST_PLANNING":
13191
13270
  case "IN_DEV":
13192
13271
  case "IN_REVIEW":
13193
- case "NEEDS_FIXES": return "in_progress";
13272
+ case "NEEDS_FIXES":
13273
+ case "CHECKPOINT": return "in_progress";
13194
13274
  case "COMPLETE": return "complete";
13195
13275
  case "ESCALATED": return "escalated";
13196
13276
  }
@@ -13279,6 +13359,7 @@ function createImplementationOrchestrator(deps) {
13279
13359
  let _contractMismatches;
13280
13360
  let _otlpEndpoint;
13281
13361
  const _stateStoreCache = new Map();
13362
+ const _checkpoints = new Map();
13282
13363
  const MEMORY_PRESSURE_BACKOFF_MS = [
13283
13364
  3e4,
13284
13365
  6e4,
@@ -13489,7 +13570,8 @@ function createImplementationOrchestrator(deps) {
13489
13570
  lastVerdict: record.lastVerdict,
13490
13571
  error: record.error,
13491
13572
  startedAt: record.startedAt,
13492
- completedAt: record.completedAt
13573
+ completedAt: record.completedAt,
13574
+ checkpointFilesCount: record.checkpointFilesCount
13493
13575
  };
13494
13576
  for (const [key, s] of _stories) stories[key] = { ...s };
13495
13577
  const status = {
@@ -13562,7 +13644,8 @@ function createImplementationOrchestrator(deps) {
13562
13644
  error: state.error,
13563
13645
  startedAt: state.startedAt,
13564
13646
  completedAt: state.completedAt,
13565
- sprint: config.sprint
13647
+ sprint: config.sprint,
13648
+ checkpointFilesCount: state.checkpointFilesCount
13566
13649
  };
13567
13650
  await stateStore.setStoryState(storyKey, record);
13568
13651
  } catch (err) {
@@ -14165,7 +14248,187 @@ function createImplementationOrchestrator(deps) {
14165
14248
  result: devResult
14166
14249
  });
14167
14250
  await persistState();
14168
- if (devResult.result === "success") devStoryWasSuccess = true;
14251
+ let checkpointHandled = false;
14252
+ if (devResult.result === "failed" && devResult.error?.startsWith("dispatch_timeout")) {
14253
+ endPhase(storyKey, "dev-story");
14254
+ const timeoutFiles = checkGitDiffFiles(projectRoot ?? process.cwd());
14255
+ if (timeoutFiles.length === 0) {
14256
+ logger$25.warn({ storyKey }, "Dev-story timeout with zero modified files — escalating immediately (no checkpoint)");
14257
+ updateStory(storyKey, {
14258
+ phase: "ESCALATED",
14259
+ error: "timeout-no-files",
14260
+ completedAt: new Date().toISOString()
14261
+ });
14262
+ await writeStoryMetricsBestEffort(storyKey, "escalated", 0);
14263
+ await emitEscalation({
14264
+ storyKey,
14265
+ lastVerdict: "timeout-no-files",
14266
+ reviewCycles: 0,
14267
+ issues: ["dev-story timed out with no partial files — nothing to checkpoint"]
14268
+ });
14269
+ await persistState();
14270
+ return;
14271
+ }
14272
+ logger$25.info({
14273
+ storyKey,
14274
+ filesCount: timeoutFiles.length
14275
+ }, "Dev-story timeout with partial files — capturing checkpoint");
14276
+ let gitDiff = "";
14277
+ try {
14278
+ gitDiff = execSync(`git diff HEAD -- ${timeoutFiles.map((f) => `"${f}"`).join(" ")}`, {
14279
+ cwd: projectRoot ?? process.cwd(),
14280
+ encoding: "utf-8",
14281
+ timeout: 1e4,
14282
+ stdio: [
14283
+ "ignore",
14284
+ "pipe",
14285
+ "pipe"
14286
+ ]
14287
+ }).trim();
14288
+ } catch (diffErr) {
14289
+ logger$25.warn({
14290
+ storyKey,
14291
+ error: diffErr instanceof Error ? diffErr.message : String(diffErr)
14292
+ }, "Failed to capture git diff for checkpoint — proceeding with empty diff");
14293
+ }
14294
+ _checkpoints.set(storyKey, {
14295
+ filesModified: timeoutFiles,
14296
+ gitDiff,
14297
+ partialOutput: devResult.error ?? ""
14298
+ });
14299
+ updateStory(storyKey, {
14300
+ phase: "CHECKPOINT",
14301
+ checkpointFilesCount: timeoutFiles.length
14302
+ });
14303
+ const diffSizeBytes = Buffer.byteLength(gitDiff, "utf-8");
14304
+ eventBus.emit("story:checkpoint-saved", {
14305
+ storyKey,
14306
+ filesCount: timeoutFiles.length,
14307
+ diffSizeBytes
14308
+ });
14309
+ if (stateStore !== void 0) stateStore.recordMetric({
14310
+ storyKey,
14311
+ taskType: "dev-story",
14312
+ result: "timeout",
14313
+ recordedAt: new Date().toISOString(),
14314
+ sprint: config.sprint
14315
+ }).catch((storeErr) => {
14316
+ logger$25.warn({
14317
+ err: storeErr,
14318
+ storyKey
14319
+ }, "Failed to record timeout metric to StateStore (best-effort)");
14320
+ });
14321
+ await persistState();
14322
+ eventBus.emit("story:checkpoint-retry", {
14323
+ storyKey,
14324
+ filesCount: timeoutFiles.length,
14325
+ attempt: 2
14326
+ });
14327
+ const checkpointData = _checkpoints.get(storyKey);
14328
+ let checkpointRetryPrompt;
14329
+ let checkpointRetryMaxTurns;
14330
+ try {
14331
+ const devStoryTemplate = await pack.getPrompt("dev-story");
14332
+ const storyContent = await readFile$1(storyFilePath ?? "", "utf-8");
14333
+ const complexity = computeStoryComplexity(storyContent);
14334
+ checkpointRetryMaxTurns = resolveDevStoryMaxTurns(complexity.complexityScore);
14335
+ logComplexityResult(storyKey, complexity, checkpointRetryMaxTurns);
14336
+ let archConstraints = "";
14337
+ try {
14338
+ const decisions = await getDecisionsByPhase(db, "solutioning");
14339
+ const constraints = decisions.filter((d) => d.category === "architecture");
14340
+ archConstraints = constraints.map((d) => `${d.key}: ${d.value}`).join("\n");
14341
+ } catch {}
14342
+ const checkpointContext = [
14343
+ "Your prior attempt timed out. Here is the work you completed:",
14344
+ "",
14345
+ `Files modified (${checkpointData.filesModified.length}):`,
14346
+ ...checkpointData.filesModified.map((f) => `- ${f}`),
14347
+ "",
14348
+ "```diff",
14349
+ checkpointData.gitDiff || "(no diff available)",
14350
+ "```",
14351
+ "",
14352
+ "Continue from where you left off. Do not redo completed work."
14353
+ ].join("\n");
14354
+ const sections = [
14355
+ {
14356
+ name: "story_content",
14357
+ content: storyContent,
14358
+ priority: "required"
14359
+ },
14360
+ {
14361
+ name: "checkpoint_context",
14362
+ content: checkpointContext,
14363
+ priority: "required"
14364
+ },
14365
+ {
14366
+ name: "arch_constraints",
14367
+ content: archConstraints,
14368
+ priority: "optional"
14369
+ }
14370
+ ];
14371
+ const assembled = assemblePrompt(devStoryTemplate, sections, 24e3);
14372
+ checkpointRetryPrompt = assembled.prompt;
14373
+ } catch {
14374
+ checkpointRetryPrompt = `Continue story ${storyKey} from checkpoint. Your prior attempt timed out. Do not redo completed work.`;
14375
+ logger$25.warn({ storyKey }, "Failed to assemble checkpoint retry prompt — using fallback");
14376
+ }
14377
+ logger$25.info({
14378
+ storyKey,
14379
+ filesCount: checkpointData.filesModified.length
14380
+ }, "Dispatching checkpoint retry for timed-out story");
14381
+ incrementDispatches(storyKey);
14382
+ updateStory(storyKey, { phase: "IN_DEV" });
14383
+ startPhase(storyKey, "dev-story-retry");
14384
+ const checkpointRetryHandle = dispatcher.dispatch({
14385
+ prompt: checkpointRetryPrompt,
14386
+ agent: "claude-code",
14387
+ taskType: "dev-story",
14388
+ outputSchema: DevStoryResultSchema,
14389
+ ...checkpointRetryMaxTurns !== void 0 ? { maxTurns: checkpointRetryMaxTurns } : {},
14390
+ ...projectRoot !== void 0 ? { workingDirectory: projectRoot } : {},
14391
+ ..._otlpEndpoint !== void 0 ? { otlpEndpoint: _otlpEndpoint } : {},
14392
+ ...config.perStoryContextCeilings?.[storyKey] !== void 0 ? { maxContextTokens: config.perStoryContextCeilings[storyKey] } : {},
14393
+ storyKey
14394
+ });
14395
+ const checkpointRetryResult = await checkpointRetryHandle.result;
14396
+ endPhase(storyKey, "dev-story-retry");
14397
+ eventBus.emit("orchestrator:story-phase-complete", {
14398
+ storyKey,
14399
+ phase: "IN_DEV",
14400
+ result: { tokenUsage: checkpointRetryResult.tokenEstimate ? {
14401
+ input: checkpointRetryResult.tokenEstimate.input,
14402
+ output: checkpointRetryResult.tokenEstimate.output
14403
+ } : void 0 }
14404
+ });
14405
+ if (checkpointRetryResult.status === "timeout") {
14406
+ logger$25.warn({ storyKey }, "Checkpoint retry dispatch timed out — escalating story");
14407
+ updateStory(storyKey, {
14408
+ phase: "ESCALATED",
14409
+ error: "checkpoint-retry-timeout",
14410
+ completedAt: new Date().toISOString()
14411
+ });
14412
+ await writeStoryMetricsBestEffort(storyKey, "escalated", 0);
14413
+ await emitEscalation({
14414
+ storyKey,
14415
+ lastVerdict: "checkpoint-retry-timeout",
14416
+ reviewCycles: 0,
14417
+ issues: ["checkpoint retry timed out — no infinite retry loop"]
14418
+ });
14419
+ await persistState();
14420
+ return;
14421
+ }
14422
+ const retryParsed = checkpointRetryResult.parsed;
14423
+ devFilesModified = retryParsed?.files_modified ?? checkGitDiffFiles(projectRoot ?? process.cwd());
14424
+ if (checkpointRetryResult.status === "completed" && retryParsed?.result === "success") devStoryWasSuccess = true;
14425
+ else logger$25.warn({
14426
+ storyKey,
14427
+ status: checkpointRetryResult.status
14428
+ }, "Checkpoint retry completed with failure — proceeding to code review");
14429
+ checkpointHandled = true;
14430
+ }
14431
+ if (!checkpointHandled) if (devResult.result === "success") devStoryWasSuccess = true;
14169
14432
  else logger$25.warn({
14170
14433
  storyKey,
14171
14434
  error: devResult.error,
@@ -19715,6 +19978,189 @@ function mapInternalPhaseToEventPhase(internalPhase) {
19715
19978
  default: return null;
19716
19979
  }
19717
19980
  }
19981
+ /**
19982
+ * Wire all NDJSON event subscriptions from the event bus to the emitter.
19983
+ * Shared helper called from both the implementation-only path and the full
19984
+ * pipeline path (AC2: shared event subscription logic).
19985
+ */
19986
+ function wireNdjsonEmitter(eventBus, ndjsonEmitter) {
19987
+ eventBus.on("orchestrator:story-phase-start", (payload) => {
19988
+ const phase = mapInternalPhaseToEventPhase(payload.phase);
19989
+ if (phase !== null) ndjsonEmitter.emit({
19990
+ type: "story:phase",
19991
+ ts: new Date().toISOString(),
19992
+ key: payload.storyKey,
19993
+ phase,
19994
+ status: "in_progress"
19995
+ });
19996
+ });
19997
+ eventBus.on("orchestrator:story-phase-complete", (payload) => {
19998
+ const phase = mapInternalPhaseToEventPhase(payload.phase);
19999
+ if (phase !== null) {
20000
+ const result = payload.result;
20001
+ ndjsonEmitter.emit({
20002
+ type: "story:phase",
20003
+ ts: new Date().toISOString(),
20004
+ key: payload.storyKey,
20005
+ phase,
20006
+ status: "complete",
20007
+ ...phase === "code-review" && result?.verdict !== void 0 ? { verdict: result.verdict } : {},
20008
+ ...phase === "create-story" && result?.story_file !== void 0 ? { file: result.story_file } : {}
20009
+ });
20010
+ }
20011
+ });
20012
+ eventBus.on("routing:model-selected", (payload) => {
20013
+ ndjsonEmitter.emit({
20014
+ type: "routing:model-selected",
20015
+ ts: new Date().toISOString(),
20016
+ dispatch_id: payload.dispatchId,
20017
+ task_type: payload.taskType,
20018
+ phase: payload.phase,
20019
+ model: payload.model,
20020
+ source: payload.source
20021
+ });
20022
+ });
20023
+ eventBus.on("orchestrator:story-complete", (payload) => {
20024
+ ndjsonEmitter.emit({
20025
+ type: "story:done",
20026
+ ts: new Date().toISOString(),
20027
+ key: payload.storyKey,
20028
+ result: "success",
20029
+ review_cycles: payload.reviewCycles
20030
+ });
20031
+ });
20032
+ eventBus.on("orchestrator:story-escalated", (payload) => {
20033
+ const rawIssues = Array.isArray(payload.issues) ? payload.issues : [];
20034
+ const issues = rawIssues.map((issue) => {
20035
+ const iss = issue;
20036
+ return {
20037
+ severity: iss.severity ?? "unknown",
20038
+ file: iss.file ?? "",
20039
+ desc: iss.desc ?? iss.description ?? ""
20040
+ };
20041
+ });
20042
+ ndjsonEmitter.emit({
20043
+ type: "story:escalation",
20044
+ ts: new Date().toISOString(),
20045
+ key: payload.storyKey,
20046
+ reason: payload.lastVerdict ?? "escalated",
20047
+ cycles: payload.reviewCycles ?? 0,
20048
+ issues,
20049
+ ...payload.diagnosis !== void 0 ? { diagnosis: payload.diagnosis } : {}
20050
+ });
20051
+ });
20052
+ eventBus.on("orchestrator:story-warn", (payload) => {
20053
+ ndjsonEmitter.emit({
20054
+ type: "story:warn",
20055
+ ts: new Date().toISOString(),
20056
+ key: payload.storyKey,
20057
+ msg: payload.msg
20058
+ });
20059
+ });
20060
+ eventBus.on("orchestrator:heartbeat", (payload) => {
20061
+ ndjsonEmitter.emit({
20062
+ type: "pipeline:heartbeat",
20063
+ ts: new Date().toISOString(),
20064
+ run_id: payload.runId,
20065
+ active_dispatches: payload.activeDispatches,
20066
+ completed_dispatches: payload.completedDispatches,
20067
+ queued_dispatches: payload.queuedDispatches
20068
+ });
20069
+ });
20070
+ eventBus.on("orchestrator:stall", (payload) => {
20071
+ ndjsonEmitter.emit({
20072
+ type: "story:stall",
20073
+ ts: new Date().toISOString(),
20074
+ run_id: payload.runId,
20075
+ story_key: payload.storyKey,
20076
+ phase: payload.phase,
20077
+ elapsed_ms: payload.elapsedMs,
20078
+ child_pids: payload.childPids,
20079
+ child_active: payload.childActive
20080
+ });
20081
+ });
20082
+ eventBus.on("orchestrator:zero-diff-escalation", (payload) => {
20083
+ ndjsonEmitter.emit({
20084
+ type: "story:zero-diff-escalation",
20085
+ ts: new Date().toISOString(),
20086
+ storyKey: payload.storyKey,
20087
+ reason: payload.reason
20088
+ });
20089
+ });
20090
+ eventBus.on("story:build-verification-passed", (payload) => {
20091
+ ndjsonEmitter.emit({
20092
+ type: "story:build-verification-passed",
20093
+ ts: new Date().toISOString(),
20094
+ storyKey: payload.storyKey
20095
+ });
20096
+ });
20097
+ eventBus.on("story:build-verification-failed", (payload) => {
20098
+ ndjsonEmitter.emit({
20099
+ type: "story:build-verification-failed",
20100
+ ts: new Date().toISOString(),
20101
+ storyKey: payload.storyKey,
20102
+ exitCode: payload.exitCode,
20103
+ output: payload.output
20104
+ });
20105
+ });
20106
+ eventBus.on("story:interface-change-warning", (payload) => {
20107
+ ndjsonEmitter.emit({
20108
+ type: "story:interface-change-warning",
20109
+ ts: new Date().toISOString(),
20110
+ storyKey: payload.storyKey,
20111
+ modifiedInterfaces: payload.modifiedInterfaces,
20112
+ potentiallyAffectedTests: payload.potentiallyAffectedTests
20113
+ });
20114
+ });
20115
+ eventBus.on("story:metrics", (payload) => {
20116
+ ndjsonEmitter.emit({
20117
+ type: "story:metrics",
20118
+ ts: new Date().toISOString(),
20119
+ storyKey: payload.storyKey,
20120
+ wallClockMs: payload.wallClockMs,
20121
+ phaseBreakdown: payload.phaseBreakdown,
20122
+ tokens: payload.tokens,
20123
+ reviewCycles: payload.reviewCycles,
20124
+ dispatches: payload.dispatches
20125
+ });
20126
+ });
20127
+ eventBus.on("pipeline:pre-flight-failure", (payload) => {
20128
+ ndjsonEmitter.emit({
20129
+ type: "pipeline:pre-flight-failure",
20130
+ ts: new Date().toISOString(),
20131
+ exitCode: payload.exitCode,
20132
+ output: payload.output + "\nTip: Use --skip-preflight to bypass, or check your build command in .substrate/project-profile.yaml"
20133
+ });
20134
+ });
20135
+ eventBus.on("pipeline:contract-mismatch", (payload) => {
20136
+ ndjsonEmitter.emit({
20137
+ type: "pipeline:contract-mismatch",
20138
+ ts: new Date().toISOString(),
20139
+ exporter: payload.exporter,
20140
+ importer: payload.importer,
20141
+ contractName: payload.contractName,
20142
+ mismatchDescription: payload.mismatchDescription
20143
+ });
20144
+ });
20145
+ eventBus.on("pipeline:contract-verification-summary", (payload) => {
20146
+ ndjsonEmitter.emit({
20147
+ type: "pipeline:contract-verification-summary",
20148
+ ts: new Date().toISOString(),
20149
+ verified: payload.verified,
20150
+ stalePruned: payload.stalePruned,
20151
+ mismatches: payload.mismatches,
20152
+ verdict: payload.verdict
20153
+ });
20154
+ });
20155
+ eventBus.on("pipeline:profile-stale", (payload) => {
20156
+ ndjsonEmitter.emit({
20157
+ type: "pipeline:profile-stale",
20158
+ ts: new Date().toISOString(),
20159
+ message: payload.message,
20160
+ indicators: payload.indicators
20161
+ });
20162
+ });
20163
+ }
19718
20164
  async function runRunAction(options) {
19719
20165
  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, epic: epicNumber, dryRun, maxReviewCycles = 2, registry: injectedRegistry } = options;
19720
20166
  if (startPhase !== void 0 && !VALID_PHASES.includes(startPhase)) {
@@ -19805,7 +20251,25 @@ async function runRunAction(options) {
19805
20251
  }
19806
20252
  }
19807
20253
  let effectiveStartPhase = startPhase;
19808
- if (effectiveStartPhase === void 0) {
20254
+ if (effectiveStartPhase === void 0) if (parsedStoryKeys.length > 0) {
20255
+ const artifactsDir = join(projectRoot, "_bmad-output", "implementation-artifacts");
20256
+ if (existsSync(artifactsDir)) {
20257
+ let files;
20258
+ try {
20259
+ const result = readdirSync(artifactsDir, { encoding: "utf-8" });
20260
+ if (Array.isArray(result)) files = result;
20261
+ } catch {}
20262
+ if (files !== void 0) for (const key of parsedStoryKeys) {
20263
+ const found = files.some((f) => f.startsWith(`${key}-`) && f.endsWith(".md"));
20264
+ if (!found) {
20265
+ const errorMsg = `Story file not found for key: ${key}`;
20266
+ if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
20267
+ else process.stderr.write(`Error: ${errorMsg}\n`);
20268
+ return 1;
20269
+ }
20270
+ }
20271
+ }
20272
+ } else {
19809
20273
  mkdirSync(dbDir, { recursive: true });
19810
20274
  try {
19811
20275
  const detectAdapter = createDatabaseAdapter({
@@ -19940,6 +20404,24 @@ async function runRunAction(options) {
19940
20404
  ...parsedStoryKeys.length > 0 ? { explicitStories: parsedStoryKeys } : {}
19941
20405
  })
19942
20406
  });
20407
+ const runIdFilePath = join(dbDir, "current-run-id");
20408
+ try {
20409
+ writeFileSync(runIdFilePath, pipelineRun.id, "utf-8");
20410
+ const cleanupRunIdFile = () => {
20411
+ try {
20412
+ unlinkSync(runIdFilePath);
20413
+ } catch {}
20414
+ };
20415
+ process.on("exit", cleanupRunIdFile);
20416
+ process.once("SIGTERM", () => {
20417
+ cleanupRunIdFile();
20418
+ process.exit(0);
20419
+ });
20420
+ process.once("SIGINT", () => {
20421
+ cleanupRunIdFile();
20422
+ process.exit(130);
20423
+ });
20424
+ } catch {}
19943
20425
  const eventBus = createEventBus();
19944
20426
  const contextCompiler = createContextCompiler({ db: adapter });
19945
20427
  if (!injectedRegistry) throw new Error("AdapterRegistry is required — must be initialized at CLI startup");
@@ -20213,182 +20695,7 @@ async function runRunAction(options) {
20213
20695
  stories: storyKeys,
20214
20696
  concurrency
20215
20697
  });
20216
- eventBus.on("orchestrator:story-phase-start", (payload) => {
20217
- const phase = mapInternalPhaseToEventPhase(payload.phase);
20218
- if (phase !== null) ndjsonEmitter.emit({
20219
- type: "story:phase",
20220
- ts: new Date().toISOString(),
20221
- key: payload.storyKey,
20222
- phase,
20223
- status: "in_progress"
20224
- });
20225
- });
20226
- eventBus.on("orchestrator:story-phase-complete", (payload) => {
20227
- const phase = mapInternalPhaseToEventPhase(payload.phase);
20228
- if (phase !== null) {
20229
- const result = payload.result;
20230
- ndjsonEmitter.emit({
20231
- type: "story:phase",
20232
- ts: new Date().toISOString(),
20233
- key: payload.storyKey,
20234
- phase,
20235
- status: "complete",
20236
- ...phase === "code-review" && result?.verdict !== void 0 ? { verdict: result.verdict } : {},
20237
- ...phase === "create-story" && result?.story_file !== void 0 ? { file: result.story_file } : {}
20238
- });
20239
- }
20240
- });
20241
- eventBus.on("routing:model-selected", (payload) => {
20242
- ndjsonEmitter.emit({
20243
- type: "routing:model-selected",
20244
- ts: new Date().toISOString(),
20245
- dispatch_id: payload.dispatchId,
20246
- task_type: payload.taskType,
20247
- phase: payload.phase,
20248
- model: payload.model,
20249
- source: payload.source
20250
- });
20251
- });
20252
- eventBus.on("orchestrator:story-complete", (payload) => {
20253
- ndjsonEmitter.emit({
20254
- type: "story:done",
20255
- ts: new Date().toISOString(),
20256
- key: payload.storyKey,
20257
- result: "success",
20258
- review_cycles: payload.reviewCycles
20259
- });
20260
- });
20261
- eventBus.on("orchestrator:story-escalated", (payload) => {
20262
- const rawIssues = Array.isArray(payload.issues) ? payload.issues : [];
20263
- const issues = rawIssues.map((issue) => {
20264
- const iss = issue;
20265
- return {
20266
- severity: iss.severity ?? "unknown",
20267
- file: iss.file ?? "",
20268
- desc: iss.desc ?? iss.description ?? ""
20269
- };
20270
- });
20271
- ndjsonEmitter.emit({
20272
- type: "story:escalation",
20273
- ts: new Date().toISOString(),
20274
- key: payload.storyKey,
20275
- reason: payload.lastVerdict ?? "escalated",
20276
- cycles: payload.reviewCycles ?? 0,
20277
- issues,
20278
- ...payload.diagnosis !== void 0 ? { diagnosis: payload.diagnosis } : {}
20279
- });
20280
- });
20281
- eventBus.on("orchestrator:story-warn", (payload) => {
20282
- ndjsonEmitter.emit({
20283
- type: "story:warn",
20284
- ts: new Date().toISOString(),
20285
- key: payload.storyKey,
20286
- msg: payload.msg
20287
- });
20288
- });
20289
- eventBus.on("orchestrator:heartbeat", (payload) => {
20290
- ndjsonEmitter.emit({
20291
- type: "pipeline:heartbeat",
20292
- ts: new Date().toISOString(),
20293
- run_id: payload.runId,
20294
- active_dispatches: payload.activeDispatches,
20295
- completed_dispatches: payload.completedDispatches,
20296
- queued_dispatches: payload.queuedDispatches
20297
- });
20298
- });
20299
- eventBus.on("orchestrator:stall", (payload) => {
20300
- ndjsonEmitter.emit({
20301
- type: "story:stall",
20302
- ts: new Date().toISOString(),
20303
- run_id: payload.runId,
20304
- story_key: payload.storyKey,
20305
- phase: payload.phase,
20306
- elapsed_ms: payload.elapsedMs,
20307
- child_pids: payload.childPids,
20308
- child_active: payload.childActive
20309
- });
20310
- });
20311
- eventBus.on("orchestrator:zero-diff-escalation", (payload) => {
20312
- ndjsonEmitter.emit({
20313
- type: "story:zero-diff-escalation",
20314
- ts: new Date().toISOString(),
20315
- storyKey: payload.storyKey,
20316
- reason: payload.reason
20317
- });
20318
- });
20319
- eventBus.on("story:build-verification-passed", (payload) => {
20320
- ndjsonEmitter.emit({
20321
- type: "story:build-verification-passed",
20322
- ts: new Date().toISOString(),
20323
- storyKey: payload.storyKey
20324
- });
20325
- });
20326
- eventBus.on("story:build-verification-failed", (payload) => {
20327
- ndjsonEmitter.emit({
20328
- type: "story:build-verification-failed",
20329
- ts: new Date().toISOString(),
20330
- storyKey: payload.storyKey,
20331
- exitCode: payload.exitCode,
20332
- output: payload.output
20333
- });
20334
- });
20335
- eventBus.on("story:interface-change-warning", (payload) => {
20336
- ndjsonEmitter.emit({
20337
- type: "story:interface-change-warning",
20338
- ts: new Date().toISOString(),
20339
- storyKey: payload.storyKey,
20340
- modifiedInterfaces: payload.modifiedInterfaces,
20341
- potentiallyAffectedTests: payload.potentiallyAffectedTests
20342
- });
20343
- });
20344
- eventBus.on("story:metrics", (payload) => {
20345
- ndjsonEmitter.emit({
20346
- type: "story:metrics",
20347
- ts: new Date().toISOString(),
20348
- storyKey: payload.storyKey,
20349
- wallClockMs: payload.wallClockMs,
20350
- phaseBreakdown: payload.phaseBreakdown,
20351
- tokens: payload.tokens,
20352
- reviewCycles: payload.reviewCycles,
20353
- dispatches: payload.dispatches
20354
- });
20355
- });
20356
- eventBus.on("pipeline:pre-flight-failure", (payload) => {
20357
- ndjsonEmitter.emit({
20358
- type: "pipeline:pre-flight-failure",
20359
- ts: new Date().toISOString(),
20360
- exitCode: payload.exitCode,
20361
- output: payload.output + "\nTip: Use --skip-preflight to bypass, or check your build command in .substrate/project-profile.yaml"
20362
- });
20363
- });
20364
- eventBus.on("pipeline:contract-mismatch", (payload) => {
20365
- ndjsonEmitter.emit({
20366
- type: "pipeline:contract-mismatch",
20367
- ts: new Date().toISOString(),
20368
- exporter: payload.exporter,
20369
- importer: payload.importer,
20370
- contractName: payload.contractName,
20371
- mismatchDescription: payload.mismatchDescription
20372
- });
20373
- });
20374
- eventBus.on("pipeline:contract-verification-summary", (payload) => {
20375
- ndjsonEmitter.emit({
20376
- type: "pipeline:contract-verification-summary",
20377
- ts: new Date().toISOString(),
20378
- verified: payload.verified,
20379
- stalePruned: payload.stalePruned,
20380
- mismatches: payload.mismatches,
20381
- verdict: payload.verdict
20382
- });
20383
- });
20384
- eventBus.on("pipeline:profile-stale", (payload) => {
20385
- ndjsonEmitter.emit({
20386
- type: "pipeline:profile-stale",
20387
- ts: new Date().toISOString(),
20388
- message: payload.message,
20389
- indicators: payload.indicators
20390
- });
20391
- });
20698
+ wireNdjsonEmitter(eventBus, ndjsonEmitter);
20392
20699
  }
20393
20700
  const ingestionServer = telemetryEnabled ? new IngestionServer({ port: telemetryPort }) : void 0;
20394
20701
  if (ingestionServer !== void 0) {
@@ -20609,6 +20916,24 @@ async function runFullPipeline(options) {
20609
20916
  });
20610
20917
  const startedAt = Date.now();
20611
20918
  const runId = await phaseOrchestrator.startRun(concept ?? "", startPhase);
20919
+ const runIdFilePath = join(dbDir, "current-run-id");
20920
+ try {
20921
+ writeFileSync(runIdFilePath, runId, "utf-8");
20922
+ const cleanupRunIdFile = () => {
20923
+ try {
20924
+ unlinkSync(runIdFilePath);
20925
+ } catch {}
20926
+ };
20927
+ process.on("exit", cleanupRunIdFile);
20928
+ process.once("SIGTERM", () => {
20929
+ cleanupRunIdFile();
20930
+ process.exit(0);
20931
+ });
20932
+ process.once("SIGINT", () => {
20933
+ cleanupRunIdFile();
20934
+ process.exit(130);
20935
+ });
20936
+ } catch {}
20612
20937
  if (explicitStories !== void 0 && explicitStories.length > 0 || options.epic !== void 0) {
20613
20938
  const existingRun = (await adapter.query("SELECT config_json FROM pipeline_runs WHERE id = ?", [runId]))[0];
20614
20939
  const existing = JSON.parse(existingRun?.config_json ?? "{}");
@@ -20623,14 +20948,34 @@ async function runFullPipeline(options) {
20623
20948
  process.stdout.write(`Starting full pipeline from phase: ${startPhase}\n`);
20624
20949
  process.stdout.write(`Pipeline run ID: ${runId}\n`);
20625
20950
  }
20951
+ let fullPipelineNdjsonEmitter;
20952
+ if (eventsFlag === true) {
20953
+ fullPipelineNdjsonEmitter = createEventEmitter(process.stdout);
20954
+ fullPipelineNdjsonEmitter.emit({
20955
+ type: "pipeline:start",
20956
+ ts: new Date().toISOString(),
20957
+ run_id: runId,
20958
+ stories: explicitStories ?? [],
20959
+ concurrency
20960
+ });
20961
+ wireNdjsonEmitter(eventBus, fullPipelineNdjsonEmitter);
20962
+ }
20626
20963
  const phaseOrder = [];
20627
20964
  if (effectiveResearch) phaseOrder.push("research");
20628
20965
  phaseOrder.push("analysis", "planning");
20629
20966
  if (effectiveUxDesign) phaseOrder.push("ux-design");
20630
20967
  phaseOrder.push("solutioning", "implementation");
20631
20968
  const startIdx = phaseOrder.indexOf(startPhase);
20969
+ const fpSucceededKeys = [];
20970
+ const fpFailedKeys = [];
20971
+ const fpEscalatedKeys = [];
20632
20972
  for (let i = startIdx; i < phaseOrder.length; i++) {
20633
20973
  const currentPhase = phaseOrder[i];
20974
+ if (fullPipelineNdjsonEmitter !== void 0) fullPipelineNdjsonEmitter.emit({
20975
+ type: "pipeline:phase-start",
20976
+ ts: new Date().toISOString(),
20977
+ phase: currentPhase
20978
+ });
20634
20979
  if (outputFormat === "human") process.stdout.write(`\n[${currentPhase.toUpperCase()}] Starting...\n`);
20635
20980
  if (currentPhase === "analysis") {
20636
20981
  const result = await runAnalysisPhase(phaseDeps, {
@@ -20831,9 +21176,18 @@ async function runFullPipeline(options) {
20831
21176
  });
20832
21177
  if (storyKeys.length === 0 && outputFormat === "human") process.stdout.write("[IMPLEMENTATION] No stories found. Run solutioning first or pass --stories.\n");
20833
21178
  if (outputFormat === "human") process.stdout.write(`[IMPLEMENTATION] Starting ${storyKeys.length} stories with concurrency=${concurrency}\n`);
20834
- await orchestrator.run(storyKeys);
21179
+ const implStatus = await orchestrator.run(storyKeys);
20835
21180
  if (outputFormat === "human") process.stdout.write("[IMPLEMENTATION] Complete\n");
21181
+ for (const [key, s] of Object.entries(implStatus.stories)) if (s.phase === "COMPLETE") fpSucceededKeys.push(key);
21182
+ else if (s.phase === "ESCALATED") if (s.error !== void 0) fpFailedKeys.push(key);
21183
+ else fpEscalatedKeys.push(key);
21184
+ else fpFailedKeys.push(key);
20836
21185
  }
21186
+ if (fullPipelineNdjsonEmitter !== void 0) fullPipelineNdjsonEmitter.emit({
21187
+ type: "pipeline:phase-complete",
21188
+ ts: new Date().toISOString(),
21189
+ phase: currentPhase
21190
+ });
20837
21191
  if (stopAfter !== void 0 && currentPhase === stopAfter) {
20838
21192
  const gate = createStopAfterGate(stopAfter);
20839
21193
  if (gate.shouldHalt()) {
@@ -20882,6 +21236,13 @@ async function runFullPipeline(options) {
20882
21236
  process.stdout.write("\n");
20883
21237
  process.stdout.write(formatTokenTelemetry(tokenSummary, BMAD_BASELINE_TOKENS_FULL) + "\n");
20884
21238
  }
21239
+ if (fullPipelineNdjsonEmitter !== void 0) fullPipelineNdjsonEmitter.emit({
21240
+ type: "pipeline:complete",
21241
+ ts: new Date().toISOString(),
21242
+ succeeded: fpSucceededKeys,
21243
+ failed: fpFailedKeys,
21244
+ escalated: fpEscalatedKeys
21245
+ });
20885
21246
  return 0;
20886
21247
  } catch (err) {
20887
21248
  const msg = err instanceof Error ? err.message : String(err);
@@ -20941,4 +21302,4 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
20941
21302
 
20942
21303
  //#endregion
20943
21304
  export { AdapterTelemetryPersistence, AppError, DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createConfigSystem, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, registerRunCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
20944
- //# sourceMappingURL=run-CfF0-tVP.js.map
21305
+ //# sourceMappingURL=run-ohzPz3r1.js.map