substrate-ai 0.21.1 → 0.21.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -157,11 +157,21 @@ Stories run in parallel across your available agents, each in its own git worktr
157
157
 
158
158
  #### Per-Story Worktree Lifecycle
159
159
 
160
- Each dispatched story runs in a dedicated git worktree at `.substrate-worktrees/story-<key>` on branch `substrate/story-<key>`. After verification produces a SHIP_IT verdict, **substrate auto-commits the dispatched agent's output** to the story branch with a `feat(story-N-M): <title>` message (v0.20.86+substrate does NOT rely on the agent running `git commit` itself, since empirical audit found agents don't reliably do so). Pre-commit hooks run on the substrate commit. The branch then merges to the base branch (typically main) and the worktree is removed. If the commit fails (e.g., a pre-commit hook rejects) or the agent's run produced no committable changes, the story escalates with `dev-story-commit-failed` or `dev-story-no-commit` instead of merging an empty branch. After a verification failure, the worktree and branch are preserved so you can inspect the partial implementation via `substrate reconcile-from-disk`. Use `--no-worktree` if your project does not support worktrees (e.g., submodules, bare repos).
160
+ Each dispatched story runs in a dedicated git worktree on branch `substrate/story-<key>`. **Since v0.20.149 the worktree base is external by default** — `~/.substrate/worktrees/<projectname>-<hash8>/<key>/`, outside your repo; set `worktree.base: in-repo` in `.substrate/config.yaml` to restore the legacy `.substrate-worktrees/<key>/` path. **Commit-first (v0.20.139+):** substrate auto-commits the agent's output to the story branch (`feat(story-N-M): <title>`) at **dev-story completion before review** so the branch is always the durable copy of the work; failure paths add `wip(story-<key>)` checkpoints. Pre-commit hooks run on the substrate commit. If the commit fails (a hook rejects) or the run produced no committable changes, the story escalates `dev-story-commit-failed` / `dev-story-no-commit`. How the verified branch integrates is controlled by [finalization mode](#finalization). After a verification failure, the worktree and branch are preserved for inspection via `substrate reconcile-from-disk`. Use `--no-worktree` for projects that don't support worktrees (submodules, bare repos).
161
+
162
+ #### Finalization
163
+
164
+ `finalization.mode` in `.substrate/config.yaml` (or `--finalization <mode>` per run) decides how a verified story's branch integrates:
165
+
166
+ - **`merge`** (default) — local merge into the run's start branch, **fast-forward-only by default** (a base that moved during the run escalates `ff-only-merge-not-possible` rather than fabricating a merge commit). `finalization.merge_strategy: three-way` enables merge commits and is **required for concurrent multi-story runs**.
167
+ - **`branch`** — nothing self-merges; the `substrate/story-<key>` branch is the deliverable (safe for brownfield repos).
168
+ - **`pr`** — branch + push + `gh pr create` (one PR per story); degrades to `branch` on push/gh failure, never blocks.
169
+
170
+ NDJSON lifecycle events `story:committed` → (`story:merged`) → `story:finalized` report the outcome, and `substrate report` lists any unmerged deliverable branches.
161
171
 
162
172
  ### Verification Pipeline
163
173
 
164
- Six gates run after code review. Each can pass, warn, or fail; failures block SHIP_IT.
174
+ A Tier-A gate sequence runs after code review. Each gate can pass, warn, or fail; failures block SHIP_IT.
165
175
 
166
176
  | Gate | What it catches |
167
177
  |---|---|
@@ -169,6 +179,10 @@ Six gates run after code review. Each can pass, warn, or fail; failures block SH
169
179
  | **trivial-output** | Output token count below threshold — likely no real work done |
170
180
  | **acceptance-criteria-evidence** | Each AC has demonstrable evidence in dev-story signals (files modified, tests added) |
171
181
  | **build** | Project build succeeds against the dev's worktree |
182
+ | **test-suite** | Runs the project's real test command (from the trusted main-tree profile) and fails on a red suite; rejects exit-code-laundering wrappers (`… \|\| true`) |
183
+ | **scope-contamination** | Foreign-language sources / foreign toolchain manifests / build-output droppings vs the project's declared languages |
184
+ | **test-mutation** | Warns when a story modifies or deletes pre-existing tracked test files (reward-hack tripwire), including committed and renamed test files |
185
+ | **net-new-implementation** | A "success" story whose diff is artifact-only, an empty stub, or a whitespace-only edit escalates `no-implementation` |
172
186
  | **runtime-probes** | Each declared `## Runtime Probes` section probe runs successfully against real or sandboxed state. Includes auto-detection for error-shape envelopes (`{"isError": true}`, `{"status": "error"}`) and production-trigger requirements for event-driven ACs. Frontmatter `external_state_dependencies` declarations hard-gate when probes section is missing. |
173
187
  | **source-ac-fidelity** | AC text in source epic appears verbatim in story artifact (paths, MUST clauses, hard contracts). Includes 4 context-aware heuristics: negation (paths the AC says NOT to deliver), dependency-context (peer packages the implementation imports), operational-path (system install destinations like `.git/hooks/`), and alternative-option groups. |
174
188
 
@@ -501,6 +515,20 @@ token_ceilings:
501
515
  # Optional: dispatch timeout overrides (ms)
502
516
  dispatch_timeouts:
503
517
  dev-story: 1800000 # 30 min
518
+
519
+ # Optional: how verified stories integrate (v0.20.147+)
520
+ finalization:
521
+ mode: merge # merge (default) | branch | pr
522
+ merge_strategy: ff-only # ff-only (default) | three-way (needed for concurrent multi-story)
523
+ # epic_gate_command: "uv run pytest" # must pass before the last story of an epic integrates
524
+
525
+ # Optional: per-story worktree location (v0.20.149+)
526
+ worktree:
527
+ base: external # external (default: ~/.substrate/worktrees/...) | in-repo
528
+
529
+ # Optional: spawned-agent permission profile (v0.20.152+)
530
+ dispatch:
531
+ permission_profile: skip # skip (default) | scoped (per-worktree settings; accident-mitigation, not a security boundary)
504
532
  ```
505
533
 
506
534
  ### Configuration Files
@@ -527,7 +555,7 @@ substrate init
527
555
 
528
556
  Without Dolt, all functionality works except for: `substrate diff`, `substrate history`, persistent OTEL observability tables, and context engineering repo-map storage.
529
557
 
530
- **On-disk operator surface:** Each pipeline run creates per-story worktrees under `.substrate-worktrees/story-<key>/`. Successful stories are merged to main and their worktrees removed. Failed or escalated stories preserve their worktrees for operator inspection via `substrate reconcile-from-disk`.
558
+ **On-disk operator surface:** Each pipeline run creates a per-story worktree — by default under `~/.substrate/worktrees/<project>-<hash>/<key>/` (external base; `worktree.base: in-repo` restores `.substrate-worktrees/<key>/`). Verified stories integrate per the [finalization mode](#finalization) and their worktrees are removed; failed or escalated stories preserve their worktree and branch for operator inspection via `substrate reconcile-from-disk`.
531
559
 
532
560
  ## CLI Command Reference
533
561
 
@@ -551,6 +579,7 @@ These commands are typically invoked by your AI assistant during pipeline operat
551
579
  | `substrate run --max-review-cycles <n>` | Cycles per story (default 2; use 3 for migrations / interface extraction) |
552
580
  | `substrate run --skip-verification` | Skip post-dispatch verification (use sparingly) |
553
581
  | `substrate run --no-worktree` | Disable per-story git worktrees (use for submodule repos or bare repos that don't support worktrees) |
582
+ | `substrate run --finalization <mode>` | How verified work integrates: `merge` (default) / `branch` / `pr` — see [Finalization](#finalization) |
554
583
  | `substrate run --help-agent` | Print agent instruction prompt fragment |
555
584
  | `substrate resume` | Resume an interrupted run |
556
585
  | `substrate cancel` | Cancel a running pipeline |
package/dist/cli/index.js CHANGED
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env node
2
- import { FileKvStore, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, buildPipelineStatusOutput, createDatabaseAdapter, createDoltOperatorReader, findPackageRoot, formatOutput, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, inspectProcessTree, parseDbTimestampAsUtc, registerHealthCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion } from "../health-Cfkv6XoQ.js";
2
+ import { FileKvStore, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, buildPipelineStatusOutput, createDatabaseAdapter, createDoltOperatorReader, findPackageRoot, formatOutput, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, inspectProcessTree, parseDbTimestampAsUtc, registerHealthCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion } from "../health-DvgJ7jj_.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, InMemoryDatabaseAdapter, IngestionServer, MonitorDatabaseImpl, OPERATIONAL_FINDING, PartialGlobalSettingsSchema, PartialProviderConfigSchema, ProvidersSchema, RoutingRecommender, STORY_METRICS, TelemetryConfigSchema, addTokenUsage, aggregateTokenUsageForRun, checkDoltInstalled, compareRunMetrics, createAmendmentRun, createConfigSystem, createDecision, createGitWorktreeManager, createPipelineRun, createStderrLogger, getActiveDecisions, getAllCostEntriesFiltered, getBaselineRunMetrics, getDecisionsByCategory, getDecisionsByPhaseForRun, getLatestCompletedRun, getLatestRun, getPipelineRunById, getPlanningCostTotal, getRetryableEscalations, getRunMetrics, getRunningPipelineRuns, getSessionCostSummary, getSessionCostSummaryFiltered, getStoryMetricsForRun, getTokenUsageSummary, incrementRunRestarts, initSchema, initWorkGraphSchema, initializeDolt, listRunMetrics, loadParentRunDecisions, supersedeDecision, swallowDebug, tagRunAsBaseline, updatePipelineRun } from "../dist-BkhYgJWo.js";
6
6
  import "../adapter-registry-DXLMTmfD.js";
7
- import { RunManifest, SupervisorLock, ZERO_FINDINGS_BY_AUTHOR, ZERO_FINDING_COUNTS, ZERO_PROBE_AUTHOR_METRICS, aggregateProbeAuthorMetrics, parseRuntimeProbes, readCurrentRunId, resolveMainRepoRoot, resolveRunManifest, rollupFindingCounts, rollupFindingsByAuthor, rollupProbeAuthorByClass, rollupProbeAuthorMetrics, runAcTraceabilityCheck } from "../manifest-read-BhNRIiqm.js";
8
- import { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, EpicIngester, GLOBSTAR, GitClient, GrammarLoader, Minimatch, Minipass, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, escape, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, registerExportCommand, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runProbeAuthor, runSolutioningPhase, unescape, validateStopAfterFromConflict } from "../run-JrEnvaB0.js";
7
+ import { JOURNEY_REGISTRY_PATH, RunManifest, SupervisorLock, ZERO_FINDINGS_BY_AUTHOR, ZERO_FINDING_COUNTS, ZERO_PROBE_AUTHOR_METRICS, aggregateProbeAuthorMetrics, loadJourneyRegistryFromFile, loadJourneyRegistryFromTrustedTree, parseRuntimeProbes, readCurrentRunId, resolveMainRepoRoot, resolveRunManifest, rollupFindingCounts, rollupFindingsByAuthor, rollupProbeAuthorByClass, rollupProbeAuthorMetrics, runAcTraceabilityCheck } from "../manifest-read-DyyFPrJH.js";
8
+ import { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, EpicIngester, GLOBSTAR, GitClient, GrammarLoader, Minimatch, Minipass, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, escape, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, registerExportCommand, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runProbeAuthor, runSolutioningPhase, unescape, validateStopAfterFromConflict } from "../run-CAmBrDky.js";
9
9
  import "../errors-C7pY6_yQ.js";
10
10
  import "../routing-DFxoKHDt.js";
11
11
  import { WorkGraphRepository } from "../work-graph-repository-DZyJv5pV.js";
12
12
  import "../decisions-CzSIEeGP.js";
13
13
  import "../decision-router-BAPpON_C.js";
14
- import "../interactive-prompt-BBfPSxyJ.js";
14
+ import "../interactive-prompt-CPF2GTvV.js";
15
15
  import "../recovery-engine-BKGBeBnW.js";
16
16
  import "../version-manager-impl-qFBiO4Eh.js";
17
17
  import { registerUpgradeCommand } from "../upgrade-S1oUMH5P.js";
@@ -699,6 +699,7 @@ const WHOLESALE_SUBSTRATE_IGNORES = new Set([
699
699
  const STAR = ".substrate/*";
700
700
  const CONFIG_NEGATION = "!.substrate/config.yaml";
701
701
  const PROFILE_NEGATION = "!.substrate/project-profile.yaml";
702
+ const ACCEPTANCE_NEGATION = "!.substrate/acceptance/";
702
703
  const CODEX_PROMPTS = ".codex/prompts/";
703
704
  const CODEX_SKILLS = ".codex/skills/";
704
705
  /**
@@ -717,6 +718,9 @@ function computeSubstrateGitignore(existing) {
717
718
  const profileNegIdx = trimmed.lastIndexOf(PROFILE_NEGATION);
718
719
  const profileNegationEffective = profileNegIdx !== -1 && profileNegIdx > starIdx;
719
720
  if (!profileNegationEffective) append.push(PROFILE_NEGATION);
721
+ const acceptanceNegIdx = trimmed.lastIndexOf(ACCEPTANCE_NEGATION);
722
+ const acceptanceNegationEffective = acceptanceNegIdx !== -1 && acceptanceNegIdx > starIdx;
723
+ if (!acceptanceNegationEffective) append.push(ACCEPTANCE_NEGATION);
720
724
  if (!trimmed.includes(CODEX_PROMPTS)) append.push(CODEX_PROMPTS);
721
725
  if (!trimmed.includes(CODEX_SKILLS)) append.push(CODEX_SKILLS);
722
726
  let content = lines.join("\n");
@@ -7126,7 +7130,7 @@ async function runStatusAction(options) {
7126
7130
  logger$15.debug({ err }, "Work graph query failed, continuing without work graph data");
7127
7131
  }
7128
7132
  if (run === void 0) {
7129
- const { inspectProcessTree: inspectProcessTree$1 } = await import("../health-Coh3BG8W.js");
7133
+ const { inspectProcessTree: inspectProcessTree$1 } = await import("../health-DDsjslMy.js");
7130
7134
  const substrateDirPath = join(projectRoot, ".substrate");
7131
7135
  const processInfo = inspectProcessTree$1({
7132
7136
  projectRoot,
@@ -8655,7 +8659,7 @@ async function runSupervisorAction(options, deps = {}) {
8655
8659
  await initSchema(expAdapter);
8656
8660
  const { runRunAction: runPipeline } = await import(
8657
8661
  /* @vite-ignore */
8658
- "../run-CKM-5T34.js"
8662
+ "../run-CF0qLJAV.js"
8659
8663
  );
8660
8664
  const runStoryFn = async (opts) => {
8661
8665
  const exitCode = await runPipeline({
@@ -12988,7 +12992,7 @@ function computeReportVerdict(summary, runStatus) {
12988
12992
  if (summary.total === 0) return "NO STORIES RUN";
12989
12993
  return "ALL PASSED";
12990
12994
  }
12991
- function renderHuman(output, manifest) {
12995
+ function renderHuman$1(output, manifest) {
12992
12996
  const lines = [];
12993
12997
  const { runId, summary, stories, escalations, cost, duration, halts } = output;
12994
12998
  const durationStr = duration.wall_clock_ms != null ? formatDurationMs(duration.wall_clock_ms) : "unknown";
@@ -13411,7 +13415,7 @@ async function runReportAction(options) {
13411
13415
  output.ac_traceability = acTraceability;
13412
13416
  }
13413
13417
  if (outputFormat === "json") process.stdout.write(renderJson(output) + "\n");
13414
- else process.stdout.write(renderHuman(output, manifest));
13418
+ else process.stdout.write(renderHuman$1(output, manifest));
13415
13419
  return 0;
13416
13420
  }
13417
13421
  /**
@@ -13435,6 +13439,53 @@ function registerReportCommand(program, _version = "0.0.0", projectRoot = proces
13435
13439
  });
13436
13440
  }
13437
13441
 
13442
+ //#endregion
13443
+ //#region src/cli/commands/acceptance.ts
13444
+ const ACCEPTANCE_EXIT_SUCCESS = 0;
13445
+ const ACCEPTANCE_EXIT_ERROR = 1;
13446
+ function summarizeRegistry(registry) {
13447
+ const journeyCount = registry.journeys.length;
13448
+ const endStateCount = registry.journeys.reduce((n, j) => n + j.end_states.length, 0);
13449
+ const criticalCount = registry.journeys.filter((j) => j.criticality === "critical").length;
13450
+ return `journey registry v${String(registry.version)}: ${String(journeyCount)} journeys (${String(criticalCount)} critical), ${String(endStateCount)} end-states`;
13451
+ }
13452
+ /** Render a load result for humans. Returns the exit code. */
13453
+ function renderHuman(result, source) {
13454
+ switch (result.status) {
13455
+ case "ok":
13456
+ process.stdout.write(`OK — ${summarizeRegistry(result.registry)} (${source})\n`);
13457
+ for (const journey of result.registry.journeys) process.stdout.write(` ${journey.id} [${journey.criticality}] ${journey.title} — ${String(journey.end_states.length)} end-state(s), surfaces: ${journey.surfaces.join(", ")}\n`);
13458
+ return ACCEPTANCE_EXIT_SUCCESS;
13459
+ case "absent":
13460
+ process.stdout.write(`NO REGISTRY — ${JOURNEY_REGISTRY_PATH} not found (${source}).\nAuthor it at planning time; see the acceptance-gate design brief for the schema.\n`);
13461
+ return ACCEPTANCE_EXIT_ERROR;
13462
+ case "invalid":
13463
+ process.stdout.write(`INVALID — ${JOURNEY_REGISTRY_PATH} failed validation (${source}):\n`);
13464
+ for (const issue of result.issues) process.stdout.write(` ${issue.path}: ${issue.message}\n`);
13465
+ return ACCEPTANCE_EXIT_ERROR;
13466
+ case "error":
13467
+ process.stdout.write(`ERROR — could not read the registry (${source}): ${result.message}\n`);
13468
+ return ACCEPTANCE_EXIT_ERROR;
13469
+ }
13470
+ }
13471
+ function registerAcceptanceCommand(program, version) {
13472
+ const acceptanceCmd = program.command("acceptance").description("Acceptance Gate — journey registry tools (the missing sprint demo)");
13473
+ acceptanceCmd.command("validate").description(`Lint ${JOURNEY_REGISTRY_PATH} with actionable, pathed errors`).option("--ref <ref>", "validate the COMMITTED registry at a git ref (trusted-tree read) instead of the working tree").option("--output-format <format>", "Output format: text (default) or json", "text").action(async (opts) => {
13474
+ const projectRoot = process.cwd();
13475
+ const source = opts.ref !== void 0 ? `committed @ ${opts.ref}` : "working tree";
13476
+ const result = opts.ref !== void 0 ? await loadJourneyRegistryFromTrustedTree(projectRoot, opts.ref) : await loadJourneyRegistryFromFile(projectRoot);
13477
+ if (opts.outputFormat === "json") {
13478
+ const output = buildJsonOutput("substrate acceptance validate", {
13479
+ source,
13480
+ ...result
13481
+ }, version);
13482
+ process.stdout.write(JSON.stringify(output, null, 2) + "\n");
13483
+ process.exit(result.status === "ok" ? ACCEPTANCE_EXIT_SUCCESS : ACCEPTANCE_EXIT_ERROR);
13484
+ }
13485
+ process.exit(renderHuman(result, source));
13486
+ });
13487
+ }
13488
+
13438
13489
  //#endregion
13439
13490
  //#region src/cli/index.ts
13440
13491
  process.setMaxListeners(20);
@@ -13494,6 +13545,7 @@ async function createProgram() {
13494
13545
  registerMonitorCommand(program, version);
13495
13546
  registerMergeCommand(program);
13496
13547
  registerWorktreesCommand(program, version);
13548
+ registerAcceptanceCommand(program, version);
13497
13549
  registerBrainstormCommand(program, version);
13498
13550
  registerExportCommand(program, version);
13499
13551
  registerIngestEpicCommand(program);
@@ -69,7 +69,17 @@ The Recovery Engine runs a 3-tier auto-fix ladder before any halt — Tier A ret
69
69
 
70
70
  ### Per-Story Worktree Behavior
71
71
 
72
- Each dispatched story runs in `.substrate-worktrees/story-<key>` on its own branch (`substrate/story-<key>`). The agent's auto-commit (e.g., `feat(story-N-M): ...`) lands on the branch, not main. Merge to main happens after verification SHIP_IT; the worktree is then removed. After verification failure, the worktree and branch are preserved for `substrate reconcile-from-disk` inspection. Use `--no-worktree` if your project doesn't support worktrees (submodules, bare repos).
72
+ Each dispatched story runs in an isolated git worktree on its own branch (`substrate/story-<key>`). By default the worktree lives OUTSIDE the repo at `~/.substrate/worktrees/<projectname>-<hash8>/<key>/`; set `worktree.base: in-repo` in `.substrate/config.yaml` to restore the legacy `.substrate-worktrees/<key>/` location. Substrate auto-commits the story's work to the branch (`feat(story-N-M): ...`) at dev-story completion — commit-first, before review so the branch is always the durable copy; failure paths add `wip(story-<key>)` checkpoints. After verification failure, the worktree and branch are preserved for `substrate reconcile-from-disk` inspection. Use `--no-worktree` if your project doesn't support worktrees (submodules, bare repos).
73
+
74
+ ### Finalization — how verified work integrates
75
+
76
+ `finalization.mode` in `.substrate/config.yaml` (or `--finalization <mode>` per run):
77
+
78
+ - **`merge`** (default): local merge into the branch the run started from. The merge is **fast-forward-only by default** — if the base branch moved during the run, the story escalates `ff-only-merge-not-possible` instead of synthesizing a merge commit. Set `finalization.merge_strategy: three-way` to allow merge commits — **required for concurrent multi-story runs** (later stories cannot fast-forward past earlier merges). A dirty parent tree whose changes intersect the story's diff escalates `parent-tree-dirtied-by-run` naming the files.
79
+ - **`branch`**: verified work stays on `substrate/story-<key>` — nothing self-merges; the branch is the deliverable. The safe brownfield mode.
80
+ - **`pr`**: branch + `git push` + `gh pr create` (one PR per story). Degrades to `branch` with a warning when push/gh fail — never blocks the story.
81
+
82
+ Lifecycle events on the NDJSON stream: `story:committed` -> (`story:merged`) -> `story:finalized {mode, branch, sha, pr_url?}`. `substrate report` lists unmerged deliverable branches under "Finalization". Optional `finalization.epic_gate_command` runs before the LAST story of an epic integrates (non-zero exit -> `epic-gate-failed`, branch preserved).
73
83
 
74
84
  ### Key Commands
75
85
 
@@ -79,6 +89,7 @@ Each dispatched story runs in `.substrate-worktrees/story-<key>` on its own bran
79
89
  | `substrate run --halt-on <severity>` | Halt policy: `all` / `critical` (default) / `none` |
80
90
  | `substrate run --non-interactive` | Suppress stdin prompts |
81
91
  | `substrate run --no-worktree` | Disable per-story git worktrees (use for submodules or bare repos) |
92
+ | `substrate run --finalization <mode>` | Integration mode: `merge` (default) / `branch` (never self-merge) / `pr` |
82
93
  | `substrate report [--run <id\|latest>]` | Per-run completion report |
83
94
  | `substrate report --verify-ac` | Append AC-to-Test traceability matrix |
84
95
  | `substrate reconcile-from-disk [--dry-run] [--yes]` | Path A reconciliation when tree is coherent |
@@ -69,7 +69,17 @@ The Recovery Engine runs a 3-tier auto-fix ladder before any halt — Tier A ret
69
69
 
70
70
  ### Per-Story Worktree Behavior
71
71
 
72
- Each dispatched story runs in `.substrate-worktrees/story-<key>` on its own branch (`substrate/story-<key>`). The agent's auto-commit (e.g., `feat(story-N-M): ...`) lands on the branch, not main. Merge to main happens after verification SHIP_IT; the worktree is then removed. After verification failure, the worktree and branch are preserved for `substrate reconcile-from-disk` inspection. Use `--no-worktree` if your project doesn't support worktrees (submodules, bare repos).
72
+ Each dispatched story runs in an isolated git worktree on its own branch (`substrate/story-<key>`). By default the worktree lives OUTSIDE the repo at `~/.substrate/worktrees/<projectname>-<hash8>/<key>/`; set `worktree.base: in-repo` in `.substrate/config.yaml` to restore the legacy `.substrate-worktrees/<key>/` location. Substrate auto-commits the story's work to the branch (`feat(story-N-M): ...`) at dev-story completion — commit-first, before review so the branch is always the durable copy; failure paths add `wip(story-<key>)` checkpoints. After verification failure, the worktree and branch are preserved for `substrate reconcile-from-disk` inspection. Use `--no-worktree` if your project doesn't support worktrees (submodules, bare repos).
73
+
74
+ ### Finalization — how verified work integrates
75
+
76
+ `finalization.mode` in `.substrate/config.yaml` (or `--finalization <mode>` per run):
77
+
78
+ - **`merge`** (default): local merge into the branch the run started from. The merge is **fast-forward-only by default** — if the base branch moved during the run, the story escalates `ff-only-merge-not-possible` instead of synthesizing a merge commit. Set `finalization.merge_strategy: three-way` to allow merge commits — **required for concurrent multi-story runs** (later stories cannot fast-forward past earlier merges). A dirty parent tree whose changes intersect the story's diff escalates `parent-tree-dirtied-by-run` naming the files.
79
+ - **`branch`**: verified work stays on `substrate/story-<key>` — nothing self-merges; the branch is the deliverable. The safe brownfield mode.
80
+ - **`pr`**: branch + `git push` + `gh pr create` (one PR per story). Degrades to `branch` with a warning when push/gh fail — never blocks the story.
81
+
82
+ Lifecycle events on the NDJSON stream: `story:committed` -> (`story:merged`) -> `story:finalized {mode, branch, sha, pr_url?}`. `substrate report` lists unmerged deliverable branches under "Finalization". Optional `finalization.epic_gate_command` runs before the LAST story of an epic integrates (non-zero exit -> `epic-gate-failed`, branch preserved).
73
83
 
74
84
  ### Key Commands
75
85
 
@@ -79,6 +89,7 @@ Each dispatched story runs in `.substrate-worktrees/story-<key>` on its own bran
79
89
  | `substrate run --halt-on <severity>` | Halt policy: `all` / `critical` (default) / `none` |
80
90
  | `substrate run --non-interactive` | Suppress stdin prompts |
81
91
  | `substrate run --no-worktree` | Disable per-story git worktrees (use for submodules or bare repos) |
92
+ | `substrate run --finalization <mode>` | Integration mode: `merge` (default) / `branch` (never self-merge) / `pr` |
82
93
  | `substrate report [--run <id\|latest>]` | Per-run completion report |
83
94
  | `substrate report --verify-ac` | Append AC-to-Test traceability matrix |
84
95
  | `substrate reconcile-from-disk [--dry-run] [--yes]` | Path A reconciliation when tree is coherent |
@@ -1,7 +1,7 @@
1
- import { DEFAULT_STALL_THRESHOLD_SECONDS, getAllDescendantPids, getAutoHealthData, inspectProcessTree, isOrchestratorProcessLine, registerHealthCommand, runHealthAction } from "./health-Cfkv6XoQ.js";
1
+ import { DEFAULT_STALL_THRESHOLD_SECONDS, getAllDescendantPids, getAutoHealthData, inspectProcessTree, isOrchestratorProcessLine, registerHealthCommand, runHealthAction } from "./health-DvgJ7jj_.js";
2
2
  import "./logger-KeHncl-f.js";
3
3
  import "./dist-BkhYgJWo.js";
4
- import "./manifest-read-BhNRIiqm.js";
4
+ import "./manifest-read-DyyFPrJH.js";
5
5
  import "./work-graph-repository-DZyJv5pV.js";
6
6
  import "./decisions-CzSIEeGP.js";
7
7
 
@@ -1,6 +1,6 @@
1
1
  import { createLogger } from "./logger-KeHncl-f.js";
2
2
  import { DoltClient, DoltQueryError, createDatabaseAdapter$1 as createDatabaseAdapter, getLatestRun, getPipelineRunById, initSchema } from "./dist-BkhYgJWo.js";
3
- import { resolveMainRepoRoot, resolveRunManifest } from "./manifest-read-BhNRIiqm.js";
3
+ import { resolveMainRepoRoot, resolveRunManifest } from "./manifest-read-DyyFPrJH.js";
4
4
  import { createRequire } from "module";
5
5
  import { dirname, join } from "path";
6
6
  import { existsSync, readFileSync } from "node:fs";
@@ -1000,4 +1000,4 @@ function registerHealthCommand(program, _version = "0.0.0", projectRoot = proces
1000
1000
 
1001
1001
  //#endregion
1002
1002
  export { BMAD_BASELINE_TOKENS_FULL, DEFAULT_STALL_THRESHOLD_SECONDS, FileKvStore, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, __commonJS, __require, __toESM, buildPipelineStatusOutput, createDatabaseAdapter$1 as createDatabaseAdapter, createDoltOperatorReader, findPackageRoot, formatOutput, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, inspectProcessTree, isOrchestratorProcessLine, parseDbTimestampAsUtc, registerHealthCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, runHealthAction, validateStoryKey };
1003
- //# sourceMappingURL=health-Cfkv6XoQ.js.map
1003
+ //# sourceMappingURL=health-DvgJ7jj_.js.map
@@ -1,5 +1,5 @@
1
1
  import { createLogger } from "./logger-KeHncl-f.js";
2
- import { readCurrentRunId, resolveMainRepoRoot } from "./manifest-read-BhNRIiqm.js";
2
+ import { readCurrentRunId, resolveMainRepoRoot } from "./manifest-read-DyyFPrJH.js";
3
3
  import { join } from "node:path";
4
4
  import { mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import * as readline from "node:readline";
@@ -180,4 +180,4 @@ async function runInteractivePrompt(decisionContext) {
180
180
 
181
181
  //#endregion
182
182
  export { runInteractivePrompt };
183
- //# sourceMappingURL=interactive-prompt-BBfPSxyJ.js.map
183
+ //# sourceMappingURL=interactive-prompt-CPF2GTvV.js.map
@@ -3087,7 +3087,21 @@ function executeProbeOnHost(probe, options = {}) {
3087
3087
  * registry, os. Open string so novel names don't cause silent skips.
3088
3088
  */
3089
3089
  const ExternalStateDependencySchema = z.string();
3090
- const StoryFrontmatterSchema = z.object({ external_state_dependencies: z.array(ExternalStateDependencySchema).optional().default([]) });
3090
+ /**
3091
+ * Journey tags (acceptance-gate story A0.2): `journeys: [UJ-x]` declares which
3092
+ * registry journeys this story claims to deliver. Tolerant by design — a bare
3093
+ * string coerces to a one-element list, and any non-coercible shape falls back
3094
+ * to `[]` via `.catch` WITHOUT failing the whole frontmatter block (a malformed
3095
+ * journey tag must never silently drop `external_state_dependencies`).
3096
+ * Untagged is legal: the epic-close coverage invariant (A0.3) is the backstop;
3097
+ * tags only buy earlier detection. Unknown-id validation happens against the
3098
+ * trusted-tree registry in the create-story workflow, not here.
3099
+ */
3100
+ const JourneyTagsSchema = z.union([z.string().transform((s) => [s]), z.array(z.string())]).catch([]);
3101
+ const StoryFrontmatterSchema = z.object({
3102
+ external_state_dependencies: z.array(ExternalStateDependencySchema).optional().default([]),
3103
+ journeys: JourneyTagsSchema.optional().default([])
3104
+ });
3091
3105
  /**
3092
3106
  * Parse the optional YAML frontmatter block from a story artifact.
3093
3107
  *
@@ -4963,6 +4977,210 @@ async function runAcTraceabilityCheck(input) {
4963
4977
  };
4964
4978
  }
4965
4979
 
4980
+ //#endregion
4981
+ //#region packages/sdlc/dist/acceptance/registry.js
4982
+ /** Repo-relative path of the registry — the single canonical location. */
4983
+ const JOURNEY_REGISTRY_PATH = ".substrate/acceptance/journeys.yaml";
4984
+ const JourneySurfaceSchema = z.enum([
4985
+ "email",
4986
+ "cli",
4987
+ "file",
4988
+ "web"
4989
+ ]);
4990
+ const JourneyEndStateSchema = z.object({
4991
+ id: z.string().min(1, "end-state id must be a non-empty string"),
4992
+ given: z.string().min(1, "given must be a non-empty string"),
4993
+ walk: z.string().min(1, "walk must be a non-empty string"),
4994
+ then: z.string().min(1, "then must be a non-empty string")
4995
+ });
4996
+ const JourneySchema = z.object({
4997
+ id: z.string().min(1, "journey id must be a non-empty string"),
4998
+ title: z.string().min(1, "title must be a non-empty string"),
4999
+ criticality: z.enum(["critical", "standard"]),
5000
+ surfaces: z.array(JourneySurfaceSchema).min(1, "a journey must declare at least one surface"),
5001
+ epic: z.number().int().positive().optional(),
5002
+ end_states: z.array(JourneyEndStateSchema).min(1, "a journey must declare at least one end_state — a journey with none is unjudgeable")
5003
+ });
5004
+ const JourneyRegistrySchema = z.object({
5005
+ version: z.number().int().positive("version must be a positive integer"),
5006
+ journeys: z.array(JourneySchema)
5007
+ }).superRefine((registry, ctx) => {
5008
+ const seenJourneyIds = new Map();
5009
+ registry.journeys.forEach((journey, i) => {
5010
+ const firstIndex = seenJourneyIds.get(journey.id);
5011
+ if (firstIndex !== void 0) ctx.addIssue({
5012
+ code: "custom",
5013
+ path: [
5014
+ "journeys",
5015
+ i,
5016
+ "id"
5017
+ ],
5018
+ message: `duplicate journey id "${journey.id}" (first declared at journeys[${firstIndex}])`
5019
+ });
5020
+ else seenJourneyIds.set(journey.id, i);
5021
+ const seenEndStateIds = new Map();
5022
+ journey.end_states.forEach((endState, j) => {
5023
+ const firstEs = seenEndStateIds.get(endState.id);
5024
+ if (firstEs !== void 0) ctx.addIssue({
5025
+ code: "custom",
5026
+ path: [
5027
+ "journeys",
5028
+ i,
5029
+ "end_states",
5030
+ j,
5031
+ "id"
5032
+ ],
5033
+ message: `duplicate end-state id "${endState.id}" in journey "${journey.id}" (first declared at end_states[${firstEs}])`
5034
+ });
5035
+ else seenEndStateIds.set(endState.id, j);
5036
+ });
5037
+ });
5038
+ });
5039
+ function zodErrorToValidationIssues(error) {
5040
+ return error.issues.map((issue) => ({
5041
+ path: issue.path.length > 0 ? issue.path.map(String).join(".") : "(root)",
5042
+ message: issue.message
5043
+ }));
5044
+ }
5045
+ /**
5046
+ * Parse and validate registry YAML content.
5047
+ *
5048
+ * Never throws: malformed YAML, non-object documents, and schema violations
5049
+ * all come back as named, pathed issues.
5050
+ */
5051
+ function parseJourneyRegistry(yamlContent) {
5052
+ let doc;
5053
+ try {
5054
+ doc = load(yamlContent);
5055
+ } catch (err) {
5056
+ const message = err instanceof YAMLException ? err.message : String(err);
5057
+ return {
5058
+ ok: false,
5059
+ issues: [{
5060
+ path: "(root)",
5061
+ message: `malformed YAML: ${message}`
5062
+ }]
5063
+ };
5064
+ }
5065
+ if (doc === null || doc === void 0) return {
5066
+ ok: false,
5067
+ issues: [{
5068
+ path: "(root)",
5069
+ message: "registry file is empty"
5070
+ }]
5071
+ };
5072
+ const result = JourneyRegistrySchema.safeParse(doc);
5073
+ if (!result.success) return {
5074
+ ok: false,
5075
+ issues: zodErrorToValidationIssues(result.error)
5076
+ };
5077
+ return {
5078
+ ok: true,
5079
+ registry: result.data
5080
+ };
5081
+ }
5082
+
5083
+ //#endregion
5084
+ //#region packages/sdlc/dist/acceptance/loader.js
5085
+ /** stderr shapes that mean "the path is not in the tree at this ref" (absent, not an error). */
5086
+ const GIT_SHOW_ABSENT_PATTERNS = [
5087
+ /does not exist in/i,
5088
+ /exists on disk, but not in/i,
5089
+ /path .* does not exist/i
5090
+ ];
5091
+ /** Array-argv spawn — no shell, no interpolation, injection-safe by construction. */
5092
+ function runGitShow(repoRoot, ref, relPath) {
5093
+ return new Promise((resolve$2) => {
5094
+ let stdout = "";
5095
+ let stderr = "";
5096
+ const proc = spawn("git", ["show", `${ref}:${relPath}`], {
5097
+ cwd: repoRoot,
5098
+ stdio: [
5099
+ "ignore",
5100
+ "pipe",
5101
+ "pipe"
5102
+ ]
5103
+ });
5104
+ proc.stdout?.on("data", (chunk) => {
5105
+ stdout += chunk.toString("utf-8");
5106
+ });
5107
+ proc.stderr?.on("data", (chunk) => {
5108
+ stderr += chunk.toString("utf-8");
5109
+ });
5110
+ proc.on("error", (err) => {
5111
+ resolve$2({
5112
+ code: null,
5113
+ stdout,
5114
+ stderr,
5115
+ spawnError: err.message
5116
+ });
5117
+ });
5118
+ proc.on("close", (code) => {
5119
+ resolve$2({
5120
+ code,
5121
+ stdout,
5122
+ stderr
5123
+ });
5124
+ });
5125
+ });
5126
+ }
5127
+ /**
5128
+ * Load the journey registry from the trusted main tree at `ref`.
5129
+ *
5130
+ * @param repoRoot absolute path of the TRUSTED repo root (the main tree, not a worktree)
5131
+ * @param ref git ref to read at — the dispatch snapshot ref (defaults to HEAD)
5132
+ */
5133
+ async function loadJourneyRegistryFromTrustedTree(repoRoot, ref = "HEAD") {
5134
+ const result = await runGitShow(repoRoot, ref, JOURNEY_REGISTRY_PATH);
5135
+ if (result.spawnError !== void 0) return {
5136
+ status: "error",
5137
+ message: `git show could not be spawned: ${result.spawnError}`
5138
+ };
5139
+ if (result.code !== 0) {
5140
+ if (GIT_SHOW_ABSENT_PATTERNS.some((p) => p.test(result.stderr))) return { status: "absent" };
5141
+ return {
5142
+ status: "error",
5143
+ message: `git show ${ref}:${JOURNEY_REGISTRY_PATH} failed (exit ${String(result.code)}): ${result.stderr.trim()}`
5144
+ };
5145
+ }
5146
+ const parsed = parseJourneyRegistry(result.stdout);
5147
+ if (!parsed.ok) return {
5148
+ status: "invalid",
5149
+ issues: parsed.issues
5150
+ };
5151
+ return {
5152
+ status: "ok",
5153
+ registry: parsed.registry
5154
+ };
5155
+ }
5156
+ /**
5157
+ * Filesystem read for OPERATOR LINT ONLY (`substrate acceptance validate`).
5158
+ * Gate/judge code paths must use `loadJourneyRegistryFromTrustedTree`.
5159
+ */
5160
+ async function loadJourneyRegistryFromFile(projectRoot) {
5161
+ const filePath = join$1(projectRoot, JOURNEY_REGISTRY_PATH);
5162
+ let content;
5163
+ try {
5164
+ content = await readFile$1(filePath, "utf-8");
5165
+ } catch (err) {
5166
+ const code = err.code;
5167
+ if (code === "ENOENT" || code === "ENOTDIR") return { status: "absent" };
5168
+ return {
5169
+ status: "error",
5170
+ message: `could not read ${filePath}: ${String(err)}`
5171
+ };
5172
+ }
5173
+ const parsed = parseJourneyRegistry(content);
5174
+ if (!parsed.ok) return {
5175
+ status: "invalid",
5176
+ issues: parsed.issues
5177
+ };
5178
+ return {
5179
+ status: "ok",
5180
+ registry: parsed.registry
5181
+ };
5182
+ }
5183
+
4966
5184
  //#endregion
4967
5185
  //#region packages/sdlc/dist/run-model/cli-flags.js
4968
5186
  /**
@@ -6381,5 +6599,5 @@ async function resolveRunManifest(dbRoot, runId) {
6381
6599
  }
6382
6600
 
6383
6601
  //#endregion
6384
- export { FindingsInjector, RunManifest, RuntimeProbeListSchema, SupervisorLock, ZERO_FINDINGS_BY_AUTHOR, ZERO_FINDING_COUNTS, ZERO_PROBE_AUTHOR_METRICS, aggregateProbeAuthorMetrics, applyConfigToGraph, createDefaultVerificationPipeline, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, detectsEventDrivenAC, detectsStateIntegratingAC, extractTargetFilesFromStoryContent, parseRuntimeProbes, readCurrentRunId, renderFindings, resolveGraphPath, resolveMainRepoRoot, resolveRunManifest, rollupFindingCounts, rollupFindingsByAuthor, rollupProbeAuthorByClass, rollupProbeAuthorMetrics, runAcTraceabilityCheck, runStaleVerificationRecovery };
6385
- //# sourceMappingURL=manifest-read-BhNRIiqm.js.map
6602
+ export { FindingsInjector, JOURNEY_REGISTRY_PATH, RunManifest, RuntimeProbeListSchema, SupervisorLock, ZERO_FINDINGS_BY_AUTHOR, ZERO_FINDING_COUNTS, ZERO_PROBE_AUTHOR_METRICS, aggregateProbeAuthorMetrics, applyConfigToGraph, createDefaultVerificationPipeline, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, detectsEventDrivenAC, detectsStateIntegratingAC, extractTargetFilesFromStoryContent, loadJourneyRegistryFromFile, loadJourneyRegistryFromTrustedTree, parseRuntimeProbes, parseStoryFrontmatter, readCurrentRunId, renderFindings, resolveGraphPath, resolveMainRepoRoot, resolveRunManifest, rollupFindingCounts, rollupFindingsByAuthor, rollupProbeAuthorByClass, rollupProbeAuthorMetrics, runAcTraceabilityCheck, runStaleVerificationRecovery };
6603
+ //# sourceMappingURL=manifest-read-DyyFPrJH.js.map
@@ -1,6 +1,6 @@
1
1
  import "../../logger-KeHncl-f.js";
2
2
  import "../../dist-BkhYgJWo.js";
3
- import "../../manifest-read-BhNRIiqm.js";
4
- import { runInteractivePrompt } from "../../interactive-prompt-BBfPSxyJ.js";
3
+ import "../../manifest-read-DyyFPrJH.js";
4
+ import { runInteractivePrompt } from "../../interactive-prompt-CPF2GTvV.js";
5
5
 
6
6
  export { runInteractivePrompt };
@@ -1,11 +1,11 @@
1
- import { BMAD_BASELINE_TOKENS_FULL, FileKvStore, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN, VALID_PHASES, __commonJS, __require, __toESM, buildPipelineStatusOutput, createDatabaseAdapter, formatOutput, formatPipelineSummary, formatTokenTelemetry, inspectProcessTree, parseDbTimestampAsUtc, validateStoryKey } from "./health-Cfkv6XoQ.js";
1
+ import { BMAD_BASELINE_TOKENS_FULL, FileKvStore, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN, VALID_PHASES, __commonJS, __require, __toESM, buildPipelineStatusOutput, createDatabaseAdapter, formatOutput, formatPipelineSummary, formatTokenTelemetry, inspectProcessTree, parseDbTimestampAsUtc, validateStoryKey } from "./health-DvgJ7jj_.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, BRANCH_PREFIX, CLAUDE_AUTH_FAILURE_HINT, CODEX_SANDBOX_BLOCK_HINT, 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, classifyVersionGap, createConfigSystem, createDatabaseAdapter$1, createDecision, createGitWorktreeManager, createPipelineRun, createRequirement, detectClaudeAuthFailure, detectCodexSandboxBlock, detectInterfaceChanges, getArtifactByTypeForRun, getArtifactsByRun, getDecisionsByCategory, getDecisionsByPhase, getDecisionsByPhaseForRun, getLatestRun, getPipelineRunById, getRunMetrics, getRunningPipelineRuns, getStoryMetricsForRun, getTokenUsageSummary, initSchema, listRequirements, loadModelRoutingConfig, registerArtifact, swallowDebug, updatePipelineRun, updatePipelineRunConfig, upsertDecision, writeRunMetrics, writeStoryMetrics } from "./dist-BkhYgJWo.js";
5
- import { FindingsInjector, RunManifest, RuntimeProbeListSchema, applyConfigToGraph, createDefaultVerificationPipeline, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, detectsEventDrivenAC, detectsStateIntegratingAC, extractTargetFilesFromStoryContent, parseRuntimeProbes, renderFindings, resolveGraphPath, resolveMainRepoRoot, runAcTraceabilityCheck, runStaleVerificationRecovery } from "./manifest-read-BhNRIiqm.js";
5
+ import { FindingsInjector, RunManifest, RuntimeProbeListSchema, applyConfigToGraph, createDefaultVerificationPipeline, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, detectsEventDrivenAC, detectsStateIntegratingAC, extractTargetFilesFromStoryContent, loadJourneyRegistryFromTrustedTree, parseRuntimeProbes, parseStoryFrontmatter, renderFindings, resolveGraphPath, resolveMainRepoRoot, runAcTraceabilityCheck, runStaleVerificationRecovery } from "./manifest-read-DyyFPrJH.js";
6
6
  import { WorkGraphRepository, detectCycles } from "./work-graph-repository-DZyJv5pV.js";
7
7
  import { deriveExitCode, routeDecision } from "./decision-router-BAPpON_C.js";
8
- import { runInteractivePrompt } from "./interactive-prompt-BBfPSxyJ.js";
8
+ import { runInteractivePrompt } from "./interactive-prompt-CPF2GTvV.js";
9
9
  import { runRecoveryEngine } from "./recovery-engine-BKGBeBnW.js";
10
10
  import { basename, dirname, extname, join } from "path";
11
11
  import { access, readFile, readdir, stat } from "fs/promises";
@@ -951,7 +951,7 @@ const PIPELINE_EVENT_METADATA = [
951
951
  {
952
952
  name: "reason",
953
953
  type: "string",
954
- description: "Escalation reason. Common values: review-cycles-exhausted (default review-loop max hit); create-story-no-file (create-story phase did not write a story artifact); dev-story-no-commit (substrate auto-commit found no committable changes — agent produced nothing or all changes were outside the worktree); dev-story-commit-failed (substrate auto-commit failed — typically a pre-commit hook rejected the commit; stderr captured in issues); merge-to-main-error (unexpected error in the merge-to-main phase); merge-conflict-detected (story branch could not be merged due to conflicts); parent-tree-dirtied-by-run (uncommitted parent changes intersect the story diff — merge refused, H3.3); ff-only-merge-not-possible (base moved and merge_strategy is ff-only — set finalization.merge_strategy: three-way or integrate manually, H3.3); epic-gate-failed (finalization.epic_gate_command exited non-zero before the last story of an epic integrated, H3.4); story-file-missing (story artifact gone from working tree AND branch HEAD — check wip/feat commits on the story branch, H5.5). v0.20.87+."
954
+ description: "Escalation reason. Common values: review-cycles-exhausted (default review-loop max hit); create-story-no-file (create-story phase did not write a story artifact); dev-story-no-commit (substrate auto-commit found no committable changes — agent produced nothing or all changes were outside the worktree); dev-story-commit-failed (substrate auto-commit failed — typically a pre-commit hook rejected the commit; stderr captured in issues); merge-to-main-error (unexpected error in the merge-to-main phase); merge-conflict-detected (story branch could not be merged due to conflicts); parent-tree-dirtied-by-run (uncommitted parent changes intersect the story diff — merge refused, H3.3); ff-only-merge-not-possible (base moved and merge_strategy is ff-only — set finalization.merge_strategy: three-way or integrate manually, H3.3); epic-gate-failed (finalization.epic_gate_command exited non-zero before the last story of an epic integrated, H3.4); story-file-missing (story artifact gone from working tree AND branch HEAD — check wip/feat commits on the story branch, H5.5); undisclosed-files-in-merge (a committed implementation file was never in the dev agent's files_modified, so no review cycle inspected it — inspect the branch, then re-dispatch with accurate files_modified or merge manually, H7). v0.20.87+."
955
955
  },
956
956
  {
957
957
  name: "cycles",
@@ -6138,6 +6138,21 @@ async function runCreateStory(deps, params) {
6138
6138
  } catch {}
6139
6139
  const archConstraintsContent = await getArchConstraints$3(deps);
6140
6140
  const storyTemplateContent = await getStoryTemplate(deps);
6141
+ const trustedRoot = deps.parentProjectRoot ?? deps.projectRoot;
6142
+ let journeyRegistry;
6143
+ if (trustedRoot !== void 0) {
6144
+ const registryLoad = await loadJourneyRegistryFromTrustedTree(trustedRoot);
6145
+ if (registryLoad.status === "ok" && registryLoad.registry.journeys.length > 0) journeyRegistry = registryLoad.registry;
6146
+ else if (registryLoad.status === "invalid") logger$21.warn({
6147
+ storyKey,
6148
+ issues: registryLoad.issues
6149
+ }, "A0.2: journey registry present but invalid — stories will be untagged (registry validity is escalated at epic close)");
6150
+ else if (registryLoad.status === "error") logger$21.warn({
6151
+ storyKey,
6152
+ error: registryLoad.message
6153
+ }, "A0.2: journey registry read failed — stories will be untagged");
6154
+ }
6155
+ const journeyRegistryContent = journeyRegistry !== void 0 ? journeyRegistry.journeys.map((j$1) => `- ${j$1.id} [${j$1.criticality}]: ${j$1.title}`).join("\n") : "";
6141
6156
  const { prompt, tokenCount, truncated } = assemblePrompt(template, [
6142
6157
  {
6143
6158
  name: "story_key",
@@ -6182,6 +6197,15 @@ async function runCreateStory(deps, params) {
6182
6197
  name: "prior_drift_feedback",
6183
6198
  content: "",
6184
6199
  priority: "optional"
6200
+ }],
6201
+ ...journeyRegistryContent.length > 0 ? [{
6202
+ name: "journey_registry",
6203
+ content: journeyRegistryContent,
6204
+ priority: "required"
6205
+ }] : [{
6206
+ name: "journey_registry",
6207
+ content: "",
6208
+ priority: "optional"
6185
6209
  }]
6186
6210
  ], TOKEN_CEILING);
6187
6211
  logger$21.debug({
@@ -6295,6 +6319,38 @@ async function runCreateStory(deps, params) {
6295
6319
  };
6296
6320
  }
6297
6321
  const parsed = parseResult.data;
6322
+ if (journeyRegistry !== void 0 && parsed.story_file !== void 0) {
6323
+ let artifactContent;
6324
+ try {
6325
+ artifactContent = await readFile$1(parsed.story_file, "utf-8");
6326
+ } catch {
6327
+ artifactContent = void 0;
6328
+ }
6329
+ if (artifactContent !== void 0) {
6330
+ const taggedJourneys = parseStoryFrontmatter(artifactContent).journeys;
6331
+ const knownIds = new Set(journeyRegistry.journeys.map((j$1) => j$1.id));
6332
+ const unknown = taggedJourneys.filter((id) => !knownIds.has(id));
6333
+ if (unknown.length > 0) {
6334
+ const details = `story artifact declares unknown journey id(s): ${unknown.join(", ")} — registry v${String(journeyRegistry.version)} defines: ${[...knownIds].join(", ")}`;
6335
+ logger$21.warn({
6336
+ epicId,
6337
+ storyKey,
6338
+ unknown
6339
+ }, "A0.2: journey tag validation failed");
6340
+ return {
6341
+ result: "failed",
6342
+ error: "schema_validation_failed",
6343
+ details,
6344
+ tokenUsage
6345
+ };
6346
+ }
6347
+ if (taggedJourneys.length > 0) logger$21.info({
6348
+ epicId,
6349
+ storyKey,
6350
+ journeys: taggedJourneys
6351
+ }, "A0.2: story tagged with journeys");
6352
+ }
6353
+ }
6298
6354
  logger$21.info({
6299
6355
  epicId,
6300
6356
  storyKey,
@@ -48092,4 +48148,4 @@ function registerRunCommand(program, version = "0.0.0", projectRoot = process.cw
48092
48148
 
48093
48149
  //#endregion
48094
48150
  export { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, EpicIngester, GLOBSTAR$1 as GLOBSTAR, GitClient, GrammarLoader, Minimatch$1 as Minimatch, Minipass, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, escape$1 as escape, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, normalizeGraphSummaryToStatus, registerExportCommand, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveMaxReviewCycles, resolveProbeAuthorStateIntegrating, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runProbeAuthor, runRunAction, runSolutioningPhase, unescape$1 as unescape, validateStopAfterFromConflict, wireNdjsonEmitter };
48095
- //# sourceMappingURL=run-JrEnvaB0.js.map
48151
+ //# sourceMappingURL=run-CAmBrDky.js.map
@@ -1,14 +1,14 @@
1
- import "./health-Cfkv6XoQ.js";
1
+ import "./health-DvgJ7jj_.js";
2
2
  import "./logger-KeHncl-f.js";
3
3
  import "./helpers-CElYrONe.js";
4
4
  import "./dist-BkhYgJWo.js";
5
- import "./manifest-read-BhNRIiqm.js";
6
- import { normalizeGraphSummaryToStatus, registerRunCommand, resolveMaxReviewCycles, resolveProbeAuthorStateIntegrating, runRunAction, wireNdjsonEmitter } from "./run-JrEnvaB0.js";
5
+ import "./manifest-read-DyyFPrJH.js";
6
+ import { normalizeGraphSummaryToStatus, registerRunCommand, resolveMaxReviewCycles, resolveProbeAuthorStateIntegrating, runRunAction, wireNdjsonEmitter } from "./run-CAmBrDky.js";
7
7
  import "./routing-DFxoKHDt.js";
8
8
  import "./work-graph-repository-DZyJv5pV.js";
9
9
  import "./decisions-CzSIEeGP.js";
10
10
  import "./decision-router-BAPpON_C.js";
11
- import "./interactive-prompt-BBfPSxyJ.js";
11
+ import "./interactive-prompt-CPF2GTvV.js";
12
12
  import "./recovery-engine-BKGBeBnW.js";
13
13
 
14
14
  export { runRunAction };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "substrate-ai",
3
- "version": "0.21.1",
3
+ "version": "0.21.2",
4
4
  "description": "Substrate — multi-agent orchestration daemon for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -191,6 +191,28 @@ Suggested values (use as many as apply):
191
191
 
192
192
  **Omit the field (or leave it empty) only for purely-algorithmic modules** where you also omit `## Runtime Probes` — parse/format/sort/score/calculate transforms, type-only refactors, documentation edits, build-config changes, and unit-test-only stories.
193
193
 
194
+ ### Journey tags (acceptance gate)
195
+
196
+ This project's journey registry (empty when the project has none):
197
+
198
+ {{journey_registry}}
199
+
200
+ If the registry above is non-empty, decide which journeys this story's acceptance criteria deliver or materially advance, and declare them in the SAME YAML frontmatter block at the top of the story file:
201
+
202
+ ```text
203
+ ---
204
+ external_state_dependencies:
205
+ - subprocess
206
+ journeys:
207
+ - UJ-2
208
+ ---
209
+ ```
210
+
211
+ Rules:
212
+ - Use ONLY journey ids that appear in the registry above — an unknown id fails the story artifact's schema validation.
213
+ - Tag a journey when the story wires, extends, or completes any part of that journey's user-facing path — not merely because it touches nearby code.
214
+ - If no registered journey applies (or the registry above is empty), OMIT the `journeys:` field entirely. Untagged stories are legal; unclaimed journeys are audited separately at epic close.
215
+
194
216
  ### Probe YAML shape
195
217
 
196
218
  Declare probes as a YAML list inside a single fenced `yaml` block directly under the `## Runtime Probes` heading. Each entry has this shape: