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.
Gate catalogue
Section titled “Gate catalogue”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.
| Gate | skippable (default) | Lifecycle surface | Notes |
|---|---|---|---|
format:check | false | prep + done | Prettier check on changed files. Safety-critical. Missing script blocks in default mode. |
lint | false | prep + done | ESLint. Safety-critical. Content cache retained on both the incremental and the full-run path — see Incremental typecheck and lint. |
typecheck | false | prep + done | Runs software_delivery.gates.commands.typecheck when configured, else full-workspace tsc --noEmit. Safety-critical. Fail-closed cache validation — see Incremental typecheck and lint. |
spec:linter | false | prep + done | WU YAML spec validity. Safety-critical. |
co-change | false | prep + done | Required co-edits per software_delivery.gates.co_change. Safety-critical. |
claim-validation | false | prep + done | Branch / lane / WU lock invariants. Safety-critical. |
unread-directed-signal | false | prep + done | Directed inbox signals must be triaged before completion. Safety-critical. |
invariants | false | prep + done | Repo-defined hard invariants (paths, status, schema). Safety-critical. |
safety-critical-test | false | prep + done | Runs on every WU by default. Narrow with applicability_paths, do not flip skippable. |
delivery_review | false | prep + done | Native 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-evidence | true | prep + done | TDD ratchet against baseline. Allow-listed. |
migration-verify | true | prep + done | DB migration up/down dry-run. Allow-listed. |
lane-health | true | prep + done | Lane wip-limit / coverage check. Allow-listed. |
build | true | prep (opt-in) | local_prep.build.enabled=true. prereq_strategy: 'fail'. |
integration-test | true | prep (opt-in) | local_prep.integration_test.enabled=true. |
e2e-smoke | true | prep (opt-in) | local_prep.e2e_smoke.enabled=true. Auto not-applicable when no Playwright config. |
test-over-deletion | true | prep | Advisory 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-contention | true | prep | Advisory 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-drift | true | CI (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-ratchets | true | prep + done | Runs 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-drift | true | prep + 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.
Discriminated GateResult states
Section titled “Discriminated GateResult states”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.
| State | Glyph | Audit source | Meaning |
|---|---|---|---|
passed | ✅ PASSED | — | Gate ran successfully. |
failed | ❌ FAILED | — | Gate ran and failed (or applicable + missing + skippable + 'fail'). |
skipped-by-agent | ⏭ SKIPPED-BY-AGENT | agent | Explicit named --skip-gate <name>. |
skipped-auto-not-applicable | ⊘ AUTO-SKIPPED | auto-skip-not-applicable | checkApplicability returned not-applicable for this change set. |
skipped-auto-missing-prereq | ⊘ AUTO-SKIPPED | auto-skip-missing-prereq | Applicable + missing prereq + skippable: true + prereq_strategy: 'auto-skip'. |
blocked-missing-prereq | 🚫 BLOCKED | blocked | Applicable + missing prereq + skippable: false. Exits non-zero. |
Decision matrix
Section titled “Decision matrix”The four-axis decision: applicability × preconditions × skippable × prereq_strategy.
| Applicability | Preconditions | skippable | prereq_strategy | Result |
|---|---|---|---|---|
| not-applicable | — | — | — | skipped-auto-not-applicable |
| applicable | satisfied | — | — | run → passed or failed |
| applicable | missing | false | — | blocked-missing-prereq (BLOCK) |
| applicable | missing | true | 'auto-skip' | skipped-auto-missing-prereq |
| applicable | missing | true | '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:
Skip semantics
Section titled “Skip semantics”Per-gate skip (preferred, 5.5.0+)
Section titled “Per-gate skip (preferred, 5.5.0+)”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.
--reason and --fix-wu are required. --fix-wu must reference a real WU that owns the eventual remediation.
Artifact-producing conditional commands
Section titled “Artifact-producing conditional commands”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:
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.
Override gates not on the allow-list
Section titled “Override gates not on the allow-list”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:
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.
Removed --skip-gates tombstone
Section titled “Removed --skip-gates tombstone”The binary --skip-gates flag has been removed. CLI parsing retains it only as a recognizable tombstone and rejects it before lifecycle side effects.
The MCP/runtime surface exposes skip_gate arrays. Its skip_gates Boolean is likewise a rejected tombstone.
Audit NDJSON & CFR/DORA
Section titled “Audit NDJSON & CFR/DORA”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.
Native Delivery Review Gate
Section titled “Native Delivery Review Gate”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:
| Property | Value |
|---|---|
Default skippable | false (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 surface | pnpm gates when enabled: true; pnpm wu:prep when both enabled: true and auto_run: true; pnpm wu:done through the normal gate lifecycle. |
| Applicability | Current 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. |
| Output | Stable JSON artifact at .lumenflow/artifacts/delivery-review/<WU-ID>.json with PASS, PARTIAL, or FAIL, findings, acceptance-criteria status, and metadata. |
| Verdict semantics | PASS passes; PARTIAL logs a warning and continues by default, or blocks when block_partial: true; FAIL blocks the gate. |
| Evidence semantics | Source-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 verification | Optional 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_mode | always (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 config | software_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.
Accepted manual evidence shape (WU-3701)
Section titled “Accepted manual evidence shape (WU-3701)”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: valuepair wherelabelnames an evidence field (qa-evidence,evidence,manual,result,surface,screenshot) andvalueis 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.
local_prep profile (5.5.0+)
Section titled “local_prep profile (5.5.0+)”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.
| Field | Default | Effect |
|---|---|---|
build.enabled | false | When 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_scope | applicable | See Build scope below. |
integration_test.enabled | false | When true, makes the integration-test gate applicable. Runs RUN_INTEGRATION_TESTS=1 pnpm vitest run '**/*.integration.*' '**/golden-*.test.*'. Missing script → failed. |
e2e_smoke.enabled + tag | false / smoke | When 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_ms | 180000 | wu: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_all | true | See Report-all mode below. |
reuse_evidence | false | See 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'.
Latency budget guidance
Section titled “Latency budget guidance”Local-prep gates intentionally lengthen wu:prep. Budget guidance:
- Default 3 min (
latency_budget_warn_ms: 180000): comfortable headroom for typical JS/TS repos withpnpm buildcached 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_smokeper-WU via--skip-gate e2e-smokefor cosmetic-only changes.
Enforced latency budget (6.2.0+)
Section titled “Enforced latency budget (6.2.0+)”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:
| Setting | Question it answers | On breach |
|---|---|---|
latency_budget_warn_ms | Is one host still enough? | Non-failing warning |
latency_budget.*_ms | Did the lifecycle get slower? | Advisory, or wu:prep fails once enabled |
Per-weight-class budgets
Section titled “Per-weight-class budgets”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:
| Class | When a prep run resolves to it | Default |
|---|---|---|
docs-only | wu:prep --docs-only | 3600000 |
broad | Scoped test paths resolved | 3600000 |
full | Unnarrowed, or --full-tests | 7200000 |
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.
Where the defaults come from
Section titled “Where the defaults come from”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.
What is measured
Section titled “What is measured”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.
Failure and the audited exception
Section titled “Failure and the audited exception”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:
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.
Safe prep-evidence reuse (WU-3949)
Section titled “Safe prep-evidence reuse (WU-3949)”reuse_evidence is default-off. The fast path is available only when the workspace explicitly
opts in and disables report-all mode:
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.
Backward compatibility
Section titled “Backward compatibility”Omitting local_prep (the default) preserves the pre-5.5.0 behaviour — none of the new gates is applicable, no new commands run.
Throughput regression gate (6.2.0+)
Section titled “Throughput regression gate (6.2.0+)”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.
| Field | Default | Effect |
|---|---|---|
path | .lumenflow/throughput-baseline.json | Repository-relative path to the recorded baseline document. |
tolerance_percent | 25 | How far a metric may move in its bad direction before it is a regression. |
The baseline file
Section titled “The baseline file”| Metric | Direction | Source |
|---|---|---|
gate_run_p50_ms | lower is better | Median of the per-day gate-attempt p50s in the window |
gate_run_p90_ms | lower is better | Median of the per-day gate-attempt p90s in the window |
warm_path_prep_ms | lower is better | Elapsed time of a warm, evidence-reusing prep |
first_attempt_landing_share_percent | higher is better | Share of landed WUs with no failed gate attempt |
Record it — never hand-write it:
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:
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.
Declared absence
Section titled “Declared absence”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.
Build scope (WU-3780)
Section titled “Build scope (WU-3780)”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:
applicable(default): derives aturbo run build --filter=<pkg>...set from the union of the WU’scode_pathsand the currently-applicable gates’applicability_paths(software_delivery.gates.overrides.<gate>.applicability_paths), each mapped to the real workspace package name (nearestpackage.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 whosecode_pathstouch onlypackages/**never builds aapps/web-style package; a WU touching that app’s own paths still builds it.full: restores the pre-WU-3780 unscopedpnpm buildinvocation byte-for-byte. Use this if your repo’s build graph has cross-package effects thecode_pathsdeclaration 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): -
A caller-supplied
NODE_OPTIONSthat already pins--max-old-space-sizeis 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.
Three classification outcomes (WU-3981)
Section titled “Three classification outcomes (WU-3981)”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:
| Outcome | Meaning | Verdict | Remedy |
|---|---|---|---|
pre-existing-on-main | The 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-branch | Main 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.lintsecurity-warning budget (WU-3905): ESLint’s--max-warningsceiling (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 classifiespre-existing-on-mainwithout 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.
Incremental typecheck and lint (WU-3976)
Section titled “Incremental typecheck and lint (WU-3976)”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:prepmain-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.*, ortools/**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 runspnpm --filter <pkg>... --stream build(verified identical prefix on both pnpm 11.4.0 and 12.3.4); pnpm’s ownError: 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
HEADor 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.
Top-level process heap default (WU-3783)
Section titled “Top-level process heap default (WU-3783)”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
runCLIwrapper) 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.
Report-all mode
Section titled “Report-all mode”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.
Two things stay true regardless of report_all:
wu:doneis unaffected. It builds its own gate-run options and never opts intoreport_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.
Skipped-due-to a failed prerequisite
Section titled “Skipped-due-to a failed prerequisite”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.
report_all: false
Section titled “report_all: false”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.
Build-output lock keying
Section titled “Build-output lock keying”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 situation | Lock directory |
|---|---|
| Every output resolves inside the run checkout | build-output-<key of that checkout>.lock |
| Outputs resolve into exactly one other checkout | build-output-<key of that other checkout>.lock |
shared_build_output_roots resolves to exactly one root | build-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.
| Field | Type | Default | Meaning |
|---|---|---|---|
software_delivery.gates.shared_build_output_roots | string[] | [] | 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.
Weighted gate admission (6.2.0+)
Section titled “Weighted gate admission (6.2.0+)”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 class | Resolved plan | Declared units |
|---|---|---|
docs-only | narrowed entirely to read-only allowlist gates | 0 — never waits |
scoped | narrowed to a subset that includes a build gate | a quarter of ceiling |
broad | no gate narrowing, tests scoped to declared paths | half the ceiling |
full | unnarrowed, or every narrowing explicitly overridden | the 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.
| Field | Type | Default | Meaning |
|---|---|---|---|
software_delivery.gates.execution_lock.concurrency | 1..16 | 2 | Workspace 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>.value | number | — | Declared 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.
Advisory heuristic gates (5.6.0+)
Section titled “Advisory heuristic gates (5.6.0+)”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.
test-over-deletion
Section titled “test-over-deletion”| Property | Value |
|---|---|
Default skippable | true (allow-listed for --skip-gate) |
| Lifecycle surface | wu:prep |
| Applicability | The WU diff includes deleted .test.ts / .test.tsx files. |
| Precondition | Each 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 fail | Lists each unrecognised symbol, the deleted file, and a remediation hint. |
| Override | pnpm config:set software_delivery.gates.overrides.test-over-deletion.skippable=false to make the gate strict. |
| Skip | pnpm wu:prep --id WU-XXX --skip-gate test-over-deletion --reason "..." --fix-wu WU-YYY |
monolithic-file-contention
Section titled “monolithic-file-contention”| Property | Value |
|---|---|
Default skippable | true (allow-listed for --skip-gate) |
| Lifecycle surface | wu:prep |
| Applicability | Always (every wu:prep). |
| Precondition | A 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 fail | Labels 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. |
| Override | pnpm config:set software_delivery.gates.overrides.monolithic-file-contention.skippable=false to make the gate strict. |
| Skip | pnpm 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) |
prod-migration-drift
Section titled “prod-migration-drift”Opt-in CI gate. Auto-skips with not-applicable until both override keys are configured — no surprise activation in fresh repos.
| Property | Value |
|---|---|
Default skippable | true (allow-listed; auto-skips when not configured) |
| Lifecycle surface | CI (advisory; not wu:prep) |
| Applicability | Both 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'. |
| Precondition | Prod DB is no more than threshold (default 5) migrations behind main’s migrations/ directory. |
| Override | pnpm config:set software_delivery.gates.overrides.prod-migration-drift.connection_string_env_var=PROD_DATABASE_URLpnpm config:set software_delivery.gates.overrides.prod-migration-drift.threshold=5pnpm 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) |
cross-cutting-ratchets
Section titled “cross-cutting-ratchets”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.
| Property | Value |
|---|---|
Default skippable | true (allow-listed escape hatch; skips audited with source: 'agent') |
| Lifecycle surface | prep + done (code gates only — docs-only WUs cannot add code drift) |
| Applicability | Running 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. |
| Budget | One serialized vitest invocation across the four suites (~13s measured); a second invocation against main runs only on worktree failure. Timeout cap 180s. |
| Override | pnpm config:set software_delivery.gates.overrides.cross-cutting-ratchets.skippable=false to remove the escape hatch and hard-block. |
| Skip | pnpm 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) |
parity-drift
Section titled “parity-drift”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).
| Property | Value |
|---|---|
Default skippable | true (allow-listed escape hatch; skips audited with source: 'agent') |
| Lifecycle surface | prep + done, docs-only AND code gate sets (unconditional registration) |
| Applicability | Running 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. |
| Override | pnpm config:set software_delivery.gates.overrides.parity-drift.skippable=false to remove the escape hatch and hard-block. |
| Skip | pnpm 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.
software_delivery.gates.parity_suites
Section titled “software_delivery.gates.parity_suites”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:
| id | Test | Drifts when… |
|---|---|---|
gitignore-merge | packages/@lumenflow/cli/__tests__/init-gitignore-merge.test.ts | the root .gitignore changes without keeping the pnpm init merge source in sync |
sync-templates-parity | packages/@lumenflow/cli/__tests__/sync-templates.test.ts | a template source doc changes without regenerating its committed .template mirror |
wu-event-types-parity | packages/@lumenflow/control-plane-sdk/__tests__/wu-events-boundary.test.ts | the 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:
| Property | Value |
|---|---|
| Config key | software_delivery.gates.parity_suites (array of repo-root-relative test paths, additive only) |
| Enforcement point | safety-critical-test gate dispatch (cli/src/gates.ts); appended to scopedTestPaths before the gate runs |
| Deny-set | Framework defaults always run; config can add entries but never remove or empty them |
| Skippable | No — inherits safety-critical-test’s immutable, non-skippable status |
wu:create / wu:edit pairing warning
Section titled “wu:create / wu:edit pairing warning”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 surface | Paired test |
|---|---|
Root .gitignore | packages/@lumenflow/cli/__tests__/init-gitignore-merge.test.ts |
A *.template file, or anything under a templates/ directory | packages/@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 |
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 trigger | Log 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 worktree | Untracked 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 unavailable | Changed 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 explicitly | Full tests requested - running full test suite |
| Running on the main branch | On main branch - running full test suite |
Consumer remedies
Section titled “Consumer remedies”- Declare
tests.uniton every WU — an empty or missing declaration is the most common cause of an unscoped run. - Set
software_delivery.gates.commands.test_incrementalto 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.
| Code | Meaning |
|---|---|
output-unavailable | The gate process’s original output was not captured at all. |
empty-output | The gate process produced no output. |
truncated-output | Captured output exceeded the evidence capture bound. |
capture-integrity-failure | The gate exited non-zero, but the captured output contains zero FAIL <file> > <test> markers anywhere (see below). |
malformed-output | Output contains failure markers, but none of them parse into a clean file/test identity. |
partial-output | Output 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.
Telemetry sync-health gate (5.15.0+)
Section titled “Telemetry sync-health gate (5.15.0+)”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):
| Level | wu:done behaviour |
|---|---|
off | No gate. Health still surfaces in lumenflow:doctor / compute:doctor. |
warn | Default. Prints per-stream pending counts + remediation; never blocks. |
block | Blocks 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:
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.
Cross-references
Section titled “Cross-references”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 thewu:deletedance.pnpm mem:signal '<msg>' --wu WU-XXX --interrupt-class <advisory|priority|urgent|soon|immediate>: canonical interrupt-class flag;--interruptremains 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 viasoftware_delivery.wu_release.branch_action.- File-overlap preflight & Lane-fit suggestions:
pnpm orchestrate:initiativeemits both sections after the wave breakdown. Configure viasoftware_delivery.orchestrate.preflight: 'advisory' | 'strict' | 'off'or pass--strict-preflightfor one run. pnpm db:journal-recover: drizzle journal collision recovery (dry-run by default;--confirmapplies and writes audit log under.lumenflow/db-recovery/). Idempotent on healthy chains.
See also
Section titled “See also”- Workspace YAML reference — full schema for
software_delivery.gates. - Agent Safety Architecture — defense-in-depth model that gates plug into.
- CLI reference —
pnpm wu:prep,pnpm wu:done,pnpm gates.