substrate-ai 0.20.23 → 0.20.27

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
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { FileStateStore, RunManifest, SUBSTRATE_OWNED_SETTINGS_KEYS, SupervisorLock, VALID_PHASES, WorkGraphRepository, ZERO_FINDING_COUNTS, buildPipelineStatusOutput, createDatabaseAdapter, createStateStore, findPackageRoot, formatOutput, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, inspectProcessTree, parseDbTimestampAsUtc, registerHealthCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveRunManifest, rollupFindingCounts } from "../health-Cq8K_jrJ.js";
2
+ import { FileStateStore, RunManifest, SUBSTRATE_OWNED_SETTINGS_KEYS, SupervisorLock, VALID_PHASES, WorkGraphRepository, ZERO_FINDING_COUNTS, buildPipelineStatusOutput, createDatabaseAdapter, createStateStore, findPackageRoot, formatOutput, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, inspectProcessTree, parseDbTimestampAsUtc, registerHealthCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveRunManifest, rollupFindingCounts } from "../health-0_axmI2t.js";
3
3
  import { createLogger } from "../logger-KeHncl-f.js";
4
4
  import { createEventBus } from "../helpers-CElYrONe.js";
5
5
  import { AdapterRegistry, BudgetConfigSchema, CURRENT_CONFIG_FORMAT_VERSION, CURRENT_TASK_GRAPH_VERSION, ConfigError, CostTrackerConfigSchema, DEFAULT_CONFIG, DoltClient, DoltNotInstalled, GlobalSettingsSchema, IngestionServer, MonitorDatabaseImpl, OPERATIONAL_FINDING, PartialGlobalSettingsSchema, PartialProviderConfigSchema, ProvidersSchema, RoutingRecommender, STORY_METRICS, TelemetryConfigSchema, addTokenUsage, aggregateTokenUsageForRun, checkDoltInstalled, compareRunMetrics, createAmendmentRun, createConfigSystem, createDecision, createDoltClient, createPipelineRun, getActiveDecisions, getAllCostEntriesFiltered, getBaselineRunMetrics, getDecisionsByCategory, getDecisionsByPhaseForRun, getLatestCompletedRun, getLatestRun, getPipelineRunById, getPlanningCostTotal, getRetryableEscalations, getRunMetrics, getRunningPipelineRuns, getSessionCostSummary, getSessionCostSummaryFiltered, getStoryMetricsForRun, getTokenUsageSummary, incrementRunRestarts, initSchema, initializeDolt, listRunMetrics, loadParentRunDecisions, supersedeDecision, tagRunAsBaseline, updatePipelineRun } from "../dist-CqtWS9wF.js";
6
6
  import "../adapter-registry-DXLMTmfD.js";
7
- import { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, EpicIngester, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, registerExportCommand, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-B3e4O0Rk.js";
7
+ import { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, EpicIngester, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, registerExportCommand, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-DQcG05Ar.js";
8
8
  import "../errors-1uLGqnvr.js";
9
9
  import "../routing-CcBOCuC9.js";
10
10
  import "../decisions-C0pz9Clx.js";
@@ -3667,7 +3667,7 @@ async function runStatusAction(options) {
3667
3667
  logger$12.debug({ err }, "Work graph query failed, continuing without work graph data");
3668
3668
  }
3669
3669
  if (run === void 0) {
3670
- const { inspectProcessTree: inspectProcessTree$1 } = await import("../health-CsRLsKgu.js");
3670
+ const { inspectProcessTree: inspectProcessTree$1 } = await import("../health-CrEdV2B3.js");
3671
3671
  const substrateDirPath = join(projectRoot, ".substrate");
3672
3672
  const processInfo = inspectProcessTree$1({
3673
3673
  projectRoot,
@@ -5198,7 +5198,7 @@ async function runSupervisorAction(options, deps = {}) {
5198
5198
  await initSchema(expAdapter);
5199
5199
  const { runRunAction: runPipeline } = await import(
5200
5200
  /* @vite-ignore */
5201
- "../run-DEeTPCdU.js"
5201
+ "../run-DQ29oNG2.js"
5202
5202
  );
5203
5203
  const runStoryFn = async (opts) => {
5204
5204
  const exitCode = await runPipeline({
@@ -3428,13 +3428,24 @@ const PROBE_TAIL_BYTES = 4 * 1024;
3428
3428
  * Required fields (`name`, `sandbox`, `command`) force authors to make
3429
3429
  * intent explicit — no silent defaults that could mask a miswritten probe.
3430
3430
  * Optional fields cover operational knobs with sensible fallbacks.
3431
+ *
3432
+ * Story 60-4: `expect_stdout_no_regex` and `expect_stdout_regex` close the
3433
+ * exit-0-with-error-body gap. A probe that calls a tool returning HTTP 200
3434
+ * with `{"isError": true}` (MCP convention) or `{"status": "error"}` (REST
3435
+ * convention) exits 0 — exit-code-only verification accepts the broken tool
3436
+ * as passing. Authors of probes that hit MCP / REST / JSON-RPC / A2A surfaces
3437
+ * declare success-shape patterns to assert response payload structure beyond
3438
+ * the shell exit code. Driven by strata Run 12 evidence: four MCP tools
3439
+ * shipped SHIP_IT while throwing real Python TypeErrors against real data.
3431
3440
  */
3432
3441
  const RuntimeProbeSchema = z.object({
3433
3442
  name: z.string().min(1, "probe name is required"),
3434
3443
  sandbox: RuntimeProbeSandboxSchema,
3435
3444
  command: z.string().min(1, "probe command is required"),
3436
3445
  timeout_ms: z.number().int().positive().optional(),
3437
- description: z.string().optional()
3446
+ description: z.string().optional(),
3447
+ expect_stdout_no_regex: z.array(z.string().min(1)).optional(),
3448
+ expect_stdout_regex: z.array(z.string().min(1)).optional()
3438
3449
  });
3439
3450
  /** Zod schema for the full list (wrapping the per-probe schema). */
3440
3451
  const RuntimeProbeListSchema = z.array(RuntimeProbeSchema);
@@ -3586,6 +3597,46 @@ function tail(text, bytes = PROBE_TAIL_BYTES) {
3586
3597
  return text.length <= bytes ? text : text.slice(text.length - bytes);
3587
3598
  }
3588
3599
  /**
3600
+ * Story 60-4: evaluate `expect_stdout_no_regex` and `expect_stdout_regex`
3601
+ * patterns against the captured stdout. Runs against the full (un-tailed)
3602
+ * stdout so authors can match payload shape even when the response is
3603
+ * larger than PROBE_TAIL_BYTES.
3604
+ *
3605
+ * Returns an array of human-readable failure descriptions. Empty array
3606
+ * means all assertions passed.
3607
+ *
3608
+ * Invalid regex patterns (RegExp constructor throws) are reported as
3609
+ * assertion failures themselves rather than crashing the executor — this
3610
+ * way a typo in one author's probe surfaces as a deterministic finding,
3611
+ * not a pipeline crash that masks the rest of the run.
3612
+ */
3613
+ function evaluateStdoutAssertions(probe, stdout) {
3614
+ const failures = [];
3615
+ for (const pattern of probe.expect_stdout_no_regex ?? []) {
3616
+ let re;
3617
+ try {
3618
+ re = new RegExp(pattern);
3619
+ } catch (err) {
3620
+ const detail = err instanceof Error ? err.message : String(err);
3621
+ failures.push(`expect_stdout_no_regex pattern is not a valid regex (${detail}): ${pattern}`);
3622
+ continue;
3623
+ }
3624
+ if (re.test(stdout)) failures.push(`expect_stdout_no_regex: stdout matched forbidden pattern: ${pattern}`);
3625
+ }
3626
+ for (const pattern of probe.expect_stdout_regex ?? []) {
3627
+ let re;
3628
+ try {
3629
+ re = new RegExp(pattern);
3630
+ } catch (err) {
3631
+ const detail = err instanceof Error ? err.message : String(err);
3632
+ failures.push(`expect_stdout_regex pattern is not a valid regex (${detail}): ${pattern}`);
3633
+ continue;
3634
+ }
3635
+ if (!re.test(stdout)) failures.push(`expect_stdout_regex: stdout did not match required pattern: ${pattern}`);
3636
+ }
3637
+ return failures;
3638
+ }
3639
+ /**
3589
3640
  * Execute one probe on the host and return a structured ProbeResult.
3590
3641
  *
3591
3642
  * Behavior notes:
@@ -3657,13 +3708,23 @@ function executeProbeOnHost(probe, options = {}) {
3657
3708
  child.on("close", (code) => {
3658
3709
  clearTimeout(timeoutHandle);
3659
3710
  const duration = Date.now() - start;
3711
+ let outcome = code === 0 ? "pass" : "fail";
3712
+ let assertionFailures;
3713
+ if (outcome === "pass") {
3714
+ const failures = evaluateStdoutAssertions(probe, stdout);
3715
+ if (failures.length > 0) {
3716
+ outcome = "fail";
3717
+ assertionFailures = failures;
3718
+ }
3719
+ }
3660
3720
  finalize({
3661
- outcome: code === 0 ? "pass" : "fail",
3721
+ outcome,
3662
3722
  command: probe.command,
3663
3723
  ...code !== null ? { exitCode: code } : {},
3664
3724
  stdoutTail: tail(stdout),
3665
3725
  stderrTail: tail(stderr),
3666
- durationMs: duration
3726
+ durationMs: duration,
3727
+ ...assertionFailures !== void 0 ? { assertionFailures } : {}
3667
3728
  });
3668
3729
  });
3669
3730
  });
@@ -3676,6 +3737,13 @@ const CATEGORY_SKIP = "runtime-probe-skip";
3676
3737
  const CATEGORY_DEFERRED = "runtime-probe-deferred";
3677
3738
  const CATEGORY_FAIL = "runtime-probe-fail";
3678
3739
  const CATEGORY_TIMEOUT = "runtime-probe-timeout";
3740
+ /**
3741
+ * Story 60-4: command exited 0 but a stdout-shape assertion declared by the
3742
+ * author tripped. Distinct from `runtime-probe-fail` (non-zero exit code)
3743
+ * so retry prompts and post-run analysis can tell "tool crashed politely"
3744
+ * from "tool errored loudly".
3745
+ */
3746
+ const CATEGORY_ASSERTION_FAIL = "runtime-probe-assertion-fail";
3679
3747
  const defaultExecutors = { host: (probe) => executeProbeOnHost(probe, { cwd: process.cwd() }) };
3680
3748
  var RuntimeProbeCheck = class {
3681
3749
  name = "runtime-probes";
@@ -3740,9 +3808,12 @@ var RuntimeProbeCheck = class {
3740
3808
  }
3741
3809
  const result = await this._executors.host(probe);
3742
3810
  if (result.outcome === "pass") continue;
3743
- const category = result.outcome === "timeout" ? CATEGORY_TIMEOUT : CATEGORY_FAIL;
3811
+ const category = result.outcome === "timeout" ? CATEGORY_TIMEOUT : result.assertionFailures !== void 0 ? CATEGORY_ASSERTION_FAIL : CATEGORY_FAIL;
3744
3812
  const descriptor = probe.description ? ` (${probe.description})` : "";
3745
- const message = result.outcome === "timeout" ? `probe "${probe.name}"${descriptor} timed out after ${result.durationMs}ms` : `probe "${probe.name}"${descriptor} failed with exit ${result.exitCode ?? "unknown"}`;
3813
+ let message;
3814
+ if (result.outcome === "timeout") message = `probe "${probe.name}"${descriptor} timed out after ${result.durationMs}ms`;
3815
+ else if (result.assertionFailures !== void 0) message = `probe "${probe.name}"${descriptor} exit 0 but stdout assertion failed: ` + result.assertionFailures.join("; ");
3816
+ else message = `probe "${probe.name}"${descriptor} failed with exit ${result.exitCode ?? "unknown"}`;
3746
3817
  findings.push({
3747
3818
  category,
3748
3819
  severity: "error",
@@ -3786,6 +3857,56 @@ const SKIP_DIRS = new Set([
3786
3857
  /** Max depth for the basename walk. Prevents pathological traversal. */
3787
3858
  const MAX_WALK_DEPTH = 8;
3788
3859
  /**
3860
+ * Story 60-7: detect operational/runtime path references in source AC.
3861
+ *
3862
+ * Source ACs frequently mention runtime locations the implementation
3863
+ * INTERACTS WITH but does not SHIP — install destinations, system paths,
3864
+ * user home references, git internals. The check's existing path-clause
3865
+ * pipeline treats every backtick path as a deliverable and emits
3866
+ * architectural-drift error when it isn't found in code. This produces
3867
+ * false-positive verification failures.
3868
+ *
3869
+ * Concrete strata example (Run a880f201, Story 1-12, 2026-04-26): source AC
3870
+ * said "When `.git/hooks/post-merge` is installed" — describing the runtime
3871
+ * install location of a hook the dev's installer script writes. The dev
3872
+ * correctly shipped `hooks/install-vault-hooks.sh` + `hooks/vault-conflict-resolver.sh`,
3873
+ * but the check flagged `.git/hooks/post-merge` as architectural drift and
3874
+ * VERIFICATION_FAILED'd the story across both review cycles.
3875
+ *
3876
+ * Patterns covered:
3877
+ * - `^\.git/...` git internals (vault hooks, repo-internal paths)
3878
+ * - `^/usr/...`, `^/etc/...`, `^/var/...`, `^/mnt/...`, `^/opt/...`,
3879
+ * `^/srv/...`, `^/tmp/...`, `^/run/...`, `^/sys/...`, `^/proc/...`,
3880
+ * `^/dev/...`, `^/home/...` Unix system / install destinations
3881
+ * - `^~/...` user home references (`~/.config/...`, `~/obsidian-vault-test/`)
3882
+ *
3883
+ * Out of scope for v1 (deferred to follow-up if real evidence accumulates):
3884
+ * - HTTP routes (`/api/embeddings`) — distinguishing a route from a system
3885
+ * path requires extra signal (extension absence + plural-noun heuristic);
3886
+ * punt until a story actually trips on this.
3887
+ */
3888
+ function isOperationalPath(pathClause) {
3889
+ const raw = pathClause.replace(/^`/, "").replace(/`$/, "");
3890
+ if (raw.startsWith(".git/")) return true;
3891
+ if (raw.startsWith("~/")) return true;
3892
+ const SYSTEM_ROOTS = [
3893
+ "usr",
3894
+ "etc",
3895
+ "var",
3896
+ "mnt",
3897
+ "opt",
3898
+ "srv",
3899
+ "tmp",
3900
+ "run",
3901
+ "sys",
3902
+ "proc",
3903
+ "dev",
3904
+ "home"
3905
+ ];
3906
+ for (const root of SYSTEM_ROOTS) if (raw.startsWith(`/${root}/`)) return true;
3907
+ return false;
3908
+ }
3909
+ /**
3789
3910
  * Return true if `base` (a filename like `discover.ts`) exists somewhere under
3790
3911
  * `root` within MAX_WALK_DEPTH levels, skipping SKIP_DIRS. The walk is
3791
3912
  * synchronous and bounded; finding a single match exits early.
@@ -3894,36 +4015,156 @@ function pathReferencedInModifiedFiles(workingDir, pathClause, modifiedFiles) {
3894
4015
  /**
3895
4016
  * Extract the story's section from the full epic content.
3896
4017
  *
3897
- * Uses the same heading pattern as `isImplicitlyCovered` in the monolith:
3898
- * `### Story <storyKey>:` or `### Story <storyKey> ` or `### Story <storyKey>\n`
4018
+ * Uses the heading pattern `### Story <storyKey>:` or `### Story <storyKey>[whitespace]`.
4019
+ *
4020
+ * **Separator-tolerant matching** (Story 60-6, mirrors create-story.ts Story
4021
+ * 58-5 normalization): Substrate's canonical storyKey form is hyphen
4022
+ * (`1-10c`) — `seed-methodology-context.ts` normalizes any author convention
4023
+ * to hyphen before storing in `wg_stories`. But strata's `epics.md` uses
4024
+ * dot-form headings (`### Story 1.10c:`). When the supplied storyKey
4025
+ * (`1-10c`) doesn't textually match the heading separator (`.`), the
4026
+ * extraction must still find the right section — silently scanning the
4027
+ * whole epic and attributing every story's clauses to this one is far worse
4028
+ * than emitting a clear "could not isolate" signal.
3899
4029
  *
3900
4030
  * Returns the extracted section text (from the heading match through to the
3901
- * next `### Story` heading or end of file), or the full content if no
3902
- * matching heading is found.
4031
+ * next `### Story` heading or end of file), or `null` if no matching heading
4032
+ * is found. Callers MUST handle null explicitly — the previous silent-fallback
4033
+ * behavior (return-full-epic) inflated findings cross-story and is gone.
3903
4034
  */
3904
4035
  function extractStorySection(epicContent, storyKey) {
3905
- const escapedKey = storyKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3906
- const headingPattern = new RegExp(`^###\\s+Story\\s+${escapedKey}[:\\s]`, "m");
4036
+ const parts = storyKey.split(/[-._ ]/);
4037
+ const normalized = parts.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[-._ ]");
4038
+ const headingPattern = new RegExp(`^###\\s+Story\\s+${normalized}[:\\s]`, "m");
3907
4039
  const match = headingPattern.exec(epicContent);
3908
- if (!match) return epicContent;
4040
+ if (!match) return null;
3909
4041
  const start = match.index;
3910
4042
  const nextHeading = /\n### Story /m.exec(epicContent.slice(start + 1));
3911
4043
  if (nextHeading) return epicContent.slice(start, start + 1 + nextHeading.index);
3912
4044
  return epicContent.slice(start);
3913
4045
  }
4046
+ const ALTERNATIVE_ITEM = /^\s*-\s+\*\*\(([a-zA-Z])\)/;
4047
+ /**
4048
+ * Scan section lines for alternative-option groups. A group requires at least
4049
+ * two consecutive lettered list items; isolated `- **(a)**` items are NOT
4050
+ * treated as alternatives because there is no second option to compare against.
4051
+ *
4052
+ * Returns a flat list of options (each item annotated with its group id) so
4053
+ * the caller can map any path-clause line back to its (group, option) bucket.
4054
+ */
4055
+ function detectAlternativeOptions(lines) {
4056
+ const options = [];
4057
+ let i = 0;
4058
+ while (i < lines.length) {
4059
+ const start = lines[i];
4060
+ const m = start !== void 0 ? ALTERNATIVE_ITEM.exec(start) : null;
4061
+ if (m) {
4062
+ const groupStartLine = i;
4063
+ const items = [{
4064
+ letter: m[1].toLowerCase(),
4065
+ line: i
4066
+ }];
4067
+ let j = i + 1;
4068
+ while (j < lines.length) {
4069
+ const line = lines[j] ?? "";
4070
+ const am = ALTERNATIVE_ITEM.exec(line);
4071
+ if (am) {
4072
+ items.push({
4073
+ letter: am[1].toLowerCase(),
4074
+ line: j
4075
+ });
4076
+ j++;
4077
+ continue;
4078
+ }
4079
+ if (line.trim() === "" || /^\s+\S/.test(line)) {
4080
+ j++;
4081
+ continue;
4082
+ }
4083
+ break;
4084
+ }
4085
+ if (items.length >= 2) {
4086
+ const groupId = `alt-L${groupStartLine}`;
4087
+ for (let k = 0; k < items.length; k++) {
4088
+ const item = items[k];
4089
+ const next = k + 1 < items.length ? items[k + 1].line : j;
4090
+ options.push({
4091
+ group: groupId,
4092
+ option: item.letter,
4093
+ lineStart: item.line,
4094
+ lineEnd: next
4095
+ });
4096
+ }
4097
+ }
4098
+ i = j;
4099
+ } else i++;
4100
+ }
4101
+ return options;
4102
+ }
4103
+ /** Resolve the (group, option) for a path clause whose match appeared on
4104
+ * `lineIndex`, or undefined if the line is not inside any alternative option. */
4105
+ function findOptionForLine(lineIndex, options) {
4106
+ for (const opt of options) if (lineIndex >= opt.lineStart && lineIndex < opt.lineEnd) return {
4107
+ group: opt.group,
4108
+ option: opt.option
4109
+ };
4110
+ return void 0;
4111
+ }
4112
+ /**
4113
+ * Story 60-5: compute the "taken" option per alternative group.
4114
+ *
4115
+ * For each group of alternative options:
4116
+ * - Each option owns one or more path clauses (tagged with the same `group`
4117
+ * and the option's letter).
4118
+ * - An option is satisfied when every path clause it owns exists in code
4119
+ * (pathSatisfiedByCode === true). Missing paths in code make the option
4120
+ * unsatisfied — the dev did not take this option.
4121
+ * - The group's taken-option is the alphabetically-first satisfied letter,
4122
+ * for deterministic selection when multiple options happen to be
4123
+ * satisfied (uncommon, but possible if both options' paths exist from
4124
+ * prior unrelated work).
4125
+ *
4126
+ * Returns a map: group-id → option-letter that was taken. Groups with no
4127
+ * satisfied option are absent from the map (caller falls back to existing
4128
+ * per-path error-severity drift detection).
4129
+ */
4130
+ function computeTakenOptionPerGroup(hardClauses, workingDir) {
4131
+ const optionState = new Map();
4132
+ for (const clause of hardClauses) {
4133
+ if (clause.type !== "path" || !clause.alternative) continue;
4134
+ const { group, option } = clause.alternative;
4135
+ if (!optionState.has(group)) optionState.set(group, new Map());
4136
+ const groupMap = optionState.get(group);
4137
+ const exists = pathSatisfiedByCode(workingDir, clause.text);
4138
+ if (!groupMap.has(option)) groupMap.set(option, exists);
4139
+ else if (!exists) groupMap.set(option, false);
4140
+ }
4141
+ const taken = new Map();
4142
+ for (const [group, opts] of optionState) {
4143
+ const sorted = [...opts.entries()].sort((a, b) => a[0].localeCompare(b[0]));
4144
+ for (const [letter, satisfied] of sorted) if (satisfied) {
4145
+ taken.set(group, letter);
4146
+ break;
4147
+ }
4148
+ }
4149
+ return taken;
4150
+ }
3914
4151
  /**
3915
4152
  * Extract hard clauses from a story section of an epic file.
3916
4153
  *
3917
4154
  * Hard clauses:
3918
4155
  * 1. Lines containing MUST NOT / MUST / SHALL NOT / SHALL as standalone keywords (case-sensitive)
3919
- * 2. Backtick-wrapped paths with at least one `/` (excludes bare filenames)
4156
+ * 2. Backtick-wrapped paths with at least one `/` (excludes bare filenames).
4157
+ * Story 60-5: paths inside `- **(letter)**` list items belonging to a
4158
+ * multi-option alternative group are tagged with `{group, option}` so
4159
+ * the verification phase can OR satisfaction across options.
3920
4160
  * 3. The presence of `## Runtime Probes` heading followed by a fenced yaml block
3921
4161
  * (represented as a single "runtime-probes-section" clause)
3922
4162
  */
3923
4163
  function extractHardClauses(sectionContent) {
3924
4164
  const clauses = [];
3925
- const mustPattern = /\b(MUST NOT|MUST|SHALL NOT|SHALL)\b/;
3926
4165
  const lines = sectionContent.split("\n");
4166
+ const alternativeOptions = detectAlternativeOptions(lines);
4167
+ const mustPattern = /\b(MUST NOT|MUST|SHALL NOT|SHALL)\b/;
3927
4168
  for (const line of lines) {
3928
4169
  const match = mustPattern.exec(line);
3929
4170
  if (match) {
@@ -3935,11 +4176,19 @@ function extractHardClauses(sectionContent) {
3935
4176
  }
3936
4177
  }
3937
4178
  const pathPattern = /`([a-zA-Z0-9_./-]+\/[a-zA-Z0-9_./-]+)`/g;
3938
- let pathMatch;
3939
- while ((pathMatch = pathPattern.exec(sectionContent)) !== null) clauses.push({
3940
- type: "path",
3941
- text: `\`${pathMatch[1]}\``
3942
- });
4179
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
4180
+ const line = lines[lineIdx] ?? "";
4181
+ pathPattern.lastIndex = 0;
4182
+ let pathMatch;
4183
+ while ((pathMatch = pathPattern.exec(line)) !== null) {
4184
+ const alt = findOptionForLine(lineIdx, alternativeOptions);
4185
+ clauses.push({
4186
+ type: "path",
4187
+ text: `\`${pathMatch[1]}\``,
4188
+ ...alt ? { alternative: alt } : {}
4189
+ });
4190
+ }
4191
+ }
3943
4192
  const probesPattern = /^##\s+Runtime Probes[\s\S]*?```yaml/m;
3944
4193
  if (probesPattern.test(sectionContent)) clauses.push({
3945
4194
  type: "runtime-probes-section",
@@ -3966,9 +4215,23 @@ var SourceAcFidelityCheck = class {
3966
4215
  };
3967
4216
  }
3968
4217
  const storySection = extractStorySection(context.sourceEpicContent, context.storyKey);
4218
+ if (storySection === null) {
4219
+ const findings$1 = [{
4220
+ category: "source-ac-section-not-found",
4221
+ severity: "warn",
4222
+ message: `could not locate "### Story ${context.storyKey}" heading in source epic content — skipping fidelity check (the heading may use a separator convention (e.g. dot vs hyphen vs underscore) the matcher does not recognize, or the story may not exist in this epic file)`
4223
+ }];
4224
+ return {
4225
+ status: "pass",
4226
+ details: renderFindings(findings$1),
4227
+ duration_ms: Date.now() - start,
4228
+ findings: findings$1
4229
+ };
4230
+ }
3969
4231
  const hardClauses = extractHardClauses(storySection);
3970
4232
  const findings = [];
3971
4233
  const storyContent = context.storyContent ?? "";
4234
+ const takenOption = computeTakenOptionPerGroup(hardClauses, context.workingDir);
3972
4235
  for (const clause of hardClauses) if (clause.type === "runtime-probes-section") {
3973
4236
  if (!storyContent.includes("## Runtime Probes")) {
3974
4237
  const truncated = clause.text.length > 120 ? clause.text.slice(0, 120) : clause.text;
@@ -3981,6 +4244,26 @@ var SourceAcFidelityCheck = class {
3981
4244
  } else if (!storyContent.includes(clause.text)) {
3982
4245
  const truncated = clause.text.length > 120 ? clause.text.slice(0, 120) : clause.text;
3983
4246
  if (clause.type === "path") {
4247
+ if (isOperationalPath(clause.text)) {
4248
+ findings.push({
4249
+ category: "source-ac-operational-path-reference",
4250
+ severity: "info",
4251
+ message: `path: "${truncated}" referenced in source AC as a runtime / install / system location (matches operational-path heuristic) — treated as informational, not a deliverable file path`
4252
+ });
4253
+ continue;
4254
+ }
4255
+ if (clause.alternative) {
4256
+ const { group, option } = clause.alternative;
4257
+ const taken = takenOption.get(group);
4258
+ if (taken !== void 0 && taken !== option) {
4259
+ findings.push({
4260
+ category: "source-ac-alternative-not-taken",
4261
+ severity: "info",
4262
+ message: `path: "${truncated}" not implemented — source AC offered this as alternative option (${option}); story implemented option (${taken}) instead`
4263
+ });
4264
+ continue;
4265
+ }
4266
+ }
3984
4267
  const existsInCode = pathSatisfiedByCode(context.workingDir, clause.text);
3985
4268
  const modifiedFiles = context.devStoryResult?.files_modified ?? [];
3986
4269
  const referencedByStory = pathReferencedInModifiedFiles(context.workingDir, clause.text, modifiedFiles);
@@ -4226,6 +4509,37 @@ const StoredVerificationSummarySchema = z.object({
4226
4509
  duration_ms: z.number().nonnegative()
4227
4510
  });
4228
4511
 
4512
+ //#endregion
4513
+ //#region packages/sdlc/dist/run-model/dev-story-signals.js
4514
+ /**
4515
+ * Persisted shape of the normalized dev-story signals.
4516
+ *
4517
+ * All fields optional because:
4518
+ * - Different dev-story dispatches surface different subsets of fields
4519
+ * depending on the agent's YAML output (some omit `tests`, some omit
4520
+ * `ac_failures` when none failed, etc.).
4521
+ * - `result` uses the open extensible-union pattern (v0.19.6 convention)
4522
+ * so future result strings (e.g. 'partial-checkpoint') don't break
4523
+ * deserialization.
4524
+ */
4525
+ const StoredDevStorySignalsSchema = z.object({
4526
+ result: z.union([
4527
+ z.literal("completed"),
4528
+ z.literal("failed"),
4529
+ z.literal("partial"),
4530
+ z.string()
4531
+ ]).optional(),
4532
+ ac_met: z.array(z.string()).optional(),
4533
+ ac_failures: z.array(z.string()).optional(),
4534
+ files_modified: z.array(z.string()).optional(),
4535
+ tests: z.union([
4536
+ z.literal("pass"),
4537
+ z.literal("fail"),
4538
+ z.literal("unknown"),
4539
+ z.string()
4540
+ ]).optional()
4541
+ });
4542
+
4229
4543
  //#endregion
4230
4544
  //#region packages/sdlc/dist/run-model/per-story-state.js
4231
4545
  /**
@@ -4271,7 +4585,8 @@ const PerStoryStateSchema = z.object({
4271
4585
  cost_usd: z.number().nonnegative().optional(),
4272
4586
  review_cycles: z.number().int().nonnegative().optional(),
4273
4587
  dispatches: z.number().int().nonnegative().optional(),
4274
- retry_count: z.number().int().nonnegative().optional()
4588
+ retry_count: z.number().int().nonnegative().optional(),
4589
+ dev_story_signals: StoredDevStorySignalsSchema.optional()
4275
4590
  });
4276
4591
 
4277
4592
  //#endregion
@@ -5681,4 +5996,4 @@ function registerHealthCommand(program, _version = "0.0.0", projectRoot = proces
5681
5996
 
5682
5997
  //#endregion
5683
5998
  export { BMAD_BASELINE_TOKENS_FULL, DEFAULT_STALL_THRESHOLD_SECONDS, DoltMergeConflict, FileStateStore, FindingsInjector, RunManifest, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN$1 as STORY_KEY_PATTERN, SUBSTRATE_OWNED_SETTINGS_KEYS, SupervisorLock, VALID_PHASES, WorkGraphRepository, ZERO_FINDING_COUNTS, __commonJS, __require, __toESM, applyConfigToGraph, buildPipelineStatusOutput, createDatabaseAdapter$1 as createDatabaseAdapter, createDefaultVerificationPipeline, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, createStateStore, detectCycles, extractTargetFilesFromStoryContent, findPackageRoot, formatOutput, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, inspectProcessTree, isOrchestratorProcessLine, parseDbTimestampAsUtc, registerHealthCommand, renderFindings, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveGraphPath, resolveMainRepoRoot, resolveRunManifest, rollupFindingCounts, runHealthAction, validateStoryKey };
5684
- //# sourceMappingURL=health-Cq8K_jrJ.js.map
5999
+ //# sourceMappingURL=health-0_axmI2t.js.map
@@ -1,4 +1,4 @@
1
- import { DEFAULT_STALL_THRESHOLD_SECONDS, getAllDescendantPids, getAutoHealthData, inspectProcessTree, isOrchestratorProcessLine, registerHealthCommand, runHealthAction } from "./health-Cq8K_jrJ.js";
1
+ import { DEFAULT_STALL_THRESHOLD_SECONDS, getAllDescendantPids, getAutoHealthData, inspectProcessTree, isOrchestratorProcessLine, registerHealthCommand, runHealthAction } from "./health-0_axmI2t.js";
2
2
  import "./logger-KeHncl-f.js";
3
3
  import "./dist-CqtWS9wF.js";
4
4
  import "./decisions-C0pz9Clx.js";
@@ -1,8 +1,8 @@
1
- import "./health-Cq8K_jrJ.js";
1
+ import "./health-0_axmI2t.js";
2
2
  import "./logger-KeHncl-f.js";
3
3
  import "./helpers-CElYrONe.js";
4
4
  import "./dist-CqtWS9wF.js";
5
- import { normalizeGraphSummaryToStatus, registerRunCommand, resolveMaxReviewCycles, runRunAction, wireNdjsonEmitter } from "./run-B3e4O0Rk.js";
5
+ import { normalizeGraphSummaryToStatus, registerRunCommand, resolveMaxReviewCycles, runRunAction, wireNdjsonEmitter } from "./run-DQcG05Ar.js";
6
6
  import "./routing-CcBOCuC9.js";
7
7
  import "./decisions-C0pz9Clx.js";
8
8
 
@@ -1,4 +1,4 @@
1
- import { BMAD_BASELINE_TOKENS_FULL, DoltMergeConflict, FileStateStore, FindingsInjector, RunManifest, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN, VALID_PHASES, WorkGraphRepository, __commonJS, __require, __toESM, applyConfigToGraph, buildPipelineStatusOutput, createDatabaseAdapter, createDefaultVerificationPipeline, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, detectCycles, extractTargetFilesFromStoryContent, formatOutput, formatPipelineSummary, formatTokenTelemetry, inspectProcessTree, parseDbTimestampAsUtc, renderFindings, resolveGraphPath, resolveMainRepoRoot, validateStoryKey } from "./health-Cq8K_jrJ.js";
1
+ import { BMAD_BASELINE_TOKENS_FULL, DoltMergeConflict, FileStateStore, FindingsInjector, RunManifest, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN, VALID_PHASES, WorkGraphRepository, __commonJS, __require, __toESM, applyConfigToGraph, buildPipelineStatusOutput, createDatabaseAdapter, createDefaultVerificationPipeline, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, detectCycles, extractTargetFilesFromStoryContent, formatOutput, formatPipelineSummary, formatTokenTelemetry, inspectProcessTree, parseDbTimestampAsUtc, renderFindings, resolveGraphPath, resolveMainRepoRoot, validateStoryKey } from "./health-0_axmI2t.js";
2
2
  import { createLogger } from "./logger-KeHncl-f.js";
3
3
  import { TypedEventBusImpl, createEventBus, createTuiApp, isTuiCapable, printNonTtyWarning, sleep } from "./helpers-CElYrONe.js";
4
4
  import { ADVISORY_NOTES, Categorizer, ConsumerAnalyzer, DEFAULT_GLOBAL_SETTINGS, DispatcherImpl, DoltClient, ESCALATION_DIAGNOSIS, EXPERIMENT_RESULT, EfficiencyScorer, IngestionServer, LogTurnAnalyzer, OPERATIONAL_FINDING, Recommender, RoutingRecommender, RoutingResolver, RoutingTelemetry, RoutingTokenAccumulator, RoutingTuner, STORY_METRICS, STORY_OUTCOME, SubstrateConfigSchema, TEST_EXPANSION_FINDING, TEST_PLAN, TelemetryNormalizer, TelemetryPipeline, TurnAnalyzer, addTokenUsage, aggregateTokenUsageForRun, aggregateTokenUsageForStory, callLLM, createConfigSystem, createDatabaseAdapter$1, createDecision, createPipelineRun, createRequirement, detectInterfaceChanges, getArtifactByTypeForRun, getArtifactsByRun, getDecisionsByCategory, getDecisionsByPhase, getDecisionsByPhaseForRun, getLatestRun, getPipelineRunById, getRunMetrics, getRunningPipelineRuns, getStoryMetricsForRun, getTokenUsageSummary, initSchema, listRequirements, loadModelRoutingConfig, registerArtifact, updatePipelineRun, updatePipelineRunConfig, upsertDecision, writeRunMetrics, writeStoryMetrics } from "./dist-CqtWS9wF.js";
@@ -10729,6 +10729,36 @@ function persistVerificationResult(storyKey, summary, runManifest) {
10729
10729
  }, "manifest verification_result write failed — pipeline continues"));
10730
10730
  }
10731
10731
  /**
10732
+ * Non-fatally persist dev-story signals to the run manifest.
10733
+ *
10734
+ * Called right before each verification dispatch so the signals that fed
10735
+ * into the verification context are durably recorded. Closes a manifest-as-
10736
+ * source-of-truth gap (Epic 52 design contract): Story 60-3's under-delivery
10737
+ * detection in source-ac-fidelity reads `context.devStoryResult.files_modified`,
10738
+ * which the orchestrator passes in-memory at dispatch time but never wrote
10739
+ * to the manifest. Resume / retry-escalated / supervisor-restart / post-mortem
10740
+ * paths read state from the manifest and saw `dev_story_signals: undefined`,
10741
+ * forcing the under-delivery check into "benefit of doubt" warn mode rather
10742
+ * than the intended error.
10743
+ *
10744
+ * Surfaced strata Run a880f201 (2026-04-26): manifest's per_story_state["1-12"]
10745
+ * had no `dev_story_signals` field even though dev-story shipped 3 files.
10746
+ *
10747
+ * Same non-fatal / fire-and-forget semantics as persistVerificationResult.
10748
+ *
10749
+ * @param storyKey - Story key being verified
10750
+ * @param signals - Normalized DevStorySignals from the orchestrator's
10751
+ * replaceDevStorySignals / mergeDevStorySignals helpers
10752
+ * @param runManifest - RunManifest instance to write to, or null/undefined to skip
10753
+ */
10754
+ function persistDevStorySignals(storyKey, signals, runManifest) {
10755
+ if (runManifest == null || signals === void 0) return Promise.resolve();
10756
+ return runManifest.patchStoryState(storyKey, { dev_story_signals: signals }).catch((err) => _logger.warn({
10757
+ err,
10758
+ storyKey
10759
+ }, "manifest dev_story_signals write failed — pipeline continues"));
10760
+ }
10761
+ /**
10732
10762
  * Flatten every finding from a VerificationSummary's checks into a single
10733
10763
  * prompt-ready string. Returns '' when the summary is undefined, contains
10734
10764
  * no checks, or every check emits zero findings (e.g. every check passed).
@@ -13807,6 +13837,7 @@ function createImplementationOrchestrator(deps) {
13807
13837
  const section = extractStorySection(epicFull, storyKey);
13808
13838
  if (section) sourceEpicContent = section;
13809
13839
  } catch {}
13840
+ await persistDevStorySignals(storyKey, devStorySignals, runManifest);
13810
13841
  const verifContext = assembleVerificationContext({
13811
13842
  storyKey,
13812
13843
  workingDir: projectRoot ?? process.cwd(),
@@ -14079,6 +14110,7 @@ function createImplementationOrchestrator(deps) {
14079
14110
  const section2 = extractStorySection(epicFull2, storyKey);
14080
14111
  if (section2) sourceEpicContent2 = section2;
14081
14112
  } catch {}
14113
+ await persistDevStorySignals(storyKey, devStorySignals, runManifest);
14082
14114
  const verifContext = assembleVerificationContext({
14083
14115
  storyKey,
14084
14116
  workingDir: projectRoot ?? process.cwd(),
@@ -14151,7 +14183,7 @@ function createImplementationOrchestrator(deps) {
14151
14183
  updateStory(storyKey, { phase: "NEEDS_FIXES" });
14152
14184
  startPhase(storyKey, "fix");
14153
14185
  const taskType = verdict === "NEEDS_MINOR_FIXES" ? "minor-fixes" : "major-rework";
14154
- const fixModel = taskType === "major-rework" ? "claude-opus-4-6" : void 0;
14186
+ const fixModel = taskType === "major-rework" ? "claude-opus-4-7" : void 0;
14155
14187
  try {
14156
14188
  let fixPrompt;
14157
14189
  const isMajorRework = taskType === "major-rework";
@@ -44456,4 +44488,4 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
44456
44488
 
44457
44489
  //#endregion
44458
44490
  export { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, EpicIngester, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, normalizeGraphSummaryToStatus, registerExportCommand, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveMaxReviewCycles, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict, wireNdjsonEmitter };
44459
- //# sourceMappingURL=run-B3e4O0Rk.js.map
44491
+ //# sourceMappingURL=run-DQcG05Ar.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "substrate-ai",
3
- "version": "0.20.23",
3
+ "version": "0.20.27",
4
4
  "description": "Substrate — multi-agent orchestration daemon for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -118,9 +118,13 @@ Declare probes as a YAML list inside a single fenced `yaml` block directly under
118
118
  command: <shell command line(s)> # required
119
119
  timeout_ms: 60000 # optional; defaults to 60000
120
120
  description: <optional context> # optional
121
+ expect_stdout_no_regex: # optional; stdout must NOT match any of these
122
+ - '<regex pattern>'
123
+ expect_stdout_regex: # optional; stdout must match each of these
124
+ - '<regex pattern>'
121
125
  ```
122
126
 
123
- Required fields: `name`, `sandbox`, `command`. `timeout_ms` and `description` are optional. Probe names must be unique within one story.
127
+ Required fields: `name`, `sandbox`, `command`. `timeout_ms`, `description`, `expect_stdout_no_regex`, and `expect_stdout_regex` are optional. Probe names must be unique within one story.
124
128
 
125
129
  ### Sandbox choice
126
130
 
@@ -134,6 +138,26 @@ For stories with multiple runtime concerns (install + start + connect), declare
134
138
 
135
139
  Probe names are hyphen-separated identifiers, not sentences: `dolt-image-pullable`, not `verify that the dolt image can be pulled`.
136
140
 
141
+ ### Asserting success-shape on structured-output probes
142
+
143
+ Exit-code success is necessary but **not sufficient** for probes calling tools that return structured payloads (MCP, REST, JSON-RPC, A2A). Many such tools respond HTTP 200 with an error envelope (`{"isError": true}`, `{"status": "error"}`, `{"error": {...}}`) — exit-0 hides the failure. Strata Run 12 shipped four broken MCP tools under SHIP_IT because probes only asserted "tool advertised", not "tool returned a success-shaped response."
144
+
145
+ **Use** `expect_stdout_no_regex` (forbidden patterns) and/or `expect_stdout_regex` (required patterns) when the probe hits MCP / REST / JSON-RPC / A2A. **Skip** for commands that exit non-zero on logical failure (`systemctl`, `podman pull`, `docker compose config`).
146
+
147
+ ```yaml
148
+ - name: mcp-semantic-search-returns-results
149
+ sandbox: host
150
+ command: |
151
+ mcp-client call strata_semantic_search '{"query": "auth"}'
152
+ expect_stdout_no_regex:
153
+ - '"isError"\s*:\s*true'
154
+ - '"status"\s*:\s*"error"'
155
+ expect_stdout_regex:
156
+ - '"similarity_score"'
157
+ ```
158
+
159
+ Patterns are JavaScript regex (`new RegExp`). Evaluated only when exit code is 0; non-zero exits emit `runtime-probe-fail` and assertions are skipped to avoid redundant findings.
160
+
137
161
  ### Examples by artifact class
138
162
 
139
163
  **Systemd unit:**