substrate-ai 0.1.19 → 0.1.21
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 +492 -25
- package/dist/index.d.ts +49 -1
- package/package.json +2 -1
package/dist/cli/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { AdapterRegistry, ConfigError, ConfigIncompatibleFormatError, DatabaseWr
|
|
|
3
3
|
import { CURRENT_CONFIG_FORMAT_VERSION, CURRENT_TASK_GRAPH_VERSION, PartialSubstrateConfigSchema, SUPPORTED_CONFIG_FORMAT_VERSIONS, SubstrateConfigSchema } from "../config-schema-C9tTMcm1.js";
|
|
4
4
|
import { defaultConfigMigrator } from "../version-manager-impl-O25ieEjS.js";
|
|
5
5
|
import { registerUpgradeCommand } from "../upgrade-CHhsJc_q.js";
|
|
6
|
+
import { createRequire } from "module";
|
|
6
7
|
import { Command } from "commander";
|
|
7
8
|
import { fileURLToPath } from "url";
|
|
8
9
|
import { dirname, extname, isAbsolute, join, relative, resolve } from "path";
|
|
@@ -23,8 +24,12 @@ import * as readline$1 from "readline";
|
|
|
23
24
|
import * as readline from "readline";
|
|
24
25
|
import { createInterface as createInterface$1 } from "readline";
|
|
25
26
|
import { randomUUID as randomUUID$1 } from "crypto";
|
|
26
|
-
import { createRequire } from "node:module";
|
|
27
|
+
import { createRequire as createRequire$1 } from "node:module";
|
|
27
28
|
|
|
29
|
+
//#region rolldown:runtime
|
|
30
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
28
33
|
//#region src/cli/utils/formatting.ts
|
|
29
34
|
/**
|
|
30
35
|
* Build adapter list rows from discovery results.
|
|
@@ -6492,6 +6497,70 @@ const PIPELINE_EVENT_METADATA = [
|
|
|
6492
6497
|
description: "Log message."
|
|
6493
6498
|
}
|
|
6494
6499
|
]
|
|
6500
|
+
},
|
|
6501
|
+
{
|
|
6502
|
+
type: "pipeline:heartbeat",
|
|
6503
|
+
description: "Periodic heartbeat emitted every 30s when no other progress events have fired.",
|
|
6504
|
+
when: "Every 30 seconds during pipeline execution. Allows detection of stalled pipelines.",
|
|
6505
|
+
fields: [
|
|
6506
|
+
{
|
|
6507
|
+
name: "ts",
|
|
6508
|
+
type: "string",
|
|
6509
|
+
description: "ISO-8601 timestamp generated at emit time."
|
|
6510
|
+
},
|
|
6511
|
+
{
|
|
6512
|
+
name: "run_id",
|
|
6513
|
+
type: "string",
|
|
6514
|
+
description: "Pipeline run ID."
|
|
6515
|
+
},
|
|
6516
|
+
{
|
|
6517
|
+
name: "active_dispatches",
|
|
6518
|
+
type: "number",
|
|
6519
|
+
description: "Number of sub-agents currently running."
|
|
6520
|
+
},
|
|
6521
|
+
{
|
|
6522
|
+
name: "completed_dispatches",
|
|
6523
|
+
type: "number",
|
|
6524
|
+
description: "Number of dispatches completed."
|
|
6525
|
+
},
|
|
6526
|
+
{
|
|
6527
|
+
name: "queued_dispatches",
|
|
6528
|
+
type: "number",
|
|
6529
|
+
description: "Number of dispatches waiting to start."
|
|
6530
|
+
}
|
|
6531
|
+
]
|
|
6532
|
+
},
|
|
6533
|
+
{
|
|
6534
|
+
type: "story:stall",
|
|
6535
|
+
description: "Emitted when the watchdog detects no progress for an extended period (default: 10 minutes).",
|
|
6536
|
+
when: "When a story has shown no progress for longer than the watchdog timeout. Indicates likely stall.",
|
|
6537
|
+
fields: [
|
|
6538
|
+
{
|
|
6539
|
+
name: "ts",
|
|
6540
|
+
type: "string",
|
|
6541
|
+
description: "ISO-8601 timestamp generated at emit time."
|
|
6542
|
+
},
|
|
6543
|
+
{
|
|
6544
|
+
name: "run_id",
|
|
6545
|
+
type: "string",
|
|
6546
|
+
description: "Pipeline run ID."
|
|
6547
|
+
},
|
|
6548
|
+
{
|
|
6549
|
+
name: "story_key",
|
|
6550
|
+
type: "string",
|
|
6551
|
+
description: "Story key that appears stalled."
|
|
6552
|
+
},
|
|
6553
|
+
{
|
|
6554
|
+
name: "phase",
|
|
6555
|
+
type: "string",
|
|
6556
|
+
description: "Phase the story was in when stall was detected."
|
|
6557
|
+
},
|
|
6558
|
+
{
|
|
6559
|
+
name: "elapsed_ms",
|
|
6560
|
+
type: "number",
|
|
6561
|
+
description: "Milliseconds since last progress event."
|
|
6562
|
+
}
|
|
6563
|
+
]
|
|
6495
6564
|
}
|
|
6496
6565
|
];
|
|
6497
6566
|
/**
|
|
@@ -7214,6 +7283,13 @@ const DEFAULT_TIMEOUTS = {
|
|
|
7214
7283
|
* Only defined for task types that benefit from explicit turn budgets.
|
|
7215
7284
|
*/
|
|
7216
7285
|
const DEFAULT_MAX_TURNS = {
|
|
7286
|
+
"analysis": 15,
|
|
7287
|
+
"planning": 20,
|
|
7288
|
+
"architecture": 25,
|
|
7289
|
+
"story-generation": 30,
|
|
7290
|
+
"readiness-check": 20,
|
|
7291
|
+
"elicitation": 15,
|
|
7292
|
+
"critique": 15,
|
|
7217
7293
|
"dev-story": 75,
|
|
7218
7294
|
"major-rework": 50,
|
|
7219
7295
|
"code-review": 25,
|
|
@@ -7900,6 +7976,23 @@ function createDecision(db, input) {
|
|
|
7900
7976
|
return row;
|
|
7901
7977
|
}
|
|
7902
7978
|
/**
|
|
7979
|
+
* Insert or update a decision record.
|
|
7980
|
+
* If a decision with the same pipeline_run_id, category, and key already exists,
|
|
7981
|
+
* update its value and rationale. Otherwise, insert a new record.
|
|
7982
|
+
*/
|
|
7983
|
+
function upsertDecision(db, input) {
|
|
7984
|
+
const validated = CreateDecisionInputSchema.parse(input);
|
|
7985
|
+
const existing = db.prepare("SELECT * FROM decisions WHERE pipeline_run_id = ? AND category = ? AND key = ? LIMIT 1").get(validated.pipeline_run_id ?? null, validated.category, validated.key);
|
|
7986
|
+
if (existing) {
|
|
7987
|
+
updateDecision(db, existing.id, {
|
|
7988
|
+
value: validated.value,
|
|
7989
|
+
rationale: validated.rationale ?? void 0
|
|
7990
|
+
});
|
|
7991
|
+
return db.prepare("SELECT * FROM decisions WHERE id = ?").get(existing.id);
|
|
7992
|
+
}
|
|
7993
|
+
return createDecision(db, input);
|
|
7994
|
+
}
|
|
7995
|
+
/**
|
|
7903
7996
|
* Get all decisions for a given phase, ordered by created_at ascending.
|
|
7904
7997
|
*/
|
|
7905
7998
|
function getDecisionsByPhase(db, phase) {
|
|
@@ -7915,6 +8008,26 @@ function getDecisionsByPhaseForRun(db, runId, phase) {
|
|
|
7915
8008
|
return stmt.all(runId, phase);
|
|
7916
8009
|
}
|
|
7917
8010
|
/**
|
|
8011
|
+
* Update a decision's value and/or rationale and set updated_at.
|
|
8012
|
+
*/
|
|
8013
|
+
function updateDecision(db, id, updates) {
|
|
8014
|
+
const setClauses = [];
|
|
8015
|
+
const values = [];
|
|
8016
|
+
if (updates.value !== void 0) {
|
|
8017
|
+
setClauses.push("value = ?");
|
|
8018
|
+
values.push(updates.value);
|
|
8019
|
+
}
|
|
8020
|
+
if (updates.rationale !== void 0) {
|
|
8021
|
+
setClauses.push("rationale = ?");
|
|
8022
|
+
values.push(updates.rationale);
|
|
8023
|
+
}
|
|
8024
|
+
if (setClauses.length === 0) return;
|
|
8025
|
+
setClauses.push("updated_at = datetime('now')");
|
|
8026
|
+
values.push(id);
|
|
8027
|
+
const stmt = db.prepare(`UPDATE decisions SET ${setClauses.join(", ")} WHERE id = ?`);
|
|
8028
|
+
stmt.run(...values);
|
|
8029
|
+
}
|
|
8030
|
+
/**
|
|
7918
8031
|
* Insert a new requirement with status = 'active'.
|
|
7919
8032
|
*/
|
|
7920
8033
|
function createRequirement(db, input) {
|
|
@@ -10022,6 +10135,10 @@ function createImplementationOrchestrator(deps) {
|
|
|
10022
10135
|
const _stories = new Map();
|
|
10023
10136
|
let _paused = false;
|
|
10024
10137
|
let _pauseGate = null;
|
|
10138
|
+
let _lastProgressTs = Date.now();
|
|
10139
|
+
let _heartbeatTimer = null;
|
|
10140
|
+
const HEARTBEAT_INTERVAL_MS = 3e4;
|
|
10141
|
+
const WATCHDOG_TIMEOUT_MS = 6e5;
|
|
10025
10142
|
function getStatus() {
|
|
10026
10143
|
const stories = {};
|
|
10027
10144
|
for (const [key, s] of _stories) stories[key] = { ...s };
|
|
@@ -10043,6 +10160,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10043
10160
|
}
|
|
10044
10161
|
function persistState() {
|
|
10045
10162
|
if (config.pipelineRunId === void 0) return;
|
|
10163
|
+
recordProgress();
|
|
10046
10164
|
try {
|
|
10047
10165
|
const serialized = JSON.stringify(getStatus());
|
|
10048
10166
|
updatePipelineRun(db, config.pipelineRunId, {
|
|
@@ -10053,6 +10171,50 @@ function createImplementationOrchestrator(deps) {
|
|
|
10053
10171
|
logger$31.warn("Failed to persist orchestrator state", { err });
|
|
10054
10172
|
}
|
|
10055
10173
|
}
|
|
10174
|
+
function recordProgress() {
|
|
10175
|
+
_lastProgressTs = Date.now();
|
|
10176
|
+
}
|
|
10177
|
+
function startHeartbeat() {
|
|
10178
|
+
if (_heartbeatTimer !== null) return;
|
|
10179
|
+
_heartbeatTimer = setInterval(() => {
|
|
10180
|
+
if (_state !== "RUNNING") return;
|
|
10181
|
+
let active = 0;
|
|
10182
|
+
let completed = 0;
|
|
10183
|
+
let queued = 0;
|
|
10184
|
+
for (const s of _stories.values()) if (s.phase === "COMPLETE" || s.phase === "ESCALATED") completed++;
|
|
10185
|
+
else if (s.phase === "PENDING") queued++;
|
|
10186
|
+
else active++;
|
|
10187
|
+
eventBus.emit("orchestrator:heartbeat", {
|
|
10188
|
+
runId: config.pipelineRunId ?? "",
|
|
10189
|
+
activeDispatches: active,
|
|
10190
|
+
completedDispatches: completed,
|
|
10191
|
+
queuedDispatches: queued
|
|
10192
|
+
});
|
|
10193
|
+
const elapsed = Date.now() - _lastProgressTs;
|
|
10194
|
+
if (elapsed >= WATCHDOG_TIMEOUT_MS) {
|
|
10195
|
+
for (const [key, s] of _stories) if (s.phase !== "PENDING" && s.phase !== "COMPLETE" && s.phase !== "ESCALATED") {
|
|
10196
|
+
logger$31.warn({
|
|
10197
|
+
storyKey: key,
|
|
10198
|
+
phase: s.phase,
|
|
10199
|
+
elapsedMs: elapsed
|
|
10200
|
+
}, "Watchdog: possible stall detected");
|
|
10201
|
+
eventBus.emit("orchestrator:stall", {
|
|
10202
|
+
runId: config.pipelineRunId ?? "",
|
|
10203
|
+
storyKey: key,
|
|
10204
|
+
phase: s.phase,
|
|
10205
|
+
elapsedMs: elapsed
|
|
10206
|
+
});
|
|
10207
|
+
}
|
|
10208
|
+
}
|
|
10209
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
10210
|
+
if (_heartbeatTimer && typeof _heartbeatTimer === "object" && "unref" in _heartbeatTimer) _heartbeatTimer.unref();
|
|
10211
|
+
}
|
|
10212
|
+
function stopHeartbeat() {
|
|
10213
|
+
if (_heartbeatTimer !== null) {
|
|
10214
|
+
clearInterval(_heartbeatTimer);
|
|
10215
|
+
_heartbeatTimer = null;
|
|
10216
|
+
}
|
|
10217
|
+
}
|
|
10056
10218
|
/**
|
|
10057
10219
|
* Wait until the orchestrator is un-paused (if currently paused).
|
|
10058
10220
|
*/
|
|
@@ -10770,6 +10932,8 @@ function createImplementationOrchestrator(deps) {
|
|
|
10770
10932
|
pipelineRunId: config.pipelineRunId
|
|
10771
10933
|
});
|
|
10772
10934
|
persistState();
|
|
10935
|
+
recordProgress();
|
|
10936
|
+
startHeartbeat();
|
|
10773
10937
|
if (projectRoot !== void 0) {
|
|
10774
10938
|
const seedResult = seedMethodologyContext(db, projectRoot);
|
|
10775
10939
|
if (seedResult.decisionsCreated > 0) logger$31.info({
|
|
@@ -10786,12 +10950,14 @@ function createImplementationOrchestrator(deps) {
|
|
|
10786
10950
|
try {
|
|
10787
10951
|
await runWithConcurrency(groups, config.maxConcurrency);
|
|
10788
10952
|
} catch (err) {
|
|
10953
|
+
stopHeartbeat();
|
|
10789
10954
|
_state = "FAILED";
|
|
10790
10955
|
_completedAt = new Date().toISOString();
|
|
10791
10956
|
persistState();
|
|
10792
10957
|
logger$31.error("Orchestrator failed with unhandled error", { err });
|
|
10793
10958
|
return getStatus();
|
|
10794
10959
|
}
|
|
10960
|
+
stopHeartbeat();
|
|
10795
10961
|
_state = "COMPLETE";
|
|
10796
10962
|
_completedAt = new Date().toISOString();
|
|
10797
10963
|
let completed = 0;
|
|
@@ -11835,12 +12001,14 @@ function createQualityGate(config) {
|
|
|
11835
12001
|
|
|
11836
12002
|
//#endregion
|
|
11837
12003
|
//#region src/modules/phase-orchestrator/phases/solutioning.ts
|
|
11838
|
-
/**
|
|
11839
|
-
const
|
|
11840
|
-
|
|
11841
|
-
|
|
11842
|
-
|
|
11843
|
-
const
|
|
12004
|
+
/** Base token budget for architecture generation (covers template + requirements) */
|
|
12005
|
+
const BASE_ARCH_PROMPT_TOKENS = 3e3;
|
|
12006
|
+
/** Base token budget for story generation (covers template + requirements + architecture) */
|
|
12007
|
+
const BASE_STORY_PROMPT_TOKENS = 4e3;
|
|
12008
|
+
/** Additional tokens per architecture decision injected into story generation prompt */
|
|
12009
|
+
const TOKENS_PER_DECISION = 100;
|
|
12010
|
+
/** Absolute maximum prompt tokens (model context safety margin) */
|
|
12011
|
+
const ABSOLUTE_MAX_PROMPT_TOKENS = 12e3;
|
|
11844
12012
|
/** Placeholder in architecture prompt template */
|
|
11845
12013
|
const REQUIREMENTS_PLACEHOLDER = "{{requirements}}";
|
|
11846
12014
|
/** Amendment context framing block prefix */
|
|
@@ -11854,6 +12022,58 @@ const STORY_REQUIREMENTS_PLACEHOLDER = "{{requirements}}";
|
|
|
11854
12022
|
const STORY_ARCHITECTURE_PLACEHOLDER = "{{architecture_decisions}}";
|
|
11855
12023
|
/** Gap analysis placeholder used in retry prompt */
|
|
11856
12024
|
const GAP_ANALYSIS_PLACEHOLDER = "{{gap_analysis}}";
|
|
12025
|
+
/** Priority order for decision categories when summarizing (higher priority kept first) */
|
|
12026
|
+
const DECISION_CATEGORY_PRIORITY = [
|
|
12027
|
+
"data",
|
|
12028
|
+
"auth",
|
|
12029
|
+
"api",
|
|
12030
|
+
"frontend",
|
|
12031
|
+
"infra",
|
|
12032
|
+
"observability",
|
|
12033
|
+
"ci"
|
|
12034
|
+
];
|
|
12035
|
+
/**
|
|
12036
|
+
* Calculate the dynamic prompt token budget based on the number of decisions
|
|
12037
|
+
* that will be injected into the prompt.
|
|
12038
|
+
*
|
|
12039
|
+
* @param baseBudget - Base token budget for the phase
|
|
12040
|
+
* @param decisionCount - Number of decisions to inject
|
|
12041
|
+
* @returns Calculated token budget, capped at ABSOLUTE_MAX_PROMPT_TOKENS
|
|
12042
|
+
*/
|
|
12043
|
+
function calculateDynamicBudget(baseBudget, decisionCount) {
|
|
12044
|
+
const budget = baseBudget + decisionCount * TOKENS_PER_DECISION;
|
|
12045
|
+
return Math.min(budget, ABSOLUTE_MAX_PROMPT_TOKENS);
|
|
12046
|
+
}
|
|
12047
|
+
/**
|
|
12048
|
+
* Summarize architecture decisions into compact key:value one-liners,
|
|
12049
|
+
* dropping rationale and optionally dropping lower-priority categories
|
|
12050
|
+
* to fit within a character budget.
|
|
12051
|
+
*
|
|
12052
|
+
* @param decisions - Full architecture decisions from the decision store
|
|
12053
|
+
* @param maxChars - Maximum character budget for the summarized output
|
|
12054
|
+
* @returns Compact summary string
|
|
12055
|
+
*/
|
|
12056
|
+
function summarizeDecisions(decisions, maxChars) {
|
|
12057
|
+
const sorted = [...decisions].sort((a, b) => {
|
|
12058
|
+
const aCat = (a.category ?? "").toLowerCase();
|
|
12059
|
+
const bCat = (b.category ?? "").toLowerCase();
|
|
12060
|
+
const aIdx = DECISION_CATEGORY_PRIORITY.indexOf(aCat);
|
|
12061
|
+
const bIdx = DECISION_CATEGORY_PRIORITY.indexOf(bCat);
|
|
12062
|
+
const aPri = aIdx === -1 ? DECISION_CATEGORY_PRIORITY.length : aIdx;
|
|
12063
|
+
const bPri = bIdx === -1 ? DECISION_CATEGORY_PRIORITY.length : bIdx;
|
|
12064
|
+
return aPri - bPri;
|
|
12065
|
+
});
|
|
12066
|
+
const lines = ["## Architecture Decisions (Summarized)"];
|
|
12067
|
+
let currentLength = lines[0].length;
|
|
12068
|
+
for (const d of sorted) {
|
|
12069
|
+
const truncatedValue = d.value.length > 120 ? d.value.slice(0, 117) + "..." : d.value;
|
|
12070
|
+
const line = `- ${d.key}: ${truncatedValue}`;
|
|
12071
|
+
if (currentLength + line.length + 1 > maxChars) break;
|
|
12072
|
+
lines.push(line);
|
|
12073
|
+
currentLength += line.length + 1;
|
|
12074
|
+
}
|
|
12075
|
+
return lines.join("\n");
|
|
12076
|
+
}
|
|
11857
12077
|
/**
|
|
11858
12078
|
* Format functional and non-functional requirements from the planning phase
|
|
11859
12079
|
* into a compact text block suitable for prompt injection.
|
|
@@ -11932,17 +12152,19 @@ async function runArchitectureGeneration(deps, params) {
|
|
|
11932
12152
|
const template = await pack.getPrompt("architecture");
|
|
11933
12153
|
const formattedRequirements = formatRequirements(db, runId);
|
|
11934
12154
|
let prompt = template.replace(REQUIREMENTS_PLACEHOLDER, formattedRequirements);
|
|
12155
|
+
const dynamicBudgetTokens = calculateDynamicBudget(BASE_ARCH_PROMPT_TOKENS, 0);
|
|
12156
|
+
const dynamicBudgetChars = dynamicBudgetTokens * 4;
|
|
11935
12157
|
if (amendmentContext !== void 0 && amendmentContext !== "") {
|
|
11936
12158
|
const framingLen = AMENDMENT_CONTEXT_HEADER.length + AMENDMENT_CONTEXT_FOOTER.length;
|
|
11937
|
-
const availableForContext =
|
|
12159
|
+
const availableForContext = dynamicBudgetChars - prompt.length - framingLen - TRUNCATED_MARKER.length;
|
|
11938
12160
|
let contextToInject = amendmentContext;
|
|
11939
12161
|
if (availableForContext <= 0) contextToInject = "";
|
|
11940
12162
|
else if (amendmentContext.length > availableForContext) contextToInject = amendmentContext.slice(0, availableForContext) + TRUNCATED_MARKER;
|
|
11941
12163
|
if (contextToInject !== "") prompt += AMENDMENT_CONTEXT_HEADER + contextToInject + AMENDMENT_CONTEXT_FOOTER;
|
|
11942
12164
|
}
|
|
11943
12165
|
const estimatedTokens = Math.ceil(prompt.length / 4);
|
|
11944
|
-
if (estimatedTokens >
|
|
11945
|
-
error: `Architecture prompt exceeds token budget: ${estimatedTokens} tokens (max ${
|
|
12166
|
+
if (estimatedTokens > dynamicBudgetTokens) return {
|
|
12167
|
+
error: `Architecture prompt exceeds token budget: ${estimatedTokens} tokens (max ${dynamicBudgetTokens})`,
|
|
11946
12168
|
tokenUsage: zeroTokenUsage
|
|
11947
12169
|
};
|
|
11948
12170
|
const handle = dispatcher.dispatch({
|
|
@@ -11974,7 +12196,7 @@ async function runArchitectureGeneration(deps, params) {
|
|
|
11974
12196
|
tokenUsage
|
|
11975
12197
|
};
|
|
11976
12198
|
const decisions = parsed.architecture_decisions;
|
|
11977
|
-
for (const decision of decisions)
|
|
12199
|
+
for (const decision of decisions) upsertDecision(db, {
|
|
11978
12200
|
pipeline_run_id: runId,
|
|
11979
12201
|
phase: "solutioning",
|
|
11980
12202
|
category: "architecture",
|
|
@@ -12017,20 +12239,34 @@ async function runStoryGeneration(deps, params, gapAnalysis) {
|
|
|
12017
12239
|
};
|
|
12018
12240
|
const template = await pack.getPrompt("story-generation");
|
|
12019
12241
|
const formattedRequirements = formatRequirements(db, runId);
|
|
12020
|
-
const
|
|
12242
|
+
const archDecisions = getDecisionsByPhaseForRun(db, runId, "solutioning").filter((d) => d.category === "architecture");
|
|
12243
|
+
const dynamicBudgetTokens = calculateDynamicBudget(BASE_STORY_PROMPT_TOKENS, archDecisions.length);
|
|
12244
|
+
const dynamicBudgetChars = dynamicBudgetTokens * 4;
|
|
12245
|
+
let formattedArchitecture = formatArchitectureDecisions(db, runId);
|
|
12021
12246
|
let prompt = template.replace(STORY_REQUIREMENTS_PLACEHOLDER, formattedRequirements).replace(STORY_ARCHITECTURE_PLACEHOLDER, formattedArchitecture);
|
|
12022
12247
|
if (gapAnalysis !== void 0) prompt = prompt.replace(GAP_ANALYSIS_PLACEHOLDER, gapAnalysis);
|
|
12023
12248
|
if (amendmentContext !== void 0 && amendmentContext !== "") {
|
|
12024
12249
|
const framingLen = AMENDMENT_CONTEXT_HEADER.length + AMENDMENT_CONTEXT_FOOTER.length;
|
|
12025
|
-
const availableForContext =
|
|
12250
|
+
const availableForContext = dynamicBudgetChars - prompt.length - framingLen - TRUNCATED_MARKER.length;
|
|
12026
12251
|
let contextToInject = amendmentContext;
|
|
12027
12252
|
if (availableForContext <= 0) contextToInject = "";
|
|
12028
12253
|
else if (amendmentContext.length > availableForContext) contextToInject = amendmentContext.slice(0, availableForContext) + TRUNCATED_MARKER;
|
|
12029
12254
|
if (contextToInject !== "") prompt += AMENDMENT_CONTEXT_HEADER + contextToInject + AMENDMENT_CONTEXT_FOOTER;
|
|
12030
12255
|
}
|
|
12031
|
-
|
|
12032
|
-
if (estimatedTokens >
|
|
12033
|
-
|
|
12256
|
+
let estimatedTokens = Math.ceil(prompt.length / 4);
|
|
12257
|
+
if (estimatedTokens > dynamicBudgetTokens) {
|
|
12258
|
+
const availableForDecisions = dynamicBudgetChars - (prompt.length - formattedArchitecture.length);
|
|
12259
|
+
formattedArchitecture = summarizeDecisions(archDecisions.map((d) => ({
|
|
12260
|
+
key: d.key,
|
|
12261
|
+
value: d.value,
|
|
12262
|
+
category: d.category
|
|
12263
|
+
})), Math.max(availableForDecisions, 200));
|
|
12264
|
+
prompt = template.replace(STORY_REQUIREMENTS_PLACEHOLDER, formattedRequirements).replace(STORY_ARCHITECTURE_PLACEHOLDER, formattedArchitecture);
|
|
12265
|
+
if (gapAnalysis !== void 0) prompt = prompt.replace(GAP_ANALYSIS_PLACEHOLDER, gapAnalysis);
|
|
12266
|
+
estimatedTokens = Math.ceil(prompt.length / 4);
|
|
12267
|
+
}
|
|
12268
|
+
if (estimatedTokens > dynamicBudgetTokens) return {
|
|
12269
|
+
error: `Story generation prompt exceeds token budget: ${estimatedTokens} tokens (max ${dynamicBudgetTokens})`,
|
|
12034
12270
|
tokenUsage: zeroTokenUsage
|
|
12035
12271
|
};
|
|
12036
12272
|
const handle = dispatcher.dispatch({
|
|
@@ -12063,7 +12299,7 @@ async function runStoryGeneration(deps, params, gapAnalysis) {
|
|
|
12063
12299
|
};
|
|
12064
12300
|
const epics = parsed.epics;
|
|
12065
12301
|
for (const [epicIndex, epic] of epics.entries()) {
|
|
12066
|
-
|
|
12302
|
+
upsertDecision(db, {
|
|
12067
12303
|
pipeline_run_id: runId,
|
|
12068
12304
|
phase: "solutioning",
|
|
12069
12305
|
category: "epics",
|
|
@@ -12073,7 +12309,7 @@ async function runStoryGeneration(deps, params, gapAnalysis) {
|
|
|
12073
12309
|
description: epic.description
|
|
12074
12310
|
})
|
|
12075
12311
|
});
|
|
12076
|
-
for (const story of epic.stories)
|
|
12312
|
+
for (const story of epic.stories) upsertDecision(db, {
|
|
12077
12313
|
pipeline_run_id: runId,
|
|
12078
12314
|
phase: "solutioning",
|
|
12079
12315
|
category: "stories",
|
|
@@ -12200,7 +12436,23 @@ async function runSolutioningPhase(deps, params) {
|
|
|
12200
12436
|
let totalInput = 0;
|
|
12201
12437
|
let totalOutput = 0;
|
|
12202
12438
|
try {
|
|
12203
|
-
const
|
|
12439
|
+
const existingArchArtifact = getArtifactByTypeForRun(deps.db, params.runId, "solutioning", "architecture");
|
|
12440
|
+
let archResult;
|
|
12441
|
+
if (existingArchArtifact) {
|
|
12442
|
+
const existingDecisions = getDecisionsByPhaseForRun(deps.db, params.runId, "solutioning").filter((d) => d.category === "architecture");
|
|
12443
|
+
archResult = {
|
|
12444
|
+
decisions: existingDecisions.map((d) => ({
|
|
12445
|
+
key: d.key,
|
|
12446
|
+
value: d.value,
|
|
12447
|
+
rationale: d.rationale ?? ""
|
|
12448
|
+
})),
|
|
12449
|
+
artifactId: existingArchArtifact.id,
|
|
12450
|
+
tokenUsage: {
|
|
12451
|
+
input: 0,
|
|
12452
|
+
output: 0
|
|
12453
|
+
}
|
|
12454
|
+
};
|
|
12455
|
+
} else archResult = await runArchitectureGeneration(deps, params);
|
|
12204
12456
|
totalInput += archResult.tokenUsage.input;
|
|
12205
12457
|
totalOutput += archResult.tokenUsage.output;
|
|
12206
12458
|
if ("error" in archResult) return {
|
|
@@ -12897,8 +13149,8 @@ const PACKAGE_ROOT = join(__dirname, "..", "..", "..");
|
|
|
12897
13149
|
*/
|
|
12898
13150
|
function resolveBmadMethodSrcPath(fromDir = __dirname) {
|
|
12899
13151
|
try {
|
|
12900
|
-
const require = createRequire(join(fromDir, "synthetic.js"));
|
|
12901
|
-
const pkgJsonPath = require.resolve("bmad-method/package.json");
|
|
13152
|
+
const require$1 = createRequire$1(join(fromDir, "synthetic.js"));
|
|
13153
|
+
const pkgJsonPath = require$1.resolve("bmad-method/package.json");
|
|
12902
13154
|
return join(dirname(pkgJsonPath), "src");
|
|
12903
13155
|
} catch {
|
|
12904
13156
|
return null;
|
|
@@ -12910,9 +13162,9 @@ function resolveBmadMethodSrcPath(fromDir = __dirname) {
|
|
|
12910
13162
|
*/
|
|
12911
13163
|
function resolveBmadMethodVersion(fromDir = __dirname) {
|
|
12912
13164
|
try {
|
|
12913
|
-
const require = createRequire(join(fromDir, "synthetic.js"));
|
|
12914
|
-
const pkgJsonPath = require.resolve("bmad-method/package.json");
|
|
12915
|
-
const pkg = require(pkgJsonPath);
|
|
13165
|
+
const require$1 = createRequire$1(join(fromDir, "synthetic.js"));
|
|
13166
|
+
const pkgJsonPath = require$1.resolve("bmad-method/package.json");
|
|
13167
|
+
const pkg = require$1(pkgJsonPath);
|
|
12916
13168
|
return pkg.version ?? "unknown";
|
|
12917
13169
|
} catch {
|
|
12918
13170
|
return "unknown";
|
|
@@ -13033,7 +13285,9 @@ function buildPipelineStatusOutput(run, tokenSummary, decisionsCount, storiesCou
|
|
|
13033
13285
|
cost_usd: totalCost
|
|
13034
13286
|
},
|
|
13035
13287
|
decisions_count: decisionsCount,
|
|
13036
|
-
stories_count: storiesCount
|
|
13288
|
+
stories_count: storiesCount,
|
|
13289
|
+
last_activity: run.updated_at,
|
|
13290
|
+
staleness_seconds: Math.round((Date.now() - new Date(run.updated_at).getTime()) / 1e3)
|
|
13037
13291
|
};
|
|
13038
13292
|
}
|
|
13039
13293
|
/**
|
|
@@ -13669,6 +13923,26 @@ async function runAutoRun(options) {
|
|
|
13669
13923
|
msg: payload.msg
|
|
13670
13924
|
});
|
|
13671
13925
|
});
|
|
13926
|
+
eventBus.on("orchestrator:heartbeat", (payload) => {
|
|
13927
|
+
ndjsonEmitter.emit({
|
|
13928
|
+
type: "pipeline:heartbeat",
|
|
13929
|
+
ts: new Date().toISOString(),
|
|
13930
|
+
run_id: payload.runId,
|
|
13931
|
+
active_dispatches: payload.activeDispatches,
|
|
13932
|
+
completed_dispatches: payload.completedDispatches,
|
|
13933
|
+
queued_dispatches: payload.queuedDispatches
|
|
13934
|
+
});
|
|
13935
|
+
});
|
|
13936
|
+
eventBus.on("orchestrator:stall", (payload) => {
|
|
13937
|
+
ndjsonEmitter.emit({
|
|
13938
|
+
type: "story:stall",
|
|
13939
|
+
ts: new Date().toISOString(),
|
|
13940
|
+
run_id: payload.runId,
|
|
13941
|
+
story_key: payload.storyKey,
|
|
13942
|
+
phase: payload.phase,
|
|
13943
|
+
elapsed_ms: payload.elapsedMs
|
|
13944
|
+
});
|
|
13945
|
+
});
|
|
13672
13946
|
}
|
|
13673
13947
|
const orchestrator = createImplementationOrchestrator({
|
|
13674
13948
|
db,
|
|
@@ -13824,6 +14098,7 @@ async function runFullPipeline(options) {
|
|
|
13824
14098
|
});
|
|
13825
14099
|
}
|
|
13826
14100
|
if (result.result === "failed") {
|
|
14101
|
+
updatePipelineRun(db, runId, { status: "failed" });
|
|
13827
14102
|
const errorMsg = `Analysis phase failed: ${result.error ?? "unknown error"}${result.details ? ` — ${result.details}` : ""}`;
|
|
13828
14103
|
if (outputFormat === "human") process.stderr.write(`Error: ${errorMsg}\n`);
|
|
13829
14104
|
else process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
@@ -13846,6 +14121,7 @@ async function runFullPipeline(options) {
|
|
|
13846
14121
|
});
|
|
13847
14122
|
}
|
|
13848
14123
|
if (result.result === "failed") {
|
|
14124
|
+
updatePipelineRun(db, runId, { status: "failed" });
|
|
13849
14125
|
const errorMsg = `Planning phase failed: ${result.error ?? "unknown error"}${result.details ? ` — ${result.details}` : ""}`;
|
|
13850
14126
|
if (outputFormat === "human") process.stderr.write(`Error: ${errorMsg}\n`);
|
|
13851
14127
|
else process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
@@ -13868,6 +14144,7 @@ async function runFullPipeline(options) {
|
|
|
13868
14144
|
});
|
|
13869
14145
|
}
|
|
13870
14146
|
if (result.result === "failed") {
|
|
14147
|
+
updatePipelineRun(db, runId, { status: "failed" });
|
|
13871
14148
|
const errorMsg = `Solutioning phase failed: ${result.error ?? "unknown error"}${result.details ? ` — ${result.details}` : ""}`;
|
|
13872
14149
|
if (outputFormat === "human") process.stderr.write(`Error: ${errorMsg}\n`);
|
|
13873
14150
|
else process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
@@ -14141,6 +14418,7 @@ async function runFullPipelineFromPhase(options) {
|
|
|
14141
14418
|
});
|
|
14142
14419
|
}
|
|
14143
14420
|
if (result.result === "failed") {
|
|
14421
|
+
updatePipelineRun(db, runId, { status: "failed" });
|
|
14144
14422
|
const errorMsg = `Analysis phase failed: ${result.error ?? "unknown error"}`;
|
|
14145
14423
|
if (outputFormat === "human") process.stderr.write(`Error: ${errorMsg}\n`);
|
|
14146
14424
|
else process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
@@ -14160,6 +14438,7 @@ async function runFullPipelineFromPhase(options) {
|
|
|
14160
14438
|
});
|
|
14161
14439
|
}
|
|
14162
14440
|
if (result.result === "failed") {
|
|
14441
|
+
updatePipelineRun(db, runId, { status: "failed" });
|
|
14163
14442
|
const errorMsg = `Planning phase failed: ${result.error ?? "unknown error"}`;
|
|
14164
14443
|
if (outputFormat === "human") process.stderr.write(`Error: ${errorMsg}\n`);
|
|
14165
14444
|
else process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
@@ -14179,6 +14458,7 @@ async function runFullPipelineFromPhase(options) {
|
|
|
14179
14458
|
});
|
|
14180
14459
|
}
|
|
14181
14460
|
if (result.result === "failed") {
|
|
14461
|
+
updatePipelineRun(db, runId, { status: "failed" });
|
|
14182
14462
|
const errorMsg = `Solutioning phase failed: ${result.error ?? "unknown error"}`;
|
|
14183
14463
|
if (outputFormat === "human") process.stderr.write(`Error: ${errorMsg}\n`);
|
|
14184
14464
|
else process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
@@ -14365,6 +14645,175 @@ async function runAutoStatus(options) {
|
|
|
14365
14645
|
} catch {}
|
|
14366
14646
|
}
|
|
14367
14647
|
}
|
|
14648
|
+
function inspectProcessTree() {
|
|
14649
|
+
const result = {
|
|
14650
|
+
orchestrator_pid: null,
|
|
14651
|
+
child_pids: [],
|
|
14652
|
+
zombies: []
|
|
14653
|
+
};
|
|
14654
|
+
try {
|
|
14655
|
+
const { execFileSync } = __require("node:child_process");
|
|
14656
|
+
const psOutput = execFileSync("ps", ["-eo", "pid,ppid,stat,command"], {
|
|
14657
|
+
encoding: "utf-8",
|
|
14658
|
+
timeout: 5e3
|
|
14659
|
+
});
|
|
14660
|
+
const lines = psOutput.split("\n");
|
|
14661
|
+
for (const line of lines) if (line.includes("substrate auto run") && !line.includes("grep")) {
|
|
14662
|
+
const match = line.trim().match(/^(\d+)/);
|
|
14663
|
+
if (match) {
|
|
14664
|
+
result.orchestrator_pid = parseInt(match[1], 10);
|
|
14665
|
+
break;
|
|
14666
|
+
}
|
|
14667
|
+
}
|
|
14668
|
+
if (result.orchestrator_pid !== null) for (const line of lines) {
|
|
14669
|
+
const parts = line.trim().split(/\s+/);
|
|
14670
|
+
if (parts.length >= 3) {
|
|
14671
|
+
const pid = parseInt(parts[0], 10);
|
|
14672
|
+
const ppid = parseInt(parts[1], 10);
|
|
14673
|
+
const stat$2 = parts[2];
|
|
14674
|
+
if (ppid === result.orchestrator_pid && pid !== result.orchestrator_pid) {
|
|
14675
|
+
result.child_pids.push(pid);
|
|
14676
|
+
if (stat$2.includes("Z")) result.zombies.push(pid);
|
|
14677
|
+
}
|
|
14678
|
+
}
|
|
14679
|
+
}
|
|
14680
|
+
} catch {}
|
|
14681
|
+
return result;
|
|
14682
|
+
}
|
|
14683
|
+
async function runAutoHealth(options) {
|
|
14684
|
+
const { outputFormat, runId, projectRoot } = options;
|
|
14685
|
+
const dbRoot = await resolveMainRepoRoot(projectRoot);
|
|
14686
|
+
const dbPath = join(dbRoot, ".substrate", "substrate.db");
|
|
14687
|
+
if (!existsSync(dbPath)) {
|
|
14688
|
+
const output = {
|
|
14689
|
+
verdict: "NO_PIPELINE_RUNNING",
|
|
14690
|
+
run_id: null,
|
|
14691
|
+
status: null,
|
|
14692
|
+
current_phase: null,
|
|
14693
|
+
staleness_seconds: 0,
|
|
14694
|
+
last_activity: "",
|
|
14695
|
+
process: {
|
|
14696
|
+
orchestrator_pid: null,
|
|
14697
|
+
child_pids: [],
|
|
14698
|
+
zombies: []
|
|
14699
|
+
},
|
|
14700
|
+
stories: {
|
|
14701
|
+
active: 0,
|
|
14702
|
+
completed: 0,
|
|
14703
|
+
escalated: 0,
|
|
14704
|
+
details: {}
|
|
14705
|
+
}
|
|
14706
|
+
};
|
|
14707
|
+
if (outputFormat === "json") process.stdout.write(formatOutput(output, "json", true) + "\n");
|
|
14708
|
+
else process.stdout.write("NO_PIPELINE_RUNNING — no substrate database found\n");
|
|
14709
|
+
return 0;
|
|
14710
|
+
}
|
|
14711
|
+
const dbWrapper = new DatabaseWrapper(dbPath);
|
|
14712
|
+
try {
|
|
14713
|
+
dbWrapper.open();
|
|
14714
|
+
const db = dbWrapper.db;
|
|
14715
|
+
let run;
|
|
14716
|
+
if (runId !== void 0) run = getPipelineRunById(db, runId);
|
|
14717
|
+
else run = getLatestRun(db);
|
|
14718
|
+
if (run === void 0) {
|
|
14719
|
+
const output$1 = {
|
|
14720
|
+
verdict: "NO_PIPELINE_RUNNING",
|
|
14721
|
+
run_id: null,
|
|
14722
|
+
status: null,
|
|
14723
|
+
current_phase: null,
|
|
14724
|
+
staleness_seconds: 0,
|
|
14725
|
+
last_activity: "",
|
|
14726
|
+
process: {
|
|
14727
|
+
orchestrator_pid: null,
|
|
14728
|
+
child_pids: [],
|
|
14729
|
+
zombies: []
|
|
14730
|
+
},
|
|
14731
|
+
stories: {
|
|
14732
|
+
active: 0,
|
|
14733
|
+
completed: 0,
|
|
14734
|
+
escalated: 0,
|
|
14735
|
+
details: {}
|
|
14736
|
+
}
|
|
14737
|
+
};
|
|
14738
|
+
if (outputFormat === "json") process.stdout.write(formatOutput(output$1, "json", true) + "\n");
|
|
14739
|
+
else process.stdout.write("NO_PIPELINE_RUNNING — no pipeline runs found\n");
|
|
14740
|
+
return 0;
|
|
14741
|
+
}
|
|
14742
|
+
const updatedAt = new Date(run.updated_at);
|
|
14743
|
+
const stalenessSeconds = Math.round((Date.now() - updatedAt.getTime()) / 1e3);
|
|
14744
|
+
let storyDetails = {};
|
|
14745
|
+
let active = 0;
|
|
14746
|
+
let completed = 0;
|
|
14747
|
+
let escalated = 0;
|
|
14748
|
+
try {
|
|
14749
|
+
if (run.token_usage_json) {
|
|
14750
|
+
const state = JSON.parse(run.token_usage_json);
|
|
14751
|
+
if (state.stories) for (const [key, s] of Object.entries(state.stories)) {
|
|
14752
|
+
storyDetails[key] = {
|
|
14753
|
+
phase: s.phase,
|
|
14754
|
+
review_cycles: s.reviewCycles
|
|
14755
|
+
};
|
|
14756
|
+
if (s.phase === "COMPLETE") completed++;
|
|
14757
|
+
else if (s.phase === "ESCALATED") escalated++;
|
|
14758
|
+
else if (s.phase !== "PENDING") active++;
|
|
14759
|
+
}
|
|
14760
|
+
}
|
|
14761
|
+
} catch {}
|
|
14762
|
+
const processInfo = inspectProcessTree();
|
|
14763
|
+
let verdict = "NO_PIPELINE_RUNNING";
|
|
14764
|
+
if (run.status === "running") if (processInfo.zombies.length > 0) verdict = "STALLED";
|
|
14765
|
+
else if (stalenessSeconds > 600) verdict = "STALLED";
|
|
14766
|
+
else if (processInfo.orchestrator_pid !== null && processInfo.child_pids.length === 0 && active > 0) verdict = "STALLED";
|
|
14767
|
+
else verdict = "HEALTHY";
|
|
14768
|
+
else if (run.status === "completed" || run.status === "failed" || run.status === "stopped") verdict = "NO_PIPELINE_RUNNING";
|
|
14769
|
+
const output = {
|
|
14770
|
+
verdict,
|
|
14771
|
+
run_id: run.id,
|
|
14772
|
+
status: run.status,
|
|
14773
|
+
current_phase: run.current_phase,
|
|
14774
|
+
staleness_seconds: stalenessSeconds,
|
|
14775
|
+
last_activity: run.updated_at,
|
|
14776
|
+
process: processInfo,
|
|
14777
|
+
stories: {
|
|
14778
|
+
active,
|
|
14779
|
+
completed,
|
|
14780
|
+
escalated,
|
|
14781
|
+
details: storyDetails
|
|
14782
|
+
}
|
|
14783
|
+
};
|
|
14784
|
+
if (outputFormat === "json") process.stdout.write(formatOutput(output, "json", true) + "\n");
|
|
14785
|
+
else {
|
|
14786
|
+
const verdictLabel = verdict === "HEALTHY" ? "HEALTHY" : verdict === "STALLED" ? "STALLED" : "NO PIPELINE RUNNING";
|
|
14787
|
+
process.stdout.write(`\nPipeline Health: ${verdictLabel}\n`);
|
|
14788
|
+
process.stdout.write(` Run: ${run.id}\n`);
|
|
14789
|
+
process.stdout.write(` Status: ${run.status}\n`);
|
|
14790
|
+
process.stdout.write(` Phase: ${run.current_phase ?? "N/A"}\n`);
|
|
14791
|
+
process.stdout.write(` Last Active: ${run.updated_at} (${stalenessSeconds}s ago)\n`);
|
|
14792
|
+
if (processInfo.orchestrator_pid !== null) {
|
|
14793
|
+
process.stdout.write(` Orchestrator: PID ${processInfo.orchestrator_pid}\n`);
|
|
14794
|
+
process.stdout.write(` Children: ${processInfo.child_pids.length} active`);
|
|
14795
|
+
if (processInfo.zombies.length > 0) process.stdout.write(` (${processInfo.zombies.length} ZOMBIE)`);
|
|
14796
|
+
process.stdout.write("\n");
|
|
14797
|
+
} else process.stdout.write(" Orchestrator: not running\n");
|
|
14798
|
+
if (Object.keys(storyDetails).length > 0) {
|
|
14799
|
+
process.stdout.write("\n Stories:\n");
|
|
14800
|
+
for (const [key, s] of Object.entries(storyDetails)) process.stdout.write(` ${key}: ${s.phase} (${s.review_cycles} review cycles)\n`);
|
|
14801
|
+
process.stdout.write(`\n Summary: ${active} active, ${completed} completed, ${escalated} escalated\n`);
|
|
14802
|
+
}
|
|
14803
|
+
}
|
|
14804
|
+
return 0;
|
|
14805
|
+
} catch (err) {
|
|
14806
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
14807
|
+
if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, msg) + "\n");
|
|
14808
|
+
else process.stderr.write(`Error: ${msg}\n`);
|
|
14809
|
+
logger$3.error({ err }, "auto health failed");
|
|
14810
|
+
return 1;
|
|
14811
|
+
} finally {
|
|
14812
|
+
try {
|
|
14813
|
+
dbWrapper.close();
|
|
14814
|
+
} catch {}
|
|
14815
|
+
}
|
|
14816
|
+
}
|
|
14368
14817
|
/**
|
|
14369
14818
|
* Detect and apply supersessions after a phase completes in an amendment run.
|
|
14370
14819
|
*
|
|
@@ -14552,6 +15001,7 @@ async function runAmendCommand(options) {
|
|
|
14552
15001
|
});
|
|
14553
15002
|
}
|
|
14554
15003
|
if (result.result === "failed") {
|
|
15004
|
+
updatePipelineRun(db, amendmentRunId, { status: "failed" });
|
|
14555
15005
|
process.stderr.write(`Error: Analysis phase failed: ${result.error ?? "unknown error"}${result.details ? ` — ${result.details}` : ""}\n`);
|
|
14556
15006
|
return 1;
|
|
14557
15007
|
}
|
|
@@ -14573,6 +15023,7 @@ async function runAmendCommand(options) {
|
|
|
14573
15023
|
});
|
|
14574
15024
|
}
|
|
14575
15025
|
if (result.result === "failed") {
|
|
15026
|
+
updatePipelineRun(db, amendmentRunId, { status: "failed" });
|
|
14576
15027
|
process.stderr.write(`Error: Planning phase failed: ${result.error ?? "unknown error"}${result.details ? ` — ${result.details}` : ""}\n`);
|
|
14577
15028
|
return 1;
|
|
14578
15029
|
}
|
|
@@ -14594,6 +15045,7 @@ async function runAmendCommand(options) {
|
|
|
14594
15045
|
});
|
|
14595
15046
|
}
|
|
14596
15047
|
if (result.result === "failed") {
|
|
15048
|
+
updatePipelineRun(db, amendmentRunId, { status: "failed" });
|
|
14597
15049
|
process.stderr.write(`Error: Solutioning phase failed: ${result.error ?? "unknown error"}${result.details ? ` — ${result.details}` : ""}\n`);
|
|
14598
15050
|
return 1;
|
|
14599
15051
|
}
|
|
@@ -14730,6 +15182,15 @@ function registerAutoCommand(program, _version = "0.0.0", projectRoot = process.
|
|
|
14730
15182
|
});
|
|
14731
15183
|
process.exitCode = exitCode;
|
|
14732
15184
|
});
|
|
15185
|
+
auto.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) => {
|
|
15186
|
+
const outputFormat = opts.outputFormat === "json" ? "json" : "human";
|
|
15187
|
+
const exitCode = await runAutoHealth({
|
|
15188
|
+
outputFormat,
|
|
15189
|
+
runId: opts.runId,
|
|
15190
|
+
projectRoot: opts.projectRoot
|
|
15191
|
+
});
|
|
15192
|
+
process.exitCode = exitCode;
|
|
15193
|
+
});
|
|
14733
15194
|
auto.command("amend").description("Run an amendment pipeline against a completed run and an existing run").option("--concept <text>", "Amendment concept description (inline)").option("--concept-file <path>", "Path to concept file").option("--run-id <id>", "Parent run ID (defaults to latest completed run)").option("--stop-after <phase>", "Stop pipeline after this phase completes").option("--from <phase>", "Start pipeline from this phase").option("--pack <name>", "Methodology pack name", "bmad").option("--project-root <path>", "Project root directory", projectRoot).option("--output-format <format>", "Output format: human (default) or json", "human").action(async (opts) => {
|
|
14734
15195
|
const exitCode = await runAmendCommand({
|
|
14735
15196
|
concept: opts.concept,
|
|
@@ -15757,6 +16218,12 @@ function registerMonitorCommand(program, version = "0.0.0", projectRoot = proces
|
|
|
15757
16218
|
//#endregion
|
|
15758
16219
|
//#region src/cli/index.ts
|
|
15759
16220
|
process.setMaxListeners(30);
|
|
16221
|
+
process.stdout.on("error", (err) => {
|
|
16222
|
+
if (err.code === "EPIPE") process.exit(0);
|
|
16223
|
+
});
|
|
16224
|
+
process.stderr.on("error", (err) => {
|
|
16225
|
+
if (err.code === "EPIPE") process.exit(0);
|
|
16226
|
+
});
|
|
15760
16227
|
const logger = createLogger("cli");
|
|
15761
16228
|
/** Resolve the package.json path relative to this file */
|
|
15762
16229
|
async function getPackageVersion() {
|
package/dist/index.d.ts
CHANGED
|
@@ -197,6 +197,40 @@ interface StoryLogEvent {
|
|
|
197
197
|
/** Log message */
|
|
198
198
|
msg: string;
|
|
199
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Emitted periodically (every 30s) when no other progress events have fired.
|
|
202
|
+
* Allows parent agents to distinguish "working silently" from "stuck".
|
|
203
|
+
*/
|
|
204
|
+
interface PipelineHeartbeatEvent {
|
|
205
|
+
type: 'pipeline:heartbeat';
|
|
206
|
+
/** ISO-8601 timestamp generated at emit time */
|
|
207
|
+
ts: string;
|
|
208
|
+
/** Unique identifier for the current pipeline run */
|
|
209
|
+
run_id: string;
|
|
210
|
+
/** Number of sub-agent dispatches currently running */
|
|
211
|
+
active_dispatches: number;
|
|
212
|
+
/** Number of dispatches that have completed */
|
|
213
|
+
completed_dispatches: number;
|
|
214
|
+
/** Number of dispatches waiting to start */
|
|
215
|
+
queued_dispatches: number;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Emitted when the watchdog timer detects no progress for an extended period.
|
|
219
|
+
* Indicates a likely stall that may require operator intervention.
|
|
220
|
+
*/
|
|
221
|
+
interface StoryStallEvent {
|
|
222
|
+
type: 'story:stall';
|
|
223
|
+
/** ISO-8601 timestamp generated at emit time */
|
|
224
|
+
ts: string;
|
|
225
|
+
/** Unique identifier for the current pipeline run */
|
|
226
|
+
run_id: string;
|
|
227
|
+
/** Story key that appears stalled */
|
|
228
|
+
story_key: string;
|
|
229
|
+
/** Phase the story was in when the stall was detected */
|
|
230
|
+
phase: string;
|
|
231
|
+
/** Milliseconds since the last progress event */
|
|
232
|
+
elapsed_ms: number;
|
|
233
|
+
}
|
|
200
234
|
/**
|
|
201
235
|
* Discriminated union of all pipeline event types.
|
|
202
236
|
*
|
|
@@ -209,7 +243,7 @@ interface StoryLogEvent {
|
|
|
209
243
|
* }
|
|
210
244
|
* ```
|
|
211
245
|
*/
|
|
212
|
-
type PipelineEvent = PipelineStartEvent | PipelineCompleteEvent | StoryPhaseEvent | StoryDoneEvent | StoryEscalationEvent | StoryWarnEvent | StoryLogEvent; //#endregion
|
|
246
|
+
type PipelineEvent = PipelineStartEvent | PipelineCompleteEvent | StoryPhaseEvent | StoryDoneEvent | StoryEscalationEvent | StoryWarnEvent | StoryLogEvent | PipelineHeartbeatEvent | StoryStallEvent; //#endregion
|
|
213
247
|
//#region src/core/errors.d.ts
|
|
214
248
|
|
|
215
249
|
/**
|
|
@@ -793,6 +827,20 @@ interface OrchestratorEvents {
|
|
|
793
827
|
'orchestrator:paused': Record<string, never>;
|
|
794
828
|
/** Implementation orchestrator has been resumed */
|
|
795
829
|
'orchestrator:resumed': Record<string, never>;
|
|
830
|
+
/** Periodic heartbeat emitted every 30s during pipeline execution */
|
|
831
|
+
'orchestrator:heartbeat': {
|
|
832
|
+
runId: string;
|
|
833
|
+
activeDispatches: number;
|
|
834
|
+
completedDispatches: number;
|
|
835
|
+
queuedDispatches: number;
|
|
836
|
+
};
|
|
837
|
+
/** Watchdog detected no progress for an extended period */
|
|
838
|
+
'orchestrator:stall': {
|
|
839
|
+
runId: string;
|
|
840
|
+
storyKey: string;
|
|
841
|
+
phase: string;
|
|
842
|
+
elapsedMs: number;
|
|
843
|
+
};
|
|
796
844
|
}
|
|
797
845
|
|
|
798
846
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "substrate-ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "Substrate — multi-agent orchestration daemon for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"js-yaml": "^4.1.1",
|
|
65
65
|
"pino": "^9.6.0",
|
|
66
66
|
"semver": "^7.6.3",
|
|
67
|
+
"substrate-ai": "^0.1.19",
|
|
67
68
|
"zod": "^4.3.6"
|
|
68
69
|
},
|
|
69
70
|
"devDependencies": {
|