Skip to content

Gates Reference

LumenFlow’s Software Delivery pack runs a layered set of gates at pnpm wu:prep and pnpm wu:done. This page is the public reference for the gate catalogue, native delivery_review, the discriminated GateResult contract, the per-gate skip surface, the opt-in local_prep profile shipped in 5.5.0, wu:prep’s default-on report-all mode, and advisory heuristic gates such as test-over-deletion, monolithic-file-contention, and prod-migration-drift.

Gates are intentionally fail-closed: a gate that is applicable, has a missing prereq, and is not skippable: true produces a blocked-missing-prereq result and exits non-zero. The silent-bypass guard cannot be defeated with --skip-gate — see Skip semantics.

The standard catalogue ships in software_delivery.gates. Defaults below show the out-of-the-box skippable value. Per-repo overrides via software_delivery.gates.overrides.<gate_id> take precedence only for mutable policy gates; the safety-critical deny-set is immutable.

Gateskippable (default)Lifecycle surfaceNotes
format:checkfalseprep + donePrettier check on changed files. Safety-critical. Missing script blocks in default mode.
lintfalseprep + doneESLint. Safety-critical. Content cache retained on both the incremental and the full-run path — see Incremental typecheck and lint.
typecheckfalseprep + doneRuns software_delivery.gates.commands.typecheck when configured, else full-workspace tsc --noEmit. Safety-critical. Fail-closed cache validation — see Incremental typecheck and lint.
spec:linterfalseprep + doneWU YAML spec validity. Safety-critical.
co-changefalseprep + doneRequired co-edits per software_delivery.gates.co_change. Safety-critical.
claim-validationfalseprep + doneBranch / lane / WU lock invariants. Safety-critical.
unread-directed-signalfalseprep + doneDirected inbox signals must be triaged before completion. Safety-critical.
invariantsfalseprep + doneRepo-defined hard invariants (paths, status, schema). Safety-critical.
safety-critical-testfalseprep + doneRuns on every WU by default. Narrow with applicability_paths, do not flip skippable.
delivery_reviewfalseprep + doneNative completion review gate. Enabled by software_delivery.gates.delivery_review.enabled; wu:prep auto-runs it when auto_run: true. Writes .lumenflow/artifacts/delivery-review/<WU-ID>.json; FAIL blocks, PARTIAL warns, PASS passes.
tdd-diff-evidencetrueprep + doneTDD ratchet against baseline. Allow-listed.
migration-verifytrueprep + doneDB migration up/down dry-run. Allow-listed.
lane-healthtrueprep + doneLane wip-limit / coverage check. Allow-listed.
buildtrueprep (opt-in)local_prep.build.enabled=true. prereq_strategy: 'fail'.
integration-testtrueprep (opt-in)local_prep.integration_test.enabled=true.
e2e-smoketrueprep (opt-in)local_prep.e2e_smoke.enabled=true. Auto not-applicable when no Playwright config.
test-over-deletiontrueprepAdvisory heuristic: flags WUs whose diff deletes .test.ts(x) whose describe() symbol is not present in the WU’s code_paths. Skip with --skip-gate test-over-deletion --reason ... --fix-wu ....
monolithic-file-contentiontrueprepAdvisory heuristic: flags files with observed ownership in 2+ in-progress WUs’ code_paths including the current WU. Unclaimed ready placeholders are ignored. Output distinguishes observed ownership from unknown editing/landing activity and includes a split-module hint plus audited skip form.
prod-migration-drifttrueCI (opt-in)Advisory CI gate: prod DB more than threshold (default 5) migrations behind main’s migrations/ directory. Auto-skips not-applicable until software_delivery.gates.overrides.prod-migration-drift.connection_string_env_var is configured.
cross-cutting-ratchetstrueprep + doneRuns the repo-wide enforcement-baseline suites on every code-gate run — including scoped/agent-mode wu:prep. NEW drift relative to main blocks; pre-existing main debt warns only. Auto-skips outside worktrees and in repos without the suites.
parity-drifttrueprep + done (docs-only + code)Runs cross-package parity/drift suites (MCP, conductor-sdk, integration-tests, CLI flag, public docs) unconditionally, exempt from changed-file scoping. Same NEW-vs-main triage as cross-cutting-ratchets. Auto-skips outside worktrees and in repos without the suites.

Gates marked skippable: false belong to the silent-bypass guard: when applicable + missing-prereq, the runtime emits blocked-missing-prereq and refuses to continue. Mutable environmental gates may opt into software_delivery.gates.overrides.<gate_id>.skippable=true. Configuration rejects that override for format:check, lint, typecheck, co-change, spec:linter, claim-validation, unread-directed-signal, invariants, and safety-critical-test. Missing scripts for the first four block even without --strict; trusted applicability policy and validated docs-only scope remain the only non-applicable paths.

Each gate run produces one of six GateResult discriminated states. Glyphs and labels are stable and surfaced verbatim by wu:prep, wu:done, and the audit NDJSON source field.

StateGlyphAudit sourceMeaning
passed✅ PASSEDGate ran successfully.
failed❌ FAILEDGate ran and failed (or applicable + missing + skippable + 'fail').
skipped-by-agent⏭ SKIPPED-BY-AGENTagentExplicit named --skip-gate <name>.
skipped-auto-not-applicable⊘ AUTO-SKIPPEDauto-skip-not-applicablecheckApplicability returned not-applicable for this change set.
skipped-auto-missing-prereq⊘ AUTO-SKIPPEDauto-skip-missing-prereqApplicable + missing prereq + skippable: true + prereq_strategy: 'auto-skip'.
blocked-missing-prereq🚫 BLOCKEDblockedApplicable + missing prereq + skippable: false. Exits non-zero.

The four-axis decision: applicability × preconditions × skippable × prereq_strategy.

ApplicabilityPreconditionsskippableprereq_strategyResult
not-applicableskipped-auto-not-applicable
applicablesatisfiedrun → passed or failed
applicablemissingfalseblocked-missing-prereq (BLOCK)
applicablemissingtrue'auto-skip'skipped-auto-missing-prereq
applicablemissingtrue'fail'failed

Each gate may implement checkApplicability and checkPreconditions. Defaults return 'applicable' and 'satisfied' so simple gates inherit always-runs semantics with no per-gate code change. Narrow a gate’s reach by setting an explicit override:

# Restrict safety-critical-test to security paths without flipping skippable
pnpm config:set software_delivery.gates.overrides.safety-critical-test.applicability_paths \
  --json-value '["app/security/**","lib/auth/**"]'

pnpm wu:prep and pnpm wu:done accept --skip-gate <name> (repeatable). The flag is symmetric across both lifecycle surfaces and the MCP wu_done tool.

# Skip a single allow-listed gate
pnpm wu:done --id WU-XXX --skip-gate lane-health \
  --reason "pre-existing on main: 50-file coverage gap" --fix-wu WU-YYY

# Repeat to skip multiple gates
pnpm wu:prep --id WU-XXX \
  --skip-gate lane-health --skip-gate migration-verify \
  --reason "no DB infra in agent worktree" --fix-wu WU-YYY

--reason and --fix-wu are required. --fix-wu must reference a real WU that owns the eventual remediation.

An audited skip records that a gate did not run; it does not declare that an artifact required by wu:done may be absent. When a matching conditional_commands entry produces an artifact or validates external state required during completion, opt it into the completion boundary:

software_delivery:
  gates:
    conditional_commands:
      - trigger_patterns:
          - 'supabase/migrations/**'
        command: 'pnpm db:verify-and-write-artifact'
        severity: error
        fresh_on_completion: true

wu:done runs every matching fresh_on_completion command after reconciliation, including when a valid prep checkpoint records an explicit conditional_commands skip. A failed command or an unavailable runtime remains blocking and names the current WU and command; neither checkpoint reuse nor the recorded skip can bypass the producer. Commands without fresh_on_completion: true keep ordinary checkpoint-reuse semantics and therefore must not be the sole producer of an artifact required by completion.

Only allow-listed gates (migration-verify, lane-health, tdd-diff-evidence, plus local_prep opt-ins build, integration-test, e2e-smoke, plus the advisory heuristic gates test-over-deletion, monolithic-file-contention, the opt-in prod-migration-drift gate, and the cross-cutting-ratchets and parity-drift gates) accept --skip-gate out of the box. To make any other gate skippable per-repo, set the override:

pnpm config:set software_delivery.gates.overrides.db-dod-gate.skippable=true

Use this sparingly. Safety-critical gates (format:check, lint, typecheck, co-change, spec:linter, claim-validation, unread-directed-signal, invariants, safety-critical-test) cannot be flipped: configuration rejects skippable: true. Trusted applicability_paths can narrow only the file-scoped subset: format:check, lint, typecheck, co-change, spec:linter, and safety-critical-test. claim-validation and invariants are lifecycle/always-on gates. After signal triage, an unread-signal exception must use the audited --allow-unread-signals --reason flow.

The binary --skip-gates flag has been removed. CLI parsing retains it only as a recognizable tombstone and rejects it before lifecycle side effects.

[wu:done] --skip-gates has been removed and is always rejected. Use --skip-gate <name>.

The MCP/runtime surface exposes skip_gate arrays. Its skip_gates Boolean is likewise a rejected tombstone.

Audit rows gain a discriminated source field (agent | auto-skip-not-applicable | auto-skip-missing-prereq | blocked) and a lifecycle field (prep | done). Legacy audit entries, including historical gate: 'all' rows, remain readable; entries without source read as agent for back-compat. New global rows cannot be written.

CFR (Change Failure Rate) DORA queries filter source: 'agent' so auto-skips and silent-block records never inflate change-failure rate. If you build custom dashboards on the audit stream, mirror this filter to preserve CFR semantics.

delivery_review is LumenFlow’s native, vendor-neutral completion review gate. It is configured under the global Software Delivery gate config, not under a specific agent client:

software_delivery:
  gates:
    delivery_review:
      enabled: true
      auto_run: true
      block_partial: true
      verifier_command: pnpm qa:evidence
      verifier_command_mode: on-insufficient-evidence
      skip_types:
        - documentation
        - process
PropertyValue
Default skippablefalse (not on the built-in --skip-gate allow-list). Repos may opt in with software_delivery.gates.overrides.delivery_review.skippable=true, but normal usage is to fix or declare the missing delivery evidence.
Lifecycle surfacepnpm gates when enabled: true; pnpm wu:prep when both enabled: true and auto_run: true; pnpm wu:done through the normal gate lifecycle.
ApplicabilityCurrent WU can be detected and its type is not listed in software_delivery.gates.delivery_review.skip_types. Default skip types are documentation and process.
OutputStable JSON artifact at .lumenflow/artifacts/delivery-review/<WU-ID>.json with PASS, PARTIAL, or FAIL, findings, acceptance-criteria status, and metadata.
Verdict semanticsPASS passes; PARTIAL logs a warning and continues by default, or blocks when block_partial: true; FAIL blocks the gate.
Evidence semanticsSource-code delivery changes need automated test evidence or meaningful manual evidence. Empty evidence, placeholders such as todo / n/a, and negative entries such as not run are treated as missing evidence and produce FAIL.
Truth verificationOptional verifier_command runs from the repo root after native evidence-shape checks. A non-zero exit adds a critical finding and blocks. LumenFlow exports LUMENFLOW_DELIVERY_REVIEW_WU_ID=<WU-ID> to the command so repo tooling can inspect the current WU and evidence files.
verifier_command_modealways (default) runs the configured verifier_command every time one is set, byte-identical to pre-WU-3982 behaviour. on-insufficient-evidence skips invoking it when the native evidence check (automated test evidence, meaningful manual evidence, or an exemption) already reports sufficient evidence for the change set — the gate output states the verifier was skipped because evidence was sufficient. Use this when the verifier is an expensive project-owned harness (e.g. a Playwright visual pipeline) that a component-test-proven change should not have to run. Any other value fails schema validation with a clear message.
Legacy client configsoftware_delivery.agents.clients.<client>.features.delivery_review is accepted temporarily for adapter UX and migration warnings, but core enforcement is controlled by software_delivery.gates.delivery_review.enabled and software_delivery.gates.delivery_review.auto_run.

Use delivery_review when a repo needs completion evidence beyond raw test execution, such as manual QA, Playwright-driven visual checks, or acceptance-criteria review that should be enforced before wu:done. The gate does not require lumenflow.cloud and behaves the same across Claude, Codex, Cursor, Windsurf, Cline, Aider, custom agents, and hosted workers.

Manual evidence should name what was exercised and what was observed. Structured entries such as surface: /records/new; action: opened create form; result: expected fields rendered; screenshot: /tmp/record-form.png are accepted, while screenshot: n/a, placeholder, todo, or not run are blocked as missing evidence.

A tests.manual entry is a placeholder (rejected) when, after trimming and lower-casing, it is:

  • blank, or exactly one of the reserved tokens: n/a, na, none, todo, tbd, placeholder, fake, dummy, empty, missing, not run, not tested, not applicable, retired;
  • a phrase matching a negative-evidence pattern, e.g. fake evidence, dummy qa evidence, retired wrapper, not run, not tested; or
  • a label: value pair where label names an evidence field (qa-evidence, evidence, manual, result, surface, screenshot) and value is itself one of the reserved tokens above (e.g. screenshot: n/a).

An entry is meaningful (accepted) when it is at least 12 characters, is not a placeholder, and either:

  • (a) names at least 2 of: a surface/route/URL/page/component/endpoint, an action/scenario/step, an observed/expected/actual result, or an artifact (screenshot/recording/trace); or
  • (b) is at least 4 words, uses a verification verb (verified, validated, confirmed, checked, exercised, passed), and references an artifact file path (e.g. *.png, *.json, *.txt).

This is the single predicate the gate blocks on. wu:create and wu:edit evaluate the exact same predicate at authoring time (packages/@lumenflow/packs/software-delivery/src/config/ delivery-review-contract.ts) and print a non-blocking WARNING as soon as a --test-paths-manual entry looks like a placeholder, so the author sees the problem immediately instead of discovering it late in wu:prep. This authoring-time check never blocks — the delivery_review gate remains the sole blocking enforcement point.

Use block_partial: true when a repo wants uncertainty to fail closed during wu:prep and wu:done. Use verifier_command for project-specific truth checks, such as confirming screenshot paths exist, Playwright traces were produced, or visual QA evidence matches a repo-defined format. Set verifier_command_mode: on-insufficient-evidence when that command is expensive and should only run as a fallback — native evidence that already passes skips the verifier entirely; leave it at the default always to keep every consumer’s existing coverage unchanged.

software_delivery.gates.local_prep is an opt-in profile that extends wu:prep with the build, integration-test, and e2e-smoke gates that previously only ran in CI. It exists because three P0 hotfixes in 24h slipped past local gates because wu:prep did not run pnpm build, pnpm test:integration, or pnpm test:e2e --smoke.

software_delivery:
  gates:
    local_prep:
      build:
        enabled: false # opt-in: pnpm build on every wu:prep
      build_scope: applicable # default: scope the build gate to code_paths; 'full' = unscoped
      integration_test:
        enabled: false # opt-in: pnpm test:integration on every wu:prep
      e2e_smoke:
        enabled: false # opt-in: pnpm test:e2e --tag <tag>
        tag: smoke
      latency_budget_warn_ms: 180000 # 3 min soft warn, never blocks
      latency_budget: # 6.2.0+: enforceable per-weight-class budget; ships OFF
        enforced: false # opt in after measuring your own distribution
        docs_only_ms: 3600000
        broad_ms: 3600000
        full_ms: 7200000
      report_all: true # default: run every applicable gate, report all failures in one cycle
      reuse_evidence: false # default: never reuse a prior prep result
      gate_main_heap_mb: 4096 # optional: override the gate-main comparison subprocess heap (MB)
FieldDefaultEffect
build.enabledfalseWhen true, registers a build gate that runs pnpm build. prereq_strategy: 'fail' — missing build script in package.json is a misconfig (failed), not an environmental gap.
build_scopeapplicableSee Build scope below.
integration_test.enabledfalseWhen true, makes the integration-test gate applicable. Runs RUN_INTEGRATION_TESTS=1 pnpm vitest run '**/*.integration.*' '**/golden-*.test.*'. Missing script → failed.
e2e_smoke.enabled + tagfalse / smokeWhen true AND playwright.config.{ts,js,mjs} exists at repo root, registers an e2e-smoke gate that runs pnpm test:e2e --tag <tag>. Repos without Playwright auto not-applicable (clean opt-out).
latency_budget_warn_ms180000wu:prep prints total elapsed seconds; if it exceeds the budget, emits a non-failing warning. Soft signal only; never blocks.
latency_budget(see below)See Enforced latency budget below. Opt-in (enforced: false by default); when enabled, blocks wu:prep on a run that exceeds the budget for its weight class.
report_alltrueSee Report-all mode below.
reuse_evidencefalseSee Safe prep-evidence reuse below.
gate_main_heap_mb(derived)See Gate-main comparison subprocess heap below.

All three local-prep gates are added to the --skip-gate allow-list (skippable: true). Use the skip for legitimate hotfix scenarios where the gate is broken or irrelevant for that WU; the audit row carries source: 'agent'.

Local-prep gates intentionally lengthen wu:prep. Budget guidance:

  • Default 3 min (latency_budget_warn_ms: 180000): comfortable headroom for typical JS/TS repos with pnpm build cached and a small smoke suite.
  • Tight (60–90s): keep agents on a fast feedback loop in greenfield repos; raises false-positive warnings as suites grow.
  • Loose (5–10 min): monorepos with full Playwright matrix; agents may want to disable e2e_smoke per-WU via --skip-gate e2e-smoke for cosmetic-only changes.

latency_budget_warn_ms above never blocks, and that is deliberate: it is the advisory target, and its breach is the recorded trigger for adding gate capacity. latency_budget is a different instrument sitting beside it — an enforceable regression tripwire that fails wu:prep when a run’s gate execution takes far longer than the workspace’s measured distribution.

It ships disabled (enforced: false). A budget derived from one workspace’s ledger is not a claim about anyone else’s host, so an upgrade never introduces a new blocking failure mode. Until a workspace opts in, an overrun prints an advisory diagnostic and prep continues exactly as before.

Two budgets, two questions:

SettingQuestion it answersOn breach
latency_budget_warn_msIs one host still enough?Non-failing warning
latency_budget.*_msDid the lifecycle get slower?Advisory, or wu:prep fails once enabled

The budget is resolved from the run’s weight class, read through the same classifier gate admission uses on the same resolved execution plan — for every plan except one:

ClassWhen a prep run resolves to itDefault
docs-onlywu:prep --docs-only3600000
broadScoped test paths resolved3600000
fullUnnarrowed, or --full-tests7200000

There is no scoped_ms. The scoped class requires a gate list containing a build-producing gate, which wu:prep never supplies, so a scoped_ms field would document a budget no prep run could ever be held to.

Set a class to 0 to disable enforcement for that class only. Leave enforced: false (the default) to keep the budgets in place but make every overrun advisory.

Measured, not chosen. The shipped numbers are derived from this project’s own governed ledger: gate_attempt rows with lifecycle: prep, grouped into runs by work unit, diff hash and commit, over the 14 days to 2026-09-05.

  • 19,190 ledger lines considered, 6,904 prep gate attempts matched, 12,286 skipped (not a prep gate attempt, or outside the window);
  • 252 runs: p50 662.9s, p90 1354.7s, p95 1552.5s, max 5207.7s;
  • of those, 91 runs were green on every gate: max 1694.9s.

The rule applied to every class: at least twice the p95, and never below the observed green maximum. Twice p95 is 3105s and the green maximum is 1694.9s, so 3600s clears both. Only a green run ever reaches the budget — a failed gate exits wu:prep first — which is why the green maximum is the floor. A full run is the costliest plan by construction and the population maximum exceeds twice p95, so full gets 7200s.

Derive your own the same way before setting enforced: true. Numbers from somebody else’s host are not evidence about yours.

Gate execution only. The time a run spends queued for gate admission is subtracted and reported separately: waiting behind a busy host is contention, not a lifecycle regression, and charging it to the budget would make your own parallelism look like the thing the budget exists to catch. The span is taken from a monotonic clock, so a wall-clock correction cannot trip it.

An overrun fails wu:prep with a diagnostic that names the gate that overran, the class, the budget, the excluded admission wait, and the slowest gate phases. A run that is legitimately slow can be waived once:

pnpm wu:prep --id WU-XXXX --latency-budget-exception --latency-budget-reason '<why this run was legitimately slow>'

The reason has its own flag: one --reason string cannot honestly justify a gate skip, an unread-signal override and a budget waiver at the same time. The flag without a reason fails with the same diagnostic.

Every outcome that changed what prep did — a granted waiver, a block, and both refusals — is appended to .lumenflow/latency-budget-exceptions.ndjson with the work unit, outcome, class, budget, elapsed time, admission wait, attributed gate, reason and committer identity. A budget that blocks and leaves no trace cannot be evaluated after the fact.

The waiver’s scope is narrow and fixed:

  • It clears the budget. It never skips a gate, and it can never be made to: it has no gate parameter, and it covers no gate name — including every gate in the never-skippable deny-set.
  • It never applies to a run whose gates did not all pass. Gate failures are reported and exit before the budget is evaluated at all, so a slow failing run cannot be waived, whatever its reason.

reuse_evidence is default-off. The fast path is available only when the workspace explicitly opts in and disables report-all mode:

software_delivery:
  gates:
    local_prep:
      reuse_evidence: true
      report_all: false

Only an identical eligible low- or medium-risk plan can reuse a passed evidence record. It is bound to source and declared-test content, dependencies, toolchain, main snapshot, policy, authority, applicability, and the exact execution plan. Missing, malformed, stale, cross-workspace, or mismatched evidence runs gates fresh. --full-tests, report-all, and high-risk prep run gates fresh; --full-tests also forces full test execution. Snapshot isolation remains OFF: reuse does not pin or execute an immutable workspace snapshot.

Omitting local_prep (the default) preserves the pre-5.5.0 behaviour — none of the new gates is applicable, no new commands run.

Gate speed is a release precondition, not an anecdote. software_delivery.gates.throughput_baseline configures a ratchet that pnpm release runs before it creates a tag: it reads a recorded baseline, takes a fresh measurement from the governed metrics command over the same window the baseline declares, and fails closed when any metric has moved in its bad direction beyond the tolerance.

This is the release-time regression gate. The per-prep latency_budget above is a separate, opt-in operational guard; the two are not interchangeable.

software_delivery:
  gates:
    throughput_baseline:
      path: .lumenflow/throughput-baseline.json
      tolerance_percent: 25
FieldDefaultEffect
path.lumenflow/throughput-baseline.jsonRepository-relative path to the recorded baseline document.
tolerance_percent25How far a metric may move in its bad direction before it is a regression.
MetricDirectionSource
gate_run_p50_mslower is betterMedian of the per-day gate-attempt p50s in the window
gate_run_p90_mslower is betterMedian of the per-day gate-attempt p90s in the window
warm_path_prep_mslower is betterElapsed time of a warm, evidence-reusing prep
first_attempt_landing_share_percenthigher is betterShare of landed WUs with no failed gate attempt

Record it — never hand-write it:

pnpm throughput:baseline:record

That reads the governed metrics command through the same projection the release comparison uses, and refuses to write a document the comparison could not read back. Every value comes from that command, never a bespoke scan:

pnpm metrics:snapshot --type flow --json --days <window>

Because that command reports gate-run cost per calendar day, and the raw per-attempt durations are not recoverable from its stable output shape, the baseline reduces a window of daily percentiles to one number per metric: the median of the per-day values, taken independently per percentile.

The statistic is calibrated, not chosen. On this project’s own ledger the per-day spread is 3.6x for p50 and 2.7x for p90, so the worst day in a window describes the noisiest afternoon rather than the lifecycle. Replaying ten sliding 7-day windows — none of which contains a regression — against a baseline recorded from the most recent one measures the gate’s false-positive rate directly: a worst-day statistic at a 20% tolerance blocks 7 of the 10; the per-day median at the shipped 25% blocks 1 of the 10. A gate that cries wolf seven times in ten gets routed around, which is precisely how an unenforced budget becomes worthless.

Calibrate your own the same way before trusting the default tolerance. The simulation lives in tools/__tests__/throughput-baseline.test.ts.

The baseline’s window_days decides the candidate’s window. Percentiles taken over different windows are not comparable, so a candidate measured over a different window fails closed rather than being compared anyway.

A metric may be recorded as null, which declares it absent for this workspace. An absence declared on both sides passes and is reported. A metric the baseline declares present that the candidate cannot supply fails — an unmeasured release is never a pass. The same applies to a missing baseline, an unparseable one, one with no window_days, one carrying a metric that is neither a number nor null, one declaring every metric absent (it would compare equal to everything and certify any release), a metrics command that will not run, and a non-finite value: every one of them is a failure with a named reason, and none of them is a way to ship faster.

The opt-in build gate used to run a single unscoped pnpm build — a full turbo build of every workspace package — regardless of what the WU actually touched. On a monorepo with a web app in the workspace graph, that meant a pack/CLI/docs-only WU still paid for a full next build with webpack workers, several minutes of a wu:prep cycle that no applicable gate consumed (mem-5ea1: WU-3768).

software_delivery.gates.local_prep.build_scope controls this:

software_delivery:
  gates:
    local_prep:
      build_scope: applicable # default
  • applicable (default): derives a turbo run build --filter=<pkg>... set from the union of the WU’s code_paths and the currently-applicable gates’ applicability_paths (software_delivery.gates.overrides.<gate>.applicability_paths), each mapped to the real workspace package name (nearest package.json) that owns it. The ... suffix is turbo’s own “package plus dependency closure” syntax, so a package another applicable package depends on still builds — no hand-rolled graph walk. A WU whose code_paths touch only packages/** never builds a apps/web-style package; a WU touching that app’s own paths still builds it.
  • full: restores the pre-WU-3780 unscoped pnpm build invocation byte-for-byte. Use this if your repo’s build graph has cross-package effects the code_paths declaration doesn’t capture (for example, a codegen step with untracked side effects).

When no code_paths resolve to a real package (for example a docs-only WU with the build gate force-enabled), applicable falls back to the unscoped invocation rather than silently building nothing.

Gate-main comparison subprocess heap (WU-3780)

Section titled “Gate-main comparison subprocess heap (WU-3780)”

wu:prep classifies a failing gate as introduced-by-branch vs pre-existing-on-main by re-running it against an isolated main-snapshot probe — the “gate-main comparison” subprocess. On a large monorepo test suite this subprocess can exceed Node’s default heap and crash with FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory (mem-5ea1: this happened twice in one WU-3768 session and only recovered once a worker exported NODE_OPTIONS=--max-old-space-size=8192 by hand — an undocumented remedy a fresh agent has no way to know).

The comparison subprocess now spawns with an explicit --max-old-space-size by default:

  • Derived from 25% of host memory, floored at 512MB and ceilinged at 8192MB, so a small dev box still gets a useful floor and a huge host doesn’t hand the subprocess an unbounded ceiling.

  • Logged once per comparison run so the value in effect is always visible.

  • Overridable via software_delivery.gates.local_prep.gate_main_heap_mb (megabytes):

    software_delivery:
      gates:
        local_prep:
          gate_main_heap_mb: 4096
  • A caller-supplied NODE_OPTIONS that already pins --max-old-space-size is respected unchanged — the default never overrides an operator’s explicit choice.

If the subprocess still exhausts the configured heap, its failure message names the exact remedy (NODE_OPTIONS="--max-old-space-size=<larger-value>" pnpm wu:prep --id <WU-ID>) instead of leaving a worker to rediscover it by hand.

Every blocking gate failure that reaches the gate-main comparison resolves to exactly one of three outcomes, each with its own distinct verdict in both the per-gate output and the report-all consolidated failure report:

OutcomeMeaningVerdictRemedy
pre-existing-on-mainThe failure identity matches an identical failure on the pinned main snapshot.Auto-skipped: ⊘ <gate>: skipped (pre-existing failure on main).None needed — the auto-skip is already audited with reason pre-existing on main.
introduced-by-branchMain passes; only the branch fails.Blocks: the classifier’s own message (for example, “passes on main but fails on this branch”).Fix the failure. This outcome can never be skipped.
unhandled (undeterminable)Main also fails, but the failure cannot be matched to the branch’s — an unrunnable comparison, lost probe infrastructure, or a genuinely different failure.Blocks: the classifier’s explanation (why it could not certify pre-existing) followed by the governed remedy.Depends on getEffectiveSkippable(<name>): skippable — with independent verification and the repository owner’s authorization, pnpm wu:prep --id <WU-ID> --skip-gate <name> --reason "<why pre-existing>" --fix-wu <WU-ID>.
Not skippable (every gate on the immutable safety-critical deny-set, plus test) — no skip remedy exists; fix the failure on this branch, or repair main. There is no automatic skip for either case.

The undeterminable outcome is not a weaker form of pre-existing-on-main — it stays blocking by design. What changed (WU-3981) is only that its explanation and remedy now reach the operator instead of being discarded for a bare <gate> failed — and the remedy itself is now honest about which gates it applies to: the classifier’s own message never printed --skip-gate for a gate getEffectiveSkippable refuses (immutable safety-critical gates, and test); it prints the real remedy for those instead.

Pre-existing-on-main fast-paths (WU-3891 / WU-3905)

Section titled “Pre-existing-on-main fast-paths (WU-3891 / WU-3905)”

The gate-main comparison above (“re-running it against an isolated main-snapshot probe”) is the default path for classifying a failing gate as introduced-by-branch vs pre-existing-on-main. Two gates additionally short-circuit that probe with a cheaper, provable comparison anchored to the same pinned mainSnapshotSha:

  • format:check (WU-3891): when every branch-failing file, plus the whole prettier config surface (.prettierrc*, prettier.config.*, package.json), is byte-identical to the pinned main snapshot, the failure is definitionally inherited — no probe needed.
  • lint security-warning budget (WU-3905): ESLint’s --max-warnings ceiling (software_delivery.gates.maxEslintWarnings, default 100) can fail on debt the branch never touched. When the branch measures the same-or-fewer warnings than the pinned main snapshot on the SAME incremental-lint file scope (with the eslint config surface unchanged), the failure classifies pre-existing-on-main without a probe. Any branch-caused increase, an unmeasurable comparison, or a changed eslint config surface fails closed (falls through to the normal probe).

Both fast-paths share one comparison seam, classifyCountBudgetGateResult (packages/@lumenflow/cli/src/gates/classification-helpers.ts): branch-count <= main-count classifies pre-existing-on-main; any non-finite input or a branch-caused increase returns no classification and the normal probe-based path runs unchanged. Other count/budget-style gates can adopt the same seam directly instead of duplicating the comparison.

The typecheck gate executes software_delivery.gates.commands.typecheck when configured (for example, an affected-only command) and falls back to the full-workspace default when unconfigured. Several safety nets keep a faster run from ever hiding a real error:

  • Main-branch fallback. On the integration main branch, “affected” has no meaningful diff base, so the full command always runs instead of the configured one.
  • Main-snapshot fallback. A wu:prep main-snapshot probe diffs its own worktree against itself (an empty diff), which would make an affected-only command select zero tasks and misreport a real pre-existing main failure as introduced by the branch; the full command always runs for a main-snapshot probe, regardless of configuration.
  • Root-owned-change fallback. A changed path that is not owned by any single workspace package’s own build graph (a root tsconfig.json, turbo.json, package.json, pnpm-workspace.yaml, eslint.config.*, or tools/** change) resolves to zero affected tasks under an affected-only command, which would otherwise report a false pass having typechecked nothing; the full command always runs when the changed-file set contains one of these paths, or when the changed-file set itself cannot be resolved (fail-safe).
  • Fail-closed cache validation. An incremental type checker’s own build-info cache is trusted only while a fingerprint over the dependency lock, toolchain, and policy configuration — the same three inputs the effective prep fingerprint hashes — still matches the fingerprint recorded after the last successful run. A missing, unreadable, or mismatched fingerprint is never treated as valid: every existing incremental build-info file is purged first, forcing a clean recompute. A stale cache can therefore never report a pass over code it never actually re-checked.

Where a project opts into an incremental type checker’s own build information, that build information lives per worktree (excluded from version control) and is invalidated by the same fingerprint. It is a private performance cache, never a shared or portable artifact — copying it between checkouts, or committing it, has no defined meaning and gains nothing, since a mismatched fingerprint forces a full recompute regardless.

The lint gate’s content cache — already used on the incremental (changed-files) path — is retained on the full-run path as well, instead of being dropped: a full lint pays only the per-file cost of what actually changed, at whatever cache-file granularity a project’s linter already exposes, rather than always paying the whole-workspace cost.

Ledger evidence (gate_attempt, reference host, 2026-09-06, n=435): before this change, typecheck cost 30.6 seconds at the median (2.39s minimum, 257s maximum) regardless of change size, because the configured affected-only command existed in workspace.yaml but nothing downstream ever read it — dispatching to it now makes that command authoritative. No package in this repository opts into an incremental type checker yet, so after-measurements for an incremental run are deferred to the follow-on that wires real per-package incremental typecheck; a stale or tampered build-info cache is rejected rather than trusted regardless.

Baseline-unbuildable classification and Tier-C emergency repair (WU-3959)

Section titled “Baseline-unbuildable classification and Tier-C emergency repair (WU-3959)”

The gate-main comparison proves whether a branch introduced a failure by building a pinned main snapshot. When main cannot build because of the exact defect a green repair branch fixes, that build failure used to classify as infrastructure-loss, so the governed lifecycle could not certify its own repair and the work had to land through an out-of-band admin merge.

A failed pinned-main snapshot build may now classify as baseline-unbuildable, but only from structured, matching failure evidence. All of the following must hold, and every other shape fails closed and stays blocking:

  • the comparison failed while building the pinned snapshot (a dependency install, worktree setup, or spawn-level failure never qualifies);
  • the captured build output carries no timeout, resource (heap/ENOSPC/ENOMEM), or network (ENOTFOUND/ECONNRESET/…) signature;
  • every normalized {file, code} diagnostic parsed from that output resolves to a file inside the branch’s repaired surface (its diff against the same pinned SHA) — one unmatched diagnostic, or output with an error the parser cannot attribute to a file, refuses. Diagnostics are read through pnpm’s non-TTY reporter prefix (<package-dir> <script>: ), which makes the printed path package-relative; the prefix is what resolves it back to a repository-relative path, and an elided .../<tail> prefix that matches no known directory — or more than one — refuses. pnpm 12 stopped adding this prefix by default, so the snapshot build runs pnpm --filter <pkg>... --stream build (verified identical prefix on both pnpm 11.4.0 and 12.3.4); pnpm’s own Error: ERR_PNPM_<CODE> recursive-run wrapper line is excluded from the “unattributed error” fail-closed check, since it carries no diagnostic content and appears whenever any child script fails, attributable or not;
  • the branch gate’s own failure is covered by the same repaired surface. A pinned-main build failure never waives an unrelated red gate: a gate failing for its own reasons (a lane-health check, an unrelated lint rule) yields no covered identity and refuses;
  • the branch carries green own-branch required-gate evidence, supplied by the caller and derived from the landing’s real gate results. Be precise about its strength: it means no other required gate had failed at that point in the run, which for a sequential runner is not the same as “the whole run is green”. That same derived evidence is what the Tier-C authorization below reads, so an emergency landing can never be authorized while an earlier gate is red — and because a later gate can still fail, the Tier-C ledger writes are staged and only committed once the whole run has passed and mode completion reports an actual merge;
  • the captured build output came from a normal non-zero exit. A null exit code means the build was killed by a signal (OOM, the timeout path), so its output is arbitrarily truncated and its diagnostics prove nothing;
  • the capture was not truncated. A capture that hit its byte cap is provably incomplete — the dropped remainder may hold the very diagnostic that falls outside the repaired surface — so it refuses however well the retained lines match;
  • the snapshot reference is a pinned immutable commit SHA. Evidence can never be produced from a mutable ambient main checkout such as HEAD or a branch name.

The recorded evidence carries the pinned main SHA, the branch SHA, the normalized failure identity, the matching rationale, the own-branch green-gate evidence, the exact repaired surface, and the failing command. A certified classification continues the gate in band under its own audited reason (baseline-unbuildable on pinned main) — it is deliberately not a generic skip gate, and it must reduce out-of-band admin merges rather than normalize them.

Tier-C emergency repair. When a pinned-main build failure demonstrably happened but cannot be certified in band, the only remaining route is the narrowly scoped ADR-014 Tier-C backstop: a fresh, direct, interactive confirmation solicited from a human at the keyboard. It cannot be synthesized by an agent role, a role-session lease, a signal, a CLI flag, an environment variable, or stored configuration — the presence of any such source is an immediate denial, and so is a session without a TTY. A grant is scoped to exactly one WU and one immutable branch SHA, expires quickly, and is refused when a required gate is red, when the branch moved after authorization, or when ordinary matching baseline classification could already certify the work. Eligibility is evaluated before the prompt opens, so an ordinary interactive wu:done is never left waiting on a confirmation the lifecycle is going to refuse. Every landing, including wu:done --already-merged, acquires one transaction lock in the canonical main checkout, and reconciles any durable intent and reads healing debt under that lock; finalize-only recovery holds it through terminal lifecycle mutation. An ordinary landing therefore cannot pass an empty-debt check and then merge behind debt created by a concurrent emergency landing, and --already-merged cannot bypass surviving intent or debt. When that finalize-only route finds open debt, it heals in an existing distinct WU worktree or fails closed before metadata, stamps, locks, sessions, or completion signals mutate. A landed grant emits a distinct append-only audit marker (.lumenflow/artifacts/emergency-repair-audit.ndjson, kind emergency-repair-landing) and opens a healing-debt record (.lumenflow/artifacts/emergency-repair-healing.ndjson). Both live under .lumenflow/artifacts/, the same durable local-evidence root the gate-comparison artifacts use, because it is genuinely git-ignored — .lumenflow/ itself is not, since the !.lumenflow/** negation re-includes the tree, and .lumenflow/state/ is only partially ignored. The lock and both ledgers use this one canonical-main namespace, never the invoking branch worktree, so distinct worktrees on the same repository/host contend on the same state.

Before merge, an accepted grant writes only .lumenflow/artifacts/emergency-repair-intent.json: an atomic, fsynced recovery intent containing the random debt ID plus the integration target state needed after restart. It is neither committed debt nor audit. A pre-merge failure or a completion mode reporting merged: false removes it. Only merged: true converts it to audit and debt. If the process dies after merge but before either append, the next landing compares the recorded target: unchanged proves no merge and discards the intent; changed is finalized only when the immutable target proves that exact WU completion. Missing or ambiguous proof fails closed and preserves the intent. The audit marker is fsynced first, the debt second, and the intent is removed only after both are durable, so retries do not duplicate either record.

The landing transaction is a short critical section (WU-4028). The lock is taken after the done-side gate run is green, not before it. Holding it across that gate run made the transaction a repository-wide landing slot: a second wu:done was refused with landing transaction is already held while the first sat in gates, which is exactly the serialization the project has ruled out. The critical section now covers only the authoritative healing-debt re-read, the re-verification of this landing’s gate evidence, and the irreversible merge with its ledger writes; it is released before worktree cleanup.

The WU-3959 guarantee is preserved by re-reading under the lock rather than by holding the lock for longer. Debt created by a concurrent emergency landing between the gate run and the merge is caught by the locked debt re-read, and no emergency landing can create debt between that re-read and the merge because it needs the same lock. The landing evidence (the gate checkout’s HEAD and the pre-gates checkpoint fingerprint) is re-read under the lock as well: if it moved while this landing queued, the gate run no longer certifies what would be merged and the landing is refused with a re-run instruction rather than merging on stale evidence. A contending landing waits with periodic progress output under a bounded timeout instead of being refused outright.

Movement of the integration target is deliberately not part of that re-verified evidence. The re-read compares this landing’s own branch HEAD and its pre-gates checkpoint fingerprint; the target commit resolved before the gate run and the one resolved inside the transaction are never compared, so a target that advanced while this landing queued does not refuse it. That is by design: a moved target is the ordinary case on a parallel fleet, and it is handled where it is actually resolved — the merge is fast-forward-only and rebases the lane branch onto the current target and retries, so it either produces a linear result on the moved target or fails closed. Re-verifying the target here would convert every concurrent landing into a refusal and reintroduce the serialization this section removes.

A Tier-C grant taken during the gate run is the one case where the lock is still held from before the merge: the emergency path acquires the lock first, re-reads the debt and branch evidence under it, and only then stages its intent, keeping the lock unchanged from the grant through the merge and ledger finalization. The critical section borrows that lock rather than re-entering it.

Healing verification. Before the next governed landing runs its gates, it builds the landing checkout and re-runs each outstanding debt’s own repaired gate there; a debt is cleared only by its own gate, never by another debt’s probe.

Be precise about what that proves. The landing checkout is the branch worktree, which is main’s content plus that landing’s own changes — so a cleared debt means “the repaired gate is green on a tree built from the main that already contains the emergency repair”, not “a pinned main snapshot was verified in isolation”. That is a real signal and it is deliberately the weaker of the two: the probe must never run pnpm build or pnpm gates inside the canonical main checkout, which a worktree landing must not mutate. Both commands are bounded by a timeout, and with no isolated checkout available the landing fails closed with the debt left open rather than clearing or bypassing it unverified.

Unresolved healing debt blocks that landing and every further emergency use, and produces an actionable recovery record naming the outstanding debt, whether the healing checkout built, whether the repaired gate is healthy, and the remediation. A healing ledger that is unreadable, zero bytes, or malformed also blocks the governed landing: silently treating damaged local evidence as “no debt” would clear an obligation that cannot actually be ruled out.

Known limit: the healing ledger is per machine. It lives git-ignored under the checkout, so the “closed until healed” invariant holds only on the host that performed the emergency landing. Another machine — a peer agent, CI, a second clone — sees no outstanding debt and would allow a further emergency use while the first host is still unhealed. Treat the invariant as host-local until the ledger is carried on a shared control-plane surface.

WU-3780 above only sized the gate-main comparison subprocess. Under report-all mode the top-level wu:prep / wu:done / gates process itself — the one running that whole gate loop — could still exhaust the Node default heap and crash with the same FATAL ERROR: Reached heap limit; this happened live during WU-3780’s own wu:prep and wu:done runs.

Both CLI launch paths now apply the same host-derived --max-old-space-size to that top-level process, so manual NODE_OPTIONS is no longer needed on this repository:

  • tools/cli-entry.mjs (the pnpm-script launcher) sets it before spawning the command process.
  • The published bin entry (every command’s shared runCLI wrapper) re-execs itself with the flag when it is missing.

As with the comparison subprocess, a caller-supplied NODE_OPTIONS that already pins --max-old-space-size is always respected, and a still-OOMing process prints the same NODE_OPTIONS=--max-old-space-size=<value> remedy.

WU-3779 changed wu:prep’s default behaviour: it used to run the applicable gates in a fixed order and stop at the first failing gate. On a repository where a single gate cycle was a full turbo build plus the safety-critical suite (several minutes, and at that time serialized behind the former capacity-1 gate execution lock), that meant paying the full cycle cost once per independent defect: a worktree with a formatting violation, a lint violation, and a stale doc all surfaced across three separate wu:prep runs instead of one.

software_delivery.gates.local_prep.report_all (default true) changes this for wu:prep only: the gate runner continues past a failing gate, runs every remaining applicable gate, and prints one consolidated failure report grouped by gate, with the same fix-command guidance each gate already prints as it runs. Exit code and checkpoint semantics are unchanged either way — a run with any failure still exits non-zero and writes no gates-passed checkpoint.

software_delivery:
  gates:
    local_prep:
      report_all: true # default; set false to restore first-failure behaviour

Two things stay true regardless of report_all:

  • wu:done is unaffected. It builds its own gate-run options and never opts into report_all; its atomic-transaction completion path remains first-failure fail-fast.
  • The immutable safety-critical gate set is untouched. Report-all changes how far a run continues after a failure, never which gates can be skipped or bypassed.

Some gates cannot produce a meaningful result once an earlier gate they depend on has already failed — for example, running the test suite against code that fails typecheck. Report-all mode recognises this narrow set of prerequisite relationships (safety-critical-test, test, and integration-test all depend on typecheck) and reports the downstream gate as skipped-due-to typecheck instead of executing it — it is never reported as passed. Every other gate (formatting, lint, spec-linter, co-change, parity checks, and so on) has no such relationship and keeps running independently, so report-all can surface every unrelated defect in the same cycle.

Setting report_all: false restores the pre-WU-3779 first-failure behaviour byte-for-byte: wu:prep stops at the first failing gate, exactly as before.

Every build-producing gate holds a capacity-1 lock under ~/.lumenflow/locks/ for the duration of that gate. The lock directory is keyed by the canonical build-output root the run writes, so runs with disjoint roots proceed concurrently and runs sharing a root queue.

Since 6.2.0 the hold is per build-producing phase rather than per run (ADR-121 D1). The guarantee is unchanged: for one resolved build-output root, no gate that writes that tree ever executes while another gate reads or writes it, so no gate can observe a partially written tree and report a pass over it. What narrowed is only when exclusion is paid — plan resolution, the read-only parallel pre-pass, telemetry, audit writes, and end-of-run bookkeeping no longer hold the writer lock, and a run whose resolved plan contains only read-only gates never acquires it at all.

Resolved situationLock directory
Every output resolves inside the run checkoutbuild-output-<key of that checkout>.lock
Outputs resolve into exactly one other checkoutbuild-output-<key of that other checkout>.lock
shared_build_output_roots resolves to exactly one rootbuild-output-<key of the declared root>.lock
No resolvable answer (see below)build-output.lock (machine-wide fallback)

<key> is the first 16 hex characters of the SHA-256 of the case-normalized native realpath of the root set, so a junction, a symlink, and a case-variant spelling of one directory all produce one key on Windows, macOS, and Linux, and the directory name stays path-length safe.

The machine-wide fallback fires — and logs the reason it fired — when the run is outside any checkout, when a checkout root or an output cannot be resolved, when outputs scatter across several other checkouts, when a declaration resolves to several distinct roots, when a workspace.yaml is present but cannot be loaded, and when the scan finds no output directory at all. The last case is deliberate: an empty enumeration proves nothing about where the run writes, so it must not be read as “everything is inside my checkout”.

Detection recognizes dist, build, out, .turbo, and .next inside the run checkout, and resolves any directory that links out of the checkout whatever it is named. Any other ecosystem’s output tree participates by declaring it below. On Windows, native realpath collapses symlinks and junctions but does not collapse subst drives or mapped network drives; two spellings reached through those mechanisms key separately and must be declared to serialize.

FieldTypeDefaultMeaning
software_delivery.gates.shared_build_output_rootsstring[][]Build-output trees this workspace shares with other checkouts. Absolute, or relative to the checkout root. Declaring a root opts every run of the workspace into one shared serialization domain.

Declared roots dominate the derivation: two checkouts — in the same repository or in different repositories — whose declarations resolve to an identical root list always serialize with each other. One lock directory names exactly one serialization domain, so a declaration resolving to several distinct roots is not keyed and falls back to the machine-wide directory; overlapping but non-identical declarations therefore serialize machine-wide rather than splitting into keys no peer computes. Fallback to the machine-wide directory is the fail-safe path and is never less strict than pre-keying behaviour.

Host load is governed separately by weighted admission (below), which never weakens build-output serialization. After root keying, parallelism on one machine is bounded by declared shared roots and by measured compute through admission, never by a machine-wide count.

Every gate run declares a weight class derived from its resolved execution plan and is admitted against a measured host ceiling, replacing both the fixed repository semaphore capacity and the capacity-1 whole-run writer hold (ADR-121 D1).

Weight classResolved planDeclared units
docs-onlynarrowed entirely to read-only allowlist gates0 — never waits
scopednarrowed to a subset that includes a build gatea quarter of ceiling
broadno gate narrowing, tests scoped to declared pathshalf the ceiling
fullunnarrowed, or every narrowing explicitly overriddenthe whole ceiling

A full run takes the whole machine and therefore never overlaps another run.

The ceiling is measured, not assumed. It is the effective quota the operating system reports to the gate process — the control-group processor and memory limit where one is in force, otherwise the effective parallelism the runtime reports (which already honours processor affinity) and the total memory — converted to units at a measured memory reserve per unit. Physical machine totals are never used directly. An unreadable or implausible measurement fails closed to the conservative grant: one unit, one worker, which is exactly the previous capacity-1 behaviour. The named reason is recorded.

Worker counts follow the grant. A declared software_delivery.gates.resources.runners value is a ceiling, never a floor: the grant may reduce it, and may never raise a run above it; where no value is declared, the grant sets it. Rules whose flag does not control parallelism (a reporter or timeout flag) are never rewritten. The grant is exported to the gate subprocess as LUMENFLOW_GATE_GRANTED_WORKERS so a tool configured by file rather than by flag can follow it.

FieldTypeDefaultMeaning
software_delivery.gates.execution_lock.concurrency1..162Workspace cap on admission units. Authoritative downwards only: it can cap below the measured ceiling, never above it. Set 1 when gate commands write shared outputs.
software_delivery.gates.resources.runners.<name>.valuenumberDeclared runner ceiling. The grant may reduce it; it is never raised.

Every admission decision is written to the gate-attempt ledger under gate id gate-admission, in an admission payload carrying weight_class, declared_weight, granted_units, granted_workers, ceiling_units, binding_resource, conservative, wait_ms, and fallback_reason.

Recent versions added three advisory heuristic gates that catch common cross-WU collisions and CI/prod drift earlier than runtime invariants would. All three ship skippable: true so they belong on the per-gate skip allow-list; --skip-gate <name> requires --reason and --fix-wu.

PropertyValue
Default skippabletrue (allow-listed for --skip-gate)
Lifecycle surfacewu:prep
ApplicabilityThe WU diff includes deleted .test.ts / .test.tsx files.
PreconditionEach deleted test’s describe() / describe.each() symbol must still be present in one of the WU’s code_paths (i.e. the test is being moved, not silently lost).
Output on failLists each unrecognised symbol, the deleted file, and a remediation hint.
Overridepnpm config:set software_delivery.gates.overrides.test-over-deletion.skippable=false to make the gate strict.
Skippnpm wu:prep --id WU-XXX --skip-gate test-over-deletion --reason "..." --fix-wu WU-YYY
PropertyValue
Default skippabletrue (allow-listed for --skip-gate)
Lifecycle surfacewu:prep
ApplicabilityAlways (every wu:prep).
PreconditionA file in the current WU’s code_paths appears in another claimed / in-progress WU’s code_paths. Unclaimed ready placeholders are ignored. The gate can observe ownership from WU state, but cannot determine whether a claimant is actively editing or waiting in a landing chain.
Output on failLabels observed in-progress ownership, states that editing or landing activity is unknown, retains the split-module/re-laning hint, and prints the audited coordinated-claimant skip form.
Overridepnpm config:set software_delivery.gates.overrides.monolithic-file-contention.skippable=false to make the gate strict.
Skippnpm wu:prep --id WU-XXX --skip-gate monolithic-file-contention --reason "coordinated landing with co-claimant" --fix-wu WU-YYY (use when a co-claimant is coordinated or frozen in a landing chain; keep the reason specific to the audited decision)

Opt-in CI gate. Auto-skips with not-applicable until both override keys are configured — no surprise activation in fresh repos.

PropertyValue
Default skippabletrue (allow-listed; auto-skips when not configured)
Lifecycle surfaceCI (advisory; not wu:prep)
ApplicabilityBoth software_delivery.gates.overrides.prod-migration-drift.connection_string_env_var AND a reachable prod DB at that env var must be set. Otherwise auto-skips under prereq_strategy: 'auto-skip'.
PreconditionProd DB is no more than threshold (default 5) migrations behind main’s migrations/ directory.
Overridepnpm config:set software_delivery.gates.overrides.prod-migration-drift.connection_string_env_var=PROD_DATABASE_URL
pnpm config:set software_delivery.gates.overrides.prod-migration-drift.threshold=5
pnpm config:set software_delivery.gates.overrides.prod-migration-drift.skippable=false (to harden)
Skip--skip-gate prod-migration-drift --reason "..." --fix-wu WU-YYY (advisory only — prefer fixing drift)

Runs the repo-wide enforcement-baseline ratchet suites (console-call baseline, throw-new-Error count, file-size baseline, path/env-literal zero-tolerance) on every code-gate run — including scoped/agent-mode wu:prep, which previously ran only the WU’s declared tests. This closes the gap where parallel WUs each passed scoped prep, merged, and main accumulated ratchet debt that surfaced only when a later WU ran a full suite (forcing dedicated reconcile WUs).

Drift attribution uses the same pre-existing-on-main triage semantics as the test-baseline ratchet:

  • Worktree passes → clean, no main comparison runs (one vitest invocation, ~13s on the LumenFlow repo).
  • Worktree fails, main passes → NEW drift introduced by this WU → blocks the WU’s own prep.
  • Worktree fails, main fails → pre-existing debt → warns only; a WU is never blocked by debt it did not add.
  • Main comparison unrunnable → fail-closed (treated as NEW drift).

Gate-spawned suite runs execute with LUMENFLOW_WRITE_CONTEXT and UPDATE_BASELINE scrubbed from the child env, so the suites never persist baseline JSON into the worktree or the main checkout.

PropertyValue
Default skippabletrue (allow-listed escape hatch; skips audited with source: 'agent')
Lifecycle surfaceprep + done (code gates only — docs-only WUs cannot add code drift)
ApplicabilityRunning inside a worktree AND the ratchet suite files exist in the repo. Consumer repos without the suites auto-skip not-applicable with zero added latency.
BudgetOne serialized vitest invocation across the four suites (~13s measured); a second invocation against main runs only on worktree failure. Timeout cap 180s.
Overridepnpm config:set software_delivery.gates.overrides.cross-cutting-ratchets.skippable=false to remove the escape hatch and hard-block.
Skippnpm wu:prep --id WU-XXX --skip-gate cross-cutting-ratchets --reason "..." --fix-wu WU-YYY (use only when a dedicated WU is already reconciling the flagged debt)

Cross-package parity/drift suites (MCP cli-integration + permission-tier-parity, conductor-sdk pack-contract-parity, the integration-tests package’s workspace-pack-pin-parity / directory-parity / config-parity / state-paths-parity suites, CLI cli-flag-parity, and the public docs-parity check) are structurally invisible to changed-file scoping: a WU whose code_paths sit in one package can silently break a contract another package depends on, and scoped wu:prep never runs the suite that would have caught it.

This gate registers those suites unconditionally — in both the docs-only and code gate sets, unlike cross-cutting-ratchets which is code-gates only — so it runs regardless of the claimed WU’s changed-file scope. Drift attribution uses the same NEW-vs-main triage as cross-cutting-ratchets:

  • Worktree passes → clean, no main comparison runs.
  • Worktree fails, main passes → NEW drift introduced by this WU → blocks the WU’s own prep, even when the failing suite sits outside the WU’s own code_paths.
  • Worktree fails, main fails → pre-existing debt → warns only.
  • Main comparison unrunnable → fail-closed (treated as NEW drift).
PropertyValue
Default skippabletrue (allow-listed escape hatch; skips audited with source: 'agent')
Lifecycle surfaceprep + done, docs-only AND code gate sets (unconditional registration)
ApplicabilityRunning inside a worktree AND every parity suite file plus tools/docs-parity-check.ts exists in the repo. Consumer repos without these suites auto-skip not-applicable.
Overridepnpm config:set software_delivery.gates.overrides.parity-drift.skippable=false to remove the escape hatch and hard-block.
Skippnpm wu:prep --id WU-XXX --skip-gate parity-drift --reason "..." --fix-wu WU-YYY (use only when a dedicated WU is already reconciling the flagged debt)

parity-drift closes a gap that changed-file scoping otherwise leaves open: the standard scoped wu:prep test run only exercises the tests declared in a WU’s own code_paths, so a contract break in a different package went unnoticed until a much later full-suite run. This gate runs the cross-package parity suites unconditionally so that gap does not reopen as more gates move to changed-test scoping. The same fail-closed posture backs release provenance preconditions and the packed-consumer smoke lane gating CI and release.

Parity-suite class and the wu:create/wu:edit pairing warning (WU-3778)

Section titled “Parity-suite class and the wu:create/wu:edit pairing warning (WU-3778)”

parity-drift above is a dedicated gate. This is a narrower, complementary mechanism that lives inside the immutable safety-critical-test gate’s own scoped test plan, not a separate gate.

WU-3773 (a root .gitignore edit) and WU-3775 (an onboarding template source edit) each landed without ever running their paired parity test: the test lives in a different package than the file that drifted, so neither WU’s changed files nor its declared tests.unit connected the two. Main went red until an unrelated WU’s parity-drift run caught the drift a day later.

A fixed, code-defined list of cross-package parity/drift test files is always appended to the safety-critical-test gate’s scoped test plan on every wu:prep, independent of which files the WU changed or declared in tests.unit:

idTestDrifts when…
gitignore-mergepackages/@lumenflow/cli/__tests__/init-gitignore-merge.test.tsthe root .gitignore changes without keeping the pnpm init merge source in sync
sync-templates-paritypackages/@lumenflow/cli/__tests__/sync-templates.test.tsa template source doc changes without regenerating its committed .template mirror
wu-event-types-paritypackages/@lumenflow/control-plane-sdk/__tests__/wu-events-boundary.test.tsthe pack’s WU_EVENT_TYPES or the control-plane-sdk mirror changes without the other

Repos may register their own additional always-on suites, but cannot remove or empty the framework defaults — the same deny-set posture the Gates and Skips section of LUMENFLOW.md documents for the immutable gate names themselves:

software_delivery:
  gates:
    parity_suites:
      - packages/my-pack/__tests__/my-own-parity-check.test.ts
PropertyValue
Config keysoftware_delivery.gates.parity_suites (array of repo-root-relative test paths, additive only)
Enforcement pointsafety-critical-test gate dispatch (cli/src/gates.ts); appended to scopedTestPaths before the gate runs
Deny-setFramework defaults always run; config can add entries but never remove or empty them
SkippableNo — inherits safety-critical-test’s immutable, non-skippable status

wu:create and wu:edit print a non-blocking warning naming the missing test whenever code_paths touch one of the following “paired surfaces” and test_paths (tests.unit) does not already declare that surface’s parity/drift test:

Paired surfacePaired test
Root .gitignorepackages/@lumenflow/cli/__tests__/init-gitignore-merge.test.ts
A *.template file, or anything under a templates/ directorypackages/@lumenflow/cli/__tests__/sync-templates.test.ts
A generated Starlight reference doc (apps/docs/src/content/docs/reference/**, docs:generate output)packages/@lumenflow/cli/__tests__/docs-generate.test.ts
The mirrored WU_EVENT_TYPES constant (pack source of truth vs. control-plane-sdk)packages/@lumenflow/control-plane-sdk/__tests__/wu-events-boundary.test.ts
[wu:create] code_paths touch the root .gitignore, but test_paths.unit is missing its paired
parity/drift test: packages/@lumenflow/cli/__tests__/init-gitignore-merge.test.ts. Add it (e.g.
--test-paths-unit packages/@lumenflow/cli/__tests__/init-gitignore-merge.test.ts) so wu:prep
actually runs it.

The warning is advisory only and never blocks WU creation or editing — it exists so a WU author notices the gap before landing, rather than relying solely on the always-run parity-suite enforcement above (or a later parity-drift/full-suite run) to catch it after the fact.

Scoped test execution and full-suite fallbacks (WU-4000)

Section titled “Scoped test execution and full-suite fallbacks (WU-4000)”

Since WU-3499 (v6.1.6), wu:prep and wu:done run every gate, but the test / safety-critical-test gate itself runs only the paths declared in the WU’s tests.unit — not the whole suite. tests.e2e is never executed by that gate; it stays manual or CI evidence. Never run the full test suite by hand as a substitute for a genuinely scoped wu:prep.

A full run is not the default — it is a fallback the gate itself triggers when scoping is not possible or not safe. Each trigger prints its own distinct log line so the cause is never ambiguous:

Fallback triggerLog line
Unscopable tests.unit paths, or a preset without direct path-scoped execution (e.g. dotnet)Running the FULL test suite (slow): <detail>
Untracked code files present in the worktreeUntracked code files detected
A test-config change (package manifest, lockfile, tsconfig, *.config.*)Test config changes detected - running full test suite
The changed-file list is unavailableChanged file list unavailable - running full test suite
No test_incremental configured (or test_incremental equals test_full)No incremental test command configured, running full suite
--full-tests passed explicitlyFull tests requested - running full test suite
Running on the main branchOn main branch - running full test suite
  • Declare tests.unit on every WU — an empty or missing declaration is the most common cause of an unscoped run.
  • Set software_delivery.gates.commands.test_incremental to a genuinely scoped command (one that accepts a changed-file list and runs only matching tests), not an alias for the full suite.
  • Keep the worktree free of untracked code files before wu:prep — an untracked source file cannot be safely excluded from the full run because its coverage is otherwise unverifiable.

Test-gate evidence capture-integrity classification (WU-3737)

Section titled “Test-gate evidence capture-integrity classification (WU-3737)”

When the test or safety-critical-test gate exits non-zero, wu:prep/wu:done parse the captured process output into failure identities (file_path + test_name) so the baseline ratchet can compare them against .lumenflow/test-baseline.json. That parser fails closed: any capture that does not clearly, completely, and unambiguously identify which tests failed is reported as unavailable rather than guessed at, and it logs [test-evidence:<code>] <reason> before falling through to a normal blocking gate failure.

CodeMeaning
output-unavailableThe gate process’s original output was not captured at all.
empty-outputThe gate process produced no output.
truncated-outputCaptured output exceeded the evidence capture bound.
capture-integrity-failureThe gate exited non-zero, but the captured output contains zero FAIL <file> > <test> markers anywhere (see below).
malformed-outputOutput contains failure markers, but none of them parse into a clean file/test identity.
partial-outputOutput contains a mix of reconciled and unreconciled signals (fatal channels, mismatched Vitest/Turbo summaries, unscoped failures).

capture-integrity-failure: a broken capture, not an anonymous test failure

Section titled “capture-integrity-failure: a broken capture, not an anonymous test failure”

Reproduced on lumenflow-cloud 6.1.6 (2026-08-19): a full pnpm gates run reported the test gate FAILED (1 failed among 9 passed) while the captured stdout held only DB-schema spinner/progress lines — no per-file FAIL markers, no final Tests N failed | M passed summary. Two immediate reruns (a direct full Vitest run, then a second full pnpm gates run) were completely green with no recurrence.

Before WU-3737, this shape fell into the same malformed-output bucket as output that has some parseable failure markers but a corrupted one — a message that reads like a specific, if unidentified, test failure. That made a broken capture indistinguishable from a real but unreproducible test failure, so operators could not tell whether to chase a flake or a tooling bug.

capture-integrity-failure is now its own code, reported only when a non-zero exit produced no FAIL markers whatsoever. The remediation is different from a real failure: rerun the underlying test command directly with a forced JSON or verbose reporter (not the gate wrapper) to get real failure evidence before treating the run as a regression. This never blocks a WU on the baseline ratchet by itself — like the other unavailable codes, it logs the classification and falls through to a normal (non-ratcheted) blocking gate failure, so a real, currently-uninvestigated build problem still stops completion.

partial-output reconciliation under concurrent-runner contention (WU-3751)

Section titled “partial-output reconciliation under concurrent-runner contention (WU-3751)”

The partial-output classifier compares each runner’s Failed Tests N / Tests N failed Vitest summary against the canonical FAIL <file> > <test> markers captured for that runner. Concurrent gate contention — several agents running pnpm gates at once, or several package-scoped Vitest processes racing under Turbo — can cause the same runner’s summary block to be captured more than once (for example, a cached log replay racing a live stream). A repeated observation that agrees with the first is redundant confirmation, not ambiguity: the classifier reconciles it to the single agreed value and produces complete evidence instead of failing closed. Only a repeated observation that disagrees (different counts for the same runner) is treated as genuinely unreconcilable.

Every reconciliation-related partial-output reason now ends with an actionable retry step — rerun the failed gate in isolation, avoiding overlapping concurrent gate runs, then retry pnpm wu:prep — instead of naming only the internal verdict.

Separate from the --skip-gate catalogue, wu:done runs an advisory telemetry sync-health gate when a control_plane endpoint is configured. It is governed by software_delivery.telemetry.cloud_sync.enforcement (off | warn | block, default warn):

Levelwu:done behaviour
offNo gate. Health still surfaces in lumenflow:doctor / compute:doctor.
warnDefault. Prints per-stream pending counts + remediation; never blocks.
blockBlocks wu:done only on a deterministic failure class (HTTP 4xx auth/config rejection, missing token).

Transient failures (timeouts, 5xx, DNS, connection resets) never block, even under block. Bypass a single completion with the audited flag — distinct from --skip-gate because this gate is not in the per-gate allow-list:

pnpm wu:done --id WU-XXX --skip-telemetry-check --reason "control plane down for maintenance"

The bypass is recorded to .lumenflow/force-bypasses.log and emitted as a governance:force_bypass_recorded kernel event, so it is cloud-visible. See Cloud Sync — Sync-health enforcement for the full telemetry routing context.

The same release also added the following non-gate vocabulary that gates docs reference. See LUMENFLOW.md for canonical specifications.

  • pnpm wu:edit --estimated-files <n> --estimated-tool-calls <n> --sizing-strategy <s>: non-destructive sizing-estimate edits for in-progress WUs without the wu:delete dance.
  • pnpm mem:signal '<msg>' --wu WU-XXX --interrupt-class <advisory|priority|urgent|soon|immediate>: canonical interrupt-class flag; --interrupt remains a back-compat alias.
  • pnpm mem:create --wu '' --type discovery --tags pre-wu-artifact ...: pre-WU memory artifacts feed §4.4 Pre-Phase Audits (wu-sizing-guide) before a WU is claimed.
  • pnpm wu:release --keep-branch: retain the lane branch instead of resetting; default behaviour configurable via software_delivery.wu_release.branch_action.
  • File-overlap preflight & Lane-fit suggestions: pnpm orchestrate:initiative emits both sections after the wave breakdown. Configure via software_delivery.orchestrate.preflight: 'advisory' | 'strict' | 'off' or pass --strict-preflight for one run.
  • pnpm db:journal-recover: drizzle journal collision recovery (dry-run by default; --confirm applies and writes audit log under .lumenflow/db-recovery/). Idempotent on healthy chains.