substrate-ai 0.19.28 → 0.19.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { FileStateStore, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, WorkGraphRepository, buildPipelineStatusOutput, createDatabaseAdapter, createStateStore, findPackageRoot, formatOutput, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, inspectProcessTree, parseDbTimestampAsUtc, registerHealthCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot } from "../health-M0iCuP26.js";
2
+ import { FileStateStore, RunManifest, SUBSTRATE_OWNED_SETTINGS_KEYS, SupervisorLock, VALID_PHASES, WorkGraphRepository, buildPipelineStatusOutput, createDatabaseAdapter, createStateStore, findPackageRoot, formatOutput, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, inspectProcessTree, parseDbTimestampAsUtc, registerHealthCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveRunManifest } from "../health-BS20i6mY.js";
3
3
  import { createLogger } from "../logger-KeHncl-f.js";
4
4
  import { createEventBus } from "../helpers-CElYrONe.js";
5
5
  import { AdapterRegistry, BudgetConfigSchema, CURRENT_CONFIG_FORMAT_VERSION, CURRENT_TASK_GRAPH_VERSION, ConfigError, CostTrackerConfigSchema, DEFAULT_CONFIG, DoltClient, DoltNotInstalled, GlobalSettingsSchema, IngestionServer, MonitorDatabaseImpl, OPERATIONAL_FINDING, PartialGlobalSettingsSchema, PartialProviderConfigSchema, ProvidersSchema, RoutingRecommender, STORY_METRICS, TelemetryConfigSchema, addTokenUsage, aggregateTokenUsageForRun, checkDoltInstalled, compareRunMetrics, createAmendmentRun, createConfigSystem, createDecision, createDoltClient, createPipelineRun, getActiveDecisions, getAllCostEntriesFiltered, getBaselineRunMetrics, getDecisionsByCategory, getDecisionsByPhaseForRun, getLatestCompletedRun, getLatestRun, getPipelineRunById, getPlanningCostTotal, getRetryableEscalations, getRunMetrics, getRunningPipelineRuns, getSessionCostSummary, getSessionCostSummaryFiltered, getStoryMetricsForRun, getTokenUsageSummary, incrementRunRestarts, initSchema, initializeDolt, listRunMetrics, loadParentRunDecisions, supersedeDecision, tagRunAsBaseline, updatePipelineRun } from "../dist-R0W4ofKv.js";
6
6
  import "../adapter-registry-DXLMTmfD.js";
7
- import { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, EpicIngester, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, registerExportCommand, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-DabSV2xH.js";
7
+ import { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, EpicIngester, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, registerExportCommand, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-BBYhrXw9.js";
8
8
  import "../errors-BJRMJyGb.js";
9
9
  import "../routing-CcBOCuC9.js";
10
10
  import "../decisions-C0pz9Clx.js";
@@ -21,6 +21,7 @@ import * as path$3 from "node:path";
21
21
  import * as path$2 from "node:path";
22
22
  import * as path$1 from "node:path";
23
23
  import { join as join$1 } from "node:path";
24
+ import { randomUUID } from "node:crypto";
24
25
  import { z } from "zod";
25
26
  import * as fs from "node:fs/promises";
26
27
  import { access as access$1, readFile as readFile$1, readdir as readdir$1 } from "node:fs/promises";
@@ -29,7 +30,7 @@ import { homedir } from "os";
29
30
  import { createRequire } from "node:module";
30
31
  import { fileURLToPath as fileURLToPath$1 } from "node:url";
31
32
  import { createInterface } from "node:readline";
32
- import { randomUUID } from "crypto";
33
+ import { randomUUID as randomUUID$1 } from "crypto";
33
34
  import { createInterface as createInterface$1 } from "readline";
34
35
 
35
36
  //#region packages/core/dist/git/git-utils.js
@@ -2761,6 +2762,23 @@ async function runResumeAction(options) {
2761
2762
  if (Array.isArray(config.explicitStories) && config.explicitStories.length > 0) scopedStories = config.explicitStories;
2762
2763
  } catch {}
2763
2764
  const dbDir = dbPath.replace("/substrate.db", "");
2765
+ if (options.stories === void 0 || options.stories.length === 0) {
2766
+ const { manifest: resolvedManifest } = await resolveRunManifest(dbRoot, runId);
2767
+ if (resolvedManifest !== null) try {
2768
+ const manifestData = await resolvedManifest.read();
2769
+ const manifestStories = manifestData.cli_flags["stories"] ?? manifestData.story_scope;
2770
+ if (Array.isArray(manifestStories) && manifestStories.length > 0) {
2771
+ scopedStories = manifestStories;
2772
+ logger$13.debug({
2773
+ runId,
2774
+ stories: scopedStories
2775
+ }, "resume scope loaded from manifest");
2776
+ }
2777
+ } catch {
2778
+ logger$13.debug({ runId }, "manifest read failed in resume — using legacy config_json scope");
2779
+ }
2780
+ else logger$13.debug({ runId }, "Run manifest not found for scope preservation — using legacy config_json scope");
2781
+ }
2764
2782
  return runFullPipelineFromPhase({
2765
2783
  packName,
2766
2784
  packPath,
@@ -3134,6 +3152,55 @@ function registerResumeCommand(program, _version = "0.0.0", projectRoot = proces
3134
3152
  //#endregion
3135
3153
  //#region src/cli/commands/status.ts
3136
3154
  const logger$12 = createLogger("status-cmd");
3155
+ /**
3156
+ * Map a manifest per-story status string to the appropriate WorkGraphCounts bucket.
3157
+ * Unknown strings are treated as `inProgress` (safe default).
3158
+ */
3159
+ function manifestStatusToWorkGraphBucket(status) {
3160
+ switch (status) {
3161
+ case "complete": return "complete";
3162
+ case "escalated": return "escalated";
3163
+ case "failed":
3164
+ case "verification-failed": return "failed";
3165
+ case "dispatched":
3166
+ case "in-review":
3167
+ case "recovered": return "inProgress";
3168
+ case "gated":
3169
+ case "pending": return "ready";
3170
+ default: return "inProgress";
3171
+ }
3172
+ }
3173
+ /**
3174
+ * Build a WorkGraphSummary from manifest `per_story_state`.
3175
+ * readyStories and blockedStories are left empty — manifest does not carry
3176
+ * dependency-graph detail (only status counts).
3177
+ */
3178
+ function buildWorkGraphFromManifest(perStoryState) {
3179
+ const counts = {
3180
+ ready: 0,
3181
+ blocked: 0,
3182
+ inProgress: 0,
3183
+ complete: 0,
3184
+ escalated: 0,
3185
+ failed: 0
3186
+ };
3187
+ for (const entry of Object.values(perStoryState)) {
3188
+ const bucket = manifestStatusToWorkGraphBucket(entry.status);
3189
+ counts[bucket]++;
3190
+ }
3191
+ return {
3192
+ summary: {
3193
+ ready: counts.ready,
3194
+ blocked: counts.blocked,
3195
+ inProgress: counts.inProgress,
3196
+ complete: counts.complete,
3197
+ escalated: counts.escalated,
3198
+ failed: counts.failed
3199
+ },
3200
+ readyStories: [],
3201
+ blockedStories: []
3202
+ };
3203
+ }
3137
3204
  async function runStatusAction(options) {
3138
3205
  const { outputFormat, runId, projectRoot, stateStore, history } = options;
3139
3206
  if (history === true) {
@@ -3176,8 +3243,29 @@ async function runStatusAction(options) {
3176
3243
  });
3177
3244
  try {
3178
3245
  await initSchema(adapter);
3246
+ let run;
3247
+ if (runId !== void 0 && runId !== "") run = await getPipelineRunById(adapter, runId);
3248
+ else {
3249
+ let currentRunId;
3250
+ try {
3251
+ const currentRunIdPath = join(dbRoot, ".substrate", "current-run-id");
3252
+ const content = readFileSync$1(currentRunIdPath, "utf-8").trim();
3253
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3254
+ if (UUID_RE.test(content)) currentRunId = content;
3255
+ } catch {}
3256
+ if (currentRunId !== void 0) run = await getPipelineRunById(adapter, currentRunId);
3257
+ if (run === void 0) run = await getLatestRun(adapter);
3258
+ }
3179
3259
  let workGraph;
3180
- try {
3260
+ const { manifest: resolvedManifest } = await resolveRunManifest(dbRoot, run?.id);
3261
+ if (resolvedManifest !== null) try {
3262
+ const manifestData = await resolvedManifest.read();
3263
+ workGraph = buildWorkGraphFromManifest(manifestData.per_story_state);
3264
+ logger$12.debug({ runId: run?.id }, "status: workGraph built from manifest per_story_state");
3265
+ } catch {
3266
+ logger$12.debug({ runId: run?.id }, "status: manifest read failed — falling back to wg_stories");
3267
+ }
3268
+ if (workGraph === void 0) try {
3181
3269
  const wgRepo = new WorkGraphRepository(adapter);
3182
3270
  const allStories = await adapter.query(`SELECT story_key, title, status FROM wg_stories`);
3183
3271
  if (allStories.length > 0) {
@@ -3214,21 +3302,8 @@ async function runStatusAction(options) {
3214
3302
  } catch (err) {
3215
3303
  logger$12.debug({ err }, "Work graph query failed, continuing without work graph data");
3216
3304
  }
3217
- let run;
3218
- if (runId !== void 0 && runId !== "") run = await getPipelineRunById(adapter, runId);
3219
- else {
3220
- let currentRunId;
3221
- try {
3222
- const currentRunIdPath = join(dbRoot, ".substrate", "current-run-id");
3223
- const content = readFileSync$1(currentRunIdPath, "utf-8").trim();
3224
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3225
- if (UUID_RE.test(content)) currentRunId = content;
3226
- } catch {}
3227
- if (currentRunId !== void 0) run = await getPipelineRunById(adapter, currentRunId);
3228
- if (run === void 0) run = await getLatestRun(adapter);
3229
- }
3230
3305
  if (run === void 0) {
3231
- const { inspectProcessTree: inspectProcessTree$1 } = await import("../health-CVfyC7j0.js");
3306
+ const { inspectProcessTree: inspectProcessTree$1 } = await import("../health-Cy_9GgQ_.js");
3232
3307
  const substrateDirPath = join(projectRoot, ".substrate");
3233
3308
  const processInfo = inspectProcessTree$1({
3234
3309
  projectRoot,
@@ -3822,7 +3897,7 @@ async function runAmendAction(options) {
3822
3897
  }
3823
3898
  parentRunId = latestCompleted.id;
3824
3899
  }
3825
- const amendmentRunId = randomUUID();
3900
+ const amendmentRunId = randomUUID$1();
3826
3901
  let methodology = packName;
3827
3902
  try {
3828
3903
  const packLoader$1 = createPackLoader();
@@ -4044,6 +4119,7 @@ function registerAmendCommand(program, _version = "0.0.0", projectRoot = process
4044
4119
 
4045
4120
  //#endregion
4046
4121
  //#region src/cli/commands/supervisor.ts
4122
+ const supervisorLogger = createLogger("supervisor-cmd");
4047
4123
  function defaultSupervisorDeps() {
4048
4124
  return {
4049
4125
  getHealth: getAutoHealthData,
@@ -4379,7 +4455,13 @@ async function handleStallRecovery(health, state, config, deps, io) {
4379
4455
  log(`Supervisor: Restarting pipeline (attempt ${newRestartCount}/${maxRestarts})`);
4380
4456
  try {
4381
4457
  let scopedStories;
4382
- if (deps.getRunConfig !== void 0 && health.run_id !== null) try {
4458
+ if (health.run_id !== null) try {
4459
+ const manifest = RunManifest.open(health.run_id, projectRoot);
4460
+ const data = await manifest.read();
4461
+ const manifestStories = data?.cli_flags?.stories;
4462
+ if (Array.isArray(manifestStories) && manifestStories.length > 0) scopedStories = manifestStories;
4463
+ } catch {}
4464
+ if (scopedStories === void 0 && deps.getRunConfig !== void 0 && health.run_id !== null) try {
4383
4465
  const runConfig = await deps.getRunConfig(health.run_id, projectRoot);
4384
4466
  if (runConfig?.explicitStories !== void 0 && runConfig.explicitStories.length > 0) scopedStories = runConfig.explicitStories;
4385
4467
  } catch {}
@@ -4442,7 +4524,7 @@ async function handleStallRecovery(health, state, config, deps, io) {
4442
4524
  * 2 — max restarts exceeded (safety valve triggered)
4443
4525
  */
4444
4526
  async function runSupervisorAction(options, deps = {}) {
4445
- const { pollInterval, stallThreshold, maxRestarts, outputFormat, projectRoot, runId, pack, experiment, maxExperiments } = options;
4527
+ const { pollInterval, stallThreshold, maxRestarts, outputFormat, projectRoot, runId, pack, experiment, maxExperiments, force } = options;
4446
4528
  const resolvedDeps = {
4447
4529
  ...defaultSupervisorDeps(),
4448
4530
  ...deps
@@ -4455,6 +4537,62 @@ async function runSupervisorAction(options, deps = {}) {
4455
4537
  };
4456
4538
  let maxRestartsExhausted = false;
4457
4539
  const startTime = Date.now();
4540
+ const sessionId = randomUUID();
4541
+ let supervisorLock = null;
4542
+ /** Track whether process exit handlers have been registered for this supervisor. */
4543
+ let exitHandlersRegistered = false;
4544
+ /**
4545
+ * Register process.once exit handlers to release the lock on exit.
4546
+ * Called exactly once, after the first successful lock acquisition.
4547
+ * Using process.once (not process.on) per Story 52-2 spec.
4548
+ */
4549
+ function registerExitHandlers(lock) {
4550
+ if (exitHandlersRegistered) return;
4551
+ exitHandlersRegistered = true;
4552
+ process.once("exit", () => {
4553
+ lock.release().catch((e) => {
4554
+ supervisorLogger.debug({ error: e }, "lock release on exit failed");
4555
+ });
4556
+ });
4557
+ process.once("SIGTERM", () => {
4558
+ lock.release().then(() => process.exit(0)).catch(() => process.exit(1));
4559
+ });
4560
+ process.once("SIGINT", () => {
4561
+ lock.release().then(() => process.exit(0)).catch(() => process.exit(1));
4562
+ });
4563
+ }
4564
+ /**
4565
+ * Acquire the supervisor lock for a given run ID.
4566
+ * Non-fatal: logs and continues on failure so the supervisor can still
4567
+ * function in degraded mode without blocking the pipeline.
4568
+ */
4569
+ async function acquireLockForRun(targetRunId) {
4570
+ if (supervisorLock !== null) return;
4571
+ try {
4572
+ const runsDir = join(projectRoot, ".substrate", "runs");
4573
+ const manifest = RunManifest.open(targetRunId, runsDir);
4574
+ const lock = new SupervisorLock(targetRunId, manifest, supervisorLogger);
4575
+ await lock.acquire(process.pid, sessionId, { force: force ?? false });
4576
+ supervisorLock = lock;
4577
+ supervisorLogger.debug({ runId: targetRunId }, "Supervisor lock acquired");
4578
+ registerExitHandlers(lock);
4579
+ } catch (lockErr) {
4580
+ const msg = lockErr instanceof Error ? lockErr.message : String(lockErr);
4581
+ supervisorLogger.warn({
4582
+ runId: targetRunId,
4583
+ error: msg
4584
+ }, "Supervisor lock acquisition failed");
4585
+ if (outputFormat === "json") process.stdout.write(JSON.stringify({
4586
+ type: "supervisor:lock-failed",
4587
+ run_id: targetRunId,
4588
+ reason: msg,
4589
+ ts: new Date().toISOString()
4590
+ }) + "\n");
4591
+ else process.stderr.write(`Warning: Supervisor lock acquisition failed: ${msg}\n`);
4592
+ if (msg.includes("is already supervised by PID") && !force) throw lockErr;
4593
+ }
4594
+ }
4595
+ if (runId !== void 0) await acquireLockForRun(runId);
4458
4596
  function emitEvent(event) {
4459
4597
  if (outputFormat === "json") {
4460
4598
  const stamped = {
@@ -4479,6 +4617,7 @@ async function runSupervisorAction(options, deps = {}) {
4479
4617
  runId: health.run_id
4480
4618
  };
4481
4619
  log(`Supervisor: auto-bound to active run ${health.run_id}`);
4620
+ await acquireLockForRun(health.run_id);
4482
4621
  }
4483
4622
  if (outputFormat === "json") {
4484
4623
  const tokenSnapshot = health.run_id !== null ? await getTokenSnapshot(health.run_id, projectRoot) : {
@@ -4571,7 +4710,7 @@ async function runSupervisorAction(options, deps = {}) {
4571
4710
  await initSchema(expAdapter);
4572
4711
  const { runRunAction: runPipeline } = await import(
4573
4712
  /* @vite-ignore */
4574
- "../run-CjwCYY8Q.js"
4713
+ "../run-D0-aXchh.js"
4575
4714
  );
4576
4715
  const runStoryFn = async (opts) => {
4577
4716
  const exitCode = await runPipeline({
@@ -4780,7 +4919,7 @@ async function runMultiProjectSupervisor(options, deps = {}) {
4780
4919
  }
4781
4920
  }
4782
4921
  function registerSupervisorCommand(program, _version = "0.0.0", projectRoot = process.cwd()) {
4783
- program.command("supervisor").description("Monitor a pipeline run and automatically recover from stalls").option("--poll-interval <seconds>", "Health poll interval in seconds", (v) => parseInt(v, 10), 60).option("--stall-threshold <seconds>", "Staleness in seconds before killing a stalled pipeline", (v) => parseInt(v, 10), 600).option("--max-restarts <n>", "Maximum automatic restarts before aborting", (v) => parseInt(v, 10), 3).option("--run-id <id>", "Pipeline run ID to monitor (defaults to latest)").option("--pack <name>", "Methodology pack name", "bmad").option("--project-root <path>", "Project root directory", projectRoot).option("--projects <paths>", "Comma-separated project root directories to monitor (multi-project mode)").option("--output-format <format>", "Output format: human (default) or json", "human").option("--experiment", "After post-run analysis, enter experiment mode: create branches, apply modifications, run single-story experiments, and report verdicts (Story 17-4)", false).option("--max-experiments <n>", "Maximum number of experiments to run per analysis cycle (default: 2, Story 17-4 AC6)", (v) => parseInt(v, 10), 2).action(async (opts) => {
4922
+ program.command("supervisor").description("Monitor a pipeline run and automatically recover from stalls").option("--poll-interval <seconds>", "Health poll interval in seconds", (v) => parseInt(v, 10), 60).option("--stall-threshold <seconds>", "Staleness in seconds before killing a stalled pipeline", (v) => parseInt(v, 10), 600).option("--max-restarts <n>", "Maximum automatic restarts before aborting", (v) => parseInt(v, 10), 3).option("--run-id <id>", "Pipeline run ID to monitor (defaults to latest)").option("--pack <name>", "Methodology pack name", "bmad").option("--project-root <path>", "Project root directory", projectRoot).option("--projects <paths>", "Comma-separated project root directories to monitor (multi-project mode)").option("--output-format <format>", "Output format: human (default) or json", "human").option("--experiment", "After post-run analysis, enter experiment mode: create branches, apply modifications, run single-story experiments, and report verdicts (Story 17-4)", false).option("--max-experiments <n>", "Maximum number of experiments to run per analysis cycle (default: 2, Story 17-4 AC6)", (v) => parseInt(v, 10), 2).option("--force", "Forcefully evict an existing supervisor process (SIGTERM + 500ms) before attaching (Story 52-2)", false).action(async (opts) => {
4784
4923
  const outputFormat = opts.outputFormat === "json" ? "json" : "human";
4785
4924
  if (opts.stallThreshold < 120) console.warn(`Warning: --stall-threshold ${opts.stallThreshold}s is below 120s. Agent steps typically take 45-90s. This may cause false stall detections and wasted restarts.`);
4786
4925
  if (opts.projects) {
@@ -4811,7 +4950,8 @@ function registerSupervisorCommand(program, _version = "0.0.0", projectRoot = pr
4811
4950
  outputFormat,
4812
4951
  projectRoot: opts.projectRoot,
4813
4952
  experiment: opts.experiment,
4814
- maxExperiments: opts.maxExperiments
4953
+ maxExperiments: opts.maxExperiments,
4954
+ force: opts.force
4815
4955
  });
4816
4956
  process.exitCode = exitCode;
4817
4957
  });