Gates
Gates are automated quality checks evaluated during WU completion. They enforce configured checks; they do not establish semantic code correctness or replace a required independent reviewer verdict.
Why Gates?
Section titled “Why Gates?”Gates provide:
- Repeatable checks against the configured policy
- Feedback while a WU is still in its worktree or branch
- Recorded results that the completion boundary can evaluate
An independent review, when required by the WU’s worker execution contract, is a separate authorization step.
Config-Driven Gates
Section titled “Config-Driven Gates”Define your gate commands in workspace.yaml under software_delivery.gates.commands,
the schema lumenflow init itself writes:
format and lint commands come from the standard format:check/lint package scripts, not
a gates.commands key. An older software_delivery.gates.execution.{setup,format,lint,typecheck,test}
shape (with an optional preset) is still read for backward compatibility, but lumenflow init
never writes it and new configuration should use commands above.
Derived incremental typecheck compatibility
Section titled “Derived incremental typecheck compatibility”When everyday typecheck has no typecheck_incremental command, a host with a Turbo typecheck
task uses a derived changed-package command. Before that derived child starts, the gate checks the
consumer’s direct Turbo declaration, installed Turbo manifest, and pnpm lockfile. If those local
files show an installed Turbo version known to be incompatible and the lockfile contains two valid
documents, the gate runs the existing full typecheck command instead. This avoids trusting an
affected selection that the incompatible Turbo can silently lose.
Compatible or unknown results leave the current selection in place; unknown file state is not treated as either incompatibility or proof of compatibility. The captured-output warning fallback still runs full typecheck if Turbo reports that it could not resolve workspaces or parse multiple lockfile documents, including after a compatible preflight. Explicit incremental commands and release-scope commands do not use this everyday derived-command preflight. The local file reads add diagnostic cost, and choosing full typecheck does not establish a wall-clock saving. Fallback results preserve the full command’s success or failure and sum the reported durations of both attempted commands; that sum excludes preflight and other gate overhead.
Optional Migration Verification
Section titled “Optional Migration Verification”Projects with manual database deploy steps can add an explicit migration-state verifier:
When migration_verify is configured, pnpm gates and pnpm wu:prep run it only when the
working diff touches schema or migration paths such as db/schema/**, prisma/schema.prisma,
supabase/schema.sql, or migration directories.
Use this for commands that check whether the target database is up to date. Do not use it to run migrations automatically.
This approach works with any language and toolchain.
Gate Execution Concurrency
Section titled “Gate Execution Concurrency”Gate runs take no admission lock. A pnpm gates,
wu:prep or wu:done gate run is never queued, weighted or refused on capacity, however many run
at once; the operating system schedules CPU. Before each gate step spawns, the
memory start guard checks live available memory. It ships in
observe mode, which records the would-be decision and never waits. To let it delay a step that
does not fit, for at most a time budget, switch it to enforce:
LUMENFLOW_START_GUARD=observe|enforce|off overrides the workspace for one shell. A deferred step always starts when its budget runs
out; nothing refuses.
Existing execution_lock blocks still parse. They no longer apply to gate runs.
Landing retry custody
Section titled “Landing retry custody”Gate execution admission and wu:done publication custody are separate
mechanisms. When a landing’s configured atomic retry sees target contention, it
releases the custody locks used by that landing: the repository merge lock and,
where the route already uses it, the integration-branch lease. The next
complete attempt reacquires its locks in the same order and repeats that
landing’s checks. The merge-phase
and branch-only routes pass the exact candidate SHA they reviewed into the
isolated publication attempt. The merge phase also keeps its independent outer
landing retry bound. Unknown publication outcomes and genuine replay conflicts
do not become contention retries; their existing proof and recovery behavior
remains in force. This documents per-attempt lock scope, not a measured
throughput increase or a change to gate-run admission.
A run’s plan is still resolved from its diff: a diff that touches only documentation runs the
docs-only gates, with no flag required. Gate children receive the largest declared
gates.resources.runners parallelism value, else the host’s available parallelism, as
LUMENFLOW_GATE_GRANTED_WORKERS.
Fresh lifecycle CLI builds use a distinct ownership-fenced lock scoped to the canonical output checkout: builds for different worktrees can overlap, while a second writer to the same output waits and reuses the first successful result.
Merge/integration lock wait
Section titled “Merge/integration lock wait”Gate runs take no lock; a separate, repository-wide merge lock serializes only the target-dependent part of wu:done’s merge phase: the lane pre-flight rebase,
fetch, isolated replay, derived-docs regeneration and the compare-and-swap push with its proof.
Staging, formatting, the metadata commit with its hooks and artifact coherence run before that lock
is requested, under the landing lock, which ordinary landings take shared (they overlap) and a Tier-C
emergency-repair grant takes exclusive. wu:create, wu:edit, wu:claim, config:set and other
main-checkout micro-worktree writers do not take the merge lock; they serialize only against another
invocation of the same operation and id, and the compare-and-swap push arbitrates the shared target.
Configure how long a merge-phase caller waits for a held merge lock before failing:
wait_ms must be an integer between 1000 and 1800000 (30 minutes); the default is 30000 (30
seconds). A caller that finds the lock already held prints a “Waiting for merge lock” line
immediately, naming the holder and the waiting WU and command, and a “Still waiting” progress line
roughly every 5 seconds until it acquires the lock or the bound is reached. wu:done records
merge_lock_wait, merge_lock_hold, fetch, worktree_add, rebase, docs_regen, proof, push
and prove_published as phase_duration events under merge_push.
Build-output-root serialization
Section titled “Build-output-root serialization”Gate runs take no admission lock, and this is the one exclusion left in the gate path. It guards correctness, not compute: two writers to one build tree produce false-green evidence. Every build-producing gate acquires a capacity-one lock in the user’s LumenFlow lock directory, keyed by the canonical build-output root that gate writes. Two gates whose roots are disjoint proceed concurrently; two gates that share a root queue instead of interleaving. The lock is never an admission or capacity mechanism: it counts no runs and refuses nothing on load, and adding a machine or a checkout adds capacity. It is built on the owned-lock primitive, with the fenced descendant lease that lets a build-producing child of a gate run under its parent’s hold without waiting for it.
The key resolves where output really lands rather than where the checkout sits. Every output root of the run checkout is resolved with the platform’s native realpath, so symlinks, junctions, and case-variant spellings collapse to one identity:
- every resolved output inside the run checkout → the key is that checkout;
- outputs resolving into one other checkout (for example a worktree whose build directory links into another checkout) → the key is that other checkout, and the two runs serialize;
- no resolvable answer → the run falls back to the machine-wide build-output lock and serializes exactly as it did before root keying. This
covers a run outside any checkout, an output that cannot be resolved, outputs scattered across
several other checkouts, a declaration resolving to several distinct roots, a
workspace.yamlthat is present but cannot be loaded, and a scan that found no output directory at all — an empty enumeration proves nothing about where the run writes. Each fallback logs the reason it fired.
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. On Windows, native realpath collapses symlinks and
junctions but does not collapse subst drives or mapped network drives, so two spellings reached
through those mechanisms key separately and must be declared to serialize.
A workspace whose gate commands write a tree that other checkouts also write declares that tree. Declaring it is the supported way to participate in serialization across checkouts or repositories: every run of the workspace keys on the declaration, so all checkouts whose declarations resolve to an identical root list serialize with one another. One lock directory names exactly one serialization domain, so a declaration resolving to several distinct roots falls back to the machine-wide lock instead of being keyed.
Entries may be absolute or relative to the checkout root. The default is an empty list, which keys each run on its own resolved output root. Parallelism on one machine is therefore bounded by declared shared roots and by available compute through the repository semaphore, never by a machine-wide count.
A project’s own build tooling may hold a separate per-checkout build lock; that lock keeps its role and is neither replaced nor weakened by this one.
Only the built-in read-only allowlist bypasses this lock: format:check, spec:linter,
backlog-sync, claim-validation, supabase-docs:linter, co-change, and cross-cutting-ratchets.
They share one parallel pre-pass. Every other built-in gate is treated as build-producing, and a
consumer-defined gate participates automatically when you register it through the normal gate
configuration—no separate lock flag or shell convention is required.
Waiting and the optional ceiling
Section titled “Waiting and the optional ceiling”While a gate waits it prints the holder (pid, host, owner, work unit, held-for time) every 15
seconds, with each setting and where it came from (default, workspace or env). One environment
variable controls the wait:
| Variable | Effect |
|---|---|
LUMENFLOW_BUILD_OUTPUT_WAIT_MS | Opt-in ceiling in milliseconds for one wait. Unset or 0 means no ceiling, which is the default. |
When the ceiling, the gate deadline or an abort ends a wait, the error names the live holder, and the waiting contender’s queue record is released so nothing leaks. A lock directory left by an older engine is honoured: a live legacy holder is waited on, a dead one is reaped, so a mixed fleet never runs two writers on one root. A run that cannot resolve one build-output root serialises on the machine-wide lock and prints that degrade once per run.
The lock’s state is observable without acquiring it: the root key, whether it is per-checkout or
machine-wide, the current holder, the number of waiters and the last reap are available from the
guard’s describe() record for the resources explain and doctor surfaces.
Bounded lock retention
Section titled “Bounded lock retention”A lock whose keyed build-output root is never targeted again (a retired worktree, a WU that never lands) previously stayed on disk forever — nothing reclaimed it once no run ever revisited that root. Every build-output lock acquisition now opportunistically sweeps sibling lock directories in the lock directory that are both past a configurable age and have no live custody — neither the top-level holder nor a live delegated fenced descendant is alive; a lock with live custody is never removed regardless of age. The bound is configurable and defaults to 14 days:
The sweep itself is throttled and capped so it is not a per-gate-run cost: a sentinel file’s mtime
limits a real sweep to at most once per build_output_lock_sweep_interval_minutes (default 60) —
every other lock acquisition between intervals pays one sub-millisecond stat and returns
immediately — and build_output_lock_sweep_max_entries (default 200) bounds how many lock
directories a single real sweep processes, so a pathologically large lock root makes bounded
progress every interval rather than costing one gate run a full scan.
Per-invocation gate/CLI logs under .logs/ are bounded the same way, by age
(cleanup.logs_max_age_days, default 30) and count (cleanup.logs_max_files, default 500). Because
the count-based pass now runs automatically on every gate-log creation rather than only on a manual
wu:prune --execute, a file younger than cleanup.logs_count_eviction_grace_minutes (default 360
minutes) is never an eviction candidate, even while the count bound stays exceeded until it ages
past the window on a later sweep. This grace window is the only protection for a live but quiet
gate log: LumenFlow does not stamp gate logs with an owning-PID marker, so there is no
process-liveness check behind it.
Automated Test Diff Evidence
Section titled “Automated Test Diff Evidence”wu:prep can enforce an extra proof step: if a WU changes code, it must also touch at least one
automated test file in the same diff. This policy is configured under
software_delivery.gates.tdd_diff_evidence.
Defaults come from software_delivery.methodology.testing:
tddsetssoftware_delivery.gates.tdd_diff_evidence.mode: blocktest-aftersetssoftware_delivery.gates.tdd_diff_evidence.mode: offnonesetssoftware_delivery.gates.tdd_diff_evidence.mode: offapplies_to_typesdefaults tofeatureandbugexempt_pathsdefaults to[], plus the built-in migration globs described belowtest_file_patternsdefaults to the built-in polyglot conventions listed belowcode_file_extensionsdefaults to TS/JS extensions (.ts,.tsx,.js,.jsx,.mjs,.cjs,.mts,.cts)
mode supports block, warn, and off. wu:prep only blocks when the mode is block; teams
that want the policy disabled can set warn or off and still document their intent in config.
Migrations are exempt from RED-first
Section titled “Migrations are exempt from RED-first”A database migration is data, not runtime behaviour. Both TDD gates (tdd_diff_evidence and the
commit-order gate controlled by tdd_ordering) exempt these migration globs by default, merged with
your configured exempt_paths (configuring a list never removes them): **/migrations/** and
**/drizzle/**/meta/*.json. Add other layouts to exempt_paths.
The commit-order gate also counts only runtime files as runtime LOC. Data and generated files
(.json, .sql, .lock, .yaml, .yml, .toml, .csv, .snap, .md, .mdx, .txt) never
count unless you list their extension in code_file_extensions. A commit that touches only exempt or
data files therefore never fails ordering, whatever its type. Type a migration-only commit chore,
as its own commit; the failure output repeats this as a fix option.
What counts as a test file
Section titled “What counts as a test file”One matcher decides test-ness for both the tdd_diff_evidence gate and the commit-order
(RED-first) gate. It recognises these path conventions by default, with no configuration:
| Ecosystem | Recognised paths |
|---|---|
| TypeScript/JavaScript | **/*.test.{ts,tsx,js,jsx,mjs}, **/*.spec.{ts,tsx,js,jsx,mjs}, **/__tests__/**, **/*.test-utils.*, **/*.mock.* |
| Go | **/*_test.go |
| Python | **/test_*.py, **/*_test.py |
| Rust | **/tests/**/*.rs (Cargo integration tests) |
Production files whose path merely contains the substring test (for example
lib/testimonials.ts or contest/scoring.go) are not test files.
Explicitly unsupported: Rust inline #[cfg(test)] modules. They live inside the production
source file and carry no distinguishing path, so path-based matching cannot see them. Put Rust tests
in a tests/ directory, or declare the source file under the WU’s tests.unit list so the
tdd_diff_evidence gate can count it.
Configuring for other toolchains
Section titled “Configuring for other toolchains”test_file_patterns and code_file_extensions make the gate language-agnostic. test_file_patterns
is additive: the globs you supply extend the built-in conventions and can never disable them, so
configuring one language never silently stops another language’s tests from counting. There is no
supported way to reduce recognition below the defaults. code_file_extensions still replaces the
default extension list.
When the commit-order gate rejects a commit with test files in same commit: 0, its output lists
every recognised convention so an unrecognised ecosystem is visible rather than silent.
C# (xUnit / NUnit / MSTest):
Python (pytest / unittest) — test paths are recognised by default; only the production extension
needs configuring, plus any extra convention such as a shared tests/ tree:
Go — *_test.go is recognised by default:
Use this gate when you want changed-test evidence for specific WU types or runtime paths. It is not a requirement to force every team into test-first development.
For one-off exceptions, document the reason in the WU notes with:
Native Delivery Review
Section titled “Native Delivery Review”The Software Delivery pack also exposes a native delivery_review evidence gate. It is
disabled by default. Its built-in checks inspect changed paths, WU declarations, and automated
or meaningful manual test evidence; they do not analyze code semantics. An optional contextual
analyzer can add candidate-bound findings. This capability is public and vendor-agnostic:
- It lives under
software_delivery.gates.delivery_review enabled: trueregisters it for every agent/client runtimeauto_run: truemakeswu:preprun it for applicable WU types- It runs through native gate execution and
wu:prep, not through a vendor-specific skill path - It does not depend on any hosted consumer or control plane
pnpm gates runs delivery_review whenever the global gate is enabled. pnpm wu:prep auto-runs
it when both enabled: true and auto_run: true are set, unless the current WU type matches
skip_types. The same public contract is catalogued in the
Gates Reference.
For source-code delivery changes, delivery_review requires automated test evidence or meaningful
manual verification evidence. A non-empty tests.manual entry is not enough by itself:
placeholders and negative values such as todo, n/a, screenshot: n/a, and not run are
treated as missing evidence and fail the gate. Use concrete manual entries that name the surface,
action, observed result, and artifact path when visual or manual QA is the right evidence.
Set block_partial: true when the repo wants delivery-review uncertainty to block rather than
warn. Set verifier_command when native evidence-shape checks should be followed by project-owned
truth checks. The command runs from the repository root, receives
LUMENFLOW_DELIVERY_REVIEW_WU_ID=<wu-id>, and blocks the gate when it exits non-zero.
verifier_command_mode controls when that command runs: always (the default) runs it
every time one is configured, matching every prior release byte-for-byte. on-insufficient-evidence
skips the run when native evidence — automated test evidence, meaningful manual evidence, or a
documented exemption — already reports sufficient evidence for the change set, and the gate log
states the verifier was skipped for that reason. Use it to keep an expensive consumer-owned
verifier (such as a Playwright visual harness) from running on a change a component test already
proves.
Client-specific config can still exist for adapter UX, hooks, or prompt surfacing, but it is not the enforcement switch:
For one release, legacy client-scoped features.delivery_review.auto_run: true is still honored
when the global gate is enabled and global auto_run is omitted. LumenFlow emits a migration
warning pointing to software_delivery.gates.delivery_review.auto_run. Client-scoped
features.delivery_review.enabled: false does not disable the core gate; disable or skip the gate
through the normal global gate config or auditable gate-skip mechanism.
Delivery Review Output Contract
Section titled “Delivery Review Output Contract”delivery_review produces a stable JSON artifact at
delivery-review/<wu-id>.json in the workspace artifacts directory. Hosts and products can consume the result
without assuming a specific vendor runtime.
Verdict behavior:
PASSmeans the configured evidence checks found sufficient delivery evidence; it does not certify code correctness or prove that every acceptance criterion is semantically satisfied.PARTIALmeans the checks found uncertainty or lower-severity findings; it warns by default and blocks whensoftware_delivery.gates.delivery_review.block_partialistrue.FAILmeans the checks found blocking evidence gaps or risks, so the gate fails.
The built-in acceptance-criterion statuses derive from generic evidence sufficiency, not a source-level determination for each criterion. The legacy artifact names the WU but contains no candidate SHA or diff digest. A configured verifier_command can add project checks; its output does not itself become an independent reviewer verdict. See Independent review for that separate authority. wu:verify retains its own lifecycle meaning.
Opt-in contextual review
Section titled “Opt-in contextual review”Set software_delivery.gates.delivery_review.contextual.enabled: true in the trusted integration checkout and configure a committed, self-contained Node analyzer. For example:
The CLI collects the exact committed base/head diff, WU criteria, revisioned rules, and bounded, explicitly authorized context. The Node script receives only that staged input as JSON on stdin in an enforced read-only snapshot with private temporary output and network access off. It must return validated findings and per-criterion JSON on stdout within a 120-second deadline and 1 MiB output limit. A missing or unenforced sandbox fails the review closed. The analyzer has no model credential or network access.
auto_run: true includes the gate in wu:prep. The CLI writes a version 2 result to delivery-review/<wu-id>.json in the workspace artifacts directory and immutable result archives under the same directory. The report binds the base/head commits, diff, criteria, rule revisions, authorization policy, retrieved source digests, and context omissions. The operator sees findings, source evidence, verdict, and stale-state warning. A changed candidate or policy needs a new run. A clean automated PASS never satisfies independent review or merge approval.
For a single-checkout PR branch, provide a separate trusted integration-policy checkout using LUMENFLOW_CONTEXTUAL_TRUSTED_POLICY_ROOT and its exact integration SHA in LUMENFLOW_CONTEXTUAL_TRUSTED_POLICY_COMMIT. A Git checkout without a verified integration tracking ref fails closed, even when delivery_review is disabled or the branch is named main, because the CLI cannot distinguish an offline main checkout from a PR checkout using candidate files alone. A checkout where Git cannot be run at all also fails closed, even when delivery_review is disabled: a missing or non-executable Git binary (including one pinned with LUMENFLOW_GIT_BINARY), a spawner that throws, a signal or timeout, or any Git failure other than the not a git repository fatal stops the gate with Git could not be run to inspect the checkout. Only Git itself reporting that the directory is not a repository lets a legacy project without Git skip a disabled review. Fetch the trusted integration ref (for example, git fetch origin main) before running gates; an intentionally offline local project can maintain refs/remotes/origin/main from its verified integration commit. A separate policy checkout also requires the candidate tracking ref to match its SHA. Hosted adapters must set LUMENFLOW_CONTEXTUAL_HOSTED_PR=1, obtain the SHA and PR head from the Git host, and verify both independently. Any provider-backed adapter must keep provider credentials outside candidate hooks, tests, and analyzer subprocesses. Local remote-tracking refs alone are not hosted authority.
Explicit contextual-result transport
Section titled “Explicit contextual-result transport”The pack’s config/delivery-review export includes a strict version 1 upload and acknowledgement contract and submitDeliveryReviewHttp. An application explicitly constructs an upload with createDeliveryReviewUpload({ workspaceId, runtimeId }, result) and supplies a trusted endpoint and bearer credential to the HTTP adapter. Gate execution never uploads automatically; there is no default destination, credential lookup or retry loop.
The identity binds workspace/runtime, WU, the entire exact candidate binding, and a SHA-256 result digest. The digest uses the existing immutable archive bytes: UTF-8 schema-normalized two-space JSON followed by one LF. Input object key order is normalized by the schema; array order is preserved. Changed result content or provenance needs a new digest. An acknowledgement must echo every identity field exactly. status: accepted means receipt of analysis; it grants no independent-review or completion authority. Schema/digest validation does not independently verify source excerpts, analyzer trust or actual landing truth.
One HTTP POST is bounded to a 1 MiB request, a 64 KiB actual streamed response, and 30 seconds including body consumption. Callers may lower these limits. Redirects are refused, and malformed or mismatched acknowledgements fail. Stable typed failures expose no raw response or exception text. Only trusted caller wiring can choose the destination and credential, which must remain isolated under the consumer’s existing policy.
The injected reference receiver authenticates workspace/runtime independently from payload claims and deduplicates identical authenticated deliveries. Its bounded in-memory store and real loopback tests are local conformance evidence.
Conditional Commands
Section titled “Conditional Commands”Define pattern-triggered commands alongside your standard gates:
| Field | Type | Required | Description |
|---|---|---|---|
trigger_patterns | string[] | Yes | Glob patterns matched against changed files |
command | string | Yes | Shell command to execute when patterns match |
severity | string | No | error (default, blocks gates), warn, or off (skip) |
fresh_on_completion | boolean | No | Rerun after completion reconciliation; defaults to false |
guidance | string | No | Actionable text shown when the command fails |
guidance_ref | string | No | File path whose content is appended to guidance |
How it works:
- When
pnpm gatesorwu:prepruns, changed files are compared against each command’strigger_patternsusing glob matching - Only commands with matching patterns execute — unmatched commands are silently skipped
- If a matching command fails with severity
error, gates fail. With severitywarn, a warning is logged but gates continue - A matching command with
fresh_on_completion: trueruns again duringwu:done, after branch reconciliation and before the landing transaction, even when ordinary gates reuse a validwu:prepcheckpoint - Completion freshness is fail-closed: a failed or unavailable fresh command blocks completion,
including a command whose normal preparation severity is
warn.severity: offstill disables it - Each fresh completion run writes durable flow evidence bound to the WU ID, reconciled commit, diff, command, completion timestamp, and result. Commands without the flag keep normal checkpoint reuse
Registering via the CLI (Constraint-9 compatible):
workspace.yaml must not be edited by hand. Two sanctioned paths exist:
-
gate:conditional(recommended, per-rule) — mirrorsgate:co-change:The
namefield is how--remove/--editaddress a specific entry. It is optional in the underlying schema (existing unnamed entries continue to work) but required for CLI-managed entries. -
config:set --json-value— writes the whole array verbatim when you need a shape the flags above don’t cover:
Both paths validate against ConditionalCommandConfigSchema and commit atomically
via micro-worktree.
Ratchets in wu:prep are per-file deltas
Section titled “Ratchets in wu:prep are per-file deltas”wu:prep runs ratchets only as a per-file delta against origin/main. The
cross-cutting-ratchets gate compares each file your branch changed with the same file at the merge
base and fails on growth: file size past its ceiling or baseline, new console.* calls, banned path
literals per rule, and direct throw new Error(). The four checks read the branch diff independently,
so they run concurrently, and the gate runs in the same read-only pre-pass as format:check,
spec:linter and co-change. If it fails, report-all mode still skips the expensive gates (tests,
the sweep, build) with skipped-due-to cross-cutting-ratchets.
The gate also runs per-file ratchets and the static ratchet suites selected by the diff. If any check
throws, the gate fails with that check’s name and error while preserving other completed results.
It logs a warning and passes only when Git explicitly reports that origin/main or a merge base is
unavailable, so the branch diff cannot be inspected. Marker-discovery errors and other check
failures remain blocking.
The spec:linter gate and the wu:prep pre-step validate the WU spec in-process with the same strict
verdict as wu:validate --id, so prep starts no package-manager process for it. spec:linter stays an
immutable gate with its audit row, and it still runs before the reality and TDD preflights.
What the prep latency budget measures
Section titled “What the prep latency budget measures”The enforced local_prep.latency_budget is a regression tripwire for LumenFlow’s own gate cost. It
does not measure how busy your machine is. Several concurrent preps share one CPU partition, and gate
admission grants each of them a share of the workers it asked for; a prep granted one of eight requested
workers takes about eight times as long on the wall clock for the same gate work. Counting that against the
budget fails green preps on a busy host, which teaches everyone to waive it.
The budget is therefore evaluated on wall time normalised by granted versus requested workers
(wall-normalised-by-granted-workers): the gate-phase wall time, with gate-admission queue wait removed,
multiplied by granted_workers / requested_workers (never above 1; an unknown or unreadable share counts
as 1, so uncertainty never discounts a run). The same gate work passes at one granted worker on a busy host
and at eight on an idle one, and a gate that really grows past the budget fails at either width. The
factor is linear, so serial gates that do not use extra workers are discounted too; that is the price of a
measure that needs no per-process accounting and behaves the same on Windows, macOS and Linux.
Every evaluation records raw wall time, queue wait, granted and requested workers, the one-minute load
average (diagnostic only, never part of the decision; 0 on Windows) and the measure used, in
the latency budget exceptions log and in the prep log. A failure names the gates that grew,
largest first, each as its normalised cost and its raw wall time, and states how far over budget the run is.
The audited --latency-budget-exception waiver stays available for genuine exceptions; a passing prep on a
busy host no longer needs it.
Using Presets
Section titled “Using Presets”For common languages, use a preset to get sensible defaults:
Available presets: node, python, go, rust, dotnet, java, java-gradle, ruby, php
Path-scoped tests.unit execution is preset-aware. When the active preset supports scoped
execution, wu:prep can use the current WU’s tests.unit entries to narrow the test gate. When a
preset does not support path-scoped execution, such as dotnet, LumenFlow falls back to the
configured default test command for that preset instead of attempting a JavaScript-specific runner.
One test plan per gate run
Section titled “One test plan per gate run”The immutable safety-critical-test gate owns the shared test plan. It runs
declared tests.unit paths when they can be scoped safely; otherwise it runs
the configured test_incremental command. The normal test gate reuses that
exact result, so the same test process does not execute twice during one
wu:prep.
lumenflow init writes a safe test_incremental by default when the project exposes an explicit
changed-test script or recognizable Vitest, Jest, Nx, or Turbo evidence. It never copies
test_full into the incremental slot. If the prepared commit and gate inputs remain unchanged,
wu:done reuses the successful prep checkpoint; only fresh completion commands rerun separately.
The planner falls back to test_full when broader coverage is required:
- main-snapshot comparison probes;
--full-testsor--full-coverage;- test-runner configuration changes;
- unsafe or missing scope, unavailable change detection, or untracked code; and
- a missing, blank, or full-equivalent
test_incrementalcommand.
These fallbacks also execute once across the safety and normal test gates. Full CI remains the final whole-repository authority.
An absent test_full command is not successful test evidence. When a WU declares tests.unit, the
gate runs those paths through the existing scoped execution plan or fails with remediation to
configure software_delivery.gates.commands.test_full or a compatible path-scoped
test_incremental command. When neither tests.unit nor test_full is configured, the gate is
labelled not-applicable and prints one warning; it never reports passed without running a test.
Configured test commands keep their existing execution behavior.
Safe prep-evidence reuse
Section titled “Safe prep-evidence reuse”Prep-evidence reuse is default-on and is not a substitute for the test-plan safety rules above. Opt out with:
Only an identical eligible low- or medium-risk plan can reuse passed evidence. Its source, tests,
dependencies, toolchain, main snapshot, policy, authority, applicability, and execution plan must
all match. Missing, malformed, stale, cross-workspace, or mismatched evidence runs gates fresh.
--full-tests and high-risk prep run gates fresh; --full-tests also forces full test
execution. report_all (default true) does not exclude reuse — it is already one of the hashed
execution-plan inputs. Snapshot isolation remains OFF: reuse does not pin or execute an immutable
workspace snapshot.
A rebase alone tolerates drift confined to the main-snapshot and applicability components, but only once an overlap check proves disjointness. That overlap set is the branch own
diff plus the WU declared gate-input paths (tests.unit, tests.integration, tests.e2e,
and scopedTestPaths) — not the branch diff alone — so a main-side change to a
declared-but-branch-untouched test path still refuses reuse and re-runs the gate that input feeds.
The match is glob-aware, not literal-string: a declared path may be an exact file, a
bare directory prefix (with or without a trailing slash), or a glob pattern (matched with
micromatch), so a tests.unit glob or a bare __tests__-style directory still catches a
concrete main-side file it covers.
Uncovered-changed-files cross-check
Section titled “Uncovered-changed-files cross-check”A green scoped run only proves the declared tests.unit paths passed — it
does not by itself prove every changed file was exercised. After the
declared scoped run passes, LumenFlow diffs the branch and checks whether
any changed code file falls outside the declared tests.unit paths. When
every changed file is already covered, nothing further runs and the scoped
run’s cost stays exactly as before.
When changed files remain uncovered, LumenFlow runs one additional, narrowly
scoped vitest related check against just those files: vitest resolves the
module graph and finds any test in the project that imports them, wherever
it lives.
- If that check finds a covering test and it fails, the gate fails
immediately and names the uncovered file(s) so
tests.unitcan be extended to include the covering suite. - If it finds no covering test anywhere (a true coverage gap, not just an
incomplete declaration), the run does not pass silently; it falls through
to the configured
test_incrementalregression flow instead of trusting the declared-only result.
Preset Defaults
Section titled “Preset Defaults”| Preset | Format | Lint | Typecheck | Test |
|---|---|---|---|---|
node | prettier --check . | eslint . | tsc --noEmit | npm test |
python | ruff format --check . | ruff check . | mypy . | pytest |
go | golangci-lint fmt --diff --enable gofmt | golangci-lint run | go vet ./... | go test |
rust | cargo fmt --check | cargo clippy | cargo check | cargo test |
dotnet | dotnet format --verify | dotnet build | - | dotnet test |
java | mvn spotless:check | mvn checkstyle:check | mvn compile -DskipTests | mvn test |
java-gradle | ./gradlew spotlessCheck | ./gradlew checkstyleMain | ./gradlew compileJava | ./gradlew test |
ruby | rubocop | rubocop | - | rspec |
php | php-cs-fixer | phpstan | - | phpunit |
Running Gates
Section titled “Running Gates”Dependency Isolation Preflight
Section titled “Dependency Isolation Preflight”Before gate context, telemetry, or any gate command starts, LumenFlow inspects the active checkout’s
dependency roots and @lumenflow workspace-package links. Each workspace dependency must resolve
inside the active checkout. Package managers may materialize shared content into a checkout-local
virtual store via hardlinks, reflinks, or copies, but a direct workspace link to an external store is
never accepted. Store-looking substrings do not establish trust. Every intermediate scope/package
component is realpath-checked, including directory junctions. Missing/non-directory dependency
roots, missing declared workspace packages, and paths into main or another worktree all fail closed.
The diagnostic includes both the exact contaminated link and its resolved target:
Remove only the listed link, run the configured frozen install inside the active worktree, and then rerun gates. Do not relink to main and do not skip the check: no gate result is trustworthy when module resolution can read another branch.
A worktree gates invocation also requires its own CLI dist. It never falls back to main’s CLI dist,
so a --skip-setup checkout cannot bypass this preflight by bootstrapping the gate runner from a
different checkout.
Main-snapshot comparison probes follow the same boundary. Each temporary probe performs its own frozen install and blocks classification if installation or isolation verification fails.
Main-Snapshot Probes In A Packed Consumer
Section titled “Main-Snapshot Probes In A Packed Consumer”The main-snapshot comparison probe normally also builds the framework’s own
@hellmai/lumenflow-cli export closure inside the temporary probe worktree, so gates that need a
built workspace can compare against it. A packed consumer’s own main snapshot has no framework
source layout at all — the CLI is a real, installed dependency. In that case the probe skips the
export-closure build for every gate, and the gate runs on the checkout alone instead of failing the
whole probe closed. The probe keeps the export-closure build, and the unchanged fail-closed
behaviour on a build failure, only when the primary checkout carries the framework package and the
main snapshot does not.
If any gate fails, wu:prep fails and you fix issues in the worktree before completion.
For migration verification failures, the expected fix is:
- Apply the pending migrations using your project’s normal process
- Re-run the configured verification command manually if needed
- Re-run
pnpm wu:prep --id <wu-id>
Lifecycle audit appends are not author changes
Section titled “Lifecycle audit appends are not author changes”The rows wu:prep appends to the skip-gates audit file do not make
the worktree dirty for wu:edit or wu:escalate --resolve --delegated, and
committing them does not invalidate the prep checkpoint, so wu:done still
reuses it. Only a strict append of well-formed rows of the same WU and worktree
onto the committed bytes qualifies. A rewrite, truncation, deletion, staged edit,
malformed or foreign row, or any other changed path remains a change and still
changes the checkpoint diff hash.
Generated audit retention during completion
Section titled “Generated audit retention during completion”A normal wu:prep can append generated evidence to
the skip-gates audit file. wu:done preserves a validated,
unstaged append through automatic rebase without committing those rows between
PREP and checkpoint reuse. It verifies the historical prefix, complete NDJSON
row schema, WU/worktree attribution, matching claim documents and the exact Git
registration. Staged audit edits, malformed rows, historical rewrites, foreign
attribution and symlinked paths block this handling. Unrelated source dirt
continues to block normal completion.
Before changing the working audit or removing the worktree, completion writes and verifies three durable artifacts outside the worktree:
The first file contains the full original audit bytes; the second contains its exact uncommitted suffix. The receipt records their hashes and lengths, source HEAD, worktree and capturing Git user/session. Completion logs the receipt path. The capture identity does not fill in missing attribution on historical rows or grant permission to skip gates. Line endings and repeated rows are preserved; a committed audit append arriving from main stays before the restored suffix. Repeated restoration of the same captured evidence does not duplicate rows.
Default cleanup verifies retention before memory promotion and worktree
removal, so the artifact survives normal worktree and branch cleanup. A
retention failure keeps the worktree available for inspection. Check the
terminal command exit as well as publication proof: a post-publication cleanup
failure is still a failed command. Retained archives live with the repository’s
Git metadata, so preserve them when retiring that metadata. Existing
--no-remove and PR-mode behavior is unchanged.
Do not commit generated rows after green PREP to make rebase clean. Audit preservation adds no checkpoint-hash exemption: source/configuration changes still invalidate evidence, and named skip requests keep their existing checks.
When completion needs a fresh gate run, it selects the launcher from the target checkout. A framework source worktree uses its own branch launcher; a consumer uses its installed package’s declared gates bin even when the parent CLI runs from another framework checkout. Missing or corrupt launchers still fail clearly, and this selection does not change licence checks.
Gate Flags
Section titled “Gate Flags”| Flag | Description |
|---|---|
--docs-only | Run only docs-related gates (format, spec-linter, backlog-sync; skip lint/typecheck/test) |
--full-tests | Force one full test_full execution instead of scoped or incremental tests |
--full-lint | Run full lint pass instead of scoped lint |
Command Options
Section titled “Command Options”The setup command can be a string or an object with execution options:
Language Examples
Section titled “Language Examples”Node.js / TypeScript
Section titled “Node.js / TypeScript”Python
Section titled “Python”Java / JVM
Section titled “Java / JVM”How Gates Work Under the Hood
Section titled “How Gates Work Under the Hood”Each gate maps to a policy rule in the Software Delivery Pack:
| Gate | Policy ID | Trigger |
|---|---|---|
| Format check | software-delivery.gate.format | on_completion |
| Lint | software-delivery.gate.lint | on_completion |
| Type check | software-delivery.gate.typecheck | on_completion |
| Test | software-delivery.gate.test | on_completion |
When wu:prep or wu:done runs, the kernel evaluates these policies. A deny from any gate makes the completion decision final — the deny-wins invariant applies. The result is recorded in the evidence store for audit.
Gate Behavior
Section titled “Gate Behavior”On Claim
Section titled “On Claim”When you wu:claim:
- Worktree is created
- Gates status is “pending”
During Work
Section titled “During Work”Run pnpm gates frequently:
On Prep and Done
Section titled “On Prep and Done”When you wu:prep:
- Gates run automatically in the worktree
- If any fail, the WU stays
in_progressuntil you fix and rerunwu:prep
When you wu:done:
- The WU merges to main
- The stamp is created
- The worktree is cleaned up
Gate Failures
Section titled “Gate Failures”Fix and retry:
Test gate: capture-integrity failures vs real failures
Section titled “Test gate: capture-integrity failures vs real failures”If the test gate fails and prints [test-evidence:capture-integrity-failure] ..., the captured
process output had no test failure markers at all (for example, spinner/progress-only output with
no per-file results or summary) even though the gate exited non-zero. This means the capture
was broken, not that a specific test failed — rerun the underlying test command directly with a
forced JSON or verbose reporter to get real evidence before treating the run as a regression. See
Gates Reference — test-gate evidence capture-integrity classification
for the full code catalogue.
If it instead prints [test-evidence:partial-output] ... under concurrent gate contention (several
agents running pnpm gates at once), the reason now names an actionable retry step. A duplicated
but agreeing runner summary reconciles automatically; only a genuinely conflicting duplicate still
fails closed. See
Gates Reference — partial-output reconciliation under concurrent-runner contention.
If it prints [test-evidence:foreign-output-noise] ..., the captured stream named failing tests that the classifier could prove were outside every known file or directory selection for the execution. For known direct, partitioned, or file-explicit related plans, each executedTestFileSelections group pairs one plan’s checkout-relative cwd (empty string for checkout root) with its selected scope paths. For file-explicit related plans, scopedTestPaths describes changed-source attribution; the runner argv supplies the actual selection. If any subplan is unknown, the combined execution omits known-selection metadata.
These scopes do not prove every file under a selected directory ran, and a cwd alone may not identify which runner emitted an unlabeled failure. When a relative failure has multiple distinct possible runner contexts and no selection matches, it goes through the existing general evidence pipeline rather than being assigned a guessed path. The legacy flattened executedTestFilePaths field remains for compatibility but has no runner context. A failure that plausibly matches a selected scope, or is unresolved or outside the checkout, also uses the general pipeline. The fast path classifies foreign noise only when every parsed failure is proven outside all known selections, so a real selected failure is not masked. Raw tests.unit entries alone do not establish what ran. See Gates Reference — foreign-output-noise.
Branch-vs-main comparison artifacts
Section titled “Branch-vs-main comparison artifacts”When an immutable gate fails, branch-vs-main attribution pins one main snapshot
and executes a resolved test plan at most once when its full fingerprint matches.
Different applicability (including test, safety-critical-test, and docs-only)
plans never share a result. The comparison remains fail-closed when it is unrunnable
or loses infrastructure. Before the temporary probe worktree is removed, a
bounded JSON artifact is retained in the workspace artifacts directory at
gate-comparison/<source-sha>-<gate>-<unique-id>.json. It records the
exact command, source SHA, runtime/environment, test name, assertion/error body,
exit or signal, duration, cleanup disposition, bounded-field truncation flags,
original lengths, and SHA-256 digests. At most 100 recent artifacts are retained.
The classification is one of
branch-only-regression, identical-main-failure, unrunnable-comparison,
timeout, or infrastructure-loss.
Pre-existing, introduced, or undeterminable
Section titled “Pre-existing, introduced, or undeterminable”That branch-vs-main attribution settles every blocking failure into one of three outcomes:
- Pre-existing on main — the same failure reproduces on the pinned main snapshot. LumenFlow auto-skips the gate; no action needed.
- Introduced by this branch — main passes, only the branch fails. The gate blocks and cannot be skipped; fix the failure.
- Undeterminable — main also fails, but the failure cannot be matched to the branch’s (an
unrunnable comparison, or a genuinely different failure). The gate stays blocking and prints why
it could not certify pre-existing, followed by a remedy that depends on whether the gate is
skippable at all: a skippable gate gets a named
--skip-gate <name> --reason "<why>" --fix-wu <wu-id>remedy, with independent verification and explicit authorization; a non-skippable gate (every gate on the immutable safety-critical deny-set, plustest) gets no skip remedy — fix the failure on this branch, or repair main. There is no automatic skip for either case. See Gates Reference — Three classification outcomes for the full table.
Lint is classified per error
Section titled “Lint is classified per error”The lint gate does not get one pre-existing verdict for the whole run. Each ESLint error is
judged by its file, rule and line:
- An error in a file your WU added, or on a line your WU changed, is introduced and fails the gate, even when other lint errors on main are pre-existing.
- An error is pre-existing only when main reports the same rule at the matching unchanged line of the same file.
- A mixed result fails and lists only the introduced errors. A changed ESLint config, or a main-side lint that cannot run, makes the affected errors introduced.
The gate log names every error as lint: introduced: ... or lint: pre-existing: .... Main is
linted only for the changed files that carry an error on an unchanged line, and the result is cached
per main commit, so the comparison costs seconds. format:check classifies the complete file list
from its own scoped Prettier output and compares each reported file plus every ancestor formatter
config candidate, package manifest, and the root package-manager lock/workspace inputs with pinned
main. The config search covers the installed Prettier CLI’s JSON, JSON5, TOML, YAML, JS, and
TypeScript config names; ignored local candidates are included. Changed failing files or formatter
inputs block immediately. Shared-config strings, executable config, plugin declarations, or inputs
that cannot be proven stay undetermined and blocking. Only a complete unchanged failure set with
provable inputs can be pre-existing. Missing, malformed, or truncated output remains blocking. Test
gates classify each failing test.
Documentation currency
Section titled “Documentation currency”A work unit does not finish while it leaves documentation behind. The docs-currency gate runs at wu:prep and
again at wu:done, and it asks one question: did product code change with neither a documentation update nor a
declaration? If the diff touches a configured docs surface, if the WU notes say docs: n/a — <reason> with a
reason, or if the WU is a documentation WU, the gate passes silently. A diff of tests, lockfiles, generated output
or lifecycle state alone never triggers it. Only silence fails, and the message names the surfaces and the exact
declaration syntax. Product paths and docs surfaces are set with pnpm config:set
(software_delivery.gates.docs_currency). The full contract is in the
Gates Reference.
Two WUs that declare one file
Section titled “Two WUs that declare one file”Overlap on ordinary source is advisory. When your WU’s code_paths name a file another in-progress WU also
declares, wu:claim and monolithic-file-contention warn, name the other WU and its owner, and let you
continue. Whichever WU lands second resolves any conflict at pnpm wu:rebase or the wu:done auto-rebase, and
wu:done re-runs the gates on the rebased tree whenever the rebase brings in changes to files your WU touches.
--force-overlap is no longer needed for ordinary source; it is accepted and ignored with a notice.
Files that cannot be merged by hand keep the refusal: generated outputs, lockfiles, ledgers and baselines. The
class is software_delivery.gates.unmergeable_paths (glob patterns; a non-empty list replaces the built-in
defaults, which cover common lockfiles, dist and generated outputs, LumenFlow workspace state, the backlog and
status projections, and baseline files). For those files, and only those, monolithic-file-contention fails
wu:prep when the file is also declared by another in-progress WU whose branch changes it. When several
in-progress WUs declare the same such file, LumenFlow gives them one deterministic landing order instead of
refusing all of them:
- The holder preps. The holder is the declarer that claimed first, judged by the order of the claim events in the tracked WU event ledger at the integration target. That order is set by push, so it does not depend on any machine’s clock. A later claim never takes the file from a holder that has already prepped. A WU that is released and claimed again goes to the back of the order.
- Every other declarer waits. Its gate fails with the holder’s id, the full landing order with claim
times, and the next step: wait for the holder to land, then run
pnpm wu:rebase --id <your WU>andpnpm wu:prep --id <your WU>again. When the holder lands, the file is no longer contended and the next WU in the order preps. If your WU does not really need the file, remove it withpnpm wu:edit --id <your WU> --remove-code-paths <file>. - The existing clears still apply to waiters. A waiter passes if the other WU’s branch never changes the file, or if the two WUs recorded an overlap authorisation and their hunks do not overlap.
- Never both refused. Every WU computes the same order, and the WU that claimed first holds every file it shares, so at least one of the contending WUs can always prep.
You see the order when the contention starts, not at prep. pnpm wu:edit --code-paths (or
--rename-code-paths) prints the holder and the order for each path it adds that another in-progress WU
already declares. So does a forced pnpm wu:claim --force-overlap on the unmergeable class, where the claiming
WU goes last. These checks only inform: they never block the edit or the claim. A path that a WU adds only on its own branch is not
visible to other WUs until it lands. In that case both WUs may prep, and the second to land resolves the
text in its wu:rebase.
Skip Flags
Section titled “Skip Flags”There is no bare pnpm gates --skip-<gate> flag. Skip a specific, named gate through wu:done
instead — both --reason and --fix-wu are required, and only gates marked skippable: true can
be named this way:
See Constraints — Gates and Named Gate Skips for the gates that are immutable and can never be skipped this way.
Or in configuration:
CI Integration
Section titled “CI Integration”Use the LumenFlow Gates GitHub Action:
The action reads your software_delivery.gates.execution config automatically. See GitHub Action docs for details.
Backwards Compatibility
Section titled “Backwards Compatibility”If no software_delivery.gates.execution config is present, LumenFlow falls back to auto-detecting your project type based on files present and uses preset defaults.
Default gate admission uses a stable host location, independent of TMPDIR,
TMP, and TEMP: /tmp/.lumenflow-locks on POSIX systems, and the OS
CommonApplicationData folder on Windows. If LumenFlow cannot resolve or
inspect that default pool, it refuses to start the gate rather than silently
creating a separate pool. Older clients configured with different temporary
roots may not share admission with the stable default until upgraded. An
explicit programmatic lockRoot remains an isolated boundary; it is not a
cross-process host-wide pool.
Next Steps
Section titled “Next Steps”- Policy Engine — How gates are evaluated as deny-wins policies
- Evidence Store — How gate results are recorded for audit
- Configuration Reference — Full gates schema
- CLI Reference — All gate commands