substrate-ai 0.2.5 → 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/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-CDYE1PT3.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-0IlA2ubQ.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-XkrV99HV.js"
2836
+ "../run-Chc5BzIz.js"
2837
2837
  );
2838
2838
  const runStoryFn = async (opts) => {
2839
2839
  const exitCode = await runPipeline({
@@ -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) return;
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 — run full suite, not filtered
3924
- - Run tests: npm test 2>&1 | grep -E "Test Files|Tests " | tail -3`;
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 = [createAnalysisPhaseDefinition(), createPlanningPhaseDefinition()];
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({ uxDesignEnabled });
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
- const packForOrchestrator = skipUx === true && pack.manifest.uxDesign === true ? {
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
- uxDesign: false
10765
+ research: effectiveResearch,
10766
+ uxDesign: effectiveUxDesign
10499
10767
  }
10500
- } : pack;
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 uxEnabled = packForOrchestrator.manifest.uxDesign === true;
10512
- const phaseOrder = uxEnabled ? [
10513
- "analysis",
10514
- "planning",
10515
- "ux-design",
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), 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) => {
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-CDYE1PT3.js.map
11067
+ //# sourceMappingURL=run-0IlA2ubQ.js.map
@@ -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-CDYE1PT3.js";
3
+ import { registerRunCommand, runRunAction } from "./run-0IlA2ubQ.js";
4
4
  import "./decisions-DNYByk0U.js";
5
5
  import "./metrics-BSg8VIHd.js";
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "substrate-ai",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Substrate — multi-agent orchestration daemon for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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: []
@@ -175,6 +200,10 @@ prompts:
175
200
  architecture-step-3-patterns: prompts/architecture-step-3-patterns.md
176
201
  stories-step-1-epics: prompts/stories-step-1-epics.md
177
202
  stories-step-2-stories: prompts/stories-step-2-stories.md
203
+ # Research phase prompts (Story 20-2)
204
+ research-step-1-discovery: prompts/research-step-1-discovery.md
205
+ research-step-2-synthesis: prompts/research-step-2-synthesis.md
206
+ critique-research: prompts/critique-research.md
178
207
  # UX design step prompts (Story 16-5)
179
208
  ux-step-1-discovery: prompts/ux-step-1-discovery.md
180
209
  ux-step-2-design-system: prompts/ux-step-2-design-system.md
@@ -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
+ ```