substrate-ai 0.2.11 → 0.2.13

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
@@ -2,7 +2,7 @@
2
2
  import { createLogger, deepMask } from "../logger-C6n1g8uP.js";
3
3
  import { AdapterRegistry, createEventBus } from "../event-bus-J-bw-pkp.js";
4
4
  import { CURRENT_CONFIG_FORMAT_VERSION, CURRENT_TASK_GRAPH_VERSION, PartialSubstrateConfigSchema, SUPPORTED_CONFIG_FORMAT_VERSIONS, SubstrateConfigSchema, defaultConfigMigrator } from "../version-manager-impl-BpVx2DkY.js";
5
- import { DatabaseWrapper, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, buildPipelineStatusOutput, createContextCompiler, createDispatcher, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getSubstrateDefaultSettings, parseDbTimestampAsUtc, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, runAnalysisPhase, runMigrations, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-L-R_XYNT.js";
5
+ import { DatabaseWrapper, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, buildPipelineStatusOutput, createContextCompiler, createDispatcher, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getSubstrateDefaultSettings, parseDbTimestampAsUtc, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, runAnalysisPhase, runMigrations, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-CoP8UQU3.js";
6
6
  import { ConfigError, ConfigIncompatibleFormatError } from "../errors-BPqtzQ4U.js";
7
7
  import { addTokenUsage, createDecision, getDecisionsByPhaseForRun, getLatestRun, getPipelineRunById, getTokenUsageSummary, listRequirements, updatePipelineRun } from "../decisions-DNYByk0U.js";
8
8
  import { aggregateTokenUsageForRun, compareRunMetrics, getBaselineRunMetrics, getRunMetrics, getStoryMetricsForRun, incrementRunRestarts, listRunMetrics, tagRunAsBaseline } from "../metrics-BSg8VIHd.js";
@@ -2414,16 +2414,24 @@ const DEFAULT_STALL_THRESHOLD_SECONDS = 600;
2414
2414
  * - `node dist/cli/index.js run` (npm run substrate:dev)
2415
2415
  * - `npx substrate run`
2416
2416
  * - any node process whose command contains `run` with `--events` or `--stories`
2417
+ *
2418
+ * When `projectRoot` is provided, additionally checks that the command line
2419
+ * contains that path (via `--project-root` flag or as part of the binary/CWD path).
2420
+ * This ensures multi-project environments match the correct orchestrator.
2417
2421
  */
2418
- function isOrchestratorProcessLine(line) {
2422
+ function isOrchestratorProcessLine(line, projectRoot) {
2419
2423
  if (line.includes("grep")) return false;
2420
- if (line.includes("substrate run")) return true;
2421
- if (line.includes("substrate-ai run")) return true;
2422
- if (line.includes("index.js run")) return true;
2423
- if (line.includes("node") && /\srun(\s|$)/.test(line) && (line.includes("--events") || line.includes("--stories"))) return true;
2424
- return false;
2425
- }
2426
- function inspectProcessTree(execFileSyncOverride) {
2424
+ let isOrchestrator = false;
2425
+ if (line.includes("substrate run")) isOrchestrator = true;
2426
+ else if (line.includes("substrate-ai run")) isOrchestrator = true;
2427
+ else if (line.includes("index.js run")) isOrchestrator = true;
2428
+ else if (line.includes("node") && /\srun(\s|$)/.test(line) && (line.includes("--events") || line.includes("--stories"))) isOrchestrator = true;
2429
+ if (!isOrchestrator) return false;
2430
+ if (projectRoot !== void 0) return line.includes(projectRoot);
2431
+ return true;
2432
+ }
2433
+ function inspectProcessTree(opts) {
2434
+ const { projectRoot, execFileSync: execFileSyncOverride } = opts ?? {};
2427
2435
  const result = {
2428
2436
  orchestrator_pid: null,
2429
2437
  child_pids: [],
@@ -2443,7 +2451,7 @@ function inspectProcessTree(execFileSyncOverride) {
2443
2451
  });
2444
2452
  }
2445
2453
  const lines = psOutput.split("\n");
2446
- for (const line of lines) if (isOrchestratorProcessLine(line)) {
2454
+ for (const line of lines) if (isOrchestratorProcessLine(line, projectRoot)) {
2447
2455
  const match = line.trim().match(/^(\d+)/);
2448
2456
  if (match) {
2449
2457
  result.orchestrator_pid = parseInt(match[1], 10);
@@ -2466,6 +2474,58 @@ function inspectProcessTree(execFileSyncOverride) {
2466
2474
  return result;
2467
2475
  }
2468
2476
  /**
2477
+ * Collect all descendant PIDs of the given root PIDs by walking the process
2478
+ * tree recursively. This ensures that grandchildren of the orchestrator
2479
+ * (e.g. node subprocesses spawned by `claude -p`) are also killed during
2480
+ * stall recovery, leaving no orphan processes.
2481
+ *
2482
+ * Returns only the descendants — the root PIDs themselves are NOT included.
2483
+ */
2484
+ function getAllDescendantPids(rootPids, execFileSyncOverride) {
2485
+ if (rootPids.length === 0) return [];
2486
+ try {
2487
+ let psOutput;
2488
+ if (execFileSyncOverride !== void 0) psOutput = execFileSyncOverride("ps", ["-eo", "pid,ppid"], {
2489
+ encoding: "utf-8",
2490
+ timeout: 5e3
2491
+ });
2492
+ else {
2493
+ const { execFileSync } = __require("node:child_process");
2494
+ psOutput = execFileSync("ps", ["-eo", "pid,ppid"], {
2495
+ encoding: "utf-8",
2496
+ timeout: 5e3
2497
+ });
2498
+ }
2499
+ const childrenOf = new Map();
2500
+ for (const line of psOutput.split("\n")) {
2501
+ const parts = line.trim().split(/\s+/);
2502
+ if (parts.length >= 2) {
2503
+ const pid = parseInt(parts[0], 10);
2504
+ const ppid = parseInt(parts[1], 10);
2505
+ if (!isNaN(pid) && !isNaN(ppid) && pid > 0) {
2506
+ if (!childrenOf.has(ppid)) childrenOf.set(ppid, []);
2507
+ childrenOf.get(ppid).push(pid);
2508
+ }
2509
+ }
2510
+ }
2511
+ const descendants = [];
2512
+ const seen = new Set(rootPids);
2513
+ const queue = [...rootPids];
2514
+ while (queue.length > 0) {
2515
+ const current = queue.shift();
2516
+ const children = childrenOf.get(current) ?? [];
2517
+ for (const child of children) if (!seen.has(child)) {
2518
+ seen.add(child);
2519
+ descendants.push(child);
2520
+ queue.push(child);
2521
+ }
2522
+ }
2523
+ return descendants;
2524
+ } catch {
2525
+ return [];
2526
+ }
2527
+ }
2528
+ /**
2469
2529
  * Fetch pipeline health data as a structured object without any stdout side-effects.
2470
2530
  * Used by runSupervisorAction to poll health without formatting overhead.
2471
2531
  *
@@ -2524,10 +2584,11 @@ async function getAutoHealthData(options) {
2524
2584
  }
2525
2585
  }
2526
2586
  } catch {}
2527
- const processInfo = inspectProcessTree();
2587
+ const processInfo = inspectProcessTree({ projectRoot });
2528
2588
  let verdict = "NO_PIPELINE_RUNNING";
2529
2589
  if (run.status === "running") if (processInfo.orchestrator_pid === null && active === 0 && completed > 0) verdict = "NO_PIPELINE_RUNNING";
2530
2590
  else if (processInfo.zombies.length > 0) verdict = "STALLED";
2591
+ else if (processInfo.orchestrator_pid !== null && processInfo.child_pids.length > 0 && stalenessSeconds > DEFAULT_STALL_THRESHOLD_SECONDS) verdict = "HEALTHY";
2531
2592
  else if (stalenessSeconds > DEFAULT_STALL_THRESHOLD_SECONDS) verdict = "STALLED";
2532
2593
  else if (processInfo.orchestrator_pid !== null && processInfo.child_pids.length === 0 && active > 0) verdict = "STALLED";
2533
2594
  else verdict = "HEALTHY";
@@ -2660,6 +2721,7 @@ function defaultSupervisorDeps() {
2660
2721
  };
2661
2722
  }
2662
2723
  },
2724
+ getAllDescendants: (rootPids) => getAllDescendantPids(rootPids),
2663
2725
  runAnalysis: async (runId, projectRoot) => {
2664
2726
  const dbPath = join(projectRoot, ".substrate", "substrate.db");
2665
2727
  if (!existsSync(dbPath)) return;
@@ -2701,7 +2763,7 @@ function defaultSupervisorDeps() {
2701
2763
  */
2702
2764
  async function runSupervisorAction(options, deps = {}) {
2703
2765
  const { pollInterval, stallThreshold, maxRestarts, outputFormat, projectRoot, runId, pack, experiment, maxExperiments } = options;
2704
- const { getHealth, killPid, resumePipeline, sleep, incrementRestarts, runAnalysis, getTokenSnapshot } = {
2766
+ const { getHealth, killPid, resumePipeline, sleep, incrementRestarts, runAnalysis, getTokenSnapshot, getAllDescendants } = {
2705
2767
  ...defaultSupervisorDeps(),
2706
2768
  ...deps
2707
2769
  };
@@ -2833,7 +2895,7 @@ async function runSupervisorAction(options, deps = {}) {
2833
2895
  const expDb = expDbWrapper.db;
2834
2896
  const { runRunAction: runPipeline } = await import(
2835
2897
  /* @vite-ignore */
2836
- "../run-C8aOWnKG.js"
2898
+ "../run-B9IglY4m.js"
2837
2899
  );
2838
2900
  const runStoryFn = async (opts) => {
2839
2901
  const exitCode = await runPipeline({
@@ -2899,7 +2961,10 @@ async function runSupervisorAction(options, deps = {}) {
2899
2961
  return failed.length > 0 || escalated.length > 0 ? 1 : 0;
2900
2962
  }
2901
2963
  if (health.staleness_seconds >= stallThreshold) {
2902
- const pids = [...health.process.orchestrator_pid !== null ? [health.process.orchestrator_pid] : [], ...health.process.child_pids];
2964
+ const directPids = [...health.process.orchestrator_pid !== null ? [health.process.orchestrator_pid] : [], ...health.process.child_pids];
2965
+ const descendantPids = getAllDescendants(directPids);
2966
+ const directPidSet = new Set(directPids);
2967
+ const pids = [...directPids, ...descendantPids.filter((p) => !directPidSet.has(p))];
2903
2968
  emitEvent({
2904
2969
  type: "supervisor:kill",
2905
2970
  run_id: health.run_id,
package/dist/index.d.ts CHANGED
@@ -1025,6 +1025,8 @@ interface OrchestratorEvents {
1025
1025
  storyKey: string;
1026
1026
  phase: string;
1027
1027
  elapsedMs: number;
1028
+ /** PID of the stalled child process, or null if not tracked */
1029
+ childPid: number | null;
1028
1030
  };
1029
1031
  /** Readiness check has completed — emitted for all verdicts (READY, NEEDS_WORK, NOT_READY) */
1030
1032
  'solutioning:readiness-check': {
@@ -1,6 +1,6 @@
1
1
  import "./logger-C6n1g8uP.js";
2
2
  import "./event-bus-J-bw-pkp.js";
3
- import { registerRunCommand, runRunAction } from "./run-L-R_XYNT.js";
3
+ import { registerRunCommand, runRunAction } from "./run-CoP8UQU3.js";
4
4
  import "./decisions-DNYByk0U.js";
5
5
  import "./metrics-BSg8VIHd.js";
6
6
 
@@ -1202,6 +1202,15 @@ function buildPipelineStatusOutput(run, tokenSummary, decisionsCount, storiesCou
1202
1202
  totalOutput += row.total_output_tokens;
1203
1203
  totalCost += row.total_cost_usd;
1204
1204
  }
1205
+ let activeDispatches = 0;
1206
+ try {
1207
+ if (run.token_usage_json) {
1208
+ const state = JSON.parse(run.token_usage_json);
1209
+ if (state.stories) {
1210
+ for (const s of Object.values(state.stories)) if (s.phase !== "PENDING" && s.phase !== "COMPLETE" && s.phase !== "ESCALATED") activeDispatches++;
1211
+ }
1212
+ }
1213
+ } catch {}
1205
1214
  return {
1206
1215
  run_id: run.id,
1207
1216
  current_phase: currentPhase,
@@ -1214,7 +1223,9 @@ function buildPipelineStatusOutput(run, tokenSummary, decisionsCount, storiesCou
1214
1223
  decisions_count: decisionsCount,
1215
1224
  stories_count: storiesCount,
1216
1225
  last_activity: run.updated_at,
1217
- staleness_seconds: Math.round((Date.now() - parseDbTimestampAsUtc(run.updated_at).getTime()) / 1e3)
1226
+ staleness_seconds: Math.round((Date.now() - parseDbTimestampAsUtc(run.updated_at).getTime()) / 1e3),
1227
+ last_event_ts: run.updated_at,
1228
+ active_dispatches: activeDispatches
1218
1229
  };
1219
1230
  }
1220
1231
  /**
@@ -5359,6 +5370,7 @@ function createImplementationOrchestrator(deps) {
5359
5370
  let _heartbeatTimer = null;
5360
5371
  const HEARTBEAT_INTERVAL_MS = 3e4;
5361
5372
  const WATCHDOG_TIMEOUT_MS = 6e5;
5373
+ const _stalledStories = new Set();
5362
5374
  const _phaseStartMs = new Map();
5363
5375
  const _phaseEndMs = new Map();
5364
5376
  const _storyDispatches = new Map();
@@ -5454,6 +5466,7 @@ function createImplementationOrchestrator(deps) {
5454
5466
  }
5455
5467
  function recordProgress() {
5456
5468
  _lastProgressTs = Date.now();
5469
+ _stalledStories.clear();
5457
5470
  }
5458
5471
  function startHeartbeat() {
5459
5472
  if (_heartbeatTimer !== null) return;
@@ -5465,7 +5478,8 @@ function createImplementationOrchestrator(deps) {
5465
5478
  for (const s of _stories.values()) if (s.phase === "COMPLETE" || s.phase === "ESCALATED") completed++;
5466
5479
  else if (s.phase === "PENDING") queued++;
5467
5480
  else active++;
5468
- eventBus.emit("orchestrator:heartbeat", {
5481
+ const timeSinceProgress = Date.now() - _lastProgressTs;
5482
+ if (timeSinceProgress >= HEARTBEAT_INTERVAL_MS) eventBus.emit("orchestrator:heartbeat", {
5469
5483
  runId: config.pipelineRunId ?? "",
5470
5484
  activeDispatches: active,
5471
5485
  completedDispatches: completed,
@@ -5474,6 +5488,8 @@ function createImplementationOrchestrator(deps) {
5474
5488
  const elapsed = Date.now() - _lastProgressTs;
5475
5489
  if (elapsed >= WATCHDOG_TIMEOUT_MS) {
5476
5490
  for (const [key, s] of _stories) if (s.phase !== "PENDING" && s.phase !== "COMPLETE" && s.phase !== "ESCALATED") {
5491
+ if (_stalledStories.has(key)) continue;
5492
+ _stalledStories.add(key);
5477
5493
  logger$16.warn({
5478
5494
  storyKey: key,
5479
5495
  phase: s.phase,
@@ -5483,7 +5499,8 @@ function createImplementationOrchestrator(deps) {
5483
5499
  runId: config.pipelineRunId ?? "",
5484
5500
  storyKey: key,
5485
5501
  phase: s.phase,
5486
- elapsedMs: elapsed
5502
+ elapsedMs: elapsed,
5503
+ childPid: null
5487
5504
  });
5488
5505
  }
5489
5506
  }
@@ -6244,7 +6261,7 @@ function createImplementationOrchestrator(deps) {
6244
6261
  });
6245
6262
  persistState();
6246
6263
  recordProgress();
6247
- startHeartbeat();
6264
+ if (config.enableHeartbeat) startHeartbeat();
6248
6265
  if (projectRoot !== void 0) {
6249
6266
  const seedResult = seedMethodologyContext(db, projectRoot);
6250
6267
  if (seedResult.decisionsCreated > 0) logger$16.info({
@@ -10697,7 +10714,8 @@ async function runRunAction(options) {
10697
10714
  run_id: payload.runId,
10698
10715
  story_key: payload.storyKey,
10699
10716
  phase: payload.phase,
10700
- elapsed_ms: payload.elapsedMs
10717
+ elapsed_ms: payload.elapsedMs,
10718
+ child_pid: payload.childPid
10701
10719
  });
10702
10720
  });
10703
10721
  }
@@ -10710,7 +10728,8 @@ async function runRunAction(options) {
10710
10728
  config: {
10711
10729
  maxConcurrency: concurrency,
10712
10730
  maxReviewCycles: 2,
10713
- pipelineRunId: pipelineRun.id
10731
+ pipelineRunId: pipelineRun.id,
10732
+ enableHeartbeat: eventsFlag === true
10714
10733
  },
10715
10734
  projectRoot
10716
10735
  });
@@ -11153,4 +11172,4 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
11153
11172
 
11154
11173
  //#endregion
11155
11174
  export { DatabaseWrapper, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, buildPipelineStatusOutput, createContextCompiler, createDispatcher, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getSubstrateDefaultSettings, parseDbTimestampAsUtc, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, runAnalysisPhase, runMigrations, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
11156
- //# sourceMappingURL=run-L-R_XYNT.js.map
11175
+ //# sourceMappingURL=run-CoP8UQU3.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "substrate-ai",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "Substrate — multi-agent orchestration daemon for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",