substrate-ai 0.2.24 → 0.2.26
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 +3 -3
- package/dist/{event-bus-CAvDMst7.js → helpers-DljGJnFF.js} +79 -2
- package/dist/index.d.ts +63 -3
- package/dist/index.js +1 -78
- package/dist/run-BXKRGSeL.js +7 -0
- package/dist/{run-CT8B9gG9.js → run-CEtHPG4I.js} +951 -534
- package/package.json +1 -1
- package/dist/run-DoxsPIlD.js +0 -7
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createLogger } from "./logger-D2fS2ccL.js";
|
|
2
|
-
import { createEventBus, createTuiApp, isTuiCapable, printNonTtyWarning } from "./
|
|
2
|
+
import { createEventBus, createTuiApp, isTuiCapable, printNonTtyWarning, sleep } from "./helpers-DljGJnFF.js";
|
|
3
3
|
import { addTokenUsage, createDecision, createPipelineRun, createRequirement, getArtifactByTypeForRun, getArtifactsByRun, getDecisionsByCategory, getDecisionsByPhase, getDecisionsByPhaseForRun, getLatestRun, getPipelineRunById, getRunningPipelineRuns, getTokenUsageSummary, registerArtifact, updatePipelineRun, updatePipelineRunConfig, upsertDecision } from "./decisions-Dq4cAA2L.js";
|
|
4
4
|
import { ESCALATION_DIAGNOSIS, OPERATIONAL_FINDING, STORY_METRICS, STORY_OUTCOME, TEST_EXPANSION_FINDING, TEST_PLAN, aggregateTokenUsageForRun, aggregateTokenUsageForStory, getStoryMetricsForRun, writeRunMetrics, writeStoryMetrics } from "./operational-CnMlvWqc.js";
|
|
5
5
|
import { createRequire } from "module";
|
|
6
6
|
import { dirname, join } from "path";
|
|
7
7
|
import { access, readFile, readdir, stat } from "fs/promises";
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
9
9
|
import yaml from "js-yaml";
|
|
10
10
|
import { createRequire as createRequire$1 } from "node:module";
|
|
11
11
|
import { z } from "zod";
|
|
@@ -677,7 +677,9 @@ const PackManifestSchema = z.object({
|
|
|
677
677
|
prompts: z.record(z.string(), z.string()),
|
|
678
678
|
constraints: z.record(z.string(), z.string()),
|
|
679
679
|
templates: z.record(z.string(), z.string()),
|
|
680
|
-
conflictGroups: z.record(z.string(), z.string()).optional()
|
|
680
|
+
conflictGroups: z.record(z.string(), z.string()).optional(),
|
|
681
|
+
verifyCommand: z.union([z.string(), z.literal(false)]).optional(),
|
|
682
|
+
verifyTimeoutMs: z.number().optional()
|
|
681
683
|
});
|
|
682
684
|
const ConstraintSeveritySchema = z.enum([
|
|
683
685
|
"required",
|
|
@@ -1244,6 +1246,8 @@ function buildPipelineStatusOutput(run, tokenSummary, decisionsCount, storiesCou
|
|
|
1244
1246
|
}
|
|
1245
1247
|
}
|
|
1246
1248
|
} catch {}
|
|
1249
|
+
const derivedStoriesCount = storiesSummary !== void 0 ? storiesSummary.completed + storiesSummary.in_progress + storiesSummary.escalated + storiesSummary.pending : storiesCount;
|
|
1250
|
+
const derivedStoriesCompleted = storiesSummary !== void 0 ? storiesSummary.completed : 0;
|
|
1247
1251
|
return {
|
|
1248
1252
|
run_id: run.id,
|
|
1249
1253
|
current_phase: currentPhase,
|
|
@@ -1254,7 +1258,8 @@ function buildPipelineStatusOutput(run, tokenSummary, decisionsCount, storiesCou
|
|
|
1254
1258
|
cost_usd: totalCost
|
|
1255
1259
|
},
|
|
1256
1260
|
decisions_count: decisionsCount,
|
|
1257
|
-
stories_count:
|
|
1261
|
+
stories_count: derivedStoriesCount,
|
|
1262
|
+
stories_completed: derivedStoriesCompleted,
|
|
1258
1263
|
last_activity: run.updated_at,
|
|
1259
1264
|
staleness_seconds: Math.round((Date.now() - parseDbTimestampAsUtc(run.updated_at).getTime()) / 1e3),
|
|
1260
1265
|
last_event_ts: run.updated_at,
|
|
@@ -2209,6 +2214,69 @@ const PIPELINE_EVENT_METADATA = [
|
|
|
2209
2214
|
description: "Error message."
|
|
2210
2215
|
}
|
|
2211
2216
|
]
|
|
2217
|
+
},
|
|
2218
|
+
{
|
|
2219
|
+
type: "story:zero-diff-escalation",
|
|
2220
|
+
description: "Dev-story reported COMPLETE but git diff shows no file changes (phantom completion).",
|
|
2221
|
+
when: "After dev-story succeeds with zero file changes in working tree.",
|
|
2222
|
+
fields: [
|
|
2223
|
+
{
|
|
2224
|
+
name: "ts",
|
|
2225
|
+
type: "string",
|
|
2226
|
+
description: "Timestamp."
|
|
2227
|
+
},
|
|
2228
|
+
{
|
|
2229
|
+
name: "storyKey",
|
|
2230
|
+
type: "string",
|
|
2231
|
+
description: "Story key."
|
|
2232
|
+
},
|
|
2233
|
+
{
|
|
2234
|
+
name: "reason",
|
|
2235
|
+
type: "string",
|
|
2236
|
+
description: "Always \"zero-diff-on-complete\"."
|
|
2237
|
+
}
|
|
2238
|
+
]
|
|
2239
|
+
},
|
|
2240
|
+
{
|
|
2241
|
+
type: "story:build-verification-failed",
|
|
2242
|
+
description: "Build verification command (default: npm run build) exited with non-zero code or timed out.",
|
|
2243
|
+
when: "After dev-story and zero-diff check pass, but before code-review is dispatched.",
|
|
2244
|
+
fields: [
|
|
2245
|
+
{
|
|
2246
|
+
name: "ts",
|
|
2247
|
+
type: "string",
|
|
2248
|
+
description: "Timestamp."
|
|
2249
|
+
},
|
|
2250
|
+
{
|
|
2251
|
+
name: "storyKey",
|
|
2252
|
+
type: "string",
|
|
2253
|
+
description: "Story key."
|
|
2254
|
+
},
|
|
2255
|
+
{
|
|
2256
|
+
name: "exitCode",
|
|
2257
|
+
type: "number",
|
|
2258
|
+
description: "Exit code from the build command (-1 for timeout)."
|
|
2259
|
+
},
|
|
2260
|
+
{
|
|
2261
|
+
name: "output",
|
|
2262
|
+
type: "string",
|
|
2263
|
+
description: "Combined stdout+stderr from the build command (truncated to 2000 chars)."
|
|
2264
|
+
}
|
|
2265
|
+
]
|
|
2266
|
+
},
|
|
2267
|
+
{
|
|
2268
|
+
type: "story:build-verification-passed",
|
|
2269
|
+
description: "Build verification command exited with code 0 — compilation clean.",
|
|
2270
|
+
when: "After dev-story completes and build verification command succeeds.",
|
|
2271
|
+
fields: [{
|
|
2272
|
+
name: "ts",
|
|
2273
|
+
type: "string",
|
|
2274
|
+
description: "Timestamp."
|
|
2275
|
+
}, {
|
|
2276
|
+
name: "storyKey",
|
|
2277
|
+
type: "string",
|
|
2278
|
+
description: "Story key."
|
|
2279
|
+
}]
|
|
2212
2280
|
}
|
|
2213
2281
|
];
|
|
2214
2282
|
/**
|
|
@@ -2969,6 +3037,7 @@ const MIN_FREE_MEMORY_BYTES = (() => {
|
|
|
2969
3037
|
return 256 * 1024 * 1024;
|
|
2970
3038
|
})();
|
|
2971
3039
|
const MEMORY_PRESSURE_POLL_MS = 1e4;
|
|
3040
|
+
let _lastKnownPressureLevel = 0;
|
|
2972
3041
|
/**
|
|
2973
3042
|
* Get available system memory in bytes, accounting for platform differences.
|
|
2974
3043
|
*
|
|
@@ -2998,6 +3067,7 @@ function getAvailableMemory() {
|
|
|
2998
3067
|
timeout: 1e3,
|
|
2999
3068
|
encoding: "utf-8"
|
|
3000
3069
|
}).trim(), 10);
|
|
3070
|
+
_lastKnownPressureLevel = pressureLevel;
|
|
3001
3071
|
if (pressureLevel >= 4) {
|
|
3002
3072
|
logger$16.warn({ pressureLevel }, "macOS kernel reports critical memory pressure");
|
|
3003
3073
|
return 0;
|
|
@@ -3025,6 +3095,7 @@ function getAvailableMemory() {
|
|
|
3025
3095
|
return freemem();
|
|
3026
3096
|
}
|
|
3027
3097
|
}
|
|
3098
|
+
_lastKnownPressureLevel = 0;
|
|
3028
3099
|
return freemem();
|
|
3029
3100
|
}
|
|
3030
3101
|
var MutableDispatchHandle = class {
|
|
@@ -3430,12 +3501,28 @@ var DispatcherImpl = class {
|
|
|
3430
3501
|
if (free < MIN_FREE_MEMORY_BYTES) {
|
|
3431
3502
|
logger$16.warn({
|
|
3432
3503
|
freeMB: Math.round(free / 1024 / 1024),
|
|
3433
|
-
thresholdMB: Math.round(MIN_FREE_MEMORY_BYTES / 1024 / 1024)
|
|
3504
|
+
thresholdMB: Math.round(MIN_FREE_MEMORY_BYTES / 1024 / 1024),
|
|
3505
|
+
pressureLevel: _lastKnownPressureLevel
|
|
3434
3506
|
}, "Memory pressure detected — holding dispatch queue");
|
|
3435
3507
|
return true;
|
|
3436
3508
|
}
|
|
3437
3509
|
return false;
|
|
3438
3510
|
}
|
|
3511
|
+
/**
|
|
3512
|
+
* Return current memory pressure state (Story 23-8, AC1).
|
|
3513
|
+
*
|
|
3514
|
+
* Used by the orchestrator before dispatching a story phase so it can
|
|
3515
|
+
* implement backoff-retry without waiting on the dispatcher's internal queue.
|
|
3516
|
+
*/
|
|
3517
|
+
getMemoryState() {
|
|
3518
|
+
const free = getAvailableMemory();
|
|
3519
|
+
return {
|
|
3520
|
+
freeMB: Math.round(free / 1024 / 1024),
|
|
3521
|
+
thresholdMB: Math.round(MIN_FREE_MEMORY_BYTES / 1024 / 1024),
|
|
3522
|
+
pressureLevel: _lastKnownPressureLevel,
|
|
3523
|
+
isPressured: free < MIN_FREE_MEMORY_BYTES
|
|
3524
|
+
};
|
|
3525
|
+
}
|
|
3439
3526
|
_startMemoryPressureTimer() {
|
|
3440
3527
|
if (this._memoryPressureTimer !== null) return;
|
|
3441
3528
|
this._memoryPressureTimer = setInterval(() => {
|
|
@@ -3450,6 +3537,114 @@ var DispatcherImpl = class {
|
|
|
3450
3537
|
}
|
|
3451
3538
|
}
|
|
3452
3539
|
};
|
|
3540
|
+
/** Default command for the build verification gate */
|
|
3541
|
+
const DEFAULT_VERIFY_COMMAND = "npm run build";
|
|
3542
|
+
/** Default timeout in milliseconds for the build verification gate */
|
|
3543
|
+
const DEFAULT_VERIFY_TIMEOUT_MS = 6e4;
|
|
3544
|
+
/**
|
|
3545
|
+
* Run the build verification gate synchronously.
|
|
3546
|
+
*
|
|
3547
|
+
* Executes the configured verifyCommand (default: "npm run build") in the
|
|
3548
|
+
* project root directory, capturing stdout and stderr. On success (exit 0)
|
|
3549
|
+
* returns { status: 'passed' }. On failure or timeout, returns a structured
|
|
3550
|
+
* result with status, exitCode, output, and reason.
|
|
3551
|
+
*
|
|
3552
|
+
* AC4/5: reads verifyCommand from options (or defaults to 'npm run build').
|
|
3553
|
+
* AC6: if verifyCommand is empty string or false, returns { status: 'skipped' }.
|
|
3554
|
+
* AC8: timeout is configurable via verifyTimeoutMs (default 60 s).
|
|
3555
|
+
*/
|
|
3556
|
+
function runBuildVerification(options) {
|
|
3557
|
+
const { verifyCommand, verifyTimeoutMs, projectRoot } = options;
|
|
3558
|
+
const cmd = verifyCommand === void 0 ? DEFAULT_VERIFY_COMMAND : verifyCommand;
|
|
3559
|
+
if (!cmd) return { status: "skipped" };
|
|
3560
|
+
const timeoutMs = verifyTimeoutMs ?? DEFAULT_VERIFY_TIMEOUT_MS;
|
|
3561
|
+
try {
|
|
3562
|
+
const stdout = execSync(cmd, {
|
|
3563
|
+
cwd: projectRoot,
|
|
3564
|
+
timeout: timeoutMs,
|
|
3565
|
+
encoding: "utf-8"
|
|
3566
|
+
});
|
|
3567
|
+
return {
|
|
3568
|
+
status: "passed",
|
|
3569
|
+
exitCode: 0,
|
|
3570
|
+
output: typeof stdout === "string" ? stdout : ""
|
|
3571
|
+
};
|
|
3572
|
+
} catch (err) {
|
|
3573
|
+
if (err != null && typeof err === "object") {
|
|
3574
|
+
const e = err;
|
|
3575
|
+
const isTimeout = e.killed === true;
|
|
3576
|
+
const exitCode = typeof e.status === "number" ? e.status : 1;
|
|
3577
|
+
const rawStdout = e.stdout;
|
|
3578
|
+
const rawStderr = e.stderr;
|
|
3579
|
+
const stdoutStr = typeof rawStdout === "string" ? rawStdout : Buffer.isBuffer(rawStdout) ? rawStdout.toString("utf-8") : "";
|
|
3580
|
+
const stderrStr = typeof rawStderr === "string" ? rawStderr : Buffer.isBuffer(rawStderr) ? rawStderr.toString("utf-8") : "";
|
|
3581
|
+
const combinedOutput = [stdoutStr, stderrStr].filter((s) => s.length > 0).join("\n");
|
|
3582
|
+
if (isTimeout) return {
|
|
3583
|
+
status: "timeout",
|
|
3584
|
+
exitCode: -1,
|
|
3585
|
+
output: combinedOutput,
|
|
3586
|
+
reason: "build-verification-timeout"
|
|
3587
|
+
};
|
|
3588
|
+
return {
|
|
3589
|
+
status: "failed",
|
|
3590
|
+
exitCode,
|
|
3591
|
+
output: combinedOutput,
|
|
3592
|
+
reason: "build-verification-failed"
|
|
3593
|
+
};
|
|
3594
|
+
}
|
|
3595
|
+
return {
|
|
3596
|
+
status: "failed",
|
|
3597
|
+
exitCode: 1,
|
|
3598
|
+
output: String(err),
|
|
3599
|
+
reason: "build-verification-failed"
|
|
3600
|
+
};
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
/**
|
|
3604
|
+
* Check git working tree for modified files using `git diff --name-only HEAD`
|
|
3605
|
+
* (unstaged + staged changes to tracked files) and `git diff --cached --name-only`
|
|
3606
|
+
* (staged new files not yet in HEAD). Returns a deduplicated array of file paths.
|
|
3607
|
+
*
|
|
3608
|
+
* Returns an empty array when:
|
|
3609
|
+
* - No files have been modified or staged
|
|
3610
|
+
* - Git commands fail (e.g., not in a git repo, git not installed)
|
|
3611
|
+
*
|
|
3612
|
+
* Used by the zero-diff detection gate (Story 24-1) to catch phantom completions
|
|
3613
|
+
* where a dev-story agent reported COMPLETE but made no actual file changes.
|
|
3614
|
+
*
|
|
3615
|
+
* @param workingDir - Directory to run git commands in (defaults to process.cwd())
|
|
3616
|
+
* @returns Array of changed file paths; empty when nothing changed
|
|
3617
|
+
*/
|
|
3618
|
+
function checkGitDiffFiles(workingDir = process.cwd()) {
|
|
3619
|
+
const results = new Set();
|
|
3620
|
+
try {
|
|
3621
|
+
const unstaged = execSync("git diff --name-only HEAD", {
|
|
3622
|
+
cwd: workingDir,
|
|
3623
|
+
encoding: "utf-8",
|
|
3624
|
+
timeout: 5e3,
|
|
3625
|
+
stdio: [
|
|
3626
|
+
"ignore",
|
|
3627
|
+
"pipe",
|
|
3628
|
+
"pipe"
|
|
3629
|
+
]
|
|
3630
|
+
});
|
|
3631
|
+
unstaged.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).forEach((f) => results.add(f));
|
|
3632
|
+
} catch {}
|
|
3633
|
+
try {
|
|
3634
|
+
const staged = execSync("git diff --cached --name-only", {
|
|
3635
|
+
cwd: workingDir,
|
|
3636
|
+
encoding: "utf-8",
|
|
3637
|
+
timeout: 5e3,
|
|
3638
|
+
stdio: [
|
|
3639
|
+
"ignore",
|
|
3640
|
+
"pipe",
|
|
3641
|
+
"pipe"
|
|
3642
|
+
]
|
|
3643
|
+
});
|
|
3644
|
+
staged.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).forEach((f) => results.add(f));
|
|
3645
|
+
} catch {}
|
|
3646
|
+
return Array.from(results);
|
|
3647
|
+
}
|
|
3453
3648
|
/**
|
|
3454
3649
|
* Create a new Dispatcher instance.
|
|
3455
3650
|
*
|
|
@@ -5868,120 +6063,403 @@ function detectConflictGroups(storyKeys, config) {
|
|
|
5868
6063
|
}
|
|
5869
6064
|
|
|
5870
6065
|
//#endregion
|
|
5871
|
-
//#region src/
|
|
5872
|
-
const logger$7 = createLogger("
|
|
5873
|
-
/**
|
|
5874
|
-
const
|
|
5875
|
-
/** Max chars per epic shard (fallback when per-story extraction returns null) */
|
|
5876
|
-
const MAX_EPIC_SHARD_CHARS = 12e3;
|
|
5877
|
-
/** Max chars for test patterns */
|
|
5878
|
-
const MAX_TEST_PATTERNS_CHARS = 2e3;
|
|
6066
|
+
//#region src/cli/commands/health.ts
|
|
6067
|
+
const logger$7 = createLogger("health-cmd");
|
|
6068
|
+
/** Default stall threshold in seconds — also used by supervisor default */
|
|
6069
|
+
const DEFAULT_STALL_THRESHOLD_SECONDS = 600;
|
|
5879
6070
|
/**
|
|
5880
|
-
*
|
|
5881
|
-
*
|
|
5882
|
-
*
|
|
5883
|
-
* -
|
|
5884
|
-
*
|
|
5885
|
-
*
|
|
6071
|
+
* Determine whether a ps output line represents the substrate pipeline orchestrator.
|
|
6072
|
+
* Handles invocation via:
|
|
6073
|
+
* - `substrate run` (globally installed)
|
|
6074
|
+
* - `substrate-ai run`
|
|
6075
|
+
* - `node dist/cli/index.js run` (npm run substrate:dev)
|
|
6076
|
+
* - `npx substrate run`
|
|
6077
|
+
* - any node process whose command contains `run` with `--events` or `--stories`
|
|
5886
6078
|
*
|
|
5887
|
-
*
|
|
5888
|
-
*
|
|
5889
|
-
*
|
|
6079
|
+
* When `projectRoot` is provided, additionally checks that the command line
|
|
6080
|
+
* contains that path (via `--project-root` flag or as part of the binary/CWD path).
|
|
6081
|
+
* This ensures multi-project environments match the correct orchestrator.
|
|
5890
6082
|
*/
|
|
5891
|
-
function
|
|
6083
|
+
function isOrchestratorProcessLine(line, projectRoot) {
|
|
6084
|
+
if (line.includes("grep")) return false;
|
|
6085
|
+
let isOrchestrator = false;
|
|
6086
|
+
if (line.includes("substrate run")) isOrchestrator = true;
|
|
6087
|
+
else if (line.includes("substrate-ai run")) isOrchestrator = true;
|
|
6088
|
+
else if (line.includes("index.js run")) isOrchestrator = true;
|
|
6089
|
+
else if (line.includes("node") && /\srun(\s|$)/.test(line) && (line.includes("--events") || line.includes("--stories"))) isOrchestrator = true;
|
|
6090
|
+
if (!isOrchestrator) return false;
|
|
6091
|
+
if (projectRoot !== void 0) return line.includes(projectRoot);
|
|
6092
|
+
return true;
|
|
6093
|
+
}
|
|
6094
|
+
function inspectProcessTree(opts) {
|
|
6095
|
+
const { projectRoot, substrateDirPath, execFileSync: execFileSyncOverride, readFileSync: readFileSyncOverride } = opts ?? {};
|
|
5892
6096
|
const result = {
|
|
5893
|
-
|
|
5894
|
-
|
|
6097
|
+
orchestrator_pid: null,
|
|
6098
|
+
child_pids: [],
|
|
6099
|
+
zombies: []
|
|
5895
6100
|
};
|
|
5896
6101
|
try {
|
|
5897
|
-
|
|
5898
|
-
if (
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
if (epicCount === -1) result.skippedCategories.push("epic-shard");
|
|
5902
|
-
else result.decisionsCreated += epicCount;
|
|
5903
|
-
const testCount = seedTestPatterns(db, projectRoot);
|
|
5904
|
-
if (testCount === -1) result.skippedCategories.push("test-patterns");
|
|
5905
|
-
else result.decisionsCreated += testCount;
|
|
5906
|
-
logger$7.info({
|
|
5907
|
-
decisionsCreated: result.decisionsCreated,
|
|
5908
|
-
skippedCategories: result.skippedCategories
|
|
5909
|
-
}, "Methodology context seeding complete");
|
|
5910
|
-
} catch (err) {
|
|
5911
|
-
logger$7.warn({ error: err instanceof Error ? err.message : String(err) }, "Methodology context seeding failed (non-fatal)");
|
|
5912
|
-
}
|
|
5913
|
-
return result;
|
|
5914
|
-
}
|
|
5915
|
-
/**
|
|
5916
|
-
* Seed architecture constraints from architecture.md.
|
|
5917
|
-
* Extracts key sections (tech stack, ADRs, component overview) as separate decisions.
|
|
5918
|
-
* Returns number of decisions created, or -1 if skipped (already seeded).
|
|
5919
|
-
*/
|
|
5920
|
-
function seedArchitecture(db, projectRoot) {
|
|
5921
|
-
const existing = getDecisionsByPhase(db, "solutioning");
|
|
5922
|
-
if (existing.some((d) => d.category === "architecture")) return -1;
|
|
5923
|
-
const archPath = findArtifact(projectRoot, [
|
|
5924
|
-
"_bmad-output/planning-artifacts/architecture.md",
|
|
5925
|
-
"_bmad-output/architecture/architecture.md",
|
|
5926
|
-
"_bmad-output/architecture.md"
|
|
5927
|
-
]);
|
|
5928
|
-
if (archPath === void 0) return 0;
|
|
5929
|
-
const content = readFileSync$1(archPath, "utf-8");
|
|
5930
|
-
if (content.length === 0) return 0;
|
|
5931
|
-
const sections = extractArchSections(content);
|
|
5932
|
-
let count = 0;
|
|
5933
|
-
for (const section of sections) {
|
|
5934
|
-
createDecision(db, {
|
|
5935
|
-
pipeline_run_id: null,
|
|
5936
|
-
phase: "solutioning",
|
|
5937
|
-
category: "architecture",
|
|
5938
|
-
key: section.key,
|
|
5939
|
-
value: section.value.slice(0, MAX_ARCH_CHARS),
|
|
5940
|
-
rationale: "Seeded from planning artifacts at orchestrator startup"
|
|
5941
|
-
});
|
|
5942
|
-
count++;
|
|
5943
|
-
}
|
|
5944
|
-
if (count === 0) {
|
|
5945
|
-
createDecision(db, {
|
|
5946
|
-
pipeline_run_id: null,
|
|
5947
|
-
phase: "solutioning",
|
|
5948
|
-
category: "architecture",
|
|
5949
|
-
key: "full",
|
|
5950
|
-
value: content.slice(0, MAX_ARCH_CHARS),
|
|
5951
|
-
rationale: "Seeded from planning artifacts at orchestrator startup (full file)"
|
|
6102
|
+
let psOutput;
|
|
6103
|
+
if (execFileSyncOverride !== void 0) psOutput = execFileSyncOverride("ps", ["-eo", "pid,ppid,stat,command"], {
|
|
6104
|
+
encoding: "utf-8",
|
|
6105
|
+
timeout: 5e3
|
|
5952
6106
|
});
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
6107
|
+
else {
|
|
6108
|
+
const { execFileSync } = __require("node:child_process");
|
|
6109
|
+
psOutput = execFileSync("ps", ["-eo", "pid,ppid,stat,command"], {
|
|
6110
|
+
encoding: "utf-8",
|
|
6111
|
+
timeout: 5e3
|
|
6112
|
+
});
|
|
6113
|
+
}
|
|
6114
|
+
const lines = psOutput.split("\n");
|
|
6115
|
+
if (substrateDirPath !== void 0) try {
|
|
6116
|
+
const readFileSyncFn = readFileSyncOverride ?? ((path$1, encoding) => readFileSync$1(path$1, encoding));
|
|
6117
|
+
const pidContent = readFileSyncFn(join(substrateDirPath, "orchestrator.pid"), "utf-8");
|
|
6118
|
+
const pid = parseInt(pidContent.trim(), 10);
|
|
6119
|
+
if (!isNaN(pid) && pid > 0) {
|
|
6120
|
+
const isAlive = lines.some((line) => {
|
|
6121
|
+
const parts = line.trim().split(/\s+/);
|
|
6122
|
+
if (parts.length < 3) return false;
|
|
6123
|
+
return parseInt(parts[0], 10) === pid && !parts[2].includes("Z");
|
|
6124
|
+
});
|
|
6125
|
+
if (isAlive) result.orchestrator_pid = pid;
|
|
6126
|
+
}
|
|
6127
|
+
} catch {}
|
|
6128
|
+
if (result.orchestrator_pid === null) {
|
|
6129
|
+
for (const line of lines) if (isOrchestratorProcessLine(line, projectRoot)) {
|
|
6130
|
+
const match = line.trim().match(/^(\d+)/);
|
|
6131
|
+
if (match) {
|
|
6132
|
+
result.orchestrator_pid = parseInt(match[1], 10);
|
|
6133
|
+
break;
|
|
6134
|
+
}
|
|
6135
|
+
}
|
|
6136
|
+
}
|
|
6137
|
+
if (result.orchestrator_pid !== null) for (const line of lines) {
|
|
6138
|
+
const parts = line.trim().split(/\s+/);
|
|
6139
|
+
if (parts.length >= 3) {
|
|
6140
|
+
const pid = parseInt(parts[0], 10);
|
|
6141
|
+
const ppid = parseInt(parts[1], 10);
|
|
6142
|
+
const stat$2 = parts[2];
|
|
6143
|
+
if (ppid === result.orchestrator_pid && pid !== result.orchestrator_pid) {
|
|
6144
|
+
result.child_pids.push(pid);
|
|
6145
|
+
if (stat$2.includes("Z")) result.zombies.push(pid);
|
|
6146
|
+
}
|
|
6147
|
+
}
|
|
6148
|
+
}
|
|
6149
|
+
} catch {}
|
|
6150
|
+
return result;
|
|
5957
6151
|
}
|
|
5958
6152
|
/**
|
|
5959
|
-
*
|
|
5960
|
-
*
|
|
5961
|
-
*
|
|
5962
|
-
*
|
|
5963
|
-
* - Computes SHA-256 of the epics file and compares to the stored `epic-shard-hash` decision.
|
|
5964
|
-
* - If hashes match: skip re-seeding (unchanged file).
|
|
5965
|
-
* - If hash differs or no hash stored: delete existing epic-shard decisions and re-seed.
|
|
6153
|
+
* Collect all descendant PIDs of the given root PIDs by walking the process
|
|
6154
|
+
* tree recursively. This ensures that grandchildren of the orchestrator
|
|
6155
|
+
* (e.g. node subprocesses spawned by `claude -p`) are also killed during
|
|
6156
|
+
* stall recovery, leaving no orphan processes.
|
|
5966
6157
|
*
|
|
5967
|
-
* Returns
|
|
6158
|
+
* Returns only the descendants — the root PIDs themselves are NOT included.
|
|
5968
6159
|
*/
|
|
5969
|
-
function
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
6160
|
+
function getAllDescendantPids(rootPids, execFileSyncOverride) {
|
|
6161
|
+
if (rootPids.length === 0) return [];
|
|
6162
|
+
try {
|
|
6163
|
+
let psOutput;
|
|
6164
|
+
if (execFileSyncOverride !== void 0) psOutput = execFileSyncOverride("ps", ["-eo", "pid,ppid"], {
|
|
6165
|
+
encoding: "utf-8",
|
|
6166
|
+
timeout: 5e3
|
|
6167
|
+
});
|
|
6168
|
+
else {
|
|
6169
|
+
const { execFileSync } = __require("node:child_process");
|
|
6170
|
+
psOutput = execFileSync("ps", ["-eo", "pid,ppid"], {
|
|
6171
|
+
encoding: "utf-8",
|
|
6172
|
+
timeout: 5e3
|
|
6173
|
+
});
|
|
6174
|
+
}
|
|
6175
|
+
const childrenOf = new Map();
|
|
6176
|
+
for (const line of psOutput.split("\n")) {
|
|
6177
|
+
const parts = line.trim().split(/\s+/);
|
|
6178
|
+
if (parts.length >= 2) {
|
|
6179
|
+
const pid = parseInt(parts[0], 10);
|
|
6180
|
+
const ppid = parseInt(parts[1], 10);
|
|
6181
|
+
if (!isNaN(pid) && !isNaN(ppid) && pid > 0) {
|
|
6182
|
+
if (!childrenOf.has(ppid)) childrenOf.set(ppid, []);
|
|
6183
|
+
childrenOf.get(ppid).push(pid);
|
|
6184
|
+
}
|
|
6185
|
+
}
|
|
6186
|
+
}
|
|
6187
|
+
const descendants = [];
|
|
6188
|
+
const seen = new Set(rootPids);
|
|
6189
|
+
const queue = [...rootPids];
|
|
6190
|
+
while (queue.length > 0) {
|
|
6191
|
+
const current = queue.shift();
|
|
6192
|
+
const children = childrenOf.get(current) ?? [];
|
|
6193
|
+
for (const child of children) if (!seen.has(child)) {
|
|
6194
|
+
seen.add(child);
|
|
6195
|
+
descendants.push(child);
|
|
6196
|
+
queue.push(child);
|
|
6197
|
+
}
|
|
6198
|
+
}
|
|
6199
|
+
return descendants;
|
|
6200
|
+
} catch {
|
|
6201
|
+
return [];
|
|
6202
|
+
}
|
|
6203
|
+
}
|
|
6204
|
+
/**
|
|
6205
|
+
* Fetch pipeline health data as a structured object without any stdout side-effects.
|
|
6206
|
+
* Used by runSupervisorAction to poll health without formatting overhead.
|
|
6207
|
+
*
|
|
6208
|
+
* Returns a NO_PIPELINE_RUNNING health object for all graceful "no data" cases
|
|
6209
|
+
* (missing DB, missing run, terminal run status). Throws only on unexpected errors.
|
|
6210
|
+
*/
|
|
6211
|
+
async function getAutoHealthData(options) {
|
|
6212
|
+
const { runId, projectRoot } = options;
|
|
6213
|
+
const dbRoot = await resolveMainRepoRoot(projectRoot);
|
|
6214
|
+
const dbPath = join(dbRoot, ".substrate", "substrate.db");
|
|
6215
|
+
const NO_PIPELINE = {
|
|
6216
|
+
verdict: "NO_PIPELINE_RUNNING",
|
|
6217
|
+
run_id: null,
|
|
6218
|
+
status: null,
|
|
6219
|
+
current_phase: null,
|
|
6220
|
+
staleness_seconds: 0,
|
|
6221
|
+
last_activity: "",
|
|
6222
|
+
process: {
|
|
6223
|
+
orchestrator_pid: null,
|
|
6224
|
+
child_pids: [],
|
|
6225
|
+
zombies: []
|
|
6226
|
+
},
|
|
6227
|
+
stories: {
|
|
6228
|
+
active: 0,
|
|
6229
|
+
completed: 0,
|
|
6230
|
+
escalated: 0,
|
|
6231
|
+
details: {}
|
|
6232
|
+
}
|
|
6233
|
+
};
|
|
6234
|
+
if (!existsSync$1(dbPath)) return NO_PIPELINE;
|
|
6235
|
+
const dbWrapper = new DatabaseWrapper(dbPath);
|
|
6236
|
+
try {
|
|
6237
|
+
dbWrapper.open();
|
|
6238
|
+
const db = dbWrapper.db;
|
|
6239
|
+
let run;
|
|
6240
|
+
if (runId !== void 0) run = getPipelineRunById(db, runId);
|
|
6241
|
+
else run = getLatestRun(db);
|
|
6242
|
+
if (run === void 0) return NO_PIPELINE;
|
|
6243
|
+
const updatedAt = parseDbTimestampAsUtc(run.updated_at);
|
|
6244
|
+
const stalenessSeconds = Math.round((Date.now() - updatedAt.getTime()) / 1e3);
|
|
6245
|
+
let storyDetails = {};
|
|
6246
|
+
let active = 0;
|
|
6247
|
+
let completed = 0;
|
|
6248
|
+
let escalated = 0;
|
|
6249
|
+
let pending = 0;
|
|
6250
|
+
try {
|
|
6251
|
+
if (run.token_usage_json) {
|
|
6252
|
+
const state = JSON.parse(run.token_usage_json);
|
|
6253
|
+
if (state.stories) for (const [key, s] of Object.entries(state.stories)) {
|
|
6254
|
+
storyDetails[key] = {
|
|
6255
|
+
phase: s.phase,
|
|
6256
|
+
review_cycles: s.reviewCycles
|
|
6257
|
+
};
|
|
6258
|
+
if (s.phase === "COMPLETE") completed++;
|
|
6259
|
+
else if (s.phase === "ESCALATED") escalated++;
|
|
6260
|
+
else if (s.phase === "PENDING") pending++;
|
|
6261
|
+
else active++;
|
|
6262
|
+
}
|
|
6263
|
+
}
|
|
6264
|
+
} catch {}
|
|
6265
|
+
const substrateDirPath = join(dbRoot, ".substrate");
|
|
6266
|
+
const processInfo = inspectProcessTree({
|
|
6267
|
+
projectRoot,
|
|
6268
|
+
substrateDirPath
|
|
6269
|
+
});
|
|
6270
|
+
let verdict = "NO_PIPELINE_RUNNING";
|
|
6271
|
+
if (run.status === "running") if (processInfo.zombies.length > 0) verdict = "STALLED";
|
|
6272
|
+
else if (processInfo.orchestrator_pid !== null && processInfo.child_pids.length > 0 && stalenessSeconds > DEFAULT_STALL_THRESHOLD_SECONDS) verdict = "HEALTHY";
|
|
6273
|
+
else if (stalenessSeconds > DEFAULT_STALL_THRESHOLD_SECONDS) verdict = "STALLED";
|
|
6274
|
+
else if (processInfo.orchestrator_pid !== null && processInfo.child_pids.length === 0 && active > 0) verdict = "STALLED";
|
|
6275
|
+
else verdict = "HEALTHY";
|
|
6276
|
+
else if (run.status === "completed" || run.status === "failed" || run.status === "stopped") verdict = "NO_PIPELINE_RUNNING";
|
|
6277
|
+
return {
|
|
6278
|
+
verdict,
|
|
6279
|
+
run_id: run.id,
|
|
6280
|
+
status: run.status,
|
|
6281
|
+
current_phase: run.current_phase,
|
|
6282
|
+
staleness_seconds: stalenessSeconds,
|
|
6283
|
+
last_activity: run.updated_at,
|
|
6284
|
+
process: processInfo,
|
|
6285
|
+
stories: {
|
|
6286
|
+
active,
|
|
6287
|
+
completed,
|
|
6288
|
+
escalated,
|
|
6289
|
+
pending,
|
|
6290
|
+
details: storyDetails
|
|
6291
|
+
}
|
|
6292
|
+
};
|
|
6293
|
+
} finally {
|
|
6294
|
+
try {
|
|
6295
|
+
dbWrapper.close();
|
|
6296
|
+
} catch {}
|
|
6297
|
+
}
|
|
6298
|
+
}
|
|
6299
|
+
async function runHealthAction(options) {
|
|
6300
|
+
const { outputFormat } = options;
|
|
6301
|
+
try {
|
|
6302
|
+
const health = await getAutoHealthData(options);
|
|
6303
|
+
if (outputFormat === "json") process.stdout.write(formatOutput(health, "json", true) + "\n");
|
|
6304
|
+
else {
|
|
6305
|
+
const verdictLabel = health.verdict === "HEALTHY" ? "HEALTHY" : health.verdict === "STALLED" ? "STALLED" : "NO PIPELINE RUNNING";
|
|
6306
|
+
process.stdout.write(`\nPipeline Health: ${verdictLabel}\n`);
|
|
6307
|
+
if (health.run_id !== null) {
|
|
6308
|
+
process.stdout.write(` Run: ${health.run_id}\n`);
|
|
6309
|
+
process.stdout.write(` Status: ${health.status}\n`);
|
|
6310
|
+
process.stdout.write(` Phase: ${health.current_phase ?? "N/A"}\n`);
|
|
6311
|
+
process.stdout.write(` Last Active: ${health.last_activity} (${health.staleness_seconds}s ago)\n`);
|
|
6312
|
+
const processInfo = health.process;
|
|
6313
|
+
if (processInfo.orchestrator_pid !== null) {
|
|
6314
|
+
process.stdout.write(` Orchestrator: PID ${processInfo.orchestrator_pid}\n`);
|
|
6315
|
+
process.stdout.write(` Children: ${processInfo.child_pids.length} active`);
|
|
6316
|
+
if (processInfo.zombies.length > 0) process.stdout.write(` (${processInfo.zombies.length} ZOMBIE)`);
|
|
6317
|
+
process.stdout.write("\n");
|
|
6318
|
+
} else process.stdout.write(" Orchestrator: not running\n");
|
|
6319
|
+
const storyDetails = health.stories.details;
|
|
6320
|
+
if (Object.keys(storyDetails).length > 0) {
|
|
6321
|
+
process.stdout.write("\n Stories:\n");
|
|
6322
|
+
for (const [key, s] of Object.entries(storyDetails)) process.stdout.write(` ${key}: ${s.phase} (${s.review_cycles} review cycles)\n`);
|
|
6323
|
+
process.stdout.write(`\n Summary: ${health.stories.active} active, ${health.stories.completed} completed, ${health.stories.escalated} escalated\n`);
|
|
6324
|
+
}
|
|
6325
|
+
}
|
|
6326
|
+
}
|
|
6327
|
+
return 0;
|
|
6328
|
+
} catch (err) {
|
|
6329
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6330
|
+
if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, msg) + "\n");
|
|
6331
|
+
else process.stderr.write(`Error: ${msg}\n`);
|
|
6332
|
+
logger$7.error({ err }, "health action failed");
|
|
6333
|
+
return 1;
|
|
6334
|
+
}
|
|
6335
|
+
}
|
|
6336
|
+
function registerHealthCommand(program, _version = "0.0.0", projectRoot = process.cwd()) {
|
|
6337
|
+
program.command("health").description("Check pipeline health: process status, stall detection, and verdict").option("--run-id <id>", "Pipeline run ID to query (defaults to latest)").option("--project-root <path>", "Project root directory", projectRoot).option("--output-format <format>", "Output format: human (default) or json", "human").action(async (opts) => {
|
|
6338
|
+
const outputFormat = opts.outputFormat === "json" ? "json" : "human";
|
|
6339
|
+
const exitCode = await runHealthAction({
|
|
6340
|
+
outputFormat,
|
|
6341
|
+
runId: opts.runId,
|
|
6342
|
+
projectRoot: opts.projectRoot
|
|
6343
|
+
});
|
|
6344
|
+
process.exitCode = exitCode;
|
|
6345
|
+
});
|
|
6346
|
+
}
|
|
6347
|
+
|
|
6348
|
+
//#endregion
|
|
6349
|
+
//#region src/modules/implementation-orchestrator/seed-methodology-context.ts
|
|
6350
|
+
const logger$6 = createLogger("implementation-orchestrator:seed");
|
|
6351
|
+
/** Max chars for the architecture summary seeded into decisions */
|
|
6352
|
+
const MAX_ARCH_CHARS = 6e3;
|
|
6353
|
+
/** Max chars per epic shard (fallback when per-story extraction returns null) */
|
|
6354
|
+
const MAX_EPIC_SHARD_CHARS = 12e3;
|
|
6355
|
+
/** Max chars for test patterns */
|
|
6356
|
+
const MAX_TEST_PATTERNS_CHARS = 2e3;
|
|
6357
|
+
/**
|
|
6358
|
+
* Seed the decision store with methodology-level context from planning artifacts.
|
|
6359
|
+
*
|
|
6360
|
+
* Reads the following from `{projectRoot}/_bmad-output/planning-artifacts/`:
|
|
6361
|
+
* - architecture.md → solutioning/architecture decisions
|
|
6362
|
+
* - epics.md → implementation/epic-shard decisions (one per epic)
|
|
6363
|
+
* - package.json → solutioning/test-patterns decisions (framework detection)
|
|
6364
|
+
*
|
|
6365
|
+
* @param db - SQLite database instance
|
|
6366
|
+
* @param projectRoot - Absolute path to the target project root
|
|
6367
|
+
* @returns SeedResult with counts of decisions created and categories skipped
|
|
6368
|
+
*/
|
|
6369
|
+
function seedMethodologyContext(db, projectRoot) {
|
|
6370
|
+
const result = {
|
|
6371
|
+
decisionsCreated: 0,
|
|
6372
|
+
skippedCategories: []
|
|
6373
|
+
};
|
|
6374
|
+
try {
|
|
6375
|
+
const archCount = seedArchitecture(db, projectRoot);
|
|
6376
|
+
if (archCount === -1) result.skippedCategories.push("architecture");
|
|
6377
|
+
else result.decisionsCreated += archCount;
|
|
6378
|
+
const epicCount = seedEpicShards(db, projectRoot);
|
|
6379
|
+
if (epicCount === -1) result.skippedCategories.push("epic-shard");
|
|
6380
|
+
else result.decisionsCreated += epicCount;
|
|
6381
|
+
const testCount = seedTestPatterns(db, projectRoot);
|
|
6382
|
+
if (testCount === -1) result.skippedCategories.push("test-patterns");
|
|
6383
|
+
else result.decisionsCreated += testCount;
|
|
6384
|
+
logger$6.info({
|
|
6385
|
+
decisionsCreated: result.decisionsCreated,
|
|
6386
|
+
skippedCategories: result.skippedCategories
|
|
6387
|
+
}, "Methodology context seeding complete");
|
|
6388
|
+
} catch (err) {
|
|
6389
|
+
logger$6.warn({ error: err instanceof Error ? err.message : String(err) }, "Methodology context seeding failed (non-fatal)");
|
|
6390
|
+
}
|
|
6391
|
+
return result;
|
|
6392
|
+
}
|
|
6393
|
+
/**
|
|
6394
|
+
* Seed architecture constraints from architecture.md.
|
|
6395
|
+
* Extracts key sections (tech stack, ADRs, component overview) as separate decisions.
|
|
6396
|
+
* Returns number of decisions created, or -1 if skipped (already seeded).
|
|
6397
|
+
*/
|
|
6398
|
+
function seedArchitecture(db, projectRoot) {
|
|
6399
|
+
const existing = getDecisionsByPhase(db, "solutioning");
|
|
6400
|
+
if (existing.some((d) => d.category === "architecture")) return -1;
|
|
6401
|
+
const archPath = findArtifact(projectRoot, [
|
|
6402
|
+
"_bmad-output/planning-artifacts/architecture.md",
|
|
6403
|
+
"_bmad-output/architecture/architecture.md",
|
|
6404
|
+
"_bmad-output/architecture.md"
|
|
6405
|
+
]);
|
|
6406
|
+
if (archPath === void 0) return 0;
|
|
6407
|
+
const content = readFileSync$1(archPath, "utf-8");
|
|
6408
|
+
if (content.length === 0) return 0;
|
|
6409
|
+
const sections = extractArchSections(content);
|
|
6410
|
+
let count = 0;
|
|
6411
|
+
for (const section of sections) {
|
|
6412
|
+
createDecision(db, {
|
|
6413
|
+
pipeline_run_id: null,
|
|
6414
|
+
phase: "solutioning",
|
|
6415
|
+
category: "architecture",
|
|
6416
|
+
key: section.key,
|
|
6417
|
+
value: section.value.slice(0, MAX_ARCH_CHARS),
|
|
6418
|
+
rationale: "Seeded from planning artifacts at orchestrator startup"
|
|
6419
|
+
});
|
|
6420
|
+
count++;
|
|
6421
|
+
}
|
|
6422
|
+
if (count === 0) {
|
|
6423
|
+
createDecision(db, {
|
|
6424
|
+
pipeline_run_id: null,
|
|
6425
|
+
phase: "solutioning",
|
|
6426
|
+
category: "architecture",
|
|
6427
|
+
key: "full",
|
|
6428
|
+
value: content.slice(0, MAX_ARCH_CHARS),
|
|
6429
|
+
rationale: "Seeded from planning artifacts at orchestrator startup (full file)"
|
|
6430
|
+
});
|
|
6431
|
+
count = 1;
|
|
6432
|
+
}
|
|
6433
|
+
logger$6.debug({ count }, "Seeded architecture decisions");
|
|
6434
|
+
return count;
|
|
6435
|
+
}
|
|
6436
|
+
/**
|
|
6437
|
+
* Seed epic shards from epics.md.
|
|
6438
|
+
* Parses each epic section and creates an implementation/epic-shard decision.
|
|
6439
|
+
*
|
|
6440
|
+
* Uses content-hash comparison (AC1, AC2, AC6):
|
|
6441
|
+
* - Computes SHA-256 of the epics file and compares to the stored `epic-shard-hash` decision.
|
|
6442
|
+
* - If hashes match: skip re-seeding (unchanged file).
|
|
6443
|
+
* - If hash differs or no hash stored: delete existing epic-shard decisions and re-seed.
|
|
6444
|
+
*
|
|
6445
|
+
* Returns number of decisions created, or -1 if skipped (hash unchanged).
|
|
6446
|
+
*/
|
|
6447
|
+
function seedEpicShards(db, projectRoot) {
|
|
6448
|
+
const epicsPath = findArtifact(projectRoot, ["_bmad-output/planning-artifacts/epics.md", "_bmad-output/epics.md"]);
|
|
6449
|
+
if (epicsPath === void 0) return 0;
|
|
6450
|
+
const content = readFileSync$1(epicsPath, "utf-8");
|
|
6451
|
+
if (content.length === 0) return 0;
|
|
6452
|
+
const currentHash = createHash("sha256").update(content).digest("hex");
|
|
6453
|
+
const implementationDecisions = getDecisionsByPhase(db, "implementation");
|
|
6454
|
+
const storedHashDecision = implementationDecisions.find((d) => d.category === "epic-shard-hash" && d.key === "epics-file");
|
|
6455
|
+
const storedHash = storedHashDecision?.value;
|
|
6456
|
+
if (storedHash === currentHash) {
|
|
6457
|
+
logger$6.debug({ hash: currentHash }, "Epic shards up-to-date (hash unchanged) — skipping re-seed");
|
|
6458
|
+
return -1;
|
|
6459
|
+
}
|
|
6460
|
+
if (implementationDecisions.some((d) => d.category === "epic-shard")) {
|
|
6461
|
+
logger$6.debug({
|
|
6462
|
+
storedHash,
|
|
5985
6463
|
currentHash
|
|
5986
6464
|
}, "Epics file changed — deleting stale epic-shard decisions");
|
|
5987
6465
|
db.prepare("DELETE FROM decisions WHERE phase = 'implementation' AND category = 'epic-shard'").run();
|
|
@@ -6008,7 +6486,7 @@ function seedEpicShards(db, projectRoot) {
|
|
|
6008
6486
|
value: currentHash,
|
|
6009
6487
|
rationale: "SHA-256 hash of epics file content for change detection"
|
|
6010
6488
|
});
|
|
6011
|
-
logger$
|
|
6489
|
+
logger$6.debug({
|
|
6012
6490
|
count,
|
|
6013
6491
|
hash: currentHash
|
|
6014
6492
|
}, "Seeded epic shard decisions");
|
|
@@ -6032,7 +6510,7 @@ function seedTestPatterns(db, projectRoot) {
|
|
|
6032
6510
|
value: patterns.slice(0, MAX_TEST_PATTERNS_CHARS),
|
|
6033
6511
|
rationale: "Detected from project configuration at orchestrator startup"
|
|
6034
6512
|
});
|
|
6035
|
-
logger$
|
|
6513
|
+
logger$6.debug("Seeded test patterns decision");
|
|
6036
6514
|
return 1;
|
|
6037
6515
|
}
|
|
6038
6516
|
/**
|
|
@@ -6235,13 +6713,19 @@ function createImplementationOrchestrator(deps) {
|
|
|
6235
6713
|
let _lastProgressTs = Date.now();
|
|
6236
6714
|
let _heartbeatTimer = null;
|
|
6237
6715
|
const HEARTBEAT_INTERVAL_MS = 3e4;
|
|
6238
|
-
const
|
|
6716
|
+
const DEFAULT_STALL_THRESHOLD_MS = 6e5;
|
|
6717
|
+
const DEV_STORY_STALL_THRESHOLD_MS = 9e5;
|
|
6239
6718
|
const _stalledStories = new Set();
|
|
6240
6719
|
const _storiesWithStall = new Set();
|
|
6241
6720
|
const _phaseStartMs = new Map();
|
|
6242
6721
|
const _phaseEndMs = new Map();
|
|
6243
6722
|
const _storyDispatches = new Map();
|
|
6244
6723
|
let _maxConcurrentActual = 0;
|
|
6724
|
+
const MEMORY_PRESSURE_BACKOFF_MS = [
|
|
6725
|
+
3e4,
|
|
6726
|
+
6e4,
|
|
6727
|
+
12e4
|
|
6728
|
+
];
|
|
6245
6729
|
function startPhase(storyKey, phase) {
|
|
6246
6730
|
if (!_phaseStartMs.has(storyKey)) _phaseStartMs.set(storyKey, new Map());
|
|
6247
6731
|
_phaseStartMs.get(storyKey).set(phase, Date.now());
|
|
@@ -6426,6 +6910,9 @@ function createImplementationOrchestrator(deps) {
|
|
|
6426
6910
|
_lastProgressTs = Date.now();
|
|
6427
6911
|
_stalledStories.clear();
|
|
6428
6912
|
}
|
|
6913
|
+
function getStallThresholdMs(phase) {
|
|
6914
|
+
return phase === "IN_DEV" ? DEV_STORY_STALL_THRESHOLD_MS : DEFAULT_STALL_THRESHOLD_MS;
|
|
6915
|
+
}
|
|
6429
6916
|
function startHeartbeat() {
|
|
6430
6917
|
if (_heartbeatTimer !== null) return;
|
|
6431
6918
|
_heartbeatTimer = setInterval(() => {
|
|
@@ -6444,24 +6931,49 @@ function createImplementationOrchestrator(deps) {
|
|
|
6444
6931
|
queuedDispatches: queued
|
|
6445
6932
|
});
|
|
6446
6933
|
const elapsed = Date.now() - _lastProgressTs;
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6934
|
+
let childPids = [];
|
|
6935
|
+
let childActive = false;
|
|
6936
|
+
let processInspected = false;
|
|
6937
|
+
for (const [key, s] of _stories) {
|
|
6938
|
+
if (s.phase === "PENDING" || s.phase === "COMPLETE" || s.phase === "ESCALATED") continue;
|
|
6939
|
+
const threshold = getStallThresholdMs(s.phase);
|
|
6940
|
+
if (elapsed < threshold) continue;
|
|
6941
|
+
if (_stalledStories.has(key)) continue;
|
|
6942
|
+
if (!processInspected) {
|
|
6943
|
+
processInspected = true;
|
|
6944
|
+
try {
|
|
6945
|
+
const processInfo = inspectProcessTree();
|
|
6946
|
+
childPids = processInfo.child_pids;
|
|
6947
|
+
const nonZombieChildren = processInfo.child_pids.filter((pid) => !processInfo.zombies.includes(pid));
|
|
6948
|
+
childActive = nonZombieChildren.length > 0;
|
|
6949
|
+
} catch {}
|
|
6950
|
+
}
|
|
6951
|
+
if (childActive) {
|
|
6952
|
+
_lastProgressTs = Date.now();
|
|
6953
|
+
logger$20.debug({
|
|
6459
6954
|
storyKey: key,
|
|
6460
6955
|
phase: s.phase,
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6956
|
+
childPids
|
|
6957
|
+
}, "Staleness exceeded but child processes are active — suppressing stall");
|
|
6958
|
+
break;
|
|
6464
6959
|
}
|
|
6960
|
+
_stalledStories.add(key);
|
|
6961
|
+
_storiesWithStall.add(key);
|
|
6962
|
+
logger$20.warn({
|
|
6963
|
+
storyKey: key,
|
|
6964
|
+
phase: s.phase,
|
|
6965
|
+
elapsedMs: elapsed,
|
|
6966
|
+
childPids,
|
|
6967
|
+
childActive
|
|
6968
|
+
}, "Watchdog: possible stall detected");
|
|
6969
|
+
eventBus.emit("orchestrator:stall", {
|
|
6970
|
+
runId: config.pipelineRunId ?? "",
|
|
6971
|
+
storyKey: key,
|
|
6972
|
+
phase: s.phase,
|
|
6973
|
+
elapsedMs: elapsed,
|
|
6974
|
+
childPids,
|
|
6975
|
+
childActive
|
|
6976
|
+
});
|
|
6465
6977
|
}
|
|
6466
6978
|
}, HEARTBEAT_INTERVAL_MS);
|
|
6467
6979
|
if (_heartbeatTimer && typeof _heartbeatTimer === "object" && "unref" in _heartbeatTimer) _heartbeatTimer.unref();
|
|
@@ -6479,6 +6991,32 @@ function createImplementationOrchestrator(deps) {
|
|
|
6479
6991
|
if (_paused && _pauseGate !== null) await _pauseGate.promise;
|
|
6480
6992
|
}
|
|
6481
6993
|
/**
|
|
6994
|
+
* Check memory pressure before dispatching a story phase (Story 23-8, AC1).
|
|
6995
|
+
*
|
|
6996
|
+
* When the dispatcher reports memory pressure, this helper waits using
|
|
6997
|
+
* exponential backoff (30 s, 60 s, 120 s) and re-checks after each interval.
|
|
6998
|
+
* If memory is still pressured after all intervals, returns false so the
|
|
6999
|
+
* caller can escalate the story with reason 'memory_pressure_exhausted'.
|
|
7000
|
+
*
|
|
7001
|
+
* If memory is OK (or clears during a wait), returns true immediately.
|
|
7002
|
+
*/
|
|
7003
|
+
async function checkMemoryPressure(storyKey) {
|
|
7004
|
+
for (let attempt = 0; attempt < MEMORY_PRESSURE_BACKOFF_MS.length; attempt++) {
|
|
7005
|
+
const memState = dispatcher.getMemoryState();
|
|
7006
|
+
if (!memState.isPressured) return true;
|
|
7007
|
+
logger$20.warn({
|
|
7008
|
+
storyKey,
|
|
7009
|
+
freeMB: memState.freeMB,
|
|
7010
|
+
thresholdMB: memState.thresholdMB,
|
|
7011
|
+
pressureLevel: memState.pressureLevel,
|
|
7012
|
+
attempt: attempt + 1,
|
|
7013
|
+
maxAttempts: MEMORY_PRESSURE_BACKOFF_MS.length
|
|
7014
|
+
}, "Memory pressure before story dispatch — backing off");
|
|
7015
|
+
await sleep(MEMORY_PRESSURE_BACKOFF_MS[attempt] ?? 0);
|
|
7016
|
+
}
|
|
7017
|
+
return !dispatcher.getMemoryState().isPressured;
|
|
7018
|
+
}
|
|
7019
|
+
/**
|
|
6482
7020
|
* Run the full pipeline for a single story key.
|
|
6483
7021
|
*
|
|
6484
7022
|
* Sequence: create-story → dev-story → code-review (with retry/rework up
|
|
@@ -6487,6 +7025,28 @@ function createImplementationOrchestrator(deps) {
|
|
|
6487
7025
|
*/
|
|
6488
7026
|
async function processStory(storyKey) {
|
|
6489
7027
|
logger$20.info("Processing story", { storyKey });
|
|
7028
|
+
{
|
|
7029
|
+
const memoryOk = await checkMemoryPressure(storyKey);
|
|
7030
|
+
if (!memoryOk) {
|
|
7031
|
+
logger$20.warn({ storyKey }, "Memory pressure exhausted — escalating story without dispatch");
|
|
7032
|
+
_stories.set(storyKey, {
|
|
7033
|
+
phase: "ESCALATED",
|
|
7034
|
+
reviewCycles: 0,
|
|
7035
|
+
error: "memory_pressure_exhausted",
|
|
7036
|
+
startedAt: new Date().toISOString(),
|
|
7037
|
+
completedAt: new Date().toISOString()
|
|
7038
|
+
});
|
|
7039
|
+
writeStoryMetricsBestEffort(storyKey, "escalated", 0);
|
|
7040
|
+
emitEscalation({
|
|
7041
|
+
storyKey,
|
|
7042
|
+
lastVerdict: "memory_pressure_exhausted",
|
|
7043
|
+
reviewCycles: 0,
|
|
7044
|
+
issues: [`Memory pressure exhausted after ${MEMORY_PRESSURE_BACKOFF_MS.length} backoff attempts`]
|
|
7045
|
+
});
|
|
7046
|
+
persistState();
|
|
7047
|
+
return;
|
|
7048
|
+
}
|
|
7049
|
+
}
|
|
6490
7050
|
await waitIfPaused();
|
|
6491
7051
|
if (_state !== "RUNNING") return;
|
|
6492
7052
|
startPhase(storyKey, "create-story");
|
|
@@ -6640,6 +7200,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
6640
7200
|
persistState();
|
|
6641
7201
|
let devFilesModified = [];
|
|
6642
7202
|
const batchFileGroups = [];
|
|
7203
|
+
let devStoryWasSuccess = false;
|
|
6643
7204
|
try {
|
|
6644
7205
|
let storyContentForAnalysis = "";
|
|
6645
7206
|
try {
|
|
@@ -6748,6 +7309,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
6748
7309
|
batchIndex: batch.batchIndex,
|
|
6749
7310
|
error: batchResult.error
|
|
6750
7311
|
}, "Batch dev-story reported failure — continuing with partial files");
|
|
7312
|
+
else devStoryWasSuccess = true;
|
|
6751
7313
|
eventBus.emit("orchestrator:story-phase-complete", {
|
|
6752
7314
|
storyKey,
|
|
6753
7315
|
phase: "IN_DEV",
|
|
@@ -6776,7 +7338,8 @@ function createImplementationOrchestrator(deps) {
|
|
|
6776
7338
|
result: devResult
|
|
6777
7339
|
});
|
|
6778
7340
|
persistState();
|
|
6779
|
-
if (devResult.result === "
|
|
7341
|
+
if (devResult.result === "success") devStoryWasSuccess = true;
|
|
7342
|
+
else logger$20.warn("Dev-story reported failure, proceeding to code review", {
|
|
6780
7343
|
storyKey,
|
|
6781
7344
|
error: devResult.error,
|
|
6782
7345
|
filesModified: devFilesModified.length
|
|
@@ -6800,7 +7363,70 @@ function createImplementationOrchestrator(deps) {
|
|
|
6800
7363
|
persistState();
|
|
6801
7364
|
return;
|
|
6802
7365
|
}
|
|
7366
|
+
if (devStoryWasSuccess) {
|
|
7367
|
+
const changedFiles = checkGitDiffFiles(projectRoot ?? process.cwd());
|
|
7368
|
+
if (changedFiles.length === 0) {
|
|
7369
|
+
logger$20.warn({ storyKey }, "Zero-diff detected after COMPLETE dev-story — no file changes in git working tree");
|
|
7370
|
+
eventBus.emit("orchestrator:zero-diff-escalation", {
|
|
7371
|
+
storyKey,
|
|
7372
|
+
reason: "zero-diff-on-complete"
|
|
7373
|
+
});
|
|
7374
|
+
endPhase(storyKey, "dev-story");
|
|
7375
|
+
updateStory(storyKey, {
|
|
7376
|
+
phase: "ESCALATED",
|
|
7377
|
+
error: "zero-diff-on-complete",
|
|
7378
|
+
completedAt: new Date().toISOString()
|
|
7379
|
+
});
|
|
7380
|
+
writeStoryMetricsBestEffort(storyKey, "escalated", 0);
|
|
7381
|
+
emitEscalation({
|
|
7382
|
+
storyKey,
|
|
7383
|
+
lastVerdict: "zero-diff-on-complete",
|
|
7384
|
+
reviewCycles: 0,
|
|
7385
|
+
issues: ["dev-story completed with COMPLETE verdict but no file changes detected in git diff"]
|
|
7386
|
+
});
|
|
7387
|
+
persistState();
|
|
7388
|
+
return;
|
|
7389
|
+
}
|
|
7390
|
+
}
|
|
6803
7391
|
endPhase(storyKey, "dev-story");
|
|
7392
|
+
{
|
|
7393
|
+
const buildVerifyResult = runBuildVerification({
|
|
7394
|
+
verifyCommand: pack.manifest.verifyCommand,
|
|
7395
|
+
verifyTimeoutMs: pack.manifest.verifyTimeoutMs,
|
|
7396
|
+
projectRoot: projectRoot ?? process.cwd()
|
|
7397
|
+
});
|
|
7398
|
+
if (buildVerifyResult.status === "passed") {
|
|
7399
|
+
eventBus.emit("story:build-verification-passed", { storyKey });
|
|
7400
|
+
logger$20.info({ storyKey }, "Build verification passed");
|
|
7401
|
+
} else if (buildVerifyResult.status === "failed" || buildVerifyResult.status === "timeout") {
|
|
7402
|
+
const truncatedOutput = (buildVerifyResult.output ?? "").slice(0, 2e3);
|
|
7403
|
+
const reason = buildVerifyResult.reason ?? "build-verification-failed";
|
|
7404
|
+
eventBus.emit("story:build-verification-failed", {
|
|
7405
|
+
storyKey,
|
|
7406
|
+
exitCode: buildVerifyResult.exitCode ?? 1,
|
|
7407
|
+
output: truncatedOutput
|
|
7408
|
+
});
|
|
7409
|
+
logger$20.warn({
|
|
7410
|
+
storyKey,
|
|
7411
|
+
reason,
|
|
7412
|
+
exitCode: buildVerifyResult.exitCode
|
|
7413
|
+
}, "Build verification failed — escalating story");
|
|
7414
|
+
updateStory(storyKey, {
|
|
7415
|
+
phase: "ESCALATED",
|
|
7416
|
+
error: reason,
|
|
7417
|
+
completedAt: new Date().toISOString()
|
|
7418
|
+
});
|
|
7419
|
+
writeStoryMetricsBestEffort(storyKey, "escalated", 0);
|
|
7420
|
+
emitEscalation({
|
|
7421
|
+
storyKey,
|
|
7422
|
+
lastVerdict: reason,
|
|
7423
|
+
reviewCycles: 0,
|
|
7424
|
+
issues: [truncatedOutput]
|
|
7425
|
+
});
|
|
7426
|
+
persistState();
|
|
7427
|
+
return;
|
|
7428
|
+
}
|
|
7429
|
+
}
|
|
6804
7430
|
let reviewCycles = 0;
|
|
6805
7431
|
let keepReviewing = true;
|
|
6806
7432
|
let timeoutRetried = false;
|
|
@@ -7304,9 +7930,18 @@ function createImplementationOrchestrator(deps) {
|
|
|
7304
7930
|
}
|
|
7305
7931
|
/**
|
|
7306
7932
|
* Process a conflict group: run stories sequentially within the group.
|
|
7933
|
+
*
|
|
7934
|
+
* After each story completes (any outcome), a GC hint is issued and a short
|
|
7935
|
+
* pause inserted so the Node.js process can reclaim memory before the next
|
|
7936
|
+
* story dispatch (Story 23-8, AC2).
|
|
7307
7937
|
*/
|
|
7308
7938
|
async function processConflictGroup(group) {
|
|
7309
|
-
for (const storyKey of group)
|
|
7939
|
+
for (const storyKey of group) {
|
|
7940
|
+
await processStory(storyKey);
|
|
7941
|
+
globalThis.gc?.();
|
|
7942
|
+
const gcPauseMs = config.gcPauseMs ?? 2e3;
|
|
7943
|
+
await sleep(gcPauseMs);
|
|
7944
|
+
}
|
|
7310
7945
|
}
|
|
7311
7946
|
/**
|
|
7312
7947
|
* Promise pool: run up to maxConcurrency groups at a time.
|
|
@@ -8091,7 +8726,7 @@ const CritiqueOutputSchema = z.object({
|
|
|
8091
8726
|
|
|
8092
8727
|
//#endregion
|
|
8093
8728
|
//#region src/modules/phase-orchestrator/critique-loop.ts
|
|
8094
|
-
const logger$
|
|
8729
|
+
const logger$5 = createLogger("critique-loop");
|
|
8095
8730
|
/**
|
|
8096
8731
|
* Maps a phase name to the critique prompt template name.
|
|
8097
8732
|
* Falls back to `critique-${phase}` for unknown phases.
|
|
@@ -8145,7 +8780,7 @@ async function runCritiqueLoop(artifact, phaseId, runId, phase, deps, options =
|
|
|
8145
8780
|
critiquePrompt = critiqueTemplate.replace("{{artifact_content}}", currentArtifact).replace("{{project_context}}", projectContext);
|
|
8146
8781
|
} catch (err) {
|
|
8147
8782
|
const message = err instanceof Error ? err.message : String(err);
|
|
8148
|
-
logger$
|
|
8783
|
+
logger$5.warn({
|
|
8149
8784
|
phaseId,
|
|
8150
8785
|
promptName: critiquePromptName,
|
|
8151
8786
|
err: message
|
|
@@ -8173,7 +8808,7 @@ async function runCritiqueLoop(artifact, phaseId, runId, phase, deps, options =
|
|
|
8173
8808
|
critiqueTokens.output += result.tokenEstimate.output;
|
|
8174
8809
|
if (result.status !== "completed" || result.parsed === null) {
|
|
8175
8810
|
const errMsg = result.parseError ?? `Critique dispatch ended with status '${result.status}'`;
|
|
8176
|
-
logger$
|
|
8811
|
+
logger$5.warn({
|
|
8177
8812
|
phaseId,
|
|
8178
8813
|
iteration: i + 1,
|
|
8179
8814
|
err: errMsg
|
|
@@ -8192,7 +8827,7 @@ async function runCritiqueLoop(artifact, phaseId, runId, phase, deps, options =
|
|
|
8192
8827
|
lastCritiqueOutput = critiqueOutput;
|
|
8193
8828
|
} catch (err) {
|
|
8194
8829
|
const message = err instanceof Error ? err.message : String(err);
|
|
8195
|
-
logger$
|
|
8830
|
+
logger$5.warn({
|
|
8196
8831
|
phaseId,
|
|
8197
8832
|
iteration: i + 1,
|
|
8198
8833
|
err: message
|
|
@@ -8232,14 +8867,14 @@ async function runCritiqueLoop(artifact, phaseId, runId, phase, deps, options =
|
|
|
8232
8867
|
});
|
|
8233
8868
|
} catch (err) {
|
|
8234
8869
|
const message = err instanceof Error ? err.message : String(err);
|
|
8235
|
-
logger$
|
|
8870
|
+
logger$5.warn({
|
|
8236
8871
|
phaseId,
|
|
8237
8872
|
iteration: i + 1,
|
|
8238
8873
|
err: message
|
|
8239
8874
|
}, "Critique loop: failed to store critique decision — continuing");
|
|
8240
8875
|
}
|
|
8241
8876
|
if (critiqueOutput.verdict === "pass") {
|
|
8242
|
-
logger$
|
|
8877
|
+
logger$5.info({
|
|
8243
8878
|
phaseId,
|
|
8244
8879
|
iteration: i + 1
|
|
8245
8880
|
}, "Critique loop: artifact passed critique — loop complete");
|
|
@@ -8252,7 +8887,7 @@ async function runCritiqueLoop(artifact, phaseId, runId, phase, deps, options =
|
|
|
8252
8887
|
totalMs: Date.now() - startMs
|
|
8253
8888
|
};
|
|
8254
8889
|
}
|
|
8255
|
-
logger$
|
|
8890
|
+
logger$5.info({
|
|
8256
8891
|
phaseId,
|
|
8257
8892
|
iteration: i + 1,
|
|
8258
8893
|
issueCount: critiqueOutput.issue_count
|
|
@@ -8265,7 +8900,7 @@ async function runCritiqueLoop(artifact, phaseId, runId, phase, deps, options =
|
|
|
8265
8900
|
refinePrompt = refineTemplate.replace("{{original_artifact}}", currentArtifact).replace("{{critique_issues}}", issuesText).replace("{{phase_context}}", phaseContext);
|
|
8266
8901
|
} catch (err) {
|
|
8267
8902
|
const message = err instanceof Error ? err.message : String(err);
|
|
8268
|
-
logger$
|
|
8903
|
+
logger$5.warn({
|
|
8269
8904
|
phaseId,
|
|
8270
8905
|
iteration: i + 1,
|
|
8271
8906
|
err: message
|
|
@@ -8286,7 +8921,7 @@ async function runCritiqueLoop(artifact, phaseId, runId, phase, deps, options =
|
|
|
8286
8921
|
const originalLength = currentArtifact.length;
|
|
8287
8922
|
const refinedLength = refineResult.output.length;
|
|
8288
8923
|
const delta = refinedLength - originalLength;
|
|
8289
|
-
logger$
|
|
8924
|
+
logger$5.info({
|
|
8290
8925
|
phaseId,
|
|
8291
8926
|
iteration: i + 1,
|
|
8292
8927
|
originalLength,
|
|
@@ -8295,7 +8930,7 @@ async function runCritiqueLoop(artifact, phaseId, runId, phase, deps, options =
|
|
|
8295
8930
|
}, "Critique loop: refinement complete");
|
|
8296
8931
|
currentArtifact = refineResult.output;
|
|
8297
8932
|
} else {
|
|
8298
|
-
logger$
|
|
8933
|
+
logger$5.warn({
|
|
8299
8934
|
phaseId,
|
|
8300
8935
|
iteration: i + 1,
|
|
8301
8936
|
status: refineResult.status
|
|
@@ -8304,7 +8939,7 @@ async function runCritiqueLoop(artifact, phaseId, runId, phase, deps, options =
|
|
|
8304
8939
|
}
|
|
8305
8940
|
} catch (err) {
|
|
8306
8941
|
const message = err instanceof Error ? err.message : String(err);
|
|
8307
|
-
logger$
|
|
8942
|
+
logger$5.warn({
|
|
8308
8943
|
phaseId,
|
|
8309
8944
|
iteration: i + 1,
|
|
8310
8945
|
err: message
|
|
@@ -8315,12 +8950,12 @@ async function runCritiqueLoop(artifact, phaseId, runId, phase, deps, options =
|
|
|
8315
8950
|
}
|
|
8316
8951
|
const remainingIssues = lastCritiqueOutput?.issues ?? [];
|
|
8317
8952
|
if (remainingIssues.length > 0) {
|
|
8318
|
-
logger$
|
|
8953
|
+
logger$5.warn({
|
|
8319
8954
|
phaseId,
|
|
8320
8955
|
maxIterations,
|
|
8321
8956
|
issueCount: remainingIssues.length
|
|
8322
8957
|
}, "Critique loop: max iterations reached with unresolved issues");
|
|
8323
|
-
for (const issue of remainingIssues) logger$
|
|
8958
|
+
for (const issue of remainingIssues) logger$5.warn({
|
|
8324
8959
|
phaseId,
|
|
8325
8960
|
severity: issue.severity,
|
|
8326
8961
|
category: issue.category,
|
|
@@ -8339,7 +8974,7 @@ async function runCritiqueLoop(artifact, phaseId, runId, phase, deps, options =
|
|
|
8339
8974
|
|
|
8340
8975
|
//#endregion
|
|
8341
8976
|
//#region src/modules/phase-orchestrator/elicitation-selector.ts
|
|
8342
|
-
const logger$
|
|
8977
|
+
const logger$4 = createLogger("elicitation-selector");
|
|
8343
8978
|
/**
|
|
8344
8979
|
* Affinity scores (0.0–1.0) for each category per content type.
|
|
8345
8980
|
*
|
|
@@ -8461,10 +9096,10 @@ function loadElicitationMethods() {
|
|
|
8461
9096
|
try {
|
|
8462
9097
|
const content = readFileSync(csvPath, "utf-8");
|
|
8463
9098
|
const methods = parseMethodsCsv(content);
|
|
8464
|
-
logger$
|
|
9099
|
+
logger$4.debug({ count: methods.length }, "Loaded elicitation methods");
|
|
8465
9100
|
return methods;
|
|
8466
9101
|
} catch (err) {
|
|
8467
|
-
logger$
|
|
9102
|
+
logger$4.warn({
|
|
8468
9103
|
csvPath,
|
|
8469
9104
|
err
|
|
8470
9105
|
}, "Failed to load elicitation methods CSV");
|
|
@@ -8784,7 +9419,7 @@ const ElicitationOutputSchema = z.object({
|
|
|
8784
9419
|
|
|
8785
9420
|
//#endregion
|
|
8786
9421
|
//#region src/modules/phase-orchestrator/step-runner.ts
|
|
8787
|
-
const logger$
|
|
9422
|
+
const logger$3 = createLogger("step-runner");
|
|
8788
9423
|
/**
|
|
8789
9424
|
* Format an array of decision records into a markdown section for injection.
|
|
8790
9425
|
*
|
|
@@ -8891,7 +9526,7 @@ async function runSteps(steps, deps, runId, phase, params) {
|
|
|
8891
9526
|
if (estimatedTokens > budgetTokens) {
|
|
8892
9527
|
const decisionRefs = step.context.filter((ref) => ref.source.startsWith("decision:"));
|
|
8893
9528
|
if (decisionRefs.length > 0) {
|
|
8894
|
-
logger$
|
|
9529
|
+
logger$3.warn({
|
|
8895
9530
|
step: step.name,
|
|
8896
9531
|
estimatedTokens,
|
|
8897
9532
|
budgetTokens
|
|
@@ -8918,7 +9553,7 @@ async function runSteps(steps, deps, runId, phase, params) {
|
|
|
8918
9553
|
}
|
|
8919
9554
|
prompt = summarizedPrompt;
|
|
8920
9555
|
estimatedTokens = Math.ceil(prompt.length / 4);
|
|
8921
|
-
if (estimatedTokens <= budgetTokens) logger$
|
|
9556
|
+
if (estimatedTokens <= budgetTokens) logger$3.info({
|
|
8922
9557
|
step: step.name,
|
|
8923
9558
|
estimatedTokens,
|
|
8924
9559
|
budgetTokens
|
|
@@ -9099,7 +9734,7 @@ async function runSteps(steps, deps, runId, phase, params) {
|
|
|
9099
9734
|
const critiqueResult = await runCritiqueLoop(artifactContent, phase, runId, phase, deps);
|
|
9100
9735
|
totalInput += critiqueResult.critiqueTokens.input + critiqueResult.refinementTokens.input;
|
|
9101
9736
|
totalOutput += critiqueResult.critiqueTokens.output + critiqueResult.refinementTokens.output;
|
|
9102
|
-
logger$
|
|
9737
|
+
logger$3.info({
|
|
9103
9738
|
step: step.name,
|
|
9104
9739
|
verdict: critiqueResult.verdict,
|
|
9105
9740
|
iterations: critiqueResult.iterations,
|
|
@@ -9107,7 +9742,7 @@ async function runSteps(steps, deps, runId, phase, params) {
|
|
|
9107
9742
|
}, "Step critique loop complete");
|
|
9108
9743
|
} catch (critiqueErr) {
|
|
9109
9744
|
const critiqueMsg = critiqueErr instanceof Error ? critiqueErr.message : String(critiqueErr);
|
|
9110
|
-
logger$
|
|
9745
|
+
logger$3.warn({
|
|
9111
9746
|
step: step.name,
|
|
9112
9747
|
err: critiqueMsg
|
|
9113
9748
|
}, "Step critique loop threw an error — continuing without critique");
|
|
@@ -9117,7 +9752,7 @@ async function runSteps(steps, deps, runId, phase, params) {
|
|
|
9117
9752
|
const contentType = deriveContentType(phase, step.name);
|
|
9118
9753
|
const selectedMethods = selectMethods({ content_type: contentType }, usedElicitationMethods);
|
|
9119
9754
|
if (selectedMethods.length > 0) {
|
|
9120
|
-
logger$
|
|
9755
|
+
logger$3.info({
|
|
9121
9756
|
step: step.name,
|
|
9122
9757
|
methods: selectedMethods.map((m) => m.name),
|
|
9123
9758
|
contentType
|
|
@@ -9156,13 +9791,13 @@ async function runSteps(steps, deps, runId, phase, params) {
|
|
|
9156
9791
|
key: `${phase}-round-${roundIndex}-insights`,
|
|
9157
9792
|
value: elicitParsed.insights
|
|
9158
9793
|
});
|
|
9159
|
-
logger$
|
|
9794
|
+
logger$3.info({
|
|
9160
9795
|
step: step.name,
|
|
9161
9796
|
method: method.name,
|
|
9162
9797
|
roundIndex
|
|
9163
9798
|
}, "Elicitation insights stored in decision store");
|
|
9164
9799
|
}
|
|
9165
|
-
} else logger$
|
|
9800
|
+
} else logger$3.warn({
|
|
9166
9801
|
step: step.name,
|
|
9167
9802
|
method: method.name,
|
|
9168
9803
|
status: elicitResult.status
|
|
@@ -9178,7 +9813,7 @@ async function runSteps(steps, deps, runId, phase, params) {
|
|
|
9178
9813
|
}
|
|
9179
9814
|
} catch (elicitErr) {
|
|
9180
9815
|
const elicitMsg = elicitErr instanceof Error ? elicitErr.message : String(elicitErr);
|
|
9181
|
-
logger$
|
|
9816
|
+
logger$3.warn({
|
|
9182
9817
|
step: step.name,
|
|
9183
9818
|
err: elicitMsg
|
|
9184
9819
|
}, "Step elicitation threw an error — continuing without elicitation");
|
|
@@ -9546,7 +10181,7 @@ async function runAnalysisPhase(deps, params) {
|
|
|
9546
10181
|
|
|
9547
10182
|
//#endregion
|
|
9548
10183
|
//#region src/modules/phase-orchestrator/phases/planning.ts
|
|
9549
|
-
const logger$
|
|
10184
|
+
const logger$2 = createLogger("planning-phase");
|
|
9550
10185
|
/** Maximum total prompt length in tokens (3,500 tokens × 4 chars/token = 14,000 chars) */
|
|
9551
10186
|
const MAX_PROMPT_TOKENS = 3500;
|
|
9552
10187
|
const MAX_PROMPT_CHARS = MAX_PROMPT_TOKENS * 4;
|
|
@@ -9773,7 +10408,7 @@ async function runPlanningMultiStep(deps, params) {
|
|
|
9773
10408
|
const techConstraintDecisions = allAnalysisDecisions.filter((d) => d.category === "technology-constraints");
|
|
9774
10409
|
const violation = detectTechStackViolation(techStack, techConstraintDecisions);
|
|
9775
10410
|
if (violation) {
|
|
9776
|
-
logger$
|
|
10411
|
+
logger$2.warn({ violation }, "Tech stack constraint violation detected — retrying step 3 with correction");
|
|
9777
10412
|
const correctionPrefix = `CRITICAL CORRECTION: Your previous output was rejected because it violates the stated technology constraints.\n\nViolation: ${violation}\n\nYou MUST NOT use TypeScript, JavaScript, or Node.js for ANY backend service. Choose from Go, Kotlin/JVM, or Rust as stated in the technology constraints.\n\nRe-generate your output with a compliant tech stack. Everything else (NFRs, domain model, out-of-scope) can remain the same.\n\n---\n\n`;
|
|
9778
10413
|
const step3Template = await deps.pack.getPrompt("planning-step-3-nfrs");
|
|
9779
10414
|
const stepOutputs = new Map();
|
|
@@ -9800,10 +10435,10 @@ async function runPlanningMultiStep(deps, params) {
|
|
|
9800
10435
|
const retryTechStack = retryParsed.tech_stack;
|
|
9801
10436
|
const retryViolation = retryTechStack ? detectTechStackViolation(retryTechStack, techConstraintDecisions) : null;
|
|
9802
10437
|
if (!retryViolation) {
|
|
9803
|
-
logger$
|
|
10438
|
+
logger$2.info("Retry produced compliant tech stack — using corrected output");
|
|
9804
10439
|
nfrsOutput = retryParsed;
|
|
9805
|
-
} else logger$
|
|
9806
|
-
} else logger$
|
|
10440
|
+
} else logger$2.warn({ retryViolation }, "Retry still violates constraints — using original output");
|
|
10441
|
+
} else logger$2.warn("Retry dispatch failed — using original output");
|
|
9807
10442
|
}
|
|
9808
10443
|
}
|
|
9809
10444
|
const frs = frsOutput.functional_requirements;
|
|
@@ -10090,7 +10725,7 @@ const ReadinessOutputSchema = z.object({
|
|
|
10090
10725
|
|
|
10091
10726
|
//#endregion
|
|
10092
10727
|
//#region src/modules/phase-orchestrator/phases/solutioning.ts
|
|
10093
|
-
const logger$
|
|
10728
|
+
const logger$1 = createLogger("solutioning");
|
|
10094
10729
|
/** Base token budget for architecture generation (covers template + requirements) */
|
|
10095
10730
|
const BASE_ARCH_PROMPT_TOKENS = 3e3;
|
|
10096
10731
|
/** Base token budget for story generation (covers template + requirements + architecture) */
|
|
@@ -10499,7 +11134,7 @@ async function runReadinessCheck(deps, runId) {
|
|
|
10499
11134
|
input: tokenEstimate.input,
|
|
10500
11135
|
output: tokenEstimate.output
|
|
10501
11136
|
};
|
|
10502
|
-
logger$
|
|
11137
|
+
logger$1.info({
|
|
10503
11138
|
runId,
|
|
10504
11139
|
durationMs: dispatchResult.durationMs,
|
|
10505
11140
|
tokens: tokenEstimate
|
|
@@ -10760,7 +11395,7 @@ async function runSolutioningPhase(deps, params) {
|
|
|
10760
11395
|
let archResult;
|
|
10761
11396
|
if (existingArchArtifact) {
|
|
10762
11397
|
const existingDecisions = getDecisionsByPhaseForRun(deps.db, params.runId, "solutioning").filter((d) => d.category === "architecture");
|
|
10763
|
-
logger$
|
|
11398
|
+
logger$1.info({
|
|
10764
11399
|
runId: params.runId,
|
|
10765
11400
|
artifactId: existingArchArtifact.id,
|
|
10766
11401
|
decisionCount: existingDecisions.length
|
|
@@ -10791,7 +11426,7 @@ async function runSolutioningPhase(deps, params) {
|
|
|
10791
11426
|
output: totalOutput
|
|
10792
11427
|
}
|
|
10793
11428
|
};
|
|
10794
|
-
logger$
|
|
11429
|
+
logger$1.info({
|
|
10795
11430
|
runId: params.runId,
|
|
10796
11431
|
decisionCount: archResult.decisions.length,
|
|
10797
11432
|
mode: hasSteps ? "multi-step" : "single-dispatch"
|
|
@@ -10813,7 +11448,7 @@ async function runSolutioningPhase(deps, params) {
|
|
|
10813
11448
|
totalInput += readinessResult.tokenUsage.input;
|
|
10814
11449
|
totalOutput += readinessResult.tokenUsage.output;
|
|
10815
11450
|
if (readinessResult.verdict === "error") {
|
|
10816
|
-
logger$
|
|
11451
|
+
logger$1.error({
|
|
10817
11452
|
runId: params.runId,
|
|
10818
11453
|
error: readinessResult.error
|
|
10819
11454
|
}, "Readiness check agent failed");
|
|
@@ -10829,7 +11464,7 @@ async function runSolutioningPhase(deps, params) {
|
|
|
10829
11464
|
}
|
|
10830
11465
|
};
|
|
10831
11466
|
}
|
|
10832
|
-
logger$
|
|
11467
|
+
logger$1.info({
|
|
10833
11468
|
runId: params.runId,
|
|
10834
11469
|
verdict: readinessResult.verdict,
|
|
10835
11470
|
coverageScore: readinessResult.coverageScore,
|
|
@@ -10845,7 +11480,7 @@ async function runSolutioningPhase(deps, params) {
|
|
|
10845
11480
|
key: `finding-${i + 1}`,
|
|
10846
11481
|
value: JSON.stringify(finding)
|
|
10847
11482
|
});
|
|
10848
|
-
logger$
|
|
11483
|
+
logger$1.error({
|
|
10849
11484
|
runId: params.runId,
|
|
10850
11485
|
verdict: "NOT_READY",
|
|
10851
11486
|
coverageScore: readinessResult.coverageScore,
|
|
@@ -10911,7 +11546,7 @@ async function runSolutioningPhase(deps, params) {
|
|
|
10911
11546
|
"",
|
|
10912
11547
|
"Please generate additional or revised stories to specifically address each blocker above."
|
|
10913
11548
|
].join("\n");
|
|
10914
|
-
logger$
|
|
11549
|
+
logger$1.info({
|
|
10915
11550
|
runId: params.runId,
|
|
10916
11551
|
blockerCount: blockers.length
|
|
10917
11552
|
}, "Readiness NEEDS_WORK with blockers — retrying story generation with gap analysis");
|
|
@@ -10950,7 +11585,7 @@ async function runSolutioningPhase(deps, params) {
|
|
|
10950
11585
|
};
|
|
10951
11586
|
if (retryReadiness.verdict === "NOT_READY" || retryReadiness.verdict === "NEEDS_WORK") {
|
|
10952
11587
|
const retryBlockers = retryReadiness.findings.filter((f) => f.severity === "blocker");
|
|
10953
|
-
logger$
|
|
11588
|
+
logger$1.error({
|
|
10954
11589
|
runId: params.runId,
|
|
10955
11590
|
verdict: retryReadiness.verdict,
|
|
10956
11591
|
retryBlockers: retryBlockers.length
|
|
@@ -10974,7 +11609,7 @@ async function runSolutioningPhase(deps, params) {
|
|
|
10974
11609
|
}
|
|
10975
11610
|
const retryStories = retryResult.epics.reduce((sum, epic) => sum + epic.stories.length, 0);
|
|
10976
11611
|
const minorFindings$1 = retryReadiness.findings.filter((f) => f.severity === "minor");
|
|
10977
|
-
if (minorFindings$1.length > 0) logger$
|
|
11612
|
+
if (minorFindings$1.length > 0) logger$1.warn({
|
|
10978
11613
|
runId: params.runId,
|
|
10979
11614
|
minorFindings: minorFindings$1
|
|
10980
11615
|
}, "Readiness READY with minor findings after retry");
|
|
@@ -11003,7 +11638,7 @@ async function runSolutioningPhase(deps, params) {
|
|
|
11003
11638
|
};
|
|
11004
11639
|
}
|
|
11005
11640
|
const majorFindings = readinessResult.findings.filter((f) => f.severity === "major");
|
|
11006
|
-
logger$
|
|
11641
|
+
logger$1.warn({
|
|
11007
11642
|
runId: params.runId,
|
|
11008
11643
|
majorCount: majorFindings.length,
|
|
11009
11644
|
findings: readinessResult.findings
|
|
@@ -11019,7 +11654,7 @@ async function runSolutioningPhase(deps, params) {
|
|
|
11019
11654
|
const minorFindings = readinessResult.findings.filter((f) => f.severity === "minor");
|
|
11020
11655
|
if (minorFindings.length > 0) {
|
|
11021
11656
|
const verdictLabel = readinessResult.verdict === "READY" ? "READY" : "NEEDS_WORK (no blockers)";
|
|
11022
|
-
logger$
|
|
11657
|
+
logger$1.warn({
|
|
11023
11658
|
runId: params.runId,
|
|
11024
11659
|
verdict: readinessResult.verdict,
|
|
11025
11660
|
minorFindings
|
|
@@ -11316,381 +11951,119 @@ function buildResearchSteps() {
|
|
|
11316
11951
|
{
|
|
11317
11952
|
field: "technical_findings",
|
|
11318
11953
|
category: "research",
|
|
11319
|
-
key: "technical_findings"
|
|
11320
|
-
}
|
|
11321
|
-
],
|
|
11322
|
-
elicitate: true
|
|
11323
|
-
}, {
|
|
11324
|
-
name: "research-step-2-synthesis",
|
|
11325
|
-
taskType: "research-synthesis",
|
|
11326
|
-
outputSchema: ResearchSynthesisOutputSchema,
|
|
11327
|
-
context: [{
|
|
11328
|
-
placeholder: "concept",
|
|
11329
|
-
source: "param:concept"
|
|
11330
|
-
}, {
|
|
11331
|
-
placeholder: "raw_findings",
|
|
11332
|
-
source: "step:research-step-1-discovery"
|
|
11333
|
-
}],
|
|
11334
|
-
persist: [
|
|
11335
|
-
{
|
|
11336
|
-
field: "market_context",
|
|
11337
|
-
category: "research",
|
|
11338
|
-
key: "market_context"
|
|
11339
|
-
},
|
|
11340
|
-
{
|
|
11341
|
-
field: "competitive_landscape",
|
|
11342
|
-
category: "research",
|
|
11343
|
-
key: "competitive_landscape"
|
|
11344
|
-
},
|
|
11345
|
-
{
|
|
11346
|
-
field: "technical_feasibility",
|
|
11347
|
-
category: "research",
|
|
11348
|
-
key: "technical_feasibility"
|
|
11349
|
-
},
|
|
11350
|
-
{
|
|
11351
|
-
field: "risk_flags",
|
|
11352
|
-
category: "research",
|
|
11353
|
-
key: "risk_flags"
|
|
11354
|
-
},
|
|
11355
|
-
{
|
|
11356
|
-
field: "opportunity_signals",
|
|
11357
|
-
category: "research",
|
|
11358
|
-
key: "opportunity_signals"
|
|
11359
|
-
}
|
|
11360
|
-
],
|
|
11361
|
-
registerArtifact: {
|
|
11362
|
-
type: "research-findings",
|
|
11363
|
-
path: "decision-store://research/research-findings",
|
|
11364
|
-
summarize: (parsed) => {
|
|
11365
|
-
const risks = Array.isArray(parsed.risk_flags) ? parsed.risk_flags : void 0;
|
|
11366
|
-
const opportunities = Array.isArray(parsed.opportunity_signals) ? parsed.opportunity_signals : void 0;
|
|
11367
|
-
const count = (risks?.length ?? 0) + (opportunities?.length ?? 0);
|
|
11368
|
-
return count > 0 ? `${count} research insights captured (risks + opportunities)` : "Research synthesis complete";
|
|
11369
|
-
}
|
|
11370
|
-
},
|
|
11371
|
-
critique: true
|
|
11372
|
-
}];
|
|
11373
|
-
}
|
|
11374
|
-
/**
|
|
11375
|
-
* Execute the research phase of the BMAD pipeline.
|
|
11376
|
-
*
|
|
11377
|
-
* Runs 2 sequential steps covering discovery and synthesis.
|
|
11378
|
-
* Each step builds on prior step decisions via the decision store.
|
|
11379
|
-
*
|
|
11380
|
-
* On success, a 'research-findings' artifact is registered and research decisions
|
|
11381
|
-
* are available to subsequent phases via `decision:research.*`.
|
|
11382
|
-
*
|
|
11383
|
-
* @param deps - Shared phase dependencies (db, pack, contextCompiler, dispatcher)
|
|
11384
|
-
* @param params - Phase parameters (runId, concept)
|
|
11385
|
-
* @returns ResearchResult with success/failure status and token usage
|
|
11386
|
-
*/
|
|
11387
|
-
async function runResearchPhase(deps, params) {
|
|
11388
|
-
const { runId } = params;
|
|
11389
|
-
const zeroTokenUsage = {
|
|
11390
|
-
input: 0,
|
|
11391
|
-
output: 0
|
|
11392
|
-
};
|
|
11393
|
-
try {
|
|
11394
|
-
const steps = buildResearchSteps();
|
|
11395
|
-
const result = await runSteps(steps, deps, runId, "research", { concept: params.concept });
|
|
11396
|
-
if (!result.success) return {
|
|
11397
|
-
result: "failed",
|
|
11398
|
-
error: result.error ?? "research_multi_step_failed",
|
|
11399
|
-
details: result.error ?? "Research multi-step execution failed",
|
|
11400
|
-
tokenUsage: result.tokenUsage
|
|
11401
|
-
};
|
|
11402
|
-
const lastStep = result.steps[result.steps.length - 1];
|
|
11403
|
-
const artifactId = lastStep?.artifactId;
|
|
11404
|
-
if (!artifactId) {
|
|
11405
|
-
const artifact = registerArtifact(deps.db, {
|
|
11406
|
-
pipeline_run_id: runId,
|
|
11407
|
-
phase: "research",
|
|
11408
|
-
type: "research-findings",
|
|
11409
|
-
path: "decision-store://research/research-findings",
|
|
11410
|
-
summary: "Research phase completed"
|
|
11411
|
-
});
|
|
11412
|
-
return {
|
|
11413
|
-
result: "success",
|
|
11414
|
-
artifact_id: artifact.id,
|
|
11415
|
-
tokenUsage: result.tokenUsage
|
|
11416
|
-
};
|
|
11417
|
-
}
|
|
11418
|
-
return {
|
|
11419
|
-
result: "success",
|
|
11420
|
-
artifact_id: artifactId,
|
|
11421
|
-
tokenUsage: result.tokenUsage
|
|
11422
|
-
};
|
|
11423
|
-
} catch (err) {
|
|
11424
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
11425
|
-
return {
|
|
11426
|
-
result: "failed",
|
|
11427
|
-
error: message,
|
|
11428
|
-
tokenUsage: zeroTokenUsage
|
|
11429
|
-
};
|
|
11430
|
-
}
|
|
11431
|
-
}
|
|
11432
|
-
|
|
11433
|
-
//#endregion
|
|
11434
|
-
//#region src/cli/commands/health.ts
|
|
11435
|
-
const logger$1 = createLogger("health-cmd");
|
|
11436
|
-
/** Default stall threshold in seconds — also used by supervisor default */
|
|
11437
|
-
const DEFAULT_STALL_THRESHOLD_SECONDS = 600;
|
|
11438
|
-
/**
|
|
11439
|
-
* Determine whether a ps output line represents the substrate pipeline orchestrator.
|
|
11440
|
-
* Handles invocation via:
|
|
11441
|
-
* - `substrate run` (globally installed)
|
|
11442
|
-
* - `substrate-ai run`
|
|
11443
|
-
* - `node dist/cli/index.js run` (npm run substrate:dev)
|
|
11444
|
-
* - `npx substrate run`
|
|
11445
|
-
* - any node process whose command contains `run` with `--events` or `--stories`
|
|
11446
|
-
*
|
|
11447
|
-
* When `projectRoot` is provided, additionally checks that the command line
|
|
11448
|
-
* contains that path (via `--project-root` flag or as part of the binary/CWD path).
|
|
11449
|
-
* This ensures multi-project environments match the correct orchestrator.
|
|
11450
|
-
*/
|
|
11451
|
-
function isOrchestratorProcessLine(line, projectRoot) {
|
|
11452
|
-
if (line.includes("grep")) return false;
|
|
11453
|
-
let isOrchestrator = false;
|
|
11454
|
-
if (line.includes("substrate run")) isOrchestrator = true;
|
|
11455
|
-
else if (line.includes("substrate-ai run")) isOrchestrator = true;
|
|
11456
|
-
else if (line.includes("index.js run")) isOrchestrator = true;
|
|
11457
|
-
else if (line.includes("node") && /\srun(\s|$)/.test(line) && (line.includes("--events") || line.includes("--stories"))) isOrchestrator = true;
|
|
11458
|
-
if (!isOrchestrator) return false;
|
|
11459
|
-
if (projectRoot !== void 0) return line.includes(projectRoot);
|
|
11460
|
-
return true;
|
|
11461
|
-
}
|
|
11462
|
-
function inspectProcessTree(opts) {
|
|
11463
|
-
const { projectRoot, execFileSync: execFileSyncOverride } = opts ?? {};
|
|
11464
|
-
const result = {
|
|
11465
|
-
orchestrator_pid: null,
|
|
11466
|
-
child_pids: [],
|
|
11467
|
-
zombies: []
|
|
11468
|
-
};
|
|
11469
|
-
try {
|
|
11470
|
-
let psOutput;
|
|
11471
|
-
if (execFileSyncOverride !== void 0) psOutput = execFileSyncOverride("ps", ["-eo", "pid,ppid,stat,command"], {
|
|
11472
|
-
encoding: "utf-8",
|
|
11473
|
-
timeout: 5e3
|
|
11474
|
-
});
|
|
11475
|
-
else {
|
|
11476
|
-
const { execFileSync } = __require("node:child_process");
|
|
11477
|
-
psOutput = execFileSync("ps", ["-eo", "pid,ppid,stat,command"], {
|
|
11478
|
-
encoding: "utf-8",
|
|
11479
|
-
timeout: 5e3
|
|
11480
|
-
});
|
|
11481
|
-
}
|
|
11482
|
-
const lines = psOutput.split("\n");
|
|
11483
|
-
for (const line of lines) if (isOrchestratorProcessLine(line, projectRoot)) {
|
|
11484
|
-
const match = line.trim().match(/^(\d+)/);
|
|
11485
|
-
if (match) {
|
|
11486
|
-
result.orchestrator_pid = parseInt(match[1], 10);
|
|
11487
|
-
break;
|
|
11488
|
-
}
|
|
11489
|
-
}
|
|
11490
|
-
if (result.orchestrator_pid !== null) for (const line of lines) {
|
|
11491
|
-
const parts = line.trim().split(/\s+/);
|
|
11492
|
-
if (parts.length >= 3) {
|
|
11493
|
-
const pid = parseInt(parts[0], 10);
|
|
11494
|
-
const ppid = parseInt(parts[1], 10);
|
|
11495
|
-
const stat$2 = parts[2];
|
|
11496
|
-
if (ppid === result.orchestrator_pid && pid !== result.orchestrator_pid) {
|
|
11497
|
-
result.child_pids.push(pid);
|
|
11498
|
-
if (stat$2.includes("Z")) result.zombies.push(pid);
|
|
11499
|
-
}
|
|
11500
|
-
}
|
|
11501
|
-
}
|
|
11502
|
-
} catch {}
|
|
11503
|
-
return result;
|
|
11504
|
-
}
|
|
11505
|
-
/**
|
|
11506
|
-
* Collect all descendant PIDs of the given root PIDs by walking the process
|
|
11507
|
-
* tree recursively. This ensures that grandchildren of the orchestrator
|
|
11508
|
-
* (e.g. node subprocesses spawned by `claude -p`) are also killed during
|
|
11509
|
-
* stall recovery, leaving no orphan processes.
|
|
11510
|
-
*
|
|
11511
|
-
* Returns only the descendants — the root PIDs themselves are NOT included.
|
|
11512
|
-
*/
|
|
11513
|
-
function getAllDescendantPids(rootPids, execFileSyncOverride) {
|
|
11514
|
-
if (rootPids.length === 0) return [];
|
|
11515
|
-
try {
|
|
11516
|
-
let psOutput;
|
|
11517
|
-
if (execFileSyncOverride !== void 0) psOutput = execFileSyncOverride("ps", ["-eo", "pid,ppid"], {
|
|
11518
|
-
encoding: "utf-8",
|
|
11519
|
-
timeout: 5e3
|
|
11520
|
-
});
|
|
11521
|
-
else {
|
|
11522
|
-
const { execFileSync } = __require("node:child_process");
|
|
11523
|
-
psOutput = execFileSync("ps", ["-eo", "pid,ppid"], {
|
|
11524
|
-
encoding: "utf-8",
|
|
11525
|
-
timeout: 5e3
|
|
11526
|
-
});
|
|
11527
|
-
}
|
|
11528
|
-
const childrenOf = new Map();
|
|
11529
|
-
for (const line of psOutput.split("\n")) {
|
|
11530
|
-
const parts = line.trim().split(/\s+/);
|
|
11531
|
-
if (parts.length >= 2) {
|
|
11532
|
-
const pid = parseInt(parts[0], 10);
|
|
11533
|
-
const ppid = parseInt(parts[1], 10);
|
|
11534
|
-
if (!isNaN(pid) && !isNaN(ppid) && pid > 0) {
|
|
11535
|
-
if (!childrenOf.has(ppid)) childrenOf.set(ppid, []);
|
|
11536
|
-
childrenOf.get(ppid).push(pid);
|
|
11537
|
-
}
|
|
11954
|
+
key: "technical_findings"
|
|
11538
11955
|
}
|
|
11539
|
-
|
|
11540
|
-
|
|
11541
|
-
|
|
11542
|
-
|
|
11543
|
-
|
|
11544
|
-
|
|
11545
|
-
|
|
11546
|
-
|
|
11547
|
-
|
|
11548
|
-
|
|
11549
|
-
|
|
11956
|
+
],
|
|
11957
|
+
elicitate: true
|
|
11958
|
+
}, {
|
|
11959
|
+
name: "research-step-2-synthesis",
|
|
11960
|
+
taskType: "research-synthesis",
|
|
11961
|
+
outputSchema: ResearchSynthesisOutputSchema,
|
|
11962
|
+
context: [{
|
|
11963
|
+
placeholder: "concept",
|
|
11964
|
+
source: "param:concept"
|
|
11965
|
+
}, {
|
|
11966
|
+
placeholder: "raw_findings",
|
|
11967
|
+
source: "step:research-step-1-discovery"
|
|
11968
|
+
}],
|
|
11969
|
+
persist: [
|
|
11970
|
+
{
|
|
11971
|
+
field: "market_context",
|
|
11972
|
+
category: "research",
|
|
11973
|
+
key: "market_context"
|
|
11974
|
+
},
|
|
11975
|
+
{
|
|
11976
|
+
field: "competitive_landscape",
|
|
11977
|
+
category: "research",
|
|
11978
|
+
key: "competitive_landscape"
|
|
11979
|
+
},
|
|
11980
|
+
{
|
|
11981
|
+
field: "technical_feasibility",
|
|
11982
|
+
category: "research",
|
|
11983
|
+
key: "technical_feasibility"
|
|
11984
|
+
},
|
|
11985
|
+
{
|
|
11986
|
+
field: "risk_flags",
|
|
11987
|
+
category: "research",
|
|
11988
|
+
key: "risk_flags"
|
|
11989
|
+
},
|
|
11990
|
+
{
|
|
11991
|
+
field: "opportunity_signals",
|
|
11992
|
+
category: "research",
|
|
11993
|
+
key: "opportunity_signals"
|
|
11550
11994
|
}
|
|
11551
|
-
|
|
11552
|
-
|
|
11553
|
-
|
|
11554
|
-
|
|
11555
|
-
|
|
11995
|
+
],
|
|
11996
|
+
registerArtifact: {
|
|
11997
|
+
type: "research-findings",
|
|
11998
|
+
path: "decision-store://research/research-findings",
|
|
11999
|
+
summarize: (parsed) => {
|
|
12000
|
+
const risks = Array.isArray(parsed.risk_flags) ? parsed.risk_flags : void 0;
|
|
12001
|
+
const opportunities = Array.isArray(parsed.opportunity_signals) ? parsed.opportunity_signals : void 0;
|
|
12002
|
+
const count = (risks?.length ?? 0) + (opportunities?.length ?? 0);
|
|
12003
|
+
return count > 0 ? `${count} research insights captured (risks + opportunities)` : "Research synthesis complete";
|
|
12004
|
+
}
|
|
12005
|
+
},
|
|
12006
|
+
critique: true
|
|
12007
|
+
}];
|
|
11556
12008
|
}
|
|
11557
12009
|
/**
|
|
11558
|
-
*
|
|
11559
|
-
* Used by runSupervisorAction to poll health without formatting overhead.
|
|
12010
|
+
* Execute the research phase of the BMAD pipeline.
|
|
11560
12011
|
*
|
|
11561
|
-
*
|
|
11562
|
-
*
|
|
12012
|
+
* Runs 2 sequential steps covering discovery and synthesis.
|
|
12013
|
+
* Each step builds on prior step decisions via the decision store.
|
|
12014
|
+
*
|
|
12015
|
+
* On success, a 'research-findings' artifact is registered and research decisions
|
|
12016
|
+
* are available to subsequent phases via `decision:research.*`.
|
|
12017
|
+
*
|
|
12018
|
+
* @param deps - Shared phase dependencies (db, pack, contextCompiler, dispatcher)
|
|
12019
|
+
* @param params - Phase parameters (runId, concept)
|
|
12020
|
+
* @returns ResearchResult with success/failure status and token usage
|
|
11563
12021
|
*/
|
|
11564
|
-
async function
|
|
11565
|
-
const { runId
|
|
11566
|
-
const
|
|
11567
|
-
|
|
11568
|
-
|
|
11569
|
-
verdict: "NO_PIPELINE_RUNNING",
|
|
11570
|
-
run_id: null,
|
|
11571
|
-
status: null,
|
|
11572
|
-
current_phase: null,
|
|
11573
|
-
staleness_seconds: 0,
|
|
11574
|
-
last_activity: "",
|
|
11575
|
-
process: {
|
|
11576
|
-
orchestrator_pid: null,
|
|
11577
|
-
child_pids: [],
|
|
11578
|
-
zombies: []
|
|
11579
|
-
},
|
|
11580
|
-
stories: {
|
|
11581
|
-
active: 0,
|
|
11582
|
-
completed: 0,
|
|
11583
|
-
escalated: 0,
|
|
11584
|
-
details: {}
|
|
11585
|
-
}
|
|
12022
|
+
async function runResearchPhase(deps, params) {
|
|
12023
|
+
const { runId } = params;
|
|
12024
|
+
const zeroTokenUsage = {
|
|
12025
|
+
input: 0,
|
|
12026
|
+
output: 0
|
|
11586
12027
|
};
|
|
11587
|
-
if (!existsSync(dbPath)) return NO_PIPELINE;
|
|
11588
|
-
const dbWrapper = new DatabaseWrapper(dbPath);
|
|
11589
12028
|
try {
|
|
11590
|
-
|
|
11591
|
-
const
|
|
11592
|
-
|
|
11593
|
-
|
|
11594
|
-
|
|
11595
|
-
|
|
11596
|
-
|
|
11597
|
-
const stalenessSeconds = Math.round((Date.now() - updatedAt.getTime()) / 1e3);
|
|
11598
|
-
let storyDetails = {};
|
|
11599
|
-
let active = 0;
|
|
11600
|
-
let completed = 0;
|
|
11601
|
-
let escalated = 0;
|
|
11602
|
-
try {
|
|
11603
|
-
if (run.token_usage_json) {
|
|
11604
|
-
const state = JSON.parse(run.token_usage_json);
|
|
11605
|
-
if (state.stories) for (const [key, s] of Object.entries(state.stories)) {
|
|
11606
|
-
storyDetails[key] = {
|
|
11607
|
-
phase: s.phase,
|
|
11608
|
-
review_cycles: s.reviewCycles
|
|
11609
|
-
};
|
|
11610
|
-
if (s.phase === "COMPLETE") completed++;
|
|
11611
|
-
else if (s.phase === "ESCALATED") escalated++;
|
|
11612
|
-
else if (s.phase !== "PENDING") active++;
|
|
11613
|
-
}
|
|
11614
|
-
}
|
|
11615
|
-
} catch {}
|
|
11616
|
-
const processInfo = inspectProcessTree({ projectRoot });
|
|
11617
|
-
let verdict = "NO_PIPELINE_RUNNING";
|
|
11618
|
-
if (run.status === "running") if (processInfo.orchestrator_pid === null && active === 0 && completed > 0) verdict = "NO_PIPELINE_RUNNING";
|
|
11619
|
-
else if (processInfo.zombies.length > 0) verdict = "STALLED";
|
|
11620
|
-
else if (processInfo.orchestrator_pid !== null && processInfo.child_pids.length > 0 && stalenessSeconds > DEFAULT_STALL_THRESHOLD_SECONDS) verdict = "HEALTHY";
|
|
11621
|
-
else if (stalenessSeconds > DEFAULT_STALL_THRESHOLD_SECONDS) verdict = "STALLED";
|
|
11622
|
-
else if (processInfo.orchestrator_pid !== null && processInfo.child_pids.length === 0 && active > 0) verdict = "STALLED";
|
|
11623
|
-
else verdict = "HEALTHY";
|
|
11624
|
-
else if (run.status === "completed" || run.status === "failed" || run.status === "stopped") verdict = "NO_PIPELINE_RUNNING";
|
|
11625
|
-
return {
|
|
11626
|
-
verdict,
|
|
11627
|
-
run_id: run.id,
|
|
11628
|
-
status: run.status,
|
|
11629
|
-
current_phase: run.current_phase,
|
|
11630
|
-
staleness_seconds: stalenessSeconds,
|
|
11631
|
-
last_activity: run.updated_at,
|
|
11632
|
-
process: processInfo,
|
|
11633
|
-
stories: {
|
|
11634
|
-
active,
|
|
11635
|
-
completed,
|
|
11636
|
-
escalated,
|
|
11637
|
-
details: storyDetails
|
|
11638
|
-
}
|
|
12029
|
+
const steps = buildResearchSteps();
|
|
12030
|
+
const result = await runSteps(steps, deps, runId, "research", { concept: params.concept });
|
|
12031
|
+
if (!result.success) return {
|
|
12032
|
+
result: "failed",
|
|
12033
|
+
error: result.error ?? "research_multi_step_failed",
|
|
12034
|
+
details: result.error ?? "Research multi-step execution failed",
|
|
12035
|
+
tokenUsage: result.tokenUsage
|
|
11639
12036
|
};
|
|
11640
|
-
|
|
11641
|
-
|
|
11642
|
-
|
|
11643
|
-
|
|
11644
|
-
|
|
11645
|
-
|
|
11646
|
-
|
|
11647
|
-
|
|
11648
|
-
|
|
11649
|
-
|
|
11650
|
-
|
|
11651
|
-
|
|
11652
|
-
|
|
11653
|
-
|
|
11654
|
-
|
|
11655
|
-
process.stdout.write(` Run: ${health.run_id}\n`);
|
|
11656
|
-
process.stdout.write(` Status: ${health.status}\n`);
|
|
11657
|
-
process.stdout.write(` Phase: ${health.current_phase ?? "N/A"}\n`);
|
|
11658
|
-
process.stdout.write(` Last Active: ${health.last_activity} (${health.staleness_seconds}s ago)\n`);
|
|
11659
|
-
const processInfo = health.process;
|
|
11660
|
-
if (processInfo.orchestrator_pid !== null) {
|
|
11661
|
-
process.stdout.write(` Orchestrator: PID ${processInfo.orchestrator_pid}\n`);
|
|
11662
|
-
process.stdout.write(` Children: ${processInfo.child_pids.length} active`);
|
|
11663
|
-
if (processInfo.zombies.length > 0) process.stdout.write(` (${processInfo.zombies.length} ZOMBIE)`);
|
|
11664
|
-
process.stdout.write("\n");
|
|
11665
|
-
} else process.stdout.write(" Orchestrator: not running\n");
|
|
11666
|
-
const storyDetails = health.stories.details;
|
|
11667
|
-
if (Object.keys(storyDetails).length > 0) {
|
|
11668
|
-
process.stdout.write("\n Stories:\n");
|
|
11669
|
-
for (const [key, s] of Object.entries(storyDetails)) process.stdout.write(` ${key}: ${s.phase} (${s.review_cycles} review cycles)\n`);
|
|
11670
|
-
process.stdout.write(`\n Summary: ${health.stories.active} active, ${health.stories.completed} completed, ${health.stories.escalated} escalated\n`);
|
|
11671
|
-
}
|
|
11672
|
-
}
|
|
12037
|
+
const lastStep = result.steps[result.steps.length - 1];
|
|
12038
|
+
const artifactId = lastStep?.artifactId;
|
|
12039
|
+
if (!artifactId) {
|
|
12040
|
+
const artifact = registerArtifact(deps.db, {
|
|
12041
|
+
pipeline_run_id: runId,
|
|
12042
|
+
phase: "research",
|
|
12043
|
+
type: "research-findings",
|
|
12044
|
+
path: "decision-store://research/research-findings",
|
|
12045
|
+
summary: "Research phase completed"
|
|
12046
|
+
});
|
|
12047
|
+
return {
|
|
12048
|
+
result: "success",
|
|
12049
|
+
artifact_id: artifact.id,
|
|
12050
|
+
tokenUsage: result.tokenUsage
|
|
12051
|
+
};
|
|
11673
12052
|
}
|
|
11674
|
-
return
|
|
12053
|
+
return {
|
|
12054
|
+
result: "success",
|
|
12055
|
+
artifact_id: artifactId,
|
|
12056
|
+
tokenUsage: result.tokenUsage
|
|
12057
|
+
};
|
|
11675
12058
|
} catch (err) {
|
|
11676
|
-
const
|
|
11677
|
-
|
|
11678
|
-
|
|
11679
|
-
|
|
11680
|
-
|
|
12059
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
12060
|
+
return {
|
|
12061
|
+
result: "failed",
|
|
12062
|
+
error: message,
|
|
12063
|
+
tokenUsage: zeroTokenUsage
|
|
12064
|
+
};
|
|
11681
12065
|
}
|
|
11682
12066
|
}
|
|
11683
|
-
function registerHealthCommand(program, _version = "0.0.0", projectRoot = process.cwd()) {
|
|
11684
|
-
program.command("health").description("Check pipeline health: process status, stall detection, and verdict").option("--run-id <id>", "Pipeline run ID to query (defaults to latest)").option("--project-root <path>", "Project root directory", projectRoot).option("--output-format <format>", "Output format: human (default) or json", "human").action(async (opts) => {
|
|
11685
|
-
const outputFormat = opts.outputFormat === "json" ? "json" : "human";
|
|
11686
|
-
const exitCode = await runHealthAction({
|
|
11687
|
-
outputFormat,
|
|
11688
|
-
runId: opts.runId,
|
|
11689
|
-
projectRoot: opts.projectRoot
|
|
11690
|
-
});
|
|
11691
|
-
process.exitCode = exitCode;
|
|
11692
|
-
});
|
|
11693
|
-
}
|
|
11694
12067
|
|
|
11695
12068
|
//#endregion
|
|
11696
12069
|
//#region src/cli/commands/run.ts
|
|
@@ -11755,6 +12128,25 @@ async function runRunAction(options) {
|
|
|
11755
12128
|
const dbRoot = await resolveMainRepoRoot(projectRoot);
|
|
11756
12129
|
const dbDir = join(dbRoot, ".substrate");
|
|
11757
12130
|
const dbPath = join(dbDir, "substrate.db");
|
|
12131
|
+
mkdirSync(dbDir, { recursive: true });
|
|
12132
|
+
const pidFilePath = join(dbDir, "orchestrator.pid");
|
|
12133
|
+
try {
|
|
12134
|
+
writeFileSync(pidFilePath, String(process.pid), "utf-8");
|
|
12135
|
+
const cleanupPidFile = () => {
|
|
12136
|
+
try {
|
|
12137
|
+
unlinkSync(pidFilePath);
|
|
12138
|
+
} catch {}
|
|
12139
|
+
};
|
|
12140
|
+
process.on("exit", cleanupPidFile);
|
|
12141
|
+
process.once("SIGTERM", () => {
|
|
12142
|
+
cleanupPidFile();
|
|
12143
|
+
process.exit(0);
|
|
12144
|
+
});
|
|
12145
|
+
process.once("SIGINT", () => {
|
|
12146
|
+
cleanupPidFile();
|
|
12147
|
+
process.exit(130);
|
|
12148
|
+
});
|
|
12149
|
+
} catch {}
|
|
11758
12150
|
if (startPhase !== void 0) return runFullPipeline({
|
|
11759
12151
|
packName,
|
|
11760
12152
|
packPath,
|
|
@@ -12110,7 +12502,32 @@ async function runRunAction(options) {
|
|
|
12110
12502
|
story_key: payload.storyKey,
|
|
12111
12503
|
phase: payload.phase,
|
|
12112
12504
|
elapsed_ms: payload.elapsedMs,
|
|
12113
|
-
|
|
12505
|
+
child_pids: payload.childPids,
|
|
12506
|
+
child_active: payload.childActive
|
|
12507
|
+
});
|
|
12508
|
+
});
|
|
12509
|
+
eventBus.on("orchestrator:zero-diff-escalation", (payload) => {
|
|
12510
|
+
ndjsonEmitter.emit({
|
|
12511
|
+
type: "story:zero-diff-escalation",
|
|
12512
|
+
ts: new Date().toISOString(),
|
|
12513
|
+
storyKey: payload.storyKey,
|
|
12514
|
+
reason: payload.reason
|
|
12515
|
+
});
|
|
12516
|
+
});
|
|
12517
|
+
eventBus.on("story:build-verification-passed", (payload) => {
|
|
12518
|
+
ndjsonEmitter.emit({
|
|
12519
|
+
type: "story:build-verification-passed",
|
|
12520
|
+
ts: new Date().toISOString(),
|
|
12521
|
+
storyKey: payload.storyKey
|
|
12522
|
+
});
|
|
12523
|
+
});
|
|
12524
|
+
eventBus.on("story:build-verification-failed", (payload) => {
|
|
12525
|
+
ndjsonEmitter.emit({
|
|
12526
|
+
type: "story:build-verification-failed",
|
|
12527
|
+
ts: new Date().toISOString(),
|
|
12528
|
+
storyKey: payload.storyKey,
|
|
12529
|
+
exitCode: payload.exitCode,
|
|
12530
|
+
output: payload.output
|
|
12114
12531
|
});
|
|
12115
12532
|
});
|
|
12116
12533
|
}
|
|
@@ -12567,4 +12984,4 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
|
|
|
12567
12984
|
|
|
12568
12985
|
//#endregion
|
|
12569
12986
|
export { DatabaseWrapper, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, buildPipelineStatusOutput, createContextCompiler, createDispatcher, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, runAnalysisPhase, runMigrations, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
|
|
12570
|
-
//# sourceMappingURL=run-
|
|
12987
|
+
//# sourceMappingURL=run-CEtHPG4I.js.map
|