substrate-ai 0.19.31 → 0.19.32

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, DoltMergeConflict, FileStateStore, RunManifest, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN, VALID_PHASES, WorkGraphRepository, __commonJS, __require, __toESM, applyConfigToGraph, buildPipelineStatusOutput, createDatabaseAdapter, createGraphOrchestrator, detectCycles, formatOutput, formatPipelineSummary, formatTokenTelemetry, inspectProcessTree, parseDbTimestampAsUtc, resolveGraphPath, resolveMainRepoRoot, validateStoryKey } from "./health-DUgvybiN.js";
1
+ import { BMAD_BASELINE_TOKENS_FULL, DoltMergeConflict, FileStateStore, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN, VALID_PHASES, WorkGraphRepository, __commonJS, __require, __toESM, buildPipelineStatusOutput, createDatabaseAdapter, detectCycles, formatOutput, formatPipelineSummary, formatTokenTelemetry, inspectProcessTree, parseDbTimestampAsUtc, resolveMainRepoRoot, validateStoryKey } from "./health-D--wDx-U.js";
2
2
  import { createLogger } from "./logger-KeHncl-f.js";
3
3
  import { TypedEventBusImpl, createEventBus, createTuiApp, isTuiCapable, printNonTtyWarning, sleep } from "./helpers-CElYrONe.js";
4
4
  import { ADVISORY_NOTES, Categorizer, ConsumerAnalyzer, DEFAULT_GLOBAL_SETTINGS, DispatcherImpl, DoltClient, ESCALATION_DIAGNOSIS, EXPERIMENT_RESULT, EfficiencyScorer, IngestionServer, LogTurnAnalyzer, OPERATIONAL_FINDING, Recommender, RoutingRecommender, RoutingResolver, RoutingTelemetry, RoutingTokenAccumulator, RoutingTuner, STORY_METRICS, STORY_OUTCOME, SubstrateConfigSchema, TEST_EXPANSION_FINDING, TEST_PLAN, TelemetryNormalizer, TelemetryPipeline, TurnAnalyzer, addTokenUsage, aggregateTokenUsageForRun, aggregateTokenUsageForStory, callLLM, createConfigSystem, createDatabaseAdapter$1, createDecision, createPipelineRun, createRequirement, detectInterfaceChanges, getArtifactByTypeForRun, getArtifactsByRun, getDecisionsByCategory, getDecisionsByPhase, getDecisionsByPhaseForRun, getLatestRun, getPipelineRunById, getRunMetrics, getRunningPipelineRuns, getStoryMetricsForRun, getTokenUsageSummary, initSchema, listRequirements, loadModelRoutingConfig, registerArtifact, updatePipelineRun, updatePipelineRunConfig, upsertDecision, writeRunMetrics, writeStoryMetrics } from "./dist-DYcDRyoS.js";
@@ -19,6 +19,7 @@ import { promisify } from "node:util";
19
19
  import { fileURLToPath } from "node:url";
20
20
  import { execFile as execFile$1, spawn as spawn$1 } from "child_process";
21
21
  import { promisify as promisify$1 } from "util";
22
+ import { FindingsInjector, RunManifest, applyConfigToGraph, createDefaultVerificationPipeline, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, extractTargetFilesFromStoryContent, resolveGraphPath } from "@substrate-ai/sdlc";
22
23
  import { createHash as createHash$1 } from "crypto";
23
24
  import * as readline from "readline";
24
25
  import Stream from "node:stream";
@@ -1706,6 +1707,76 @@ const PIPELINE_EVENT_METADATA = [
1706
1707
  description: "Total duration of all checks in milliseconds."
1707
1708
  }
1708
1709
  ]
1710
+ },
1711
+ {
1712
+ type: "cost:warning",
1713
+ description: "Cumulative pipeline cost has crossed 80% of the --cost-ceiling threshold.",
1714
+ when: "Emitted at most once per run, between story dispatches, when cumulative cost ≥ 80% of ceiling.",
1715
+ fields: [
1716
+ {
1717
+ name: "ts",
1718
+ type: "string",
1719
+ description: "Timestamp."
1720
+ },
1721
+ {
1722
+ name: "cumulative_cost",
1723
+ type: "number",
1724
+ description: "Cumulative pipeline cost in USD at time of check."
1725
+ },
1726
+ {
1727
+ name: "ceiling",
1728
+ type: "number",
1729
+ description: "Configured cost ceiling in USD."
1730
+ },
1731
+ {
1732
+ name: "percent_used",
1733
+ type: "number",
1734
+ description: "(cumulative / ceiling) * 100, rounded to two decimal places."
1735
+ }
1736
+ ]
1737
+ },
1738
+ {
1739
+ type: "cost:ceiling-reached",
1740
+ description: "Cost ceiling reached — remaining undispatched stories are skipped.",
1741
+ when: "Emitted between story dispatches when cumulative cost ≥ 100% of ceiling.",
1742
+ fields: [
1743
+ {
1744
+ name: "ts",
1745
+ type: "string",
1746
+ description: "Timestamp."
1747
+ },
1748
+ {
1749
+ name: "cumulative_cost",
1750
+ type: "number",
1751
+ description: "Cumulative pipeline cost in USD at time of check."
1752
+ },
1753
+ {
1754
+ name: "ceiling",
1755
+ type: "number",
1756
+ description: "Configured cost ceiling in USD."
1757
+ },
1758
+ {
1759
+ name: "halt_on",
1760
+ type: "string",
1761
+ description: "--halt-on value in effect (none, all, critical)."
1762
+ },
1763
+ {
1764
+ name: "action",
1765
+ type: "string",
1766
+ description: "Action taken (always stopped in this story; interactive prompt is Epic 54 scope)."
1767
+ },
1768
+ {
1769
+ name: "skipped_stories",
1770
+ type: "string[]",
1771
+ description: "Story keys skipped because budget was exhausted."
1772
+ },
1773
+ {
1774
+ name: "severity",
1775
+ type: "string",
1776
+ description: "critical when halt_on is all or critical; absent when none.",
1777
+ optional: true
1778
+ }
1779
+ ]
1709
1780
  }
1710
1781
  ];
1711
1782
  /**
@@ -4216,8 +4287,8 @@ var Minimatch = class {
4216
4287
  });
4217
4288
  return pp.filter((p) => p !== GLOBSTAR).join("/");
4218
4289
  }).join("|");
4219
- const [open$1, close] = set.length > 1 ? ["(?:", ")"] : ["", ""];
4220
- re = "^" + open$1 + re + close + "$";
4290
+ const [open, close] = set.length > 1 ? ["(?:", ")"] : ["", ""];
4291
+ re = "^" + open + re + close + "$";
4221
4292
  if (this.negate) re = "^(?!" + re + ").+$";
4222
4293
  try {
4223
4294
  this.regexp = new RegExp(re, [...flags].join(""));
@@ -6507,7 +6578,7 @@ const DEFAULT_TIMEOUT_MS$1 = 18e5;
6507
6578
  * @returns DevStoryResult with result, ac_met, ac_failures, files_modified, tests, tokenUsage
6508
6579
  */
6509
6580
  async function runDevStory(deps, params) {
6510
- const { storyKey, storyFilePath, taskScope, priorFiles } = params;
6581
+ const { storyKey, storyFilePath, taskScope, priorFiles, findingsPrompt: handlerFindingsPrompt } = params;
6511
6582
  logger$15.info({
6512
6583
  storyKey,
6513
6584
  storyFilePath
@@ -6640,14 +6711,24 @@ async function runDevStory(deps, params) {
6640
6711
  }, "Repo-map context assembled");
6641
6712
  }
6642
6713
  let priorFindingsContent = "";
6643
- try {
6644
- const findings = await getProjectFindings(deps.db);
6714
+ if (handlerFindingsPrompt !== void 0 && handlerFindingsPrompt.length > 0) {
6715
+ priorFindingsContent = handlerFindingsPrompt;
6716
+ logger$15.debug({
6717
+ storyKey,
6718
+ findingsLen: handlerFindingsPrompt.length
6719
+ }, "Using pre-computed findings from handler (Story 53-8 AC2)");
6720
+ } else try {
6721
+ const findings = await FindingsInjector.inject(deps.db, {
6722
+ storyKey,
6723
+ runId: params.pipelineRunId ?? "",
6724
+ targetFiles: extractTargetFilesFromStoryContent(storyContent)
6725
+ });
6645
6726
  if (findings.length > 0) {
6646
- priorFindingsContent = "Previous pipeline runs encountered these issues — avoid repeating them:\n\n" + findings;
6727
+ priorFindingsContent = findings;
6647
6728
  logger$15.debug({
6648
6729
  storyKey,
6649
6730
  findingsLen: findings.length
6650
- }, "Injecting prior findings into dev-story prompt");
6731
+ }, "Injecting relevance-scored findings into dev-story prompt");
6651
6732
  }
6652
6733
  } catch {}
6653
6734
  let testPlanContent = "";
@@ -8213,809 +8294,6 @@ function detectConflictGroupsWithContracts(storyKeys, config, declarations) {
8213
8294
  };
8214
8295
  }
8215
8296
 
8216
- //#endregion
8217
- //#region packages/sdlc/dist/handlers/sdlc-create-story-handler.js
8218
- /**
8219
- * SdlcCreateStoryHandler — wraps the runCreateStory compiled workflow
8220
- * as a graph NodeHandler for sdlc.create-story nodes.
8221
- *
8222
- * Story 43-3.
8223
- *
8224
- * Architecture note (ADR-003): The SDLC package does not compile-time-depend on
8225
- * @substrate-ai/factory to avoid circular references. Compatible types are
8226
- * defined locally using TypeScript structural typing — they are assignable to
8227
- * the factory types when the CLI composition root wires them together at runtime.
8228
- */
8229
- /**
8230
- * Create an sdlc.create-story node handler.
8231
- *
8232
- * The returned handler:
8233
- * 1. Validates storyKey and epicId are present in GraphContext (AC5)
8234
- * 2. Emits orchestrator:story-phase-start telemetry (AC4)
8235
- * 3. Delegates to runCreateStory(deps, { epicId, storyKey, pipelineRunId }) (AC1)
8236
- * 4. Emits orchestrator:story-phase-complete telemetry (AC4)
8237
- * 5. Maps the CreateStoryResult to an Outcome (AC2, AC3)
8238
- *
8239
- * @param options - Handler configuration.
8240
- * @returns A NodeHandler function ready for registration under the 'sdlc.create-story' key.
8241
- */
8242
- function createSdlcCreateStoryHandler(options) {
8243
- return async (_node, context, _graph) => {
8244
- const storyKey = context.getString("storyKey", "");
8245
- if (!storyKey) return {
8246
- status: "FAILURE",
8247
- failureReason: "storyKey is required in GraphContext"
8248
- };
8249
- const epicId = context.getString("epicId", "");
8250
- if (!epicId) return {
8251
- status: "FAILURE",
8252
- failureReason: "epicId is required in GraphContext"
8253
- };
8254
- const pipelineRunIdRaw = context.getString("pipelineRunId", "");
8255
- const createStoryParams = pipelineRunIdRaw !== "" ? {
8256
- epicId,
8257
- storyKey,
8258
- pipelineRunId: pipelineRunIdRaw
8259
- } : {
8260
- epicId,
8261
- storyKey
8262
- };
8263
- options.eventBus.emit("orchestrator:story-phase-start", {
8264
- storyKey,
8265
- phase: "create-story"
8266
- });
8267
- let workflowResult;
8268
- try {
8269
- workflowResult = await options.runCreateStory(options.deps, createStoryParams);
8270
- } catch (err) {
8271
- const failureReason = err instanceof Error ? err.message : String(err);
8272
- const errorResult = {
8273
- result: "failed",
8274
- error: failureReason,
8275
- tokenUsage: {
8276
- input: 0,
8277
- output: 0
8278
- }
8279
- };
8280
- options.eventBus.emit("orchestrator:story-phase-complete", {
8281
- storyKey,
8282
- phase: "create-story",
8283
- result: errorResult
8284
- });
8285
- return {
8286
- status: "FAILURE",
8287
- failureReason
8288
- };
8289
- }
8290
- options.eventBus.emit("orchestrator:story-phase-complete", {
8291
- storyKey,
8292
- phase: "create-story",
8293
- result: workflowResult
8294
- });
8295
- if (workflowResult.result === "success") return {
8296
- status: "SUCCESS",
8297
- contextUpdates: {
8298
- storyFilePath: workflowResult.story_file,
8299
- storyKey: workflowResult.story_key,
8300
- storyTitle: workflowResult.story_title
8301
- }
8302
- };
8303
- return {
8304
- status: "FAILURE",
8305
- failureReason: workflowResult.error ?? workflowResult.details ?? "create-story workflow failed"
8306
- };
8307
- };
8308
- }
8309
-
8310
- //#endregion
8311
- //#region packages/sdlc/dist/handlers/sdlc-phase-handler.js
8312
- /**
8313
- * SdlcPhaseHandler — wraps SDLC pipeline phase execution as a graph NodeHandler.
8314
- *
8315
- * Story 43-2.
8316
- *
8317
- * Architecture note (ADR-003): This package does not import from @substrate-ai/factory
8318
- * or from the monolith source tree at compile time. All external dependencies
8319
- * (orchestrator, phaseDeps, phase runner functions) are injected via
8320
- * SdlcPhaseHandlerDeps at construction time.
8321
- *
8322
- * TypeScript structural typing ensures the returned SdlcNodeHandler is
8323
- * assignable to NodeHandler from @substrate-ai/factory at the CLI composition
8324
- * root — no sdlc→factory import required.
8325
- */
8326
- /**
8327
- * Create an sdlc.phase node handler.
8328
- *
8329
- * The returned handler:
8330
- * 1. Resolves the phase name from node.id (AC5)
8331
- * 2. Looks up the corresponding runner in the PHASE_RUNNERS map (AC7)
8332
- * 3. Calls the runner with phaseDeps and phase-specific params (AC1, AC2, AC5)
8333
- * 4. On runner error, returns FAILURE without re-throwing (AC3)
8334
- * 5. If advanceAfterRun !== false, calls orchestrator.advancePhase(runId) (AC4)
8335
- * 6. If gate check fails (advanced === false), returns FAILURE with gate messages (AC4)
8336
- * 7. On full success, returns SUCCESS with phase output in contextUpdates (AC1, AC2)
8337
- *
8338
- * @param deps - Injected dependencies: orchestrator, phaseDeps, phase runners.
8339
- * @returns A SdlcNodeHandler ready for registration under the 'sdlc.phase' key.
8340
- */
8341
- function createSdlcPhaseHandler(deps) {
8342
- const PHASE_RUNNERS = new Map(Object.entries(deps.phases));
8343
- return async (node, context, _graph) => {
8344
- const phaseName = node.id;
8345
- const runner = PHASE_RUNNERS.get(phaseName);
8346
- if (runner === void 0) return {
8347
- status: "FAILURE",
8348
- failureReason: `No phase runner registered for phase: ${phaseName}`
8349
- };
8350
- const runId = context.getString("runId");
8351
- const concept = phaseName === "analysis" ? context.getString("concept", "") : "";
8352
- const PRE_IMPL_PHASES = [
8353
- "analysis",
8354
- "planning",
8355
- "solutioning"
8356
- ];
8357
- const storyKey = context.getString("storyKey", "");
8358
- if (storyKey && PRE_IMPL_PHASES.includes(phaseName)) return {
8359
- status: "SUCCESS",
8360
- notes: `Phase ${phaseName} skipped — explicit story dispatch (storyKey=${storyKey})`
8361
- };
8362
- const PHASE_ARTIFACT_TYPES = {
8363
- analysis: ["product-brief"],
8364
- planning: ["prd"],
8365
- solutioning: ["architecture", "stories"]
8366
- };
8367
- const artifactTypes = PHASE_ARTIFACT_TYPES[phaseName];
8368
- if (artifactTypes !== void 0) try {
8369
- const db = deps.phaseDeps.db;
8370
- if (db) {
8371
- let allExist = true;
8372
- for (const at of artifactTypes) {
8373
- const rows = await db.query("SELECT id, path, content_hash, summary FROM artifacts WHERE phase = ? AND type = ? ORDER BY created_at DESC LIMIT 1", [phaseName, at]);
8374
- if (!Array.isArray(rows) || rows.length === 0) {
8375
- allExist = false;
8376
- break;
8377
- }
8378
- }
8379
- if (allExist) {
8380
- const pipelineRunId = context.getString("pipelineRunId", "");
8381
- if (pipelineRunId) for (const at of artifactTypes) {
8382
- const existing = await db.query("SELECT id, path, content_hash, summary FROM artifacts WHERE phase = ? AND type = ? ORDER BY created_at DESC LIMIT 1", [phaseName, at]);
8383
- const src = existing[0];
8384
- if (src) {
8385
- const alreadyRegistered = await db.query("SELECT id FROM artifacts WHERE pipeline_run_id = ? AND phase = ? AND type = ? LIMIT 1", [
8386
- pipelineRunId,
8387
- phaseName,
8388
- at
8389
- ]);
8390
- if (!Array.isArray(alreadyRegistered) || alreadyRegistered.length === 0) {
8391
- const newId = crypto.randomUUID();
8392
- await db.query("INSERT INTO artifacts (id, pipeline_run_id, phase, type, path, content_hash, summary) VALUES (?, ?, ?, ?, ?, ?, ?)", [
8393
- newId,
8394
- pipelineRunId,
8395
- phaseName,
8396
- at,
8397
- src.path,
8398
- src.content_hash ?? null,
8399
- src.summary ?? null
8400
- ]);
8401
- }
8402
- }
8403
- }
8404
- return {
8405
- status: "SUCCESS",
8406
- notes: `Phase ${phaseName} already complete — artifact(s) exist, skipping dispatch`
8407
- };
8408
- }
8409
- }
8410
- } catch {}
8411
- const params = phaseName === "analysis" ? {
8412
- runId,
8413
- concept
8414
- } : { runId };
8415
- try {
8416
- const entryGateResult = await deps.orchestrator.evaluateEntryGates(runId);
8417
- if (!entryGateResult.passed) {
8418
- const failures = entryGateResult.failures?.map((f$1) => `${f$1.gate}: ${f$1.error}`).join("; ") ?? "no details";
8419
- return {
8420
- status: "FAILURE",
8421
- failureReason: `entry gate failed: ${failures}`
8422
- };
8423
- }
8424
- const phaseOutput = await runner(deps.phaseDeps, params);
8425
- if (deps.advanceAfterRun !== false) {
8426
- const advanceResult = await deps.orchestrator.advancePhase(runId);
8427
- if (!advanceResult.advanced) {
8428
- const failures = advanceResult.gateFailures?.map((f$1) => `${f$1.gate}: ${f$1.error}`).join("; ") ?? "no details";
8429
- return {
8430
- status: "FAILURE",
8431
- failureReason: `exit gate failed: ${failures}`
8432
- };
8433
- }
8434
- return {
8435
- status: "SUCCESS",
8436
- contextUpdates: {
8437
- ...phaseOutput,
8438
- advancedPhase: advanceResult.phase
8439
- }
8440
- };
8441
- }
8442
- return {
8443
- status: "SUCCESS",
8444
- contextUpdates: phaseOutput
8445
- };
8446
- } catch (err) {
8447
- const message = err instanceof Error ? err.message : String(err);
8448
- return {
8449
- status: "FAILURE",
8450
- failureReason: message
8451
- };
8452
- }
8453
- };
8454
- }
8455
-
8456
- //#endregion
8457
- //#region packages/sdlc/dist/handlers/sdlc-dev-story-handler.js
8458
- /**
8459
- * SdlcDevStoryHandler — wraps the runDevStory compiled workflow
8460
- * as a graph NodeHandler for sdlc.dev-story nodes.
8461
- *
8462
- * Story 43-4.
8463
- *
8464
- * Architecture note (ADR-003): The SDLC package does not compile-time-depend on
8465
- * @substrate-ai/factory to avoid circular references. Compatible types are
8466
- * defined locally using TypeScript structural typing — they are assignable to
8467
- * the factory types when the CLI composition root wires them together at runtime.
8468
- */
8469
- /**
8470
- * Create an sdlc.dev-story node handler.
8471
- *
8472
- * The returned handler:
8473
- * 1. Validates storyKey and storyFilePath are present in GraphContext (AC6)
8474
- * 2. Reads optional retry remediation context from prior iteration (AC4)
8475
- * 3. Emits orchestrator:story-phase-start telemetry (AC5)
8476
- * 4. Delegates to runDevStory(deps, params) (AC1)
8477
- * 5. Emits orchestrator:story-phase-complete telemetry in finally block (AC5)
8478
- * 6. Maps the DevStoryResult to an Outcome (AC2, AC3)
8479
- *
8480
- * @param options - Handler configuration.
8481
- * @returns A NodeHandler function ready for registration under the 'sdlc.dev-story' key.
8482
- */
8483
- function createSdlcDevStoryHandler(options) {
8484
- return async (_node, context, _graph) => {
8485
- const storyKey = context.getString("storyKey", "");
8486
- const storyFilePath = context.getString("storyFilePath", "");
8487
- if (!storyKey || !storyFilePath) {
8488
- const missingFields = [!storyKey && "storyKey", !storyFilePath && "storyFilePath"].filter(Boolean);
8489
- return {
8490
- status: "FAILURE",
8491
- failureReason: `Missing required context: ${missingFields.join(", ")}`
8492
- };
8493
- }
8494
- const pipelineRunIdRaw = context.getString("pipelineRunId", "");
8495
- const priorFiles = context.getList?.("devStoryFilesModified") ?? [];
8496
- const priorAcFailures = context.getList?.("devStoryAcFailures") ?? [];
8497
- const devStoryParams = {
8498
- storyKey,
8499
- storyFilePath,
8500
- ...pipelineRunIdRaw !== "" ? { pipelineRunId: pipelineRunIdRaw } : {},
8501
- ...priorFiles.length > 0 ? { priorFiles } : {},
8502
- ...priorAcFailures.length > 0 ? { taskScope: `Prior attempt failed ACs: ${priorAcFailures.join(", ")}` } : {}
8503
- };
8504
- options.eventBus.emit("orchestrator:story-phase-start", {
8505
- storyKey,
8506
- phase: "dev-story",
8507
- ...pipelineRunIdRaw !== "" ? { pipelineRunId: pipelineRunIdRaw } : {}
8508
- });
8509
- let outcome = {
8510
- status: "FAILURE",
8511
- failureReason: "unexpected error in dev-story handler"
8512
- };
8513
- try {
8514
- const workflowResult = await options.runDevStory(options.deps, devStoryParams);
8515
- if (workflowResult.result === "success") {
8516
- if (options.buildVerifier) {
8517
- const projectRoot = context.getString("projectRoot", "");
8518
- if (projectRoot) {
8519
- const buildResult = options.buildVerifier(projectRoot);
8520
- if (buildResult.status === "failed" || buildResult.status === "timeout") {
8521
- outcome = {
8522
- status: "FAILURE",
8523
- failureReason: `build verification failed after dev-story: ${buildResult.output?.slice(0, 500) ?? "no output"}`,
8524
- contextUpdates: {
8525
- filesModified: workflowResult.files_modified,
8526
- devStoryFilesModified: workflowResult.files_modified,
8527
- devStoryAcFailures: ["build-verification"]
8528
- }
8529
- };
8530
- return outcome;
8531
- }
8532
- }
8533
- }
8534
- outcome = {
8535
- status: "SUCCESS",
8536
- contextUpdates: {
8537
- filesModified: workflowResult.files_modified,
8538
- acMet: workflowResult.ac_met,
8539
- devStoryFilesModified: workflowResult.files_modified
8540
- }
8541
- };
8542
- } else {
8543
- const failureReason = workflowResult.error ?? (workflowResult.ac_failures.length > 0 ? `dev-story failed ACs: ${workflowResult.ac_failures.join(", ")}` : "dev-story workflow failed");
8544
- outcome = {
8545
- status: "FAILURE",
8546
- failureReason,
8547
- contextUpdates: {
8548
- acFailures: workflowResult.ac_failures,
8549
- filesModified: workflowResult.files_modified,
8550
- devStoryFilesModified: workflowResult.files_modified,
8551
- devStoryAcFailures: workflowResult.ac_failures
8552
- }
8553
- };
8554
- }
8555
- } catch (err) {
8556
- const failureReason = err instanceof Error ? err.message : String(err);
8557
- outcome = {
8558
- status: "FAILURE",
8559
- failureReason
8560
- };
8561
- } finally {
8562
- options.eventBus.emit("orchestrator:story-phase-complete", {
8563
- storyKey,
8564
- phase: "dev-story",
8565
- result: { status: outcome.status },
8566
- ...pipelineRunIdRaw !== "" ? { pipelineRunId: pipelineRunIdRaw } : {}
8567
- });
8568
- }
8569
- return outcome;
8570
- };
8571
- }
8572
-
8573
- //#endregion
8574
- //#region packages/sdlc/dist/handlers/sdlc-code-review-handler.js
8575
- /**
8576
- * SdlcCodeReviewHandler — wraps the runCodeReview compiled workflow
8577
- * as a graph NodeHandler for sdlc.code-review nodes.
8578
- *
8579
- * Story 43-5.
8580
- *
8581
- * Architecture note (ADR-003): The SDLC package does not compile-time-depend on
8582
- * @substrate-ai/factory to avoid circular references. Compatible types are
8583
- * defined locally using TypeScript structural typing — they are assignable to
8584
- * the factory types when the CLI composition root wires them together at runtime.
8585
- */
8586
- /**
8587
- * Create an sdlc.code-review node handler.
8588
- *
8589
- * The returned handler:
8590
- * 1. Validates storyKey and storyFilePath are present in GraphContext (AC4)
8591
- * 2. Reads optional context fields: pipelineRunId, filesModified, codeReviewIssueList (AC5)
8592
- * 3. Emits orchestrator:story-phase-start telemetry (AC6)
8593
- * 4. Delegates to runCodeReview(deps, params)
8594
- * 5. Emits orchestrator:story-phase-complete telemetry in finally block (AC6)
8595
- * 6. Maps the CodeReviewResult verdict to an Outcome (AC1, AC2, AC3)
8596
- *
8597
- * Verdict mapping:
8598
- * SHIP_IT / LGTM_WITH_NOTES → SUCCESS with preferredLabel: 'SHIP_IT' (AC1)
8599
- * NEEDS_MINOR_FIXES / NEEDS_MAJOR_REWORK → FAILURE with preferredLabel: 'NEEDS_FIXES' (AC2)
8600
- * dispatchFailed: true → FAILURE with escalation failureReason, no contextUpdates (AC3)
8601
- * throws → FAILURE with error message, no contextUpdates
8602
- *
8603
- * @param options - Handler configuration.
8604
- * @returns A NodeHandler function ready for registration under the 'sdlc.code-review' key.
8605
- */
8606
- function createSdlcCodeReviewHandler(options) {
8607
- return async (_node, context, _graph) => {
8608
- const storyKey = context.getString("storyKey", "");
8609
- const storyFilePath = context.getString("storyFilePath", "");
8610
- if (!storyKey || !storyFilePath) {
8611
- const missingFields = [!storyKey && "storyKey", !storyFilePath && "storyFilePath"].filter(Boolean);
8612
- return {
8613
- status: "FAILURE",
8614
- failureReason: `Missing required context: ${missingFields.join(", ")}`
8615
- };
8616
- }
8617
- const pipelineRunIdRaw = context.getString("pipelineRunId", "");
8618
- const pipelineRunId = pipelineRunIdRaw !== "" ? pipelineRunIdRaw : void 0;
8619
- const filesModifiedRaw = context.getList?.("filesModified") ?? [];
8620
- const filesModified = filesModifiedRaw.length > 0 ? filesModifiedRaw : void 0;
8621
- const codeReviewIssueListRaw = context.get?.("codeReviewIssueList");
8622
- const previousIssues = Array.isArray(codeReviewIssueListRaw) && codeReviewIssueListRaw.length > 0 ? codeReviewIssueListRaw : void 0;
8623
- const params = {
8624
- storyKey,
8625
- storyFilePath,
8626
- ...pipelineRunId !== void 0 ? { pipelineRunId } : {},
8627
- ...filesModified !== void 0 ? { filesModified } : {},
8628
- ...previousIssues !== void 0 ? { previousIssues } : {}
8629
- };
8630
- options.eventBus.emit("orchestrator:story-phase-start", {
8631
- storyKey,
8632
- phase: "code-review"
8633
- });
8634
- let outcome = {
8635
- status: "FAILURE",
8636
- failureReason: "unexpected error in code-review handler"
8637
- };
8638
- let codeReviewVerdict;
8639
- try {
8640
- const result = await options.runCodeReview(options.deps, params);
8641
- if (result.dispatchFailed === true) {
8642
- outcome = {
8643
- status: "FAILURE",
8644
- failureReason: `escalation: code-review dispatch failed: ${result.error ?? "unknown error"}`
8645
- };
8646
- return outcome;
8647
- }
8648
- codeReviewVerdict = result.verdict;
8649
- const contextUpdates = {
8650
- codeReviewVerdict: result.verdict,
8651
- codeReviewIssues: result.issues,
8652
- codeReviewIssueList: result.issue_list
8653
- };
8654
- if (result.verdict === "SHIP_IT" || result.verdict === "LGTM_WITH_NOTES") outcome = {
8655
- status: "SUCCESS",
8656
- preferredLabel: "SHIP_IT",
8657
- contextUpdates
8658
- };
8659
- else outcome = {
8660
- status: "FAILURE",
8661
- preferredLabel: "NEEDS_FIXES",
8662
- failureReason: `${result.verdict}: ${result.issues} issue(s)`,
8663
- contextUpdates
8664
- };
8665
- } catch (err) {
8666
- const failureReason = err instanceof Error ? err.message : String(err);
8667
- outcome = {
8668
- status: "FAILURE",
8669
- failureReason
8670
- };
8671
- } finally {
8672
- options.eventBus.emit("orchestrator:story-phase-complete", {
8673
- storyKey,
8674
- phase: "code-review",
8675
- result: {
8676
- status: outcome.status,
8677
- verdict: codeReviewVerdict
8678
- }
8679
- });
8680
- }
8681
- return outcome;
8682
- };
8683
- }
8684
-
8685
- //#endregion
8686
- //#region packages/sdlc/dist/verification/checks/phantom-review-check.js
8687
- /**
8688
- * PhantomReviewCheck — Story 51-2.
8689
- *
8690
- * Tier A verification check that detects when a code review dispatch failed
8691
- * but was recorded as a passing verdict. Stories that were never actually
8692
- * reviewed should not be counted as verified.
8693
- *
8694
- * Architecture constraints (FR-V9):
8695
- * - No LLM calls.
8696
- * - No shell invocations — pure static signal inspection over VerificationContext fields.
8697
- * - Runs first in Tier A (before TrivialOutputCheck, before BuildCheck).
8698
- */
8699
- /**
8700
- * Detects phantom reviews — dispatches that failed or produced no output but
8701
- * were recorded as passing verdicts.
8702
- *
8703
- * AC1: dispatch failed (non-zero exit, timeout, crash) → fail
8704
- * AC2: empty or null rawOutput → fail
8705
- * AC3: schema_validation_failed error → fail
8706
- * AC5: valid review (non-empty rawOutput, no dispatchFailed) → pass
8707
- * AC6: name='phantom-review', tier='A'
8708
- */
8709
- var PhantomReviewCheck = class {
8710
- name = "phantom-review";
8711
- tier = "A";
8712
- async run(context) {
8713
- const start = Date.now();
8714
- const review = context.reviewResult;
8715
- if (!review) return {
8716
- status: "pass",
8717
- details: "phantom-review: no review result in context — skipping check",
8718
- duration_ms: Date.now() - start
8719
- };
8720
- if (review.dispatchFailed === true) {
8721
- const reason = review.error === "schema_validation_failed" ? "schema validation failed" : `dispatch failed${review.error ? ` — ${review.error}` : ""}`;
8722
- return {
8723
- status: "fail",
8724
- details: `phantom-review: ${reason}`,
8725
- duration_ms: Date.now() - start
8726
- };
8727
- }
8728
- if (review.rawOutput !== void 0 && review.rawOutput.trim().length === 0) return {
8729
- status: "fail",
8730
- details: "phantom-review: empty review output",
8731
- duration_ms: Date.now() - start
8732
- };
8733
- return {
8734
- status: "pass",
8735
- details: "phantom-review: review output is valid",
8736
- duration_ms: Date.now() - start
8737
- };
8738
- }
8739
- };
8740
-
8741
- //#endregion
8742
- //#region packages/sdlc/dist/verification/checks/trivial-output-check.js
8743
- /**
8744
- * TrivialOutputCheck — Story 51-3.
8745
- *
8746
- * Tier A verification check that flags story dispatches which produced
8747
- * fewer output tokens than the configured threshold. A very low output
8748
- * token count is a strong signal that the agent exited early (e.g. hit a
8749
- * maxTurns limit, encountered a fatal error, or did no real work).
8750
- *
8751
- * Architecture constraints (DC-6, FR-V9):
8752
- * - No LLM calls.
8753
- * - No shell invocations — pure in-process computation.
8754
- * - Runs in Tier A: before BuildCheck, after PhantomReviewCheck.
8755
- */
8756
- /**
8757
- * Default minimum output-token count a story must produce to be
8758
- * considered non-trivial. Configurable via trivialOutputThreshold config field.
8759
- */
8760
- const DEFAULT_TRIVIAL_OUTPUT_THRESHOLD = 100;
8761
- /**
8762
- * Checks that a completed story dispatch produced at least `threshold` output
8763
- * tokens. Dispatches that produced fewer tokens are flagged as failures with
8764
- * an actionable suggestion to re-run with increased maxTurns.
8765
- *
8766
- * AC1: fail when outputTokenCount < threshold.
8767
- * AC2: details string includes "Re-run with increased maxTurns".
8768
- * AC3: pass when outputTokenCount >= threshold.
8769
- * AC4: threshold is configurable via trivialOutputThreshold config field.
8770
- * AC5: warn (not fail) when outputTokenCount is undefined.
8771
- * AC6: implements VerificationCheck with name='trivial-output', tier='A'.
8772
- */
8773
- var TrivialOutputCheck = class {
8774
- name = "trivial-output";
8775
- tier = "A";
8776
- threshold;
8777
- constructor(config) {
8778
- this.threshold = config?.trivialOutputThreshold ?? DEFAULT_TRIVIAL_OUTPUT_THRESHOLD;
8779
- }
8780
- async run(context) {
8781
- const start = Date.now();
8782
- if (context.outputTokenCount === void 0) return {
8783
- status: "warn",
8784
- details: "trivial-output: output token count unavailable — skipping check",
8785
- duration_ms: Date.now() - start
8786
- };
8787
- const count = context.outputTokenCount;
8788
- if (count < this.threshold) return {
8789
- status: "fail",
8790
- details: `trivial-output: output token count ${count} is below threshold ${this.threshold} — Re-run with increased maxTurns`,
8791
- duration_ms: Date.now() - start
8792
- };
8793
- return {
8794
- status: "pass",
8795
- details: `output token count ${count} meets threshold ${this.threshold}`,
8796
- duration_ms: Date.now() - start
8797
- };
8798
- }
8799
- };
8800
-
8801
- //#endregion
8802
- //#region packages/sdlc/dist/verification/checks/build-check.js
8803
- /** Hard timeout for the build command in milliseconds (FR-V11). */
8804
- const BUILD_CHECK_TIMEOUT_MS = 6e4;
8805
- /** Maximum characters to include in details string from build output. */
8806
- const MAX_OUTPUT_CHARS = 2e3;
8807
- /**
8808
- * Detect the build command for a project based on files present in `workingDir`.
8809
- *
8810
- * Returns an empty string when no recognized build system is found, which
8811
- * causes BuildCheck to return a 'warn' result without blocking the pipeline.
8812
- *
8813
- * NOTE: Do NOT import from src/modules/agent-dispatch/dispatcher-impl.ts —
8814
- * that would create a circular dependency from packages/sdlc/ → monolith src/.
8815
- * This function inlines the detection logic independently.
8816
- */
8817
- function detectBuildCommand(workingDir) {
8818
- if (existsSync(join$1(workingDir, "turbo.json"))) return "turbo build";
8819
- if (existsSync(join$1(workingDir, "pnpm-lock.yaml"))) return "pnpm run build";
8820
- if (existsSync(join$1(workingDir, "yarn.lock"))) return "yarn build";
8821
- if (existsSync(join$1(workingDir, "bun.lockb"))) return "bun run build";
8822
- if (existsSync(join$1(workingDir, "package.json"))) return "npm run build";
8823
- const nonNodeMarkers = [
8824
- "pyproject.toml",
8825
- "poetry.lock",
8826
- "setup.py",
8827
- "Cargo.toml",
8828
- "go.mod"
8829
- ];
8830
- for (const marker of nonNodeMarkers) if (existsSync(join$1(workingDir, marker))) return "";
8831
- return "";
8832
- }
8833
- /**
8834
- * Runs the project's build command and returns pass/warn/fail based on exit code.
8835
- *
8836
- * AC1: exit code 0 → pass
8837
- * AC2: non-zero exit code → fail with truncated output in details
8838
- * AC3: timeout → kill process group, return fail with timeout message
8839
- * AC4: no recognized build system → warn without blocking
8840
- * AC5: explicit buildCommand override respected; empty string → warn (skip)
8841
- * AC6: name === 'build', tier === 'A'
8842
- */
8843
- var BuildCheck = class {
8844
- name = "build";
8845
- tier = "A";
8846
- async run(context) {
8847
- const start = Date.now();
8848
- const cmd = context.buildCommand !== void 0 ? context.buildCommand : detectBuildCommand(context.workingDir);
8849
- if (cmd === "") return {
8850
- status: "warn",
8851
- details: `build-skip: no build command detected for project at ${context.workingDir}`,
8852
- duration_ms: Date.now() - start
8853
- };
8854
- return new Promise((resolve$6) => {
8855
- const child = spawn(cmd, [], {
8856
- cwd: context.workingDir,
8857
- detached: true,
8858
- shell: true,
8859
- stdio: [
8860
- "ignore",
8861
- "pipe",
8862
- "pipe"
8863
- ]
8864
- });
8865
- let output = "";
8866
- child.stdout?.on("data", (chunk) => {
8867
- output += chunk.toString();
8868
- });
8869
- child.stderr?.on("data", (chunk) => {
8870
- output += chunk.toString();
8871
- });
8872
- const timeoutHandle = setTimeout(() => {
8873
- try {
8874
- process.kill(-child.pid, "SIGKILL");
8875
- } catch {}
8876
- resolve$6({
8877
- status: "fail",
8878
- details: `build-timeout: command exceeded ${BUILD_CHECK_TIMEOUT_MS}ms`,
8879
- duration_ms: Date.now() - start
8880
- });
8881
- }, BUILD_CHECK_TIMEOUT_MS);
8882
- child.on("close", (code) => {
8883
- clearTimeout(timeoutHandle);
8884
- if (code === 0) resolve$6({
8885
- status: "pass",
8886
- details: "build passed",
8887
- duration_ms: Date.now() - start
8888
- });
8889
- else {
8890
- const truncated = output.length > MAX_OUTPUT_CHARS ? output.slice(0, MAX_OUTPUT_CHARS) + "... (truncated)" : output;
8891
- resolve$6({
8892
- status: "fail",
8893
- details: `build failed (exit ${code}): ${truncated}`,
8894
- duration_ms: Date.now() - start
8895
- });
8896
- }
8897
- });
8898
- });
8899
- }
8900
- };
8901
-
8902
- //#endregion
8903
- //#region packages/sdlc/dist/verification/verification-pipeline.js
8904
- /**
8905
- * Compute the worst-case aggregate status across a list of check results.
8906
- * Precedence: fail > warn > pass.
8907
- */
8908
- function aggregateStatus(checks) {
8909
- let result = "pass";
8910
- for (const c of checks) {
8911
- if (c.status === "fail") return "fail";
8912
- if (c.status === "warn") result = "warn";
8913
- }
8914
- return result;
8915
- }
8916
- /**
8917
- * Runs an ordered chain of VerificationCheck implementations after each story dispatch.
8918
- *
8919
- * Checks are stored in registration order. When `run()` is called with `tier: 'A'`
8920
- * only Tier A checks execute; when called with `tier: 'B'` only Tier B checks execute.
8921
- * (Story 51-5 will invoke both tiers at the appropriate orchestration points.)
8922
- */
8923
- var VerificationPipeline = class {
8924
- _bus;
8925
- _checks = [];
8926
- /**
8927
- * @param bus Typed event bus for emitting verification events.
8928
- * @param checks Optional initial list of checks to register at construction time.
8929
- */
8930
- constructor(bus, checks = []) {
8931
- this._bus = bus;
8932
- for (const check of checks) this.register(check);
8933
- }
8934
- /**
8935
- * Register a VerificationCheck.
8936
- *
8937
- * Checks are stored in insertion order within their tier.
8938
- * Tier A checks always run before Tier B checks regardless of registration order.
8939
- */
8940
- register(check) {
8941
- this._checks.push(check);
8942
- }
8943
- /**
8944
- * Execute all checks matching the specified tier sequentially.
8945
- *
8946
- * AC2: Tier A checks execute in registration order.
8947
- * AC4: Results are aggregated into a VerificationSummary.
8948
- * AC5: verification:check-complete and verification:story-complete events are emitted.
8949
- * AC6: Unhandled exceptions are caught and recorded as warn.
8950
- *
8951
- * @param context Verification context for the story being verified.
8952
- * @param tier Which tier of checks to execute ('A' | 'B'). Defaults to 'A'.
8953
- */
8954
- async run(context, tier = "A") {
8955
- const pipelineStart = Date.now();
8956
- const checks = this._checks.filter((c) => c.tier === tier);
8957
- const checkResults = [];
8958
- for (const check of checks) {
8959
- const checkStart = Date.now();
8960
- let result;
8961
- try {
8962
- const runResult = await check.run(context);
8963
- result = {
8964
- checkName: check.name,
8965
- status: runResult.status,
8966
- details: runResult.details,
8967
- duration_ms: runResult.duration_ms
8968
- };
8969
- } catch (err) {
8970
- const elapsed = Date.now() - checkStart;
8971
- const message = err instanceof Error ? err.message : String(err);
8972
- process.stderr.write(`[verification-pipeline] check "${check.name}" threw an unhandled exception: ${message}\n`);
8973
- result = {
8974
- checkName: check.name,
8975
- status: "warn",
8976
- details: message,
8977
- duration_ms: elapsed
8978
- };
8979
- }
8980
- checkResults.push(result);
8981
- this._bus.emit("verification:check-complete", {
8982
- storyKey: context.storyKey,
8983
- checkName: result.checkName,
8984
- status: result.status,
8985
- details: result.details,
8986
- duration_ms: result.duration_ms
8987
- });
8988
- }
8989
- const summary = {
8990
- storyKey: context.storyKey,
8991
- checks: checkResults,
8992
- status: aggregateStatus(checkResults),
8993
- duration_ms: Date.now() - pipelineStart
8994
- };
8995
- this._bus.emit("verification:story-complete", summary);
8996
- return summary;
8997
- }
8998
- };
8999
- /**
9000
- * Create a VerificationPipeline pre-loaded with the canonical check set.
9001
- *
9002
- * Canonical Tier A check order (architecture section 3.5):
9003
- * 1. PhantomReviewCheck — story 51-2 (runs first: unreviewed stories skipped)
9004
- * 2. TrivialOutputCheck — story 51-3
9005
- * 3. BuildCheck — story 51-4
9006
- *
9007
- * @param bus Typed event bus for verification events.
9008
- * @param config Optional config (used to forward threshold to TrivialOutputCheck).
9009
- */
9010
- function createDefaultVerificationPipeline(bus, config) {
9011
- const checks = [
9012
- new PhantomReviewCheck(),
9013
- new TrivialOutputCheck(config),
9014
- new BuildCheck()
9015
- ];
9016
- return new VerificationPipeline(bus, checks);
9017
- }
9018
-
9019
8297
  //#endregion
9020
8298
  //#region src/modules/implementation-orchestrator/seed-methodology-context.ts
9021
8299
  const logger$11 = createLogger("implementation-orchestrator:seed");
@@ -10867,6 +10145,58 @@ function persistVerificationResult(storyKey, summary, runManifest) {
10867
10145
  }, "manifest verification_result write failed — pipeline continues"));
10868
10146
  }
10869
10147
 
10148
+ //#endregion
10149
+ //#region src/modules/implementation-orchestrator/cost-governance.ts
10150
+ /**
10151
+ * Pure checker for run-level cost governance.
10152
+ *
10153
+ * Instantiate with `new CostGovernanceChecker()` — no constructor arguments.
10154
+ * All methods are stateless; results depend only on the manifest data passed in.
10155
+ */
10156
+ var CostGovernanceChecker = class {
10157
+ /**
10158
+ * Compute cumulative run cost from the manifest.
10159
+ *
10160
+ * Sums `per_story_state[key].cost_usd ?? 0` for all story keys, then adds
10161
+ * `manifest.cost_accumulation.run_total` (retry cost).
10162
+ */
10163
+ computeCumulativeCost(manifest) {
10164
+ const dispatchCost = Object.values(manifest.per_story_state).reduce((sum, s$1) => sum + (s$1.cost_usd ?? 0), 0);
10165
+ return dispatchCost + manifest.cost_accumulation.run_total;
10166
+ }
10167
+ /**
10168
+ * Estimate the cost of the next story.
10169
+ *
10170
+ * Returns the average `cost_usd` of stories that have a non-zero `cost_usd`.
10171
+ * Returns `0` if no completed stories with a cost exist.
10172
+ */
10173
+ estimateNextStoryCost(manifest) {
10174
+ const completed = Object.values(manifest.per_story_state).map((s$1) => s$1.cost_usd).filter((c) => c !== void 0 && c > 0);
10175
+ if (completed.length === 0) return 0;
10176
+ return completed.reduce((s$1, c) => s$1 + c, 0) / completed.length;
10177
+ }
10178
+ /**
10179
+ * Check the cumulative run cost against the provided ceiling.
10180
+ *
10181
+ * @param manifest - Current run manifest data
10182
+ * @param ceiling - Cost ceiling in USD (must be > 0)
10183
+ * @returns CeilingCheckResult with status, cumulative, ceiling, percentUsed, estimatedNext
10184
+ */
10185
+ checkCeiling(manifest, ceiling) {
10186
+ const cumulative = this.computeCumulativeCost(manifest);
10187
+ const estimatedNext = this.estimateNextStoryCost(manifest);
10188
+ const percentUsed = Math.round(cumulative / ceiling * 1e4) / 100;
10189
+ const status = percentUsed >= 100 ? "exceeded" : percentUsed >= 80 ? "warning" : "ok";
10190
+ return {
10191
+ status,
10192
+ cumulative,
10193
+ ceiling,
10194
+ percentUsed,
10195
+ estimatedNext
10196
+ };
10197
+ }
10198
+ };
10199
+
10870
10200
  //#endregion
10871
10201
  //#region src/modules/work-graph/epic-ingester.ts
10872
10202
  var EpicIngester = class {
@@ -11663,10 +10993,14 @@ function createImplementationOrchestrator(deps) {
11663
10993
  const _phaseStartMs = new Map();
11664
10994
  const _phaseEndMs = new Map();
11665
10995
  const _storyDispatches = new Map();
10996
+ const _storyRetryCount = new Map();
11666
10997
  let _completedDispatches = 0;
11667
10998
  let _maxConcurrentActual = 0;
11668
10999
  let _packageSnapshot;
11669
11000
  let _contractMismatches;
11001
+ const _costChecker = new CostGovernanceChecker();
11002
+ let _costWarningEmitted = false;
11003
+ let _budgetExhausted = false;
11670
11004
  let _otlpEndpoint;
11671
11005
  const verificationStore = new VerificationStore();
11672
11006
  const verificationPipeline = createDefaultVerificationPipeline(toSdlcEventBus(eventBus));
@@ -11689,6 +11023,41 @@ function createImplementationOrchestrator(deps) {
11689
11023
  function incrementDispatches(storyKey) {
11690
11024
  _storyDispatches.set(storyKey, (_storyDispatches.get(storyKey) ?? 0) + 1);
11691
11025
  }
11026
+ /**
11027
+ * Initialize `_storyRetryCount` from the run manifest for crash-recovery durability (AC6, Story 53-4).
11028
+ * Reads persisted retry_count so that budget gate correctly accounts for prior-session retries.
11029
+ * Best-effort: failures result in a starting count of 0 (safe — may allow one extra retry).
11030
+ */
11031
+ async function initRetryCount(storyKey) {
11032
+ if (runManifest === null || runManifest === void 0) {
11033
+ _storyRetryCount.set(storyKey, 0);
11034
+ return;
11035
+ }
11036
+ try {
11037
+ const data = await runManifest.read();
11038
+ const storyState = data.per_story_state[storyKey];
11039
+ const existingCount = storyState?.retry_count ?? 0;
11040
+ _storyRetryCount.set(storyKey, existingCount);
11041
+ } catch (err) {
11042
+ logger$23.warn({
11043
+ err,
11044
+ storyKey
11045
+ }, "initRetryCount: failed to read manifest — starting at 0");
11046
+ _storyRetryCount.set(storyKey, 0);
11047
+ }
11048
+ }
11049
+ /**
11050
+ * Increment the in-memory retry count and persist best-effort to the run manifest (AC4, Story 53-4).
11051
+ */
11052
+ function incrementRetryCount(storyKey) {
11053
+ const current = _storyRetryCount.get(storyKey) ?? 0;
11054
+ const next = current + 1;
11055
+ _storyRetryCount.set(storyKey, next);
11056
+ if (runManifest !== null && runManifest !== void 0) runManifest.patchStoryState(storyKey, { retry_count: next }).catch((err) => logger$23.warn({
11057
+ err,
11058
+ storyKey
11059
+ }, "patchStoryState(retry_count) failed — pipeline continues"));
11060
+ }
11692
11061
  function buildPhaseDurationsJson(storyKey) {
11693
11062
  const starts = _phaseStartMs.get(storyKey);
11694
11063
  const ends = _phaseEndMs.get(storyKey);
@@ -12219,6 +11588,7 @@ function createImplementationOrchestrator(deps) {
12219
11588
  */
12220
11589
  async function processStory(storyKey, storyOptions) {
12221
11590
  logger$23.info({ storyKey }, "Processing story");
11591
+ await initRetryCount(storyKey);
12222
11592
  {
12223
11593
  const memoryOk = await checkMemoryPressure(storyKey);
12224
11594
  if (!memoryOk) {
@@ -13222,6 +12592,31 @@ function createImplementationOrchestrator(deps) {
13222
12592
  await waitIfPaused();
13223
12593
  if (_state !== "RUNNING") return;
13224
12594
  if (reviewCycles === 0) startPhase(storyKey, "code-review");
12595
+ if (reviewCycles > 0) {
12596
+ const currentRetries = _storyRetryCount.get(storyKey) ?? 0;
12597
+ const budget = config.retryBudget ?? 2;
12598
+ if (currentRetries >= budget) {
12599
+ endPhase(storyKey, "code-review");
12600
+ updateStory(storyKey, {
12601
+ phase: "ESCALATED",
12602
+ reviewCycles,
12603
+ completedAt: new Date().toISOString(),
12604
+ error: "retry_budget_exhausted"
12605
+ });
12606
+ await writeStoryMetricsBestEffort(storyKey, "escalated", reviewCycles);
12607
+ await emitEscalation({
12608
+ storyKey,
12609
+ lastVerdict: "retry_budget_exhausted",
12610
+ reviewCycles,
12611
+ issues: [`retry budget exhausted: ${currentRetries}/${budget} retries used`],
12612
+ retryBudget: budget,
12613
+ retryCount: currentRetries
12614
+ });
12615
+ await persistState();
12616
+ return;
12617
+ }
12618
+ incrementRetryCount(storyKey);
12619
+ }
13225
12620
  updateStory(storyKey, {
13226
12621
  phase: "IN_REVIEW",
13227
12622
  reviewCycles
@@ -13952,6 +13347,45 @@ function createImplementationOrchestrator(deps) {
13952
13347
  }
13953
13348
  }
13954
13349
  /**
13350
+ * Handle the cost ceiling being exceeded before dispatching a story (Story 53-3).
13351
+ *
13352
+ * Transitions all skipped stories to ESCALATED phase, emits the
13353
+ * cost:ceiling-reached NDJSON event, and sets _budgetExhausted so that
13354
+ * runWithConcurrency stops enqueuing new groups.
13355
+ *
13356
+ * @param triggeredStoryKey - The story that would have been dispatched next
13357
+ * @param remainingInGroup - Other stories in the same conflict group after triggeredStoryKey
13358
+ * @param result - The ceiling check result
13359
+ * @param manifest - The current run manifest data
13360
+ */
13361
+ async function handleCeilingExceeded(triggeredStoryKey, remainingInGroup, result, manifest) {
13362
+ const haltOn = manifest.cli_flags.halt_on ?? "none";
13363
+ const allSkipped = [triggeredStoryKey, ...remainingInGroup];
13364
+ for (const [key, state] of _stories) if (state.phase === "PENDING" && !allSkipped.includes(key)) allSkipped.push(key);
13365
+ for (const key of allSkipped) {
13366
+ updateStory(key, {
13367
+ phase: "ESCALATED",
13368
+ error: "cost-ceiling-reached",
13369
+ completedAt: new Date().toISOString()
13370
+ });
13371
+ if (runManifest !== null && runManifest !== void 0) runManifest.patchStoryState(key, { status: "escalated" }).catch(() => {});
13372
+ }
13373
+ eventBus.emit("cost:ceiling-reached", {
13374
+ cumulative_cost: result.cumulative,
13375
+ ceiling: result.ceiling,
13376
+ halt_on: haltOn,
13377
+ action: "stopped",
13378
+ skipped_stories: allSkipped,
13379
+ ...haltOn !== "none" ? { severity: "critical" } : {}
13380
+ });
13381
+ _budgetExhausted = true;
13382
+ logger$23.warn({
13383
+ skipped: allSkipped.length,
13384
+ cumulative: result.cumulative,
13385
+ ceiling: result.ceiling
13386
+ }, "Cost ceiling reached — stopping dispatch");
13387
+ }
13388
+ /**
13955
13389
  * Process a conflict group: run stories sequentially within the group.
13956
13390
  *
13957
13391
  * After each story completes (any outcome), a GC hint is issued and a short
@@ -13961,6 +13395,28 @@ function createImplementationOrchestrator(deps) {
13961
13395
  async function processConflictGroup(group) {
13962
13396
  const completedStoryKeys = [];
13963
13397
  for (const storyKey of group) {
13398
+ if (runManifest !== null && runManifest !== void 0) try {
13399
+ const manifestData = await runManifest.read();
13400
+ const ceiling = manifestData.cli_flags.cost_ceiling;
13401
+ if (ceiling !== void 0 && ceiling > 0) {
13402
+ const checkResult = _costChecker.checkCeiling(manifestData, ceiling);
13403
+ if (checkResult.status === "warning" && !_costWarningEmitted) {
13404
+ _costWarningEmitted = true;
13405
+ eventBus.emit("cost:warning", {
13406
+ cumulative_cost: checkResult.cumulative,
13407
+ ceiling: checkResult.ceiling,
13408
+ percent_used: checkResult.percentUsed
13409
+ });
13410
+ }
13411
+ if (checkResult.status === "exceeded") {
13412
+ const remainingInGroup = group.slice(group.indexOf(storyKey) + 1);
13413
+ await handleCeilingExceeded(storyKey, remainingInGroup, checkResult, manifestData);
13414
+ return;
13415
+ }
13416
+ }
13417
+ } catch (err) {
13418
+ logger$23.debug({ err }, "Cost ceiling check failed — proceeding without enforcement");
13419
+ }
13964
13420
  let optimizationDirectives;
13965
13421
  if (telemetryAdvisor !== void 0 && completedStoryKeys.length > 0) try {
13966
13422
  const recs = await telemetryAdvisor.getRecommendationsForRun(completedStoryKeys);
@@ -13996,6 +13452,7 @@ function createImplementationOrchestrator(deps) {
13996
13452
  const queue = [...groups];
13997
13453
  const running = new Set();
13998
13454
  function enqueue() {
13455
+ if (_budgetExhausted) return;
13999
13456
  const group = queue.shift();
14000
13457
  if (group === void 0) return;
14001
13458
  const p = processConflictGroup(group).finally(() => {
@@ -37842,10 +37299,10 @@ var PathScurryBase = class {
37842
37299
  if (er) return results.emit("error", er);
37843
37300
  /* c8 ignore stop */
37844
37301
  if (follow && !didRealpaths) {
37845
- const promises$1 = [];
37846
- for (const e of entries) if (e.isSymbolicLink()) promises$1.push(e.realpath().then((r) => r?.isUnknown() ? r.lstat() : r));
37847
- if (promises$1.length) {
37848
- Promise.all(promises$1).then(() => onReaddir(null, entries, true));
37302
+ const promises = [];
37303
+ for (const e of entries) if (e.isSymbolicLink()) promises.push(e.realpath().then((r) => r?.isUnknown() ? r.lstat() : r));
37304
+ if (promises.length) {
37305
+ Promise.all(promises).then(() => onReaddir(null, entries, true));
37849
37306
  return;
37850
37307
  }
37851
37308
  }
@@ -41209,6 +40666,27 @@ function wireNdjsonEmitter(eventBus, ndjsonEmitter) {
41209
40666
  duration_ms: payload.duration_ms
41210
40667
  });
41211
40668
  });
40669
+ eventBus.on("cost:warning", (payload) => {
40670
+ ndjsonEmitter.emit({
40671
+ type: "cost:warning",
40672
+ ts: new Date().toISOString(),
40673
+ cumulative_cost: payload.cumulative_cost,
40674
+ ceiling: payload.ceiling,
40675
+ percent_used: payload.percent_used
40676
+ });
40677
+ });
40678
+ eventBus.on("cost:ceiling-reached", (payload) => {
40679
+ ndjsonEmitter.emit({
40680
+ type: "cost:ceiling-reached",
40681
+ ts: new Date().toISOString(),
40682
+ cumulative_cost: payload.cumulative_cost,
40683
+ ceiling: payload.ceiling,
40684
+ halt_on: payload.halt_on,
40685
+ action: payload.action,
40686
+ skipped_stories: payload.skipped_stories,
40687
+ ...payload.severity !== void 0 ? { severity: payload.severity } : {}
40688
+ });
40689
+ });
41212
40690
  }
41213
40691
  async function runRunAction(options) {
41214
40692
  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, skipVerification, epic: epicNumber, dryRun, maxReviewCycles = 2, engine, agent: agentId, registry: injectedRegistry, haltOn, costCeiling } = options;
@@ -41305,6 +40783,7 @@ async function runRunAction(options) {
41305
40783
  let telemetryPort = 4318;
41306
40784
  let meshUrl;
41307
40785
  let meshProjectId;
40786
+ let configRetryBudget;
41308
40787
  try {
41309
40788
  const configSystem = createConfigSystem({ projectConfigDir: dbDir });
41310
40789
  await configSystem.load();
@@ -41322,6 +40801,7 @@ async function runRunAction(options) {
41322
40801
  meshUrl = cfg.telemetry.meshUrl;
41323
40802
  meshProjectId = cfg.telemetry.projectId;
41324
40803
  }
40804
+ if (typeof cfg.retry_budget === "number") configRetryBudget = cfg.retry_budget;
41325
40805
  } catch {
41326
40806
  logger.debug("Config loading skipped — using default token ceilings and telemetry settings");
41327
40807
  }
@@ -41406,6 +40886,7 @@ async function runRunAction(options) {
41406
40886
  } : {},
41407
40887
  engineType: resolvedEngine,
41408
40888
  maxReviewCycles: effectiveMaxReviewCycles,
40889
+ retryBudget: configRetryBudget ?? 2,
41409
40890
  agentId
41410
40891
  });
41411
40892
  let storyKeys = [...parsedStoryKeys];
@@ -41911,6 +41392,7 @@ async function runRunAction(options) {
41911
41392
  eventBus,
41912
41393
  pipelineRunId: pipelineRun.id,
41913
41394
  maxReviewCycles: effectiveMaxReviewCycles,
41395
+ retryBudget: configRetryBudget ?? 2,
41914
41396
  gcPauseMs: 0
41915
41397
  });
41916
41398
  if (outputFormat === "human" && progressRenderer === void 0 && ndjsonEmitter === void 0) {
@@ -41930,6 +41412,7 @@ async function runRunAction(options) {
41930
41412
  config: {
41931
41413
  maxConcurrency: concurrency,
41932
41414
  maxReviewCycles: effectiveMaxReviewCycles,
41415
+ retryBudget: configRetryBudget ?? 2,
41933
41416
  pipelineRunId: pipelineRun.id,
41934
41417
  enableHeartbeat: eventsFlag === true,
41935
41418
  skipPreflight: skipPreflight === true,
@@ -42043,13 +41526,13 @@ async function runRunAction(options) {
42043
41526
  process.stdout.write("\n");
42044
41527
  process.stdout.write(formatTokenTelemetry(tokenSummary) + "\n");
42045
41528
  }
42046
- if (meshUrl !== void 0) reportToMesh(adapter, pipelineRun.id, meshUrl, {
41529
+ if (meshUrl !== void 0) await reportToMesh(adapter, pipelineRun.id, meshUrl, {
42047
41530
  projectId: meshProjectId,
42048
41531
  projectRoot,
42049
41532
  agentBackend: agentId ?? "claude-code",
42050
41533
  engineType: resolvedEngine,
42051
41534
  concurrency
42052
- }).catch(() => {});
41535
+ });
42053
41536
  return 0;
42054
41537
  } catch (err) {
42055
41538
  const msg = err instanceof Error ? err.message : String(err);
@@ -42064,7 +41547,7 @@ async function runRunAction(options) {
42064
41547
  }
42065
41548
  }
42066
41549
  async function runFullPipeline(options) {
42067
- const { packName, packPath, dbDir, dbPath, startPhase, stopAfter, concept, concurrency, outputFormat, projectRoot, events: eventsFlag, skipUx, research: researchFlag, skipResearch: skipResearchFlag, skipPreflight, skipVerification, maxReviewCycles = 2, registry: injectedRegistry, tokenCeilings, stories: explicitStories, telemetryEnabled: fullTelemetryEnabled, telemetryPort: fullTelemetryPort, agentId, meshUrl: fpMeshUrl, meshProjectId: fpMeshProjectId, engineType: fpEngineType } = options;
41550
+ const { packName, packPath, dbDir, dbPath, startPhase, stopAfter, concept, concurrency, outputFormat, projectRoot, events: eventsFlag, skipUx, research: researchFlag, skipResearch: skipResearchFlag, skipPreflight, skipVerification, maxReviewCycles = 2, retryBudget, registry: injectedRegistry, tokenCeilings, stories: explicitStories, telemetryEnabled: fullTelemetryEnabled, telemetryPort: fullTelemetryPort, agentId, meshUrl: fpMeshUrl, meshProjectId: fpMeshProjectId, engineType: fpEngineType } = options;
42068
41551
  if (!existsSync$1(dbDir)) mkdirSync$1(dbDir, { recursive: true });
42069
41552
  const adapter = createDatabaseAdapter({
42070
41553
  backend: "auto",
@@ -42357,6 +41840,7 @@ async function runFullPipeline(options) {
42357
41840
  config: {
42358
41841
  maxConcurrency: concurrency,
42359
41842
  maxReviewCycles,
41843
+ retryBudget: retryBudget ?? 2,
42360
41844
  pipelineRunId: runId,
42361
41845
  skipPreflight: skipPreflight === true,
42362
41846
  ...skipVerification === true ? { skipVerification: true } : {}
@@ -42487,13 +41971,13 @@ async function runFullPipeline(options) {
42487
41971
  failed: fpFailedKeys,
42488
41972
  escalated: fpEscalatedKeys
42489
41973
  });
42490
- if (fpMeshUrl !== void 0) reportToMesh(adapter, runId, fpMeshUrl, {
41974
+ if (fpMeshUrl !== void 0) await reportToMesh(adapter, runId, fpMeshUrl, {
42491
41975
  projectId: fpMeshProjectId,
42492
41976
  projectRoot,
42493
41977
  agentBackend: agentId ?? "claude-code",
42494
41978
  engineType: fpEngineType ?? "linear",
42495
41979
  concurrency
42496
- }).catch(() => {});
41980
+ });
42497
41981
  return 0;
42498
41982
  } catch (err) {
42499
41983
  const msg = err instanceof Error ? err.message : String(err);
@@ -42558,4 +42042,4 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
42558
42042
 
42559
42043
  //#endregion
42560
42044
  export { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, EpicIngester, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, normalizeGraphSummaryToStatus, registerExportCommand, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveMaxReviewCycles, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict, wireNdjsonEmitter };
42561
- //# sourceMappingURL=run-CFmp4-qj.js.map
42045
+ //# sourceMappingURL=run-0MJeYpdb.js.map