substrate-ai 0.2.5 → 0.2.7
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 +2 -2
- package/dist/{run-XkrV99HV.js → run-BaBWZUaK.js} +1 -1
- package/dist/{run-CDYE1PT3.js → run-CTOLQ2MR.js} +320 -32
- package/package.json +1 -1
- package/packs/bmad/manifest.yaml +31 -0
- package/packs/bmad/prompts/analysis-step-1-vision.md +5 -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
package/dist/cli/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createLogger, deepMask } from "../logger-C6n1g8uP.js";
|
|
3
3
|
import { AdapterRegistry, createEventBus } from "../event-bus-J-bw-pkp.js";
|
|
4
4
|
import { CURRENT_CONFIG_FORMAT_VERSION, CURRENT_TASK_GRAPH_VERSION, PartialSubstrateConfigSchema, SUPPORTED_CONFIG_FORMAT_VERSIONS, SubstrateConfigSchema, defaultConfigMigrator } from "../version-manager-impl-BpVx2DkY.js";
|
|
5
|
-
import { DatabaseWrapper, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, buildPipelineStatusOutput, createContextCompiler, createDispatcher, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getSubstrateDefaultSettings, parseDbTimestampAsUtc, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, runAnalysisPhase, runMigrations, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-
|
|
5
|
+
import { DatabaseWrapper, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, buildPipelineStatusOutput, createContextCompiler, createDispatcher, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getSubstrateDefaultSettings, parseDbTimestampAsUtc, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, runAnalysisPhase, runMigrations, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-CTOLQ2MR.js";
|
|
6
6
|
import { ConfigError, ConfigIncompatibleFormatError } from "../errors-BPqtzQ4U.js";
|
|
7
7
|
import { addTokenUsage, createDecision, getDecisionsByPhaseForRun, getLatestRun, getPipelineRunById, getTokenUsageSummary, listRequirements, updatePipelineRun } from "../decisions-DNYByk0U.js";
|
|
8
8
|
import { aggregateTokenUsageForRun, compareRunMetrics, getBaselineRunMetrics, getRunMetrics, getStoryMetricsForRun, incrementRunRestarts, listRunMetrics, tagRunAsBaseline } from "../metrics-BSg8VIHd.js";
|
|
@@ -2833,7 +2833,7 @@ async function runSupervisorAction(options, deps = {}) {
|
|
|
2833
2833
|
const expDb = expDbWrapper.db;
|
|
2834
2834
|
const { runRunAction: runPipeline } = await import(
|
|
2835
2835
|
/* @vite-ignore */
|
|
2836
|
-
"../run-
|
|
2836
|
+
"../run-BaBWZUaK.js"
|
|
2837
2837
|
);
|
|
2838
2838
|
const runStoryFn = async (opts) => {
|
|
2839
2839
|
const exitCode = await runPipeline({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "./logger-C6n1g8uP.js";
|
|
2
2
|
import "./event-bus-J-bw-pkp.js";
|
|
3
|
-
import { registerRunCommand, runRunAction } from "./run-
|
|
3
|
+
import { registerRunCommand, runRunAction } from "./run-CTOLQ2MR.js";
|
|
4
4
|
import "./decisions-DNYByk0U.js";
|
|
5
5
|
import "./metrics-BSg8VIHd.js";
|
|
6
6
|
|
|
@@ -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
|
|
|
@@ -2840,6 +2841,8 @@ const logger$12 = createLogger("agent-dispatch");
|
|
|
2840
2841
|
const SHUTDOWN_GRACE_MS = 1e4;
|
|
2841
2842
|
const SHUTDOWN_MAX_WAIT_MS = 3e4;
|
|
2842
2843
|
const CHARS_PER_TOKEN = 4;
|
|
2844
|
+
const MIN_FREE_MEMORY_BYTES = 512 * 1024 * 1024;
|
|
2845
|
+
const MEMORY_PRESSURE_POLL_MS = 1e4;
|
|
2843
2846
|
var MutableDispatchHandle = class {
|
|
2844
2847
|
id;
|
|
2845
2848
|
status;
|
|
@@ -2860,6 +2863,7 @@ var DispatcherImpl = class {
|
|
|
2860
2863
|
_running = new Map();
|
|
2861
2864
|
_queue = [];
|
|
2862
2865
|
_shuttingDown = false;
|
|
2866
|
+
_memoryPressureTimer = null;
|
|
2863
2867
|
constructor(eventBus, adapterRegistry, config) {
|
|
2864
2868
|
this._eventBus = eventBus;
|
|
2865
2869
|
this._adapterRegistry = adapterRegistry;
|
|
@@ -2874,7 +2878,7 @@ var DispatcherImpl = class {
|
|
|
2874
2878
|
const id = randomUUID();
|
|
2875
2879
|
const resultPromise = new Promise((resolve$2, reject) => {
|
|
2876
2880
|
const typedResolve = resolve$2;
|
|
2877
|
-
if (this._running.size < this._config.maxConcurrency) {
|
|
2881
|
+
if (this._running.size < this._config.maxConcurrency && !this._isMemoryPressured()) {
|
|
2878
2882
|
this._reserveSlot(id);
|
|
2879
2883
|
this._startDispatch(id, request, typedResolve).catch((err) => {
|
|
2880
2884
|
this._running.delete(id);
|
|
@@ -2935,6 +2939,7 @@ var DispatcherImpl = class {
|
|
|
2935
2939
|
}
|
|
2936
2940
|
async shutdown() {
|
|
2937
2941
|
this._shuttingDown = true;
|
|
2942
|
+
this._stopMemoryPressureTimer();
|
|
2938
2943
|
logger$12.info({
|
|
2939
2944
|
running: this._running.size,
|
|
2940
2945
|
queued: this._queue.length
|
|
@@ -3005,6 +3010,8 @@ var DispatcherImpl = class {
|
|
|
3005
3010
|
});
|
|
3006
3011
|
const timeoutMs = timeout ?? this._config.defaultTimeouts[taskType] ?? DEFAULT_TIMEOUTS[taskType] ?? 3e5;
|
|
3007
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();
|
|
3008
3015
|
if (cmd.env !== void 0) Object.assign(env, cmd.env);
|
|
3009
3016
|
if (cmd.unsetEnvKeys !== void 0) for (const key of cmd.unsetEnvKeys) delete env[key];
|
|
3010
3017
|
const proc = spawn(cmd.binary, cmd.args, {
|
|
@@ -3211,9 +3218,16 @@ var DispatcherImpl = class {
|
|
|
3211
3218
|
this._running.set(id, placeholder);
|
|
3212
3219
|
}
|
|
3213
3220
|
_drainQueue() {
|
|
3214
|
-
if (this._queue.length === 0)
|
|
3221
|
+
if (this._queue.length === 0) {
|
|
3222
|
+
this._stopMemoryPressureTimer();
|
|
3223
|
+
return;
|
|
3224
|
+
}
|
|
3215
3225
|
if (this._running.size >= this._config.maxConcurrency) return;
|
|
3216
3226
|
if (this._shuttingDown) return;
|
|
3227
|
+
if (this._isMemoryPressured()) {
|
|
3228
|
+
this._startMemoryPressureTimer();
|
|
3229
|
+
return;
|
|
3230
|
+
}
|
|
3217
3231
|
const next = this._queue.shift();
|
|
3218
3232
|
if (next === void 0) return;
|
|
3219
3233
|
next.handle.status = "running";
|
|
@@ -3227,6 +3241,30 @@ var DispatcherImpl = class {
|
|
|
3227
3241
|
const idx = this._queue.findIndex((q) => q.id === id);
|
|
3228
3242
|
if (idx !== -1) this._queue.splice(idx, 1);
|
|
3229
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
|
+
}
|
|
3230
3268
|
};
|
|
3231
3269
|
/**
|
|
3232
3270
|
* Create a new Dispatcher instance.
|
|
@@ -3920,8 +3958,11 @@ const DEFAULT_VITEST_PATTERNS = `## Test Patterns (defaults)
|
|
|
3920
3958
|
- Mock approach: vi.mock() with hoisting for module-level mocks
|
|
3921
3959
|
- Assertion style: expect().toBe(), expect().toEqual(), expect().toThrow()
|
|
3922
3960
|
- Test structure: describe/it blocks with beforeEach/afterEach
|
|
3923
|
-
- Coverage: 80% enforced
|
|
3924
|
-
-
|
|
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`;
|
|
3925
3966
|
/**
|
|
3926
3967
|
* Execute the compiled dev-story workflow.
|
|
3927
3968
|
*
|
|
@@ -6301,16 +6342,43 @@ function createArtifactExistsGate(phase, artifactType) {
|
|
|
6301
6342
|
}
|
|
6302
6343
|
async function noOp(_db, _runId) {}
|
|
6303
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
|
+
/**
|
|
6304
6370
|
* Create the Analysis phase definition.
|
|
6305
6371
|
*
|
|
6306
|
-
* 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
|
|
6307
6374
|
* Exit gates: 'product-brief' artifact must exist for this run
|
|
6308
6375
|
*/
|
|
6309
|
-
function createAnalysisPhaseDefinition() {
|
|
6376
|
+
function createAnalysisPhaseDefinition(options) {
|
|
6377
|
+
const entryGates = options?.requiresResearch === true ? [createArtifactExistsGate("research", "research-findings")] : [];
|
|
6310
6378
|
return {
|
|
6311
6379
|
name: "analysis",
|
|
6312
6380
|
description: "Analyze the user concept and produce a product brief capturing requirements, constraints, and goals.",
|
|
6313
|
-
entryGates
|
|
6381
|
+
entryGates,
|
|
6314
6382
|
exitGates: [createArtifactExistsGate("analysis", "product-brief")],
|
|
6315
6383
|
onEnter: async (_db, runId) => {
|
|
6316
6384
|
logPhase(`Analysis phase starting for run ${runId}`);
|
|
@@ -6425,13 +6493,19 @@ function createImplementationPhaseDefinition() {
|
|
|
6425
6493
|
/**
|
|
6426
6494
|
* Return the built-in phase definitions in execution order.
|
|
6427
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
|
+
*
|
|
6428
6499
|
* When `uxDesignEnabled` is true, the `ux-design` phase is inserted between
|
|
6429
6500
|
* `planning` and `solutioning`, with its own entry/exit gates.
|
|
6430
6501
|
*
|
|
6431
6502
|
* @param config - Optional configuration for conditional phase inclusion
|
|
6432
6503
|
*/
|
|
6433
6504
|
function createBuiltInPhases(config) {
|
|
6434
|
-
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());
|
|
6435
6509
|
if (config?.uxDesignEnabled === true) phases.push(createUxDesignPhaseDefinition());
|
|
6436
6510
|
phases.push(createSolutioningPhaseDefinition());
|
|
6437
6511
|
phases.push(createImplementationPhaseDefinition());
|
|
@@ -6498,8 +6572,12 @@ var PhaseOrchestratorImpl = class {
|
|
|
6498
6572
|
this._db = deps.db;
|
|
6499
6573
|
this._pack = deps.pack;
|
|
6500
6574
|
this._qualityGates = deps.qualityGates;
|
|
6575
|
+
const researchEnabled = this._pack.manifest.research === true;
|
|
6501
6576
|
const uxDesignEnabled = this._pack.manifest.uxDesign === true;
|
|
6502
|
-
this._phases = createBuiltInPhases({
|
|
6577
|
+
this._phases = createBuiltInPhases({
|
|
6578
|
+
researchEnabled,
|
|
6579
|
+
uxDesignEnabled
|
|
6580
|
+
});
|
|
6503
6581
|
const builtInNames = new Set(this._phases.map((p) => p.name));
|
|
6504
6582
|
const packPhases = this._pack.getPhases();
|
|
6505
6583
|
for (const packPhase of packPhases) if (!builtInNames.has(packPhase.name)) this._phases.push({
|
|
@@ -6816,7 +6894,8 @@ function getCritiquePromptName(phase) {
|
|
|
6816
6894
|
planning: "critique-planning",
|
|
6817
6895
|
solutioning: "critique-architecture",
|
|
6818
6896
|
architecture: "critique-architecture",
|
|
6819
|
-
stories: "critique-stories"
|
|
6897
|
+
stories: "critique-stories",
|
|
6898
|
+
research: "critique-research"
|
|
6820
6899
|
};
|
|
6821
6900
|
return mapping[phase] ?? `critique-${phase}`;
|
|
6822
6901
|
}
|
|
@@ -7462,6 +7541,31 @@ const UxJourneysOutputSchema = z.object({
|
|
|
7462
7541
|
accessibility_guidelines: z.array(z.string()).default([])
|
|
7463
7542
|
});
|
|
7464
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
|
+
/**
|
|
7465
7569
|
* Zod schema for the YAML output emitted by an elicitation sub-agent.
|
|
7466
7570
|
* The agent returns structured insights from applying an elicitation method.
|
|
7467
7571
|
*/
|
|
@@ -9936,6 +10040,162 @@ async function runUxDesignPhase(deps, params) {
|
|
|
9936
10040
|
}
|
|
9937
10041
|
}
|
|
9938
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
|
+
|
|
9939
10199
|
//#endregion
|
|
9940
10200
|
//#region src/cli/commands/run.ts
|
|
9941
10201
|
const logger = createLogger("run-cmd");
|
|
@@ -9955,7 +10215,7 @@ function mapInternalPhaseToEventPhase(internalPhase) {
|
|
|
9955
10215
|
}
|
|
9956
10216
|
}
|
|
9957
10217
|
async function runRunAction(options) {
|
|
9958
|
-
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;
|
|
9959
10219
|
if (startPhase !== void 0 && !VALID_PHASES.includes(startPhase)) {
|
|
9960
10220
|
const errorMsg = `Invalid phase '${startPhase}'. Valid phases: ${VALID_PHASES.join(", ")}`;
|
|
9961
10221
|
if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
@@ -10011,7 +10271,9 @@ async function runRunAction(options) {
|
|
|
10011
10271
|
outputFormat,
|
|
10012
10272
|
projectRoot,
|
|
10013
10273
|
...eventsFlag === true ? { events: true } : {},
|
|
10014
|
-
...skipUx === true ? { skipUx: true } : {}
|
|
10274
|
+
...skipUx === true ? { skipUx: true } : {},
|
|
10275
|
+
...researchFlag === true ? { research: true } : {},
|
|
10276
|
+
...skipResearchFlag === true ? { skipResearch: true } : {}
|
|
10015
10277
|
});
|
|
10016
10278
|
let storyKeys = [];
|
|
10017
10279
|
if (storiesArg !== void 0 && storiesArg !== "") {
|
|
@@ -10451,7 +10713,7 @@ async function runRunAction(options) {
|
|
|
10451
10713
|
}
|
|
10452
10714
|
}
|
|
10453
10715
|
async function runFullPipeline(options) {
|
|
10454
|
-
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;
|
|
10455
10717
|
if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
|
|
10456
10718
|
const dbWrapper = new DatabaseWrapper(dbPath);
|
|
10457
10719
|
try {
|
|
@@ -10491,13 +10753,19 @@ async function runFullPipeline(options) {
|
|
|
10491
10753
|
contextCompiler,
|
|
10492
10754
|
dispatcher
|
|
10493
10755
|
};
|
|
10494
|
-
|
|
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 = {
|
|
10495
10762
|
...pack,
|
|
10496
10763
|
manifest: {
|
|
10497
10764
|
...pack.manifest,
|
|
10498
|
-
|
|
10765
|
+
research: effectiveResearch,
|
|
10766
|
+
uxDesign: effectiveUxDesign
|
|
10499
10767
|
}
|
|
10500
|
-
}
|
|
10768
|
+
};
|
|
10501
10769
|
const phaseOrchestrator = createPhaseOrchestrator({
|
|
10502
10770
|
db,
|
|
10503
10771
|
pack: packForOrchestrator
|
|
@@ -10508,19 +10776,11 @@ async function runFullPipeline(options) {
|
|
|
10508
10776
|
process.stdout.write(`Starting full pipeline from phase: ${startPhase}\n`);
|
|
10509
10777
|
process.stdout.write(`Pipeline run ID: ${runId}\n`);
|
|
10510
10778
|
}
|
|
10511
|
-
const
|
|
10512
|
-
|
|
10513
|
-
|
|
10514
|
-
|
|
10515
|
-
|
|
10516
|
-
"solutioning",
|
|
10517
|
-
"implementation"
|
|
10518
|
-
] : [
|
|
10519
|
-
"analysis",
|
|
10520
|
-
"planning",
|
|
10521
|
-
"solutioning",
|
|
10522
|
-
"implementation"
|
|
10523
|
-
];
|
|
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");
|
|
10524
10784
|
const startIdx = phaseOrder.indexOf(startPhase);
|
|
10525
10785
|
for (let i = startIdx; i < phaseOrder.length; i++) {
|
|
10526
10786
|
const currentPhase = phaseOrder[i];
|
|
@@ -10574,6 +10834,32 @@ async function runFullPipeline(options) {
|
|
|
10574
10834
|
process.stdout.write(`[PLANNING] Complete — ${result.requirements_count ?? 0} requirements, ${result.user_stories_count ?? 0} user stories\n`);
|
|
10575
10835
|
process.stdout.write(` Tokens: ${result.tokenUsage.input.toLocaleString()} input / ${result.tokenUsage.output.toLocaleString()} output\n`);
|
|
10576
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
|
+
}
|
|
10577
10863
|
} else if (currentPhase === "ux-design") {
|
|
10578
10864
|
const result = await runUxDesignPhase(phaseDeps, { runId });
|
|
10579
10865
|
if (result.tokenUsage.input > 0 || result.tokenUsage.output > 0) {
|
|
@@ -10738,7 +11024,7 @@ async function runFullPipeline(options) {
|
|
|
10738
11024
|
}
|
|
10739
11025
|
}
|
|
10740
11026
|
function registerRunCommand(program, _version = "0.0.0", projectRoot = process.cwd()) {
|
|
10741
|
-
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), 3).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").action(async (opts) => {
|
|
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), 3).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) => {
|
|
10742
11028
|
if (opts.helpAgent) {
|
|
10743
11029
|
process.exitCode = await runHelpAgent();
|
|
10744
11030
|
return;
|
|
@@ -10768,7 +11054,9 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
|
|
|
10768
11054
|
events: opts.events,
|
|
10769
11055
|
verbose: opts.verbose,
|
|
10770
11056
|
tui: opts.tui,
|
|
10771
|
-
skipUx: opts.skipUx
|
|
11057
|
+
skipUx: opts.skipUx,
|
|
11058
|
+
research: opts.research,
|
|
11059
|
+
skipResearch: opts.skipResearch
|
|
10772
11060
|
});
|
|
10773
11061
|
process.exitCode = exitCode;
|
|
10774
11062
|
});
|
|
@@ -10776,4 +11064,4 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
|
|
|
10776
11064
|
|
|
10777
11065
|
//#endregion
|
|
10778
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 };
|
|
10779
|
-
//# sourceMappingURL=run-
|
|
11067
|
+
//# sourceMappingURL=run-CTOLQ2MR.js.map
|
package/package.json
CHANGED
package/packs/bmad/manifest.yaml
CHANGED
|
@@ -2,12 +2,37 @@ name: bmad
|
|
|
2
2
|
version: 1.0.0
|
|
3
3
|
description: BMAD methodology for autonomous software development
|
|
4
4
|
|
|
5
|
+
# Optional research phase (Story 20.1).
|
|
6
|
+
# When true, a 'research' phase runs before analysis, conducting market/competitive research.
|
|
7
|
+
# Set to false (or omit) to skip research and proceed directly to analysis.
|
|
8
|
+
research: true
|
|
9
|
+
|
|
5
10
|
# Optional UX design phase (Story 16.5).
|
|
6
11
|
# When true, a 'ux-design' phase runs between planning and solutioning.
|
|
7
12
|
# Set to false (or omit) to skip UX design and proceed directly to solutioning.
|
|
8
13
|
uxDesign: true
|
|
9
14
|
|
|
10
15
|
phases:
|
|
16
|
+
- name: research
|
|
17
|
+
description: Market research, competitive landscape analysis, and technical feasibility (runs before analysis when research is enabled)
|
|
18
|
+
entryGates: []
|
|
19
|
+
exitGates: [research-complete]
|
|
20
|
+
artifacts: [research-findings]
|
|
21
|
+
steps:
|
|
22
|
+
- name: research-step-1-discovery
|
|
23
|
+
template: research-step-1-discovery
|
|
24
|
+
context:
|
|
25
|
+
- placeholder: concept
|
|
26
|
+
source: "param:concept"
|
|
27
|
+
elicitate: true
|
|
28
|
+
- name: research-step-2-synthesis
|
|
29
|
+
template: research-step-2-synthesis
|
|
30
|
+
context:
|
|
31
|
+
- placeholder: concept
|
|
32
|
+
source: "param:concept"
|
|
33
|
+
- placeholder: raw_findings
|
|
34
|
+
source: "step:research-step-1-discovery"
|
|
35
|
+
critique: true
|
|
11
36
|
- name: analysis
|
|
12
37
|
description: Product discovery and brief creation
|
|
13
38
|
entryGates: []
|
|
@@ -19,6 +44,8 @@ phases:
|
|
|
19
44
|
context:
|
|
20
45
|
- placeholder: concept
|
|
21
46
|
source: "param:concept"
|
|
47
|
+
- placeholder: research_findings
|
|
48
|
+
source: "decision:research.findings"
|
|
22
49
|
elicitate: true
|
|
23
50
|
- name: analysis-step-2-scope
|
|
24
51
|
template: analysis-step-2-scope
|
|
@@ -175,6 +202,10 @@ prompts:
|
|
|
175
202
|
architecture-step-3-patterns: prompts/architecture-step-3-patterns.md
|
|
176
203
|
stories-step-1-epics: prompts/stories-step-1-epics.md
|
|
177
204
|
stories-step-2-stories: prompts/stories-step-2-stories.md
|
|
205
|
+
# Research phase prompts (Story 20-2)
|
|
206
|
+
research-step-1-discovery: prompts/research-step-1-discovery.md
|
|
207
|
+
research-step-2-synthesis: prompts/research-step-2-synthesis.md
|
|
208
|
+
critique-research: prompts/critique-research.md
|
|
178
209
|
# UX design step prompts (Story 16-5)
|
|
179
210
|
ux-step-1-discovery: prompts/ux-step-1-discovery.md
|
|
180
211
|
ux-step-2-design-system: prompts/ux-step-2-design-system.md
|
|
@@ -5,12 +5,17 @@
|
|
|
5
5
|
### Project Concept
|
|
6
6
|
{{concept}}
|
|
7
7
|
|
|
8
|
+
### Research Context
|
|
9
|
+
{{research_findings}}
|
|
10
|
+
|
|
8
11
|
---
|
|
9
12
|
|
|
10
13
|
## Mission
|
|
11
14
|
|
|
12
15
|
Analyze the project concept above and produce a focused **vision analysis**: a clear problem statement and identification of target users. Do NOT define features or metrics yet — those come in a subsequent step.
|
|
13
16
|
|
|
17
|
+
When Research Context is provided above, ground your vision analysis in that evidence: reference specific market signals, competitive gaps, or feasibility findings to justify your problem statement and user segmentation. When Research Context is empty, proceed using the concept alone — output quality must be identical.
|
|
18
|
+
|
|
14
19
|
## Instructions
|
|
15
20
|
|
|
16
21
|
1. **Analyze the concept deeply:**
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# BMAD Critique Agent — Research Phase
|
|
2
|
+
|
|
3
|
+
## Artifact Under Review
|
|
4
|
+
|
|
5
|
+
{{artifact_content}}
|
|
6
|
+
|
|
7
|
+
## Project Context
|
|
8
|
+
|
|
9
|
+
{{project_context}}
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Your Role
|
|
14
|
+
|
|
15
|
+
You are an adversarial quality reviewer. Your job is to find what's wrong with this research document before the team builds a product brief on a flawed foundation.
|
|
16
|
+
|
|
17
|
+
Adopt a critical mindset: assume the research is incomplete, biased, or stale until proven otherwise.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Quality Standards for Research Artifacts
|
|
22
|
+
|
|
23
|
+
A high-quality research artifact must satisfy ALL of these criteria:
|
|
24
|
+
|
|
25
|
+
### 1. Source Credibility
|
|
26
|
+
- Findings must reference identifiable, credible sources (industry reports, named companies, published standards, or well-known open source projects).
|
|
27
|
+
- Vague attributions like "industry experts say" or "research shows" without specifics are unacceptable.
|
|
28
|
+
- Market sizing claims must include a source or methodology (e.g., "Gartner 2024", "company 10-K", "author's estimate based on TAM").
|
|
29
|
+
- At minimum, 2-3 named companies or products must be referenced as evidence.
|
|
30
|
+
|
|
31
|
+
### 2. Finding Relevance
|
|
32
|
+
- Every finding must be directly relevant to the stated concept — tangential observations about adjacent markets are noise.
|
|
33
|
+
- Market findings must describe the actual target buyer, not a proxy audience.
|
|
34
|
+
- Technical findings must reflect the technology decisions the concept will actually face, not hypothetical stacks.
|
|
35
|
+
- Risk flags must be specific and actionable (not generic "the market is competitive").
|
|
36
|
+
|
|
37
|
+
### 3. Gap Identification
|
|
38
|
+
- The research must acknowledge what it does NOT know — gaps are acceptable, but must be named explicitly.
|
|
39
|
+
- If web search was unavailable, the agent must state that findings are based on training knowledge and may be stale.
|
|
40
|
+
- Missing dimensions: if any of market, competitive, technical, or risk analysis is absent, it is a blocker.
|
|
41
|
+
- Opportunity signals must be grounded in research — speculative "we could do X" signals are unacceptable.
|
|
42
|
+
|
|
43
|
+
### 4. Synthesis Coherence
|
|
44
|
+
- The competitive landscape must identify named competitors, not generic categories ("some incumbents").
|
|
45
|
+
- Risk flags must be distinct from each other — no duplicates or slight rewording of the same risk.
|
|
46
|
+
- Opportunity signals must logically follow from the findings — they must be traceable to specific evidence in the research.
|
|
47
|
+
- Market context and competitive landscape must be internally consistent — contradictions are blockers.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Instructions
|
|
52
|
+
|
|
53
|
+
1. Read the artifact carefully. Do not assume anything is correct.
|
|
54
|
+
2. For each quality dimension above, identify whether it is met, partially met, or missing.
|
|
55
|
+
3. For each issue found, classify its severity:
|
|
56
|
+
- **blocker**: The research cannot be used to proceed — a critical dimension is missing, contradictory, or completely uncredible.
|
|
57
|
+
- **major**: Significant quality gap that will bias the product brief if not addressed.
|
|
58
|
+
- **minor**: Improvement that would increase quality but does not block progress.
|
|
59
|
+
|
|
60
|
+
4. If the artifact meets all criteria, emit a `pass` verdict with zero issues.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Output Contract
|
|
65
|
+
|
|
66
|
+
Emit ONLY this YAML block — no preamble, no explanation, no other text.
|
|
67
|
+
|
|
68
|
+
If no issues found:
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
verdict: pass
|
|
72
|
+
issue_count: 0
|
|
73
|
+
issues: []
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
If issues found:
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
verdict: needs_work
|
|
80
|
+
issue_count: 2
|
|
81
|
+
issues:
|
|
82
|
+
- severity: major
|
|
83
|
+
category: source-credibility
|
|
84
|
+
description: "Market size claim of '$15B by 2027' has no cited source or methodology."
|
|
85
|
+
suggestion: "Add the source (e.g., 'per Gartner 2024 Cloud Infrastructure Report') or note it as an author estimate with the derivation method."
|
|
86
|
+
- severity: minor
|
|
87
|
+
category: finding-relevance
|
|
88
|
+
description: "Technical findings describe a microservices architecture that is not relevant to the stated single-tenant SaaS concept."
|
|
89
|
+
suggestion: "Replace with findings specific to single-tenant deployment patterns, data isolation models, and per-tenant customization approaches."
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**IMPORTANT**: `issue_count` must equal the exact number of items in `issues`.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# BMAD Research Step 1: Discovery
|
|
2
|
+
|
|
3
|
+
## Context (pre-assembled by pipeline)
|
|
4
|
+
|
|
5
|
+
### Concept
|
|
6
|
+
{{concept}}
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Mission
|
|
11
|
+
|
|
12
|
+
Conduct a thorough **research discovery** for this concept. Your goal is to gather and organize raw findings across three dimensions:
|
|
13
|
+
|
|
14
|
+
1. **Concept Classification** — what type of product or tool is this, who is it for, and what domain does it operate in?
|
|
15
|
+
2. **Market Findings** — market size, target customers, pricing models, and market trends
|
|
16
|
+
3. **Domain Findings** — best practices, industry standards, regulatory requirements, and use cases
|
|
17
|
+
4. **Technical Findings** — technical architecture patterns, technology stacks, open source alternatives, and implementation challenges
|
|
18
|
+
|
|
19
|
+
This raw discovery output will feed directly into a synthesis step that distills the findings into actionable insights.
|
|
20
|
+
|
|
21
|
+
## Instructions
|
|
22
|
+
|
|
23
|
+
### 1. Classify the Concept
|
|
24
|
+
|
|
25
|
+
Before searching, classify the concept:
|
|
26
|
+
- **Product type**: Is this a product sold to customers, or an internal tool / developer tooling?
|
|
27
|
+
- **Industry vertical**: What industry or sector does it primarily serve (e.g., fintech, healthcare, devtools, SaaS platform, e-commerce)?
|
|
28
|
+
- **Tech domain**: What is the primary technical domain (e.g., data pipelines, mobile apps, APIs, AI/ML, infrastructure)?
|
|
29
|
+
|
|
30
|
+
### 2. Conduct Web Research
|
|
31
|
+
|
|
32
|
+
Use web search to gather findings across the three dimensions below. Execute approximately 12 searches total — 3-4 per dimension.
|
|
33
|
+
|
|
34
|
+
**Market dimension queries:**
|
|
35
|
+
- `"{{concept}} market size"`
|
|
36
|
+
- `"{{concept}} target customers"`
|
|
37
|
+
- `"{{concept}} pricing models"`
|
|
38
|
+
- `"{{concept}} market trends 2025"`
|
|
39
|
+
|
|
40
|
+
**Domain dimension queries:**
|
|
41
|
+
- `"{{concept}} best practices"`
|
|
42
|
+
- `"{{concept}} industry standards"`
|
|
43
|
+
- `"{{concept}} regulatory requirements"`
|
|
44
|
+
- `"{{concept}} use cases"`
|
|
45
|
+
|
|
46
|
+
**Technical dimension queries:**
|
|
47
|
+
- `"{{concept}} technical architecture"`
|
|
48
|
+
- `"{{concept}} technology stack"`
|
|
49
|
+
- `"{{concept}} open source alternatives"`
|
|
50
|
+
- `"{{concept}} implementation challenges"`
|
|
51
|
+
|
|
52
|
+
> **Fallback**: If web search is unavailable in your environment, proceed with concept analysis using your training knowledge — acknowledge that findings may not reflect the latest market conditions.
|
|
53
|
+
|
|
54
|
+
### 3. Organize Findings
|
|
55
|
+
|
|
56
|
+
For each dimension, summarize the key findings in 2-4 sentences. Be specific: name actual companies, technologies, standards, or regulations where found. Avoid vague generalizations.
|
|
57
|
+
|
|
58
|
+
## Output Contract
|
|
59
|
+
|
|
60
|
+
Emit ONLY this YAML block as your final output — no other text, no preamble.
|
|
61
|
+
|
|
62
|
+
**CRITICAL**: All string values MUST be quoted with double quotes.
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
result: success
|
|
66
|
+
concept_classification: "B2B SaaS product targeting mid-market DevOps teams in the cloud infrastructure space"
|
|
67
|
+
market_findings: "The cloud infrastructure automation market is valued at $12B in 2024, growing at 18% CAGR. Primary customers are platform engineering teams at companies with 50-500 engineers. Pricing models cluster around per-seat ($30-80/month) and usage-based (per compute hour). Key trend: shift from IaaS to developer-experience platforms."
|
|
68
|
+
domain_findings: "Industry standards include Terraform HCL for IaC and GitOps workflows (CNCF). Regulatory requirements vary by industry: SOC 2 Type II is table stakes for enterprise; HIPAA for healthcare customers. Key use cases: multi-cloud deployment, drift detection, cost optimization, and compliance reporting."
|
|
69
|
+
technical_findings: "Dominant architectural pattern is event-driven with a control plane / data plane separation. Common stack: Go or Rust for the agent, React for dashboard, PostgreSQL + TimescaleDB for time-series data. Open source alternatives include Pulumi, OpenTofu, and Crossplane. Primary implementation challenges are state reconciliation under network partitions and secret management at scale."
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
If you cannot produce valid output:
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
result: failed
|
|
76
|
+
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# BMAD Research Step 2: Synthesis
|
|
2
|
+
|
|
3
|
+
## Context (pre-assembled by pipeline)
|
|
4
|
+
|
|
5
|
+
### Concept
|
|
6
|
+
{{concept}}
|
|
7
|
+
|
|
8
|
+
### Raw Research Findings
|
|
9
|
+
{{raw_findings}}
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Mission
|
|
14
|
+
|
|
15
|
+
Synthesize the raw research findings into a structured, actionable research report. Your goal is to distill the discovery output into five key sections:
|
|
16
|
+
|
|
17
|
+
1. **Market Context** — the market landscape, sizing, and customer dynamics
|
|
18
|
+
2. **Competitive Landscape** — who the key competitors are, their positioning, and differentiation opportunities
|
|
19
|
+
3. **Technical Feasibility** — how technically viable this concept is, key technology choices, and build vs. buy considerations
|
|
20
|
+
4. **Risk Flags** — specific risks that could threaten success (market, technical, regulatory, execution)
|
|
21
|
+
5. **Opportunity Signals** — specific indicators of where this concept has an advantage or untapped potential
|
|
22
|
+
|
|
23
|
+
This synthesis output feeds directly into the analysis phase to ground the product brief in real-world context.
|
|
24
|
+
|
|
25
|
+
## Instructions
|
|
26
|
+
|
|
27
|
+
1. **Market Context**: Synthesize the market dimension findings. Quantify the opportunity where possible. Identify the primary buyer profile and decision-maker. Note any market timing signals (growing, contracting, consolidating).
|
|
28
|
+
|
|
29
|
+
2. **Competitive Landscape**: Identify named competitors (direct and adjacent). Describe how they are positioned. Identify gaps or differentiation opportunities that the concept could exploit.
|
|
30
|
+
|
|
31
|
+
3. **Technical Feasibility**: Assess how technically achievable the concept is given the technology landscape. Highlight proven patterns to adopt, and identify areas where the technical approach is risky or unproven.
|
|
32
|
+
|
|
33
|
+
4. **Risk Flags**: List 3-6 specific, concrete risks. Each risk should name the threat and its potential impact. Avoid generic risks like "execution risk" — be specific (e.g., "Compliance with HIPAA BAA requirements may add 3-6 months to enterprise sales cycles").
|
|
34
|
+
|
|
35
|
+
5. **Opportunity Signals**: List 3-6 specific indicators that suggest this concept has real potential. These should be grounded in the research findings, not wishful thinking.
|
|
36
|
+
|
|
37
|
+
## Output Contract
|
|
38
|
+
|
|
39
|
+
Emit ONLY this YAML block as your final output — no other text, no preamble.
|
|
40
|
+
|
|
41
|
+
**CRITICAL**: All string values MUST be quoted with double quotes. List items in `risk_flags` and `opportunity_signals` must also be double-quoted.
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
result: success
|
|
45
|
+
market_context: "The cloud infrastructure automation market is a $12B opportunity growing at 18% CAGR, driven by the shift from DevOps to platform engineering. Primary buyers are VPs of Engineering and Platform Engineering leads at Series B+ startups and mid-market companies. Market is in early growth phase with high willingness to pay for workflow automation."
|
|
46
|
+
competitive_landscape: "Direct competitors are Terraform Cloud (HashiCorp/IBM), Spacelift, and Scalr — all targeting the same DevOps persona. Pulumi competes on developer experience with a code-first approach. Differentiation opportunity: none of the incumbent tools offer AI-assisted drift detection or natural-language policy authoring. Open source (OpenTofu) commoditizes the IaC layer, making the control plane the primary value surface."
|
|
47
|
+
technical_feasibility: "High feasibility using proven patterns: Go agent with event-driven control plane (used by Argo CD, Flux), React dashboard, and PostgreSQL for state. Primary technical risk is distributed state reconciliation under network partitions. Build recommendation: agent core in Go, leverage existing Terraform/OpenTofu compatibility, avoid building a custom DSL."
|
|
48
|
+
risk_flags:
|
|
49
|
+
- "Regulatory: HIPAA and SOC 2 Type II compliance are table stakes for enterprise sales — adds 4-6 months to first enterprise close"
|
|
50
|
+
- "Competitive: HashiCorp's BSL license change accelerated OpenTofu adoption — if IBM reverses the decision, momentum could shift back"
|
|
51
|
+
- "Technical: Distributed state reconciliation under network partitions is an unsolved problem that all incumbents struggle with — high engineering cost"
|
|
52
|
+
- "Market: Per-seat pricing erodes at scale (>500 engineers) — customers will demand volume discounts or switch to usage-based pricing"
|
|
53
|
+
opportunity_signals:
|
|
54
|
+
- "AI-native workflows: no incumbent offers natural-language policy authoring or AI-assisted remediation — clear whitespace"
|
|
55
|
+
- "OpenTofu migration wave: 30%+ of Terraform users are evaluating alternatives following the BSL license change — timing is favorable"
|
|
56
|
+
- "Platform engineering trend: Gartner predicts 80% of large orgs will have platform engineering teams by 2026 — growing buyer segment"
|
|
57
|
+
- "Developer experience gap: incumbent UIs are functional but dated — a modern, keyboard-first interface is a differentiator"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
If you cannot produce valid output:
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
result: failed
|
|
64
|
+
```
|