substrate-ai 0.2.4 → 0.2.6

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