substrate-ai 0.2.4 → 0.2.6
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/README.md +9 -0
- package/dist/cli/index.js +120 -130
- package/dist/index.d.ts +40 -1
- package/dist/{run-D3ZscMlL.js → run-0IlA2ubQ.js} +396 -54
- package/dist/{run-Bwyy5-RY.js → run-Chc5BzIz.js} +1 -1
- package/package.json +1 -1
- package/packs/bmad/manifest.yaml +29 -0
- package/packs/bmad/prompts/critique-research.md +92 -0
- package/packs/bmad/prompts/research-step-1-discovery.md +76 -0
- package/packs/bmad/prompts/research-step-2-synthesis.md +64 -0
|
@@ -13,6 +13,7 @@ import { dirname as dirname$1, join as join$1, resolve as resolve$1 } from "node
|
|
|
13
13
|
import BetterSqlite3 from "better-sqlite3";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { existsSync as existsSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1 } from "node:fs";
|
|
16
|
+
import { freemem } from "node:os";
|
|
16
17
|
import { randomUUID } from "node:crypto";
|
|
17
18
|
import { readFile as readFile$1, stat as stat$1 } from "node:fs/promises";
|
|
18
19
|
|
|
@@ -1024,6 +1025,21 @@ function validateStopAfterFromConflict(stopAfter, from) {
|
|
|
1024
1025
|
|
|
1025
1026
|
//#endregion
|
|
1026
1027
|
//#region src/cli/commands/pipeline-shared.ts
|
|
1028
|
+
/**
|
|
1029
|
+
* Parse a DB timestamp string to a Date, correctly treating it as UTC.
|
|
1030
|
+
*
|
|
1031
|
+
* SQLite stores timestamps as "YYYY-MM-DD HH:MM:SS" without a timezone suffix.
|
|
1032
|
+
* JavaScript's Date constructor parses strings without a timezone suffix as
|
|
1033
|
+
* *local time*, which causes staleness/duration to be calculated incorrectly
|
|
1034
|
+
* on machines not in UTC.
|
|
1035
|
+
*
|
|
1036
|
+
* Fix: append 'Z' if the string has no timezone marker so it is always
|
|
1037
|
+
* parsed as UTC.
|
|
1038
|
+
*/
|
|
1039
|
+
function parseDbTimestampAsUtc(ts) {
|
|
1040
|
+
if (ts.endsWith("Z") || /[+-]\d{2}:\d{2}$/.test(ts)) return new Date(ts);
|
|
1041
|
+
return new Date(ts.replace(" ", "T") + "Z");
|
|
1042
|
+
}
|
|
1027
1043
|
const __filename = fileURLToPath(import.meta.url);
|
|
1028
1044
|
const __dirname = dirname(__filename);
|
|
1029
1045
|
/**
|
|
@@ -1197,7 +1213,7 @@ function buildPipelineStatusOutput(run, tokenSummary, decisionsCount, storiesCou
|
|
|
1197
1213
|
decisions_count: decisionsCount,
|
|
1198
1214
|
stories_count: storiesCount,
|
|
1199
1215
|
last_activity: run.updated_at,
|
|
1200
|
-
staleness_seconds: Math.round((Date.now() -
|
|
1216
|
+
staleness_seconds: Math.round((Date.now() - parseDbTimestampAsUtc(run.updated_at).getTime()) / 1e3)
|
|
1201
1217
|
};
|
|
1202
1218
|
}
|
|
1203
1219
|
/**
|
|
@@ -1817,6 +1833,53 @@ const PIPELINE_EVENT_METADATA = [
|
|
|
1817
1833
|
}
|
|
1818
1834
|
]
|
|
1819
1835
|
},
|
|
1836
|
+
{
|
|
1837
|
+
type: "supervisor:poll",
|
|
1838
|
+
description: "Heartbeat each poll (JSON only).",
|
|
1839
|
+
when: "Per cycle.",
|
|
1840
|
+
fields: [
|
|
1841
|
+
{
|
|
1842
|
+
name: "ts",
|
|
1843
|
+
type: "string",
|
|
1844
|
+
description: "Timestamp."
|
|
1845
|
+
},
|
|
1846
|
+
{
|
|
1847
|
+
name: "run_id",
|
|
1848
|
+
type: "string|null",
|
|
1849
|
+
description: "Run ID."
|
|
1850
|
+
},
|
|
1851
|
+
{
|
|
1852
|
+
name: "verdict",
|
|
1853
|
+
type: "HEALTHY|STALLED|NO_PIPELINE_RUNNING",
|
|
1854
|
+
description: "Verdict."
|
|
1855
|
+
},
|
|
1856
|
+
{
|
|
1857
|
+
name: "staleness_seconds",
|
|
1858
|
+
type: "number",
|
|
1859
|
+
description: "Seconds stale."
|
|
1860
|
+
},
|
|
1861
|
+
{
|
|
1862
|
+
name: "stories",
|
|
1863
|
+
type: "object",
|
|
1864
|
+
description: "active/completed/escalated."
|
|
1865
|
+
},
|
|
1866
|
+
{
|
|
1867
|
+
name: "story_details",
|
|
1868
|
+
type: "object",
|
|
1869
|
+
description: "phase+cycles per story."
|
|
1870
|
+
},
|
|
1871
|
+
{
|
|
1872
|
+
name: "tokens",
|
|
1873
|
+
type: "object",
|
|
1874
|
+
description: "input/output/cost_usd."
|
|
1875
|
+
},
|
|
1876
|
+
{
|
|
1877
|
+
name: "process",
|
|
1878
|
+
type: "object",
|
|
1879
|
+
description: "pid/child/zombie counts."
|
|
1880
|
+
}
|
|
1881
|
+
]
|
|
1882
|
+
},
|
|
1820
1883
|
{
|
|
1821
1884
|
type: "supervisor:kill",
|
|
1822
1885
|
description: "Supervisor killed stalled pipeline process tree.",
|
|
@@ -2238,42 +2301,34 @@ function generateInteractionPatternsSection() {
|
|
|
2238
2301
|
Use this decision flowchart when handling events from \`substrate run --events\`:
|
|
2239
2302
|
|
|
2240
2303
|
### On \`story:done\` with \`result: success\`
|
|
2241
|
-
- Report
|
|
2242
|
-
- Note the story key and number of review_cycles for telemetry.
|
|
2304
|
+
- Report success to the user.
|
|
2243
2305
|
|
|
2244
2306
|
### On \`story:done\` with \`result: failed\`
|
|
2245
|
-
- Report failure
|
|
2246
|
-
- Suggest checking logs or running \`substrate status\` for details.
|
|
2307
|
+
- Report failure with the story key.
|
|
2247
2308
|
|
|
2248
2309
|
### On \`story:escalation\`
|
|
2249
|
-
- Read
|
|
2250
|
-
- Present
|
|
2251
|
-
- Offer to fix the issues or explain them.
|
|
2252
|
-
- Ask the user whether to retry or abandon the story.
|
|
2310
|
+
- Read \`issues\`: each has \`severity\`, \`file\`, \`desc\`.
|
|
2311
|
+
- Present grouped by severity; ask user to retry or abandon.
|
|
2253
2312
|
|
|
2254
2313
|
### On \`story:phase\` with \`verdict: NEEDS_MINOR_FIXES\`
|
|
2255
|
-
-
|
|
2256
|
-
- Offer to apply the fixes or skip.
|
|
2257
|
-
- This is non-blocking — pipeline continues unless you intervene.
|
|
2314
|
+
- Non-blocking minor suggestions. Offer to apply or skip.
|
|
2258
2315
|
|
|
2259
2316
|
### On \`story:warn\`
|
|
2260
|
-
-
|
|
2261
|
-
- Common warnings: token ceiling truncation, partial batch failures.
|
|
2262
|
-
- Pipeline continues normally after a warn event.
|
|
2317
|
+
- Non-blocking warning; pipeline continues normally.
|
|
2263
2318
|
|
|
2264
2319
|
### On \`story:log\`
|
|
2265
|
-
-
|
|
2266
|
-
- Display if verbose mode is active; otherwise buffer or discard.
|
|
2320
|
+
- Informational only. Display in verbose mode.
|
|
2267
2321
|
|
|
2268
2322
|
### On \`pipeline:complete\`
|
|
2269
|
-
- Summarize
|
|
2270
|
-
- List any \`failed\` or \`escalated\` stories with reasons if available.
|
|
2271
|
-
- This is always the last event emitted.
|
|
2323
|
+
- Summarize \`succeeded\`, \`failed\`, \`escalated\` counts.
|
|
2272
2324
|
|
|
2273
2325
|
## Supervisor Interaction Patterns
|
|
2274
2326
|
|
|
2275
2327
|
Patterns for \`substrate supervisor --output-format json\` events:
|
|
2276
2328
|
|
|
2329
|
+
### On \`supervisor:poll\`
|
|
2330
|
+
- Track \`verdict\` and \`tokens.cost_usd\` each cycle. JSON only.
|
|
2331
|
+
|
|
2277
2332
|
### On \`supervisor:summary\`
|
|
2278
2333
|
- Summarize \`succeeded\`, \`failed\`, \`escalated\` counts and \`restarts\`.
|
|
2279
2334
|
- Offer analysis: \`substrate metrics --analysis <run_id> --output-format json\`.
|
|
@@ -2786,6 +2841,8 @@ const logger$12 = createLogger("agent-dispatch");
|
|
|
2786
2841
|
const SHUTDOWN_GRACE_MS = 1e4;
|
|
2787
2842
|
const SHUTDOWN_MAX_WAIT_MS = 3e4;
|
|
2788
2843
|
const CHARS_PER_TOKEN = 4;
|
|
2844
|
+
const MIN_FREE_MEMORY_BYTES = 512 * 1024 * 1024;
|
|
2845
|
+
const MEMORY_PRESSURE_POLL_MS = 1e4;
|
|
2789
2846
|
var MutableDispatchHandle = class {
|
|
2790
2847
|
id;
|
|
2791
2848
|
status;
|
|
@@ -2806,6 +2863,7 @@ var DispatcherImpl = class {
|
|
|
2806
2863
|
_running = new Map();
|
|
2807
2864
|
_queue = [];
|
|
2808
2865
|
_shuttingDown = false;
|
|
2866
|
+
_memoryPressureTimer = null;
|
|
2809
2867
|
constructor(eventBus, adapterRegistry, config) {
|
|
2810
2868
|
this._eventBus = eventBus;
|
|
2811
2869
|
this._adapterRegistry = adapterRegistry;
|
|
@@ -2820,7 +2878,7 @@ var DispatcherImpl = class {
|
|
|
2820
2878
|
const id = randomUUID();
|
|
2821
2879
|
const resultPromise = new Promise((resolve$2, reject) => {
|
|
2822
2880
|
const typedResolve = resolve$2;
|
|
2823
|
-
if (this._running.size < this._config.maxConcurrency) {
|
|
2881
|
+
if (this._running.size < this._config.maxConcurrency && !this._isMemoryPressured()) {
|
|
2824
2882
|
this._reserveSlot(id);
|
|
2825
2883
|
this._startDispatch(id, request, typedResolve).catch((err) => {
|
|
2826
2884
|
this._running.delete(id);
|
|
@@ -2881,6 +2939,7 @@ var DispatcherImpl = class {
|
|
|
2881
2939
|
}
|
|
2882
2940
|
async shutdown() {
|
|
2883
2941
|
this._shuttingDown = true;
|
|
2942
|
+
this._stopMemoryPressureTimer();
|
|
2884
2943
|
logger$12.info({
|
|
2885
2944
|
running: this._running.size,
|
|
2886
2945
|
queued: this._queue.length
|
|
@@ -2951,6 +3010,8 @@ var DispatcherImpl = class {
|
|
|
2951
3010
|
});
|
|
2952
3011
|
const timeoutMs = timeout ?? this._config.defaultTimeouts[taskType] ?? DEFAULT_TIMEOUTS[taskType] ?? 3e5;
|
|
2953
3012
|
const env = { ...process.env };
|
|
3013
|
+
const parentNodeOpts = env["NODE_OPTIONS"] ?? "";
|
|
3014
|
+
if (!parentNodeOpts.includes("--max-old-space-size")) env["NODE_OPTIONS"] = `${parentNodeOpts} --max-old-space-size=512`.trim();
|
|
2954
3015
|
if (cmd.env !== void 0) Object.assign(env, cmd.env);
|
|
2955
3016
|
if (cmd.unsetEnvKeys !== void 0) for (const key of cmd.unsetEnvKeys) delete env[key];
|
|
2956
3017
|
const proc = spawn(cmd.binary, cmd.args, {
|
|
@@ -3157,9 +3218,16 @@ var DispatcherImpl = class {
|
|
|
3157
3218
|
this._running.set(id, placeholder);
|
|
3158
3219
|
}
|
|
3159
3220
|
_drainQueue() {
|
|
3160
|
-
if (this._queue.length === 0)
|
|
3221
|
+
if (this._queue.length === 0) {
|
|
3222
|
+
this._stopMemoryPressureTimer();
|
|
3223
|
+
return;
|
|
3224
|
+
}
|
|
3161
3225
|
if (this._running.size >= this._config.maxConcurrency) return;
|
|
3162
3226
|
if (this._shuttingDown) return;
|
|
3227
|
+
if (this._isMemoryPressured()) {
|
|
3228
|
+
this._startMemoryPressureTimer();
|
|
3229
|
+
return;
|
|
3230
|
+
}
|
|
3163
3231
|
const next = this._queue.shift();
|
|
3164
3232
|
if (next === void 0) return;
|
|
3165
3233
|
next.handle.status = "running";
|
|
@@ -3173,6 +3241,30 @@ var DispatcherImpl = class {
|
|
|
3173
3241
|
const idx = this._queue.findIndex((q) => q.id === id);
|
|
3174
3242
|
if (idx !== -1) this._queue.splice(idx, 1);
|
|
3175
3243
|
}
|
|
3244
|
+
_isMemoryPressured() {
|
|
3245
|
+
const free = freemem();
|
|
3246
|
+
if (free < MIN_FREE_MEMORY_BYTES) {
|
|
3247
|
+
logger$12.warn({
|
|
3248
|
+
freeMB: Math.round(free / 1024 / 1024),
|
|
3249
|
+
thresholdMB: Math.round(MIN_FREE_MEMORY_BYTES / 1024 / 1024)
|
|
3250
|
+
}, "Memory pressure detected — holding dispatch queue");
|
|
3251
|
+
return true;
|
|
3252
|
+
}
|
|
3253
|
+
return false;
|
|
3254
|
+
}
|
|
3255
|
+
_startMemoryPressureTimer() {
|
|
3256
|
+
if (this._memoryPressureTimer !== null) return;
|
|
3257
|
+
this._memoryPressureTimer = setInterval(() => {
|
|
3258
|
+
this._drainQueue();
|
|
3259
|
+
}, MEMORY_PRESSURE_POLL_MS);
|
|
3260
|
+
this._memoryPressureTimer.unref();
|
|
3261
|
+
}
|
|
3262
|
+
_stopMemoryPressureTimer() {
|
|
3263
|
+
if (this._memoryPressureTimer !== null) {
|
|
3264
|
+
clearInterval(this._memoryPressureTimer);
|
|
3265
|
+
this._memoryPressureTimer = null;
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3176
3268
|
};
|
|
3177
3269
|
/**
|
|
3178
3270
|
* Create a new Dispatcher instance.
|
|
@@ -3866,8 +3958,11 @@ const DEFAULT_VITEST_PATTERNS = `## Test Patterns (defaults)
|
|
|
3866
3958
|
- Mock approach: vi.mock() with hoisting for module-level mocks
|
|
3867
3959
|
- Assertion style: expect().toBe(), expect().toEqual(), expect().toThrow()
|
|
3868
3960
|
- Test structure: describe/it blocks with beforeEach/afterEach
|
|
3869
|
-
- Coverage: 80% enforced
|
|
3870
|
-
-
|
|
3961
|
+
- Coverage: 80% enforced
|
|
3962
|
+
- IMPORTANT: During development, run ONLY your relevant tests to save memory:
|
|
3963
|
+
npx vitest run --no-coverage -- "your-module-name"
|
|
3964
|
+
- Final validation ONLY: npm test 2>&1 | grep -E "Test Files|Tests " | tail -3
|
|
3965
|
+
- Do NOT run the full suite (npm test) repeatedly — it consumes excessive memory when multiple agents run in parallel`;
|
|
3871
3966
|
/**
|
|
3872
3967
|
* Execute the compiled dev-story workflow.
|
|
3873
3968
|
*
|
|
@@ -6247,16 +6342,43 @@ function createArtifactExistsGate(phase, artifactType) {
|
|
|
6247
6342
|
}
|
|
6248
6343
|
async function noOp(_db, _runId) {}
|
|
6249
6344
|
/**
|
|
6345
|
+
* Create the Research phase definition.
|
|
6346
|
+
*
|
|
6347
|
+
* Entry gates: empty array (research is always the pipeline entrypoint when enabled)
|
|
6348
|
+
* Exit gates: 'research-findings' artifact must exist for this run
|
|
6349
|
+
*
|
|
6350
|
+
* This phase is inserted before analysis when research is enabled in the pack
|
|
6351
|
+
* manifest (`research: true`) or via the `--research` CLI flag.
|
|
6352
|
+
*/
|
|
6353
|
+
function createResearchPhaseDefinition() {
|
|
6354
|
+
return {
|
|
6355
|
+
name: "research",
|
|
6356
|
+
description: "Conduct pre-analysis research: market landscape, competitive analysis, technical feasibility, and synthesized findings.",
|
|
6357
|
+
entryGates: [],
|
|
6358
|
+
exitGates: [createArtifactExistsGate("research", "research-findings")],
|
|
6359
|
+
onEnter: async (_db, runId) => {
|
|
6360
|
+
logPhase(`Research phase starting for run ${runId}`);
|
|
6361
|
+
},
|
|
6362
|
+
onExit: async (db, runId) => {
|
|
6363
|
+
const artifact = getArtifactByTypeForRun(db, runId, "research", "research-findings");
|
|
6364
|
+
if (artifact === void 0) logPhase(`Research phase exit WARNING: research-findings artifact not found for run ${runId}`);
|
|
6365
|
+
else logPhase(`Research phase completed for run ${runId} — research-findings artifact registered: ${artifact.id}`);
|
|
6366
|
+
}
|
|
6367
|
+
};
|
|
6368
|
+
}
|
|
6369
|
+
/**
|
|
6250
6370
|
* Create the Analysis phase definition.
|
|
6251
6371
|
*
|
|
6252
|
-
* Entry gates: none (first phase — always can be entered)
|
|
6372
|
+
* Entry gates: none by default (first phase — always can be entered);
|
|
6373
|
+
* when research is enabled, requires 'research-findings' artifact
|
|
6253
6374
|
* Exit gates: 'product-brief' artifact must exist for this run
|
|
6254
6375
|
*/
|
|
6255
|
-
function createAnalysisPhaseDefinition() {
|
|
6376
|
+
function createAnalysisPhaseDefinition(options) {
|
|
6377
|
+
const entryGates = options?.requiresResearch === true ? [createArtifactExistsGate("research", "research-findings")] : [];
|
|
6256
6378
|
return {
|
|
6257
6379
|
name: "analysis",
|
|
6258
6380
|
description: "Analyze the user concept and produce a product brief capturing requirements, constraints, and goals.",
|
|
6259
|
-
entryGates
|
|
6381
|
+
entryGates,
|
|
6260
6382
|
exitGates: [createArtifactExistsGate("analysis", "product-brief")],
|
|
6261
6383
|
onEnter: async (_db, runId) => {
|
|
6262
6384
|
logPhase(`Analysis phase starting for run ${runId}`);
|
|
@@ -6371,13 +6493,19 @@ function createImplementationPhaseDefinition() {
|
|
|
6371
6493
|
/**
|
|
6372
6494
|
* Return the built-in phase definitions in execution order.
|
|
6373
6495
|
*
|
|
6496
|
+
* When `researchEnabled` is true, the `research` phase is inserted at position 0
|
|
6497
|
+
* (before analysis), and the analysis phase gains a `research-findings` entry gate.
|
|
6498
|
+
*
|
|
6374
6499
|
* When `uxDesignEnabled` is true, the `ux-design` phase is inserted between
|
|
6375
6500
|
* `planning` and `solutioning`, with its own entry/exit gates.
|
|
6376
6501
|
*
|
|
6377
6502
|
* @param config - Optional configuration for conditional phase inclusion
|
|
6378
6503
|
*/
|
|
6379
6504
|
function createBuiltInPhases(config) {
|
|
6380
|
-
const phases = [
|
|
6505
|
+
const phases = [];
|
|
6506
|
+
if (config?.researchEnabled === true) phases.push(createResearchPhaseDefinition());
|
|
6507
|
+
phases.push(createAnalysisPhaseDefinition({ requiresResearch: config?.researchEnabled === true }));
|
|
6508
|
+
phases.push(createPlanningPhaseDefinition());
|
|
6381
6509
|
if (config?.uxDesignEnabled === true) phases.push(createUxDesignPhaseDefinition());
|
|
6382
6510
|
phases.push(createSolutioningPhaseDefinition());
|
|
6383
6511
|
phases.push(createImplementationPhaseDefinition());
|
|
@@ -6444,8 +6572,12 @@ var PhaseOrchestratorImpl = class {
|
|
|
6444
6572
|
this._db = deps.db;
|
|
6445
6573
|
this._pack = deps.pack;
|
|
6446
6574
|
this._qualityGates = deps.qualityGates;
|
|
6575
|
+
const researchEnabled = this._pack.manifest.research === true;
|
|
6447
6576
|
const uxDesignEnabled = this._pack.manifest.uxDesign === true;
|
|
6448
|
-
this._phases = createBuiltInPhases({
|
|
6577
|
+
this._phases = createBuiltInPhases({
|
|
6578
|
+
researchEnabled,
|
|
6579
|
+
uxDesignEnabled
|
|
6580
|
+
});
|
|
6449
6581
|
const builtInNames = new Set(this._phases.map((p) => p.name));
|
|
6450
6582
|
const packPhases = this._pack.getPhases();
|
|
6451
6583
|
for (const packPhase of packPhases) if (!builtInNames.has(packPhase.name)) this._phases.push({
|
|
@@ -6762,7 +6894,8 @@ function getCritiquePromptName(phase) {
|
|
|
6762
6894
|
planning: "critique-planning",
|
|
6763
6895
|
solutioning: "critique-architecture",
|
|
6764
6896
|
architecture: "critique-architecture",
|
|
6765
|
-
stories: "critique-stories"
|
|
6897
|
+
stories: "critique-stories",
|
|
6898
|
+
research: "critique-research"
|
|
6766
6899
|
};
|
|
6767
6900
|
return mapping[phase] ?? `critique-${phase}`;
|
|
6768
6901
|
}
|
|
@@ -7408,6 +7541,31 @@ const UxJourneysOutputSchema = z.object({
|
|
|
7408
7541
|
accessibility_guidelines: z.array(z.string()).default([])
|
|
7409
7542
|
});
|
|
7410
7543
|
/**
|
|
7544
|
+
* Step 1 output: Research Discovery.
|
|
7545
|
+
* Covers concept classification and raw findings across market, domain, and technical dimensions.
|
|
7546
|
+
* Content fields are optional to allow `{result: 'failed'}` without Zod rejection.
|
|
7547
|
+
*/
|
|
7548
|
+
const ResearchDiscoveryOutputSchema = z.object({
|
|
7549
|
+
result: z.enum(["success", "failed"]),
|
|
7550
|
+
concept_classification: z.string().optional(),
|
|
7551
|
+
market_findings: z.string().optional(),
|
|
7552
|
+
domain_findings: z.string().optional(),
|
|
7553
|
+
technical_findings: z.string().optional()
|
|
7554
|
+
});
|
|
7555
|
+
/**
|
|
7556
|
+
* Step 2 output: Research Synthesis.
|
|
7557
|
+
* Covers distilled research findings, risk flags, and opportunity signals.
|
|
7558
|
+
* Content fields are optional to allow `{result: 'failed'}` without Zod rejection.
|
|
7559
|
+
*/
|
|
7560
|
+
const ResearchSynthesisOutputSchema = z.object({
|
|
7561
|
+
result: z.enum(["success", "failed"]),
|
|
7562
|
+
market_context: z.string().optional(),
|
|
7563
|
+
competitive_landscape: z.string().optional(),
|
|
7564
|
+
technical_feasibility: z.string().optional(),
|
|
7565
|
+
risk_flags: z.array(z.string()).default([]),
|
|
7566
|
+
opportunity_signals: z.array(z.string()).default([])
|
|
7567
|
+
});
|
|
7568
|
+
/**
|
|
7411
7569
|
* Zod schema for the YAML output emitted by an elicitation sub-agent.
|
|
7412
7570
|
* The agent returns structured insights from applying an elicitation method.
|
|
7413
7571
|
*/
|
|
@@ -9882,6 +10040,162 @@ async function runUxDesignPhase(deps, params) {
|
|
|
9882
10040
|
}
|
|
9883
10041
|
}
|
|
9884
10042
|
|
|
10043
|
+
//#endregion
|
|
10044
|
+
//#region src/modules/phase-orchestrator/phases/research.ts
|
|
10045
|
+
/**
|
|
10046
|
+
* Build step definitions for 2-step research decomposition.
|
|
10047
|
+
*
|
|
10048
|
+
* Step 1: Discovery
|
|
10049
|
+
* - Injects concept context
|
|
10050
|
+
* - Produces: concept_classification, market_findings, domain_findings, technical_findings
|
|
10051
|
+
*
|
|
10052
|
+
* Step 2: Synthesis
|
|
10053
|
+
* - Injects concept and Step 1 raw findings
|
|
10054
|
+
* - Produces: market_context, competitive_landscape, technical_feasibility, risk_flags, opportunity_signals
|
|
10055
|
+
* - Registers 'research-findings' artifact
|
|
10056
|
+
*/
|
|
10057
|
+
function buildResearchSteps() {
|
|
10058
|
+
return [{
|
|
10059
|
+
name: "research-step-1-discovery",
|
|
10060
|
+
taskType: "research-discovery",
|
|
10061
|
+
outputSchema: ResearchDiscoveryOutputSchema,
|
|
10062
|
+
context: [{
|
|
10063
|
+
placeholder: "concept",
|
|
10064
|
+
source: "param:concept"
|
|
10065
|
+
}],
|
|
10066
|
+
persist: [
|
|
10067
|
+
{
|
|
10068
|
+
field: "concept_classification",
|
|
10069
|
+
category: "research",
|
|
10070
|
+
key: "concept_classification"
|
|
10071
|
+
},
|
|
10072
|
+
{
|
|
10073
|
+
field: "market_findings",
|
|
10074
|
+
category: "research",
|
|
10075
|
+
key: "market_findings"
|
|
10076
|
+
},
|
|
10077
|
+
{
|
|
10078
|
+
field: "domain_findings",
|
|
10079
|
+
category: "research",
|
|
10080
|
+
key: "domain_findings"
|
|
10081
|
+
},
|
|
10082
|
+
{
|
|
10083
|
+
field: "technical_findings",
|
|
10084
|
+
category: "research",
|
|
10085
|
+
key: "technical_findings"
|
|
10086
|
+
}
|
|
10087
|
+
],
|
|
10088
|
+
elicitate: true
|
|
10089
|
+
}, {
|
|
10090
|
+
name: "research-step-2-synthesis",
|
|
10091
|
+
taskType: "research-synthesis",
|
|
10092
|
+
outputSchema: ResearchSynthesisOutputSchema,
|
|
10093
|
+
context: [{
|
|
10094
|
+
placeholder: "concept",
|
|
10095
|
+
source: "param:concept"
|
|
10096
|
+
}, {
|
|
10097
|
+
placeholder: "raw_findings",
|
|
10098
|
+
source: "step:research-step-1-discovery"
|
|
10099
|
+
}],
|
|
10100
|
+
persist: [
|
|
10101
|
+
{
|
|
10102
|
+
field: "market_context",
|
|
10103
|
+
category: "research",
|
|
10104
|
+
key: "market_context"
|
|
10105
|
+
},
|
|
10106
|
+
{
|
|
10107
|
+
field: "competitive_landscape",
|
|
10108
|
+
category: "research",
|
|
10109
|
+
key: "competitive_landscape"
|
|
10110
|
+
},
|
|
10111
|
+
{
|
|
10112
|
+
field: "technical_feasibility",
|
|
10113
|
+
category: "research",
|
|
10114
|
+
key: "technical_feasibility"
|
|
10115
|
+
},
|
|
10116
|
+
{
|
|
10117
|
+
field: "risk_flags",
|
|
10118
|
+
category: "research",
|
|
10119
|
+
key: "risk_flags"
|
|
10120
|
+
},
|
|
10121
|
+
{
|
|
10122
|
+
field: "opportunity_signals",
|
|
10123
|
+
category: "research",
|
|
10124
|
+
key: "opportunity_signals"
|
|
10125
|
+
}
|
|
10126
|
+
],
|
|
10127
|
+
registerArtifact: {
|
|
10128
|
+
type: "research-findings",
|
|
10129
|
+
path: "decision-store://research/research-findings",
|
|
10130
|
+
summarize: (parsed) => {
|
|
10131
|
+
const risks = Array.isArray(parsed.risk_flags) ? parsed.risk_flags : void 0;
|
|
10132
|
+
const opportunities = Array.isArray(parsed.opportunity_signals) ? parsed.opportunity_signals : void 0;
|
|
10133
|
+
const count = (risks?.length ?? 0) + (opportunities?.length ?? 0);
|
|
10134
|
+
return count > 0 ? `${count} research insights captured (risks + opportunities)` : "Research synthesis complete";
|
|
10135
|
+
}
|
|
10136
|
+
},
|
|
10137
|
+
critique: true
|
|
10138
|
+
}];
|
|
10139
|
+
}
|
|
10140
|
+
/**
|
|
10141
|
+
* Execute the research phase of the BMAD pipeline.
|
|
10142
|
+
*
|
|
10143
|
+
* Runs 2 sequential steps covering discovery and synthesis.
|
|
10144
|
+
* Each step builds on prior step decisions via the decision store.
|
|
10145
|
+
*
|
|
10146
|
+
* On success, a 'research-findings' artifact is registered and research decisions
|
|
10147
|
+
* are available to subsequent phases via `decision:research.*`.
|
|
10148
|
+
*
|
|
10149
|
+
* @param deps - Shared phase dependencies (db, pack, contextCompiler, dispatcher)
|
|
10150
|
+
* @param params - Phase parameters (runId, concept)
|
|
10151
|
+
* @returns ResearchResult with success/failure status and token usage
|
|
10152
|
+
*/
|
|
10153
|
+
async function runResearchPhase(deps, params) {
|
|
10154
|
+
const { runId } = params;
|
|
10155
|
+
const zeroTokenUsage = {
|
|
10156
|
+
input: 0,
|
|
10157
|
+
output: 0
|
|
10158
|
+
};
|
|
10159
|
+
try {
|
|
10160
|
+
const steps = buildResearchSteps();
|
|
10161
|
+
const result = await runSteps(steps, deps, runId, "research", { concept: params.concept });
|
|
10162
|
+
if (!result.success) return {
|
|
10163
|
+
result: "failed",
|
|
10164
|
+
error: result.error ?? "research_multi_step_failed",
|
|
10165
|
+
details: result.error ?? "Research multi-step execution failed",
|
|
10166
|
+
tokenUsage: result.tokenUsage
|
|
10167
|
+
};
|
|
10168
|
+
const lastStep = result.steps[result.steps.length - 1];
|
|
10169
|
+
const artifactId = lastStep?.artifactId;
|
|
10170
|
+
if (!artifactId) {
|
|
10171
|
+
const artifact = registerArtifact(deps.db, {
|
|
10172
|
+
pipeline_run_id: runId,
|
|
10173
|
+
phase: "research",
|
|
10174
|
+
type: "research-findings",
|
|
10175
|
+
path: "decision-store://research/research-findings",
|
|
10176
|
+
summary: "Research phase completed"
|
|
10177
|
+
});
|
|
10178
|
+
return {
|
|
10179
|
+
result: "success",
|
|
10180
|
+
artifact_id: artifact.id,
|
|
10181
|
+
tokenUsage: result.tokenUsage
|
|
10182
|
+
};
|
|
10183
|
+
}
|
|
10184
|
+
return {
|
|
10185
|
+
result: "success",
|
|
10186
|
+
artifact_id: artifactId,
|
|
10187
|
+
tokenUsage: result.tokenUsage
|
|
10188
|
+
};
|
|
10189
|
+
} catch (err) {
|
|
10190
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
10191
|
+
return {
|
|
10192
|
+
result: "failed",
|
|
10193
|
+
error: message,
|
|
10194
|
+
tokenUsage: zeroTokenUsage
|
|
10195
|
+
};
|
|
10196
|
+
}
|
|
10197
|
+
}
|
|
10198
|
+
|
|
9885
10199
|
//#endregion
|
|
9886
10200
|
//#region src/cli/commands/run.ts
|
|
9887
10201
|
const logger = createLogger("run-cmd");
|
|
@@ -9901,7 +10215,7 @@ function mapInternalPhaseToEventPhase(internalPhase) {
|
|
|
9901
10215
|
}
|
|
9902
10216
|
}
|
|
9903
10217
|
async function runRunAction(options) {
|
|
9904
|
-
const { pack: packName, from: startPhase, stopAfter, concept: conceptArg, conceptFile, stories: storiesArg, concurrency, outputFormat, projectRoot, events: eventsFlag, verbose: verboseFlag, tui: tuiFlag, skipUx } = options;
|
|
10218
|
+
const { pack: packName, from: startPhase, stopAfter, concept: conceptArg, conceptFile, stories: storiesArg, concurrency, outputFormat, projectRoot, events: eventsFlag, verbose: verboseFlag, tui: tuiFlag, skipUx, research: researchFlag, skipResearch: skipResearchFlag } = options;
|
|
9905
10219
|
if (startPhase !== void 0 && !VALID_PHASES.includes(startPhase)) {
|
|
9906
10220
|
const errorMsg = `Invalid phase '${startPhase}'. Valid phases: ${VALID_PHASES.join(", ")}`;
|
|
9907
10221
|
if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
@@ -9957,7 +10271,9 @@ async function runRunAction(options) {
|
|
|
9957
10271
|
outputFormat,
|
|
9958
10272
|
projectRoot,
|
|
9959
10273
|
...eventsFlag === true ? { events: true } : {},
|
|
9960
|
-
...skipUx === true ? { skipUx: true } : {}
|
|
10274
|
+
...skipUx === true ? { skipUx: true } : {},
|
|
10275
|
+
...researchFlag === true ? { research: true } : {},
|
|
10276
|
+
...skipResearchFlag === true ? { skipResearch: true } : {}
|
|
9961
10277
|
});
|
|
9962
10278
|
let storyKeys = [];
|
|
9963
10279
|
if (storiesArg !== void 0 && storiesArg !== "") {
|
|
@@ -10318,7 +10634,7 @@ async function runRunAction(options) {
|
|
|
10318
10634
|
else failedKeys.push(key);
|
|
10319
10635
|
try {
|
|
10320
10636
|
const runEndMs = Date.now();
|
|
10321
|
-
const runStartMs =
|
|
10637
|
+
const runStartMs = parseDbTimestampAsUtc(pipelineRun.created_at).getTime();
|
|
10322
10638
|
const tokenAgg = aggregateTokenUsageForRun(db, pipelineRun.id);
|
|
10323
10639
|
const storyMetrics = getStoryMetricsForRun(db, pipelineRun.id);
|
|
10324
10640
|
const totalReviewCycles = storyMetrics.reduce((sum, m) => sum + (m.review_cycles ?? 0), 0);
|
|
@@ -10397,7 +10713,7 @@ async function runRunAction(options) {
|
|
|
10397
10713
|
}
|
|
10398
10714
|
}
|
|
10399
10715
|
async function runFullPipeline(options) {
|
|
10400
|
-
const { packName, packPath, dbDir, dbPath, startPhase, stopAfter, concept, concurrency, outputFormat, projectRoot, events: eventsFlag, skipUx } = options;
|
|
10716
|
+
const { packName, packPath, dbDir, dbPath, startPhase, stopAfter, concept, concurrency, outputFormat, projectRoot, events: eventsFlag, skipUx, research: researchFlag, skipResearch: skipResearchFlag } = options;
|
|
10401
10717
|
if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
|
|
10402
10718
|
const dbWrapper = new DatabaseWrapper(dbPath);
|
|
10403
10719
|
try {
|
|
@@ -10437,13 +10753,19 @@ async function runFullPipeline(options) {
|
|
|
10437
10753
|
contextCompiler,
|
|
10438
10754
|
dispatcher
|
|
10439
10755
|
};
|
|
10440
|
-
|
|
10756
|
+
let effectiveResearch = pack.manifest.research === true;
|
|
10757
|
+
if (researchFlag === true) effectiveResearch = true;
|
|
10758
|
+
if (skipResearchFlag === true) effectiveResearch = false;
|
|
10759
|
+
let effectiveUxDesign = pack.manifest.uxDesign === true;
|
|
10760
|
+
if (skipUx === true) effectiveUxDesign = false;
|
|
10761
|
+
const packForOrchestrator = {
|
|
10441
10762
|
...pack,
|
|
10442
10763
|
manifest: {
|
|
10443
10764
|
...pack.manifest,
|
|
10444
|
-
|
|
10765
|
+
research: effectiveResearch,
|
|
10766
|
+
uxDesign: effectiveUxDesign
|
|
10445
10767
|
}
|
|
10446
|
-
}
|
|
10768
|
+
};
|
|
10447
10769
|
const phaseOrchestrator = createPhaseOrchestrator({
|
|
10448
10770
|
db,
|
|
10449
10771
|
pack: packForOrchestrator
|
|
@@ -10454,19 +10776,11 @@ async function runFullPipeline(options) {
|
|
|
10454
10776
|
process.stdout.write(`Starting full pipeline from phase: ${startPhase}\n`);
|
|
10455
10777
|
process.stdout.write(`Pipeline run ID: ${runId}\n`);
|
|
10456
10778
|
}
|
|
10457
|
-
const
|
|
10458
|
-
|
|
10459
|
-
|
|
10460
|
-
|
|
10461
|
-
|
|
10462
|
-
"solutioning",
|
|
10463
|
-
"implementation"
|
|
10464
|
-
] : [
|
|
10465
|
-
"analysis",
|
|
10466
|
-
"planning",
|
|
10467
|
-
"solutioning",
|
|
10468
|
-
"implementation"
|
|
10469
|
-
];
|
|
10779
|
+
const phaseOrder = [];
|
|
10780
|
+
if (effectiveResearch) phaseOrder.push("research");
|
|
10781
|
+
phaseOrder.push("analysis", "planning");
|
|
10782
|
+
if (effectiveUxDesign) phaseOrder.push("ux-design");
|
|
10783
|
+
phaseOrder.push("solutioning", "implementation");
|
|
10470
10784
|
const startIdx = phaseOrder.indexOf(startPhase);
|
|
10471
10785
|
for (let i = startIdx; i < phaseOrder.length; i++) {
|
|
10472
10786
|
const currentPhase = phaseOrder[i];
|
|
@@ -10520,6 +10834,32 @@ async function runFullPipeline(options) {
|
|
|
10520
10834
|
process.stdout.write(`[PLANNING] Complete — ${result.requirements_count ?? 0} requirements, ${result.user_stories_count ?? 0} user stories\n`);
|
|
10521
10835
|
process.stdout.write(` Tokens: ${result.tokenUsage.input.toLocaleString()} input / ${result.tokenUsage.output.toLocaleString()} output\n`);
|
|
10522
10836
|
}
|
|
10837
|
+
} else if (currentPhase === "research") {
|
|
10838
|
+
const result = await runResearchPhase(phaseDeps, {
|
|
10839
|
+
runId,
|
|
10840
|
+
concept: concept ?? ""
|
|
10841
|
+
});
|
|
10842
|
+
if (result.tokenUsage.input > 0 || result.tokenUsage.output > 0) {
|
|
10843
|
+
const costUsd = (result.tokenUsage.input * 3 + result.tokenUsage.output * 15) / 1e6;
|
|
10844
|
+
addTokenUsage(db, runId, {
|
|
10845
|
+
phase: "research",
|
|
10846
|
+
agent: "claude-code",
|
|
10847
|
+
input_tokens: result.tokenUsage.input,
|
|
10848
|
+
output_tokens: result.tokenUsage.output,
|
|
10849
|
+
cost_usd: costUsd
|
|
10850
|
+
});
|
|
10851
|
+
}
|
|
10852
|
+
if (result.result === "failed") {
|
|
10853
|
+
updatePipelineRun(db, runId, { status: "failed" });
|
|
10854
|
+
const errorMsg = `Research phase failed: ${result.error ?? "unknown error"}${result.details ? ` — ${result.details}` : ""}`;
|
|
10855
|
+
if (outputFormat === "human") process.stderr.write(`Error: ${errorMsg}\n`);
|
|
10856
|
+
else process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
10857
|
+
return 1;
|
|
10858
|
+
}
|
|
10859
|
+
if (outputFormat === "human") {
|
|
10860
|
+
process.stdout.write(`[RESEARCH] Complete — research findings artifact registered (artifact: ${result.artifact_id ?? "n/a"})\n`);
|
|
10861
|
+
process.stdout.write(` Tokens: ${result.tokenUsage.input.toLocaleString()} input / ${result.tokenUsage.output.toLocaleString()} output\n`);
|
|
10862
|
+
}
|
|
10523
10863
|
} else if (currentPhase === "ux-design") {
|
|
10524
10864
|
const result = await runUxDesignPhase(phaseDeps, { runId });
|
|
10525
10865
|
if (result.tokenUsage.input > 0 || result.tokenUsage.output > 0) {
|
|
@@ -10684,7 +11024,7 @@ async function runFullPipeline(options) {
|
|
|
10684
11024
|
}
|
|
10685
11025
|
}
|
|
10686
11026
|
function registerRunCommand(program, _version = "0.0.0", projectRoot = process.cwd()) {
|
|
10687
|
-
program.command("run").description("Run the autonomous pipeline (use --from to start from a specific phase)").option("--pack <name>", "Methodology pack name", "bmad").option("--from <phase>", "Start from this phase: analysis, planning, solutioning, implementation").option("--stop-after <phase>", "Stop pipeline after this phase completes").option("--concept <text>", "Inline concept text (required when --from analysis)").option("--concept-file <path>", "Path to a file containing the concept text").option("--stories <keys>", "Comma-separated story keys (e.g., 10-1,10-2)").option("--concurrency <n>", "Maximum parallel conflict groups", (v) => parseInt(v, 10),
|
|
11027
|
+
program.command("run").description("Run the autonomous pipeline (use --from to start from a specific phase)").option("--pack <name>", "Methodology pack name", "bmad").option("--from <phase>", "Start from this phase: analysis, planning, solutioning, implementation").option("--stop-after <phase>", "Stop pipeline after this phase completes").option("--concept <text>", "Inline concept text (required when --from analysis)").option("--concept-file <path>", "Path to a file containing the concept text").option("--stories <keys>", "Comma-separated story keys (e.g., 10-1,10-2)").option("--concurrency <n>", "Maximum parallel conflict groups", (v) => parseInt(v, 10), 2).option("--project-root <path>", "Project root directory", projectRoot).option("--output-format <format>", "Output format: human (default) or json", "human").option("--events", "Emit structured NDJSON events on stdout for programmatic consumption").option("--verbose", "Show detailed pino log output").option("--help-agent", "Print a machine-optimized prompt fragment for AI agents and exit").option("--tui", "Show TUI dashboard").option("--skip-ux", "Skip the UX design phase even if enabled in the pack manifest").option("--research", "Enable the research phase even if not set in the pack manifest").option("--skip-research", "Skip the research phase even if enabled in the pack manifest").action(async (opts) => {
|
|
10688
11028
|
if (opts.helpAgent) {
|
|
10689
11029
|
process.exitCode = await runHelpAgent();
|
|
10690
11030
|
return;
|
|
@@ -10714,12 +11054,14 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
|
|
|
10714
11054
|
events: opts.events,
|
|
10715
11055
|
verbose: opts.verbose,
|
|
10716
11056
|
tui: opts.tui,
|
|
10717
|
-
skipUx: opts.skipUx
|
|
11057
|
+
skipUx: opts.skipUx,
|
|
11058
|
+
research: opts.research,
|
|
11059
|
+
skipResearch: opts.skipResearch
|
|
10718
11060
|
});
|
|
10719
11061
|
process.exitCode = exitCode;
|
|
10720
11062
|
});
|
|
10721
11063
|
}
|
|
10722
11064
|
|
|
10723
11065
|
//#endregion
|
|
10724
|
-
export { DatabaseWrapper, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, buildPipelineStatusOutput, createContextCompiler, createDispatcher, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getSubstrateDefaultSettings, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, runAnalysisPhase, runMigrations, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
|
|
10725
|
-
//# sourceMappingURL=run-
|
|
11066
|
+
export { DatabaseWrapper, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, buildPipelineStatusOutput, createContextCompiler, createDispatcher, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getSubstrateDefaultSettings, parseDbTimestampAsUtc, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, runAnalysisPhase, runMigrations, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
|
|
11067
|
+
//# sourceMappingURL=run-0IlA2ubQ.js.map
|