Skip to content

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.

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.

Define your gate commands in workspace.yaml under software_delivery.gates.commands, the schema lumenflow init itself writes:

# workspace.yaml
version: '2.0'

software_delivery:
  gates:
    commands:
      test_full: pnpm turbo run test
      test_incremental: pnpm vitest run --changed origin/main --passWithNoTests
      typecheck: pnpm turbo run typecheck --affected
      typecheck_incremental: pnpm turbo run typecheck --filter=[origin/main]

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.

Projects with manual database deploy steps can add an explicit migration-state verifier:

software_delivery:
  gates:
    commands:
      migration_verify: 'pnpm db:preflight'

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 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:

software_delivery:
  gates:
    resource_guard:
      memory:
        mode: enforce # observe (default) | enforce | off
        defer_budget_ms: 300000 # 0 disables delay

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.

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.

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:

software_delivery:
  gates:
    merge_lock:
      wait_ms: 30000

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.

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.yaml that 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.

software_delivery:
  gates:
    shared_build_output_roots:
      - '/srv/shared-build-output'

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.

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:

VariableEffect
LUMENFLOW_BUILD_OUTPUT_WAIT_MSOpt-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.

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:

software_delivery:
  cleanup:
    build_output_lock_max_age_days: 14

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.

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.

software_delivery:
  gates:
    tdd_diff_evidence:
      mode: block
      applies_to_types:
        - feature
        - bug
      exempt_paths:
        - '.github/workflows/**'
        - '**/*.yml'
        - '**/*.yaml'

Defaults come from software_delivery.methodology.testing:

  • tdd sets software_delivery.gates.tdd_diff_evidence.mode: block
  • test-after sets software_delivery.gates.tdd_diff_evidence.mode: off
  • none sets software_delivery.gates.tdd_diff_evidence.mode: off
  • applies_to_types defaults to feature and bug
  • exempt_paths defaults to [], plus the built-in migration globs described below
  • test_file_patterns defaults to the built-in polyglot conventions listed below
  • code_file_extensions defaults 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.

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.

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:

EcosystemRecognised 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.

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):

software_delivery:
  gates:
    tdd_diff_evidence:
      test_file_patterns:
        - '**/*Tests.cs'
        - '**/*.Tests.cs'
        - '**/Test*.cs'
      code_file_extensions:
        - '.cs'

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:

software_delivery:
  gates:
    tdd_diff_evidence:
      test_file_patterns:
        - '**/tests/**/*.py'
      code_file_extensions:
        - '.py'

Go — *_test.go is recognised by default:

software_delivery:
  gates:
    tdd_diff_evidence:
      code_file_extensions:
        - '.go'

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:

tdd-exception: <reason>

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: true registers it for every agent/client runtime
  • auto_run: true makes wu:prep run 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
software_delivery:
  gates:
    delivery_review:
      enabled: true
      auto_run: true
      block_partial: true
      verifier_command: pnpm qa:evidence
      skip_types:
        - documentation
        - process

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:

software_delivery:
  agents:
    defaultClient: codex-cli
    clients:
      claude-code:
        features:
          delivery_review:
            enabled: true
            auto_run: true

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 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.

type DeliveryReviewVerdict = 'PASS' | 'FAIL' | 'PARTIAL';

interface DeliveryReviewResult {
  wuId: string;
  verdict: DeliveryReviewVerdict;
  summary: string;
  findings: Array<{
    severity: 'critical' | 'high' | 'medium' | 'low';
    title: string;
    detail: string;
    acceptanceCriteriaRefs?: string[];
    fileRefs?: string[];
  }>;
  acceptanceCriteria: Array<{
    criterion: string;
    status: 'satisfied' | 'unclear' | 'not_satisfied';
    evidence?: string[];
  }>;
  metadata: {
    runtimeClient?: string;
    startedAt: string;
    completedAt: string;
  };
}

Verdict behavior:

  • PASS means the configured evidence checks found sufficient delivery evidence; it does not certify code correctness or prove that every acceptance criterion is semantically satisfied.
  • PARTIAL means the checks found uncertainty or lower-severity findings; it warns by default and blocks when software_delivery.gates.delivery_review.block_partial is true.
  • FAIL means 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.

Set software_delivery.gates.delivery_review.contextual.enabled: true in the trusted integration checkout and configure a committed, self-contained Node analyzer. For example:

software_delivery:
  gates:
    delivery_review:
      enabled: true
      auto_run: true
      contextual:
        enabled: true
        analyzer_argv: [node, review/analyzer.cjs]
        rules:
          - id: discount-sign
            revision: '1'
            instruction: Discounts must reduce the total.

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.

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.

Define pattern-triggered commands alongside your standard gates:

software_delivery:
  gates:
    commands:
      test_full: 'pnpm test'
      test_incremental: 'pnpm test --changed'
      typecheck: 'pnpm typecheck'

    conditional_commands:
      - trigger_patterns:
          - 'supabase/migrations/**'
          - 'supabase/schema.sql'
        command: 'pnpm db:verify'
        severity: error
        fresh_on_completion: true
        guidance: 'Apply pending migrations locally before verifying database state.'
        guidance_ref: 'docs/db-verification-guide.md'

      - trigger_patterns:
          - 'prisma/migrations/**'
          - 'prisma/schema.prisma'
        command: 'npm run prisma:validate'
        severity: warn
FieldTypeRequiredDescription
trigger_patternsstring[]YesGlob patterns matched against changed files
commandstringYesShell command to execute when patterns match
severitystringNoerror (default, blocks gates), warn, or off (skip)
fresh_on_completionbooleanNoRerun after completion reconciliation; defaults to false
guidancestringNoActionable text shown when the command fails
guidance_refstringNoFile path whose content is appended to guidance

How it works:

  1. When pnpm gates or wu:prep runs, changed files are compared against each command’s trigger_patterns using glob matching
  2. Only commands with matching patterns execute — unmatched commands are silently skipped
  3. If a matching command fails with severity error, gates fail. With severity warn, a warning is logged but gates continue
  4. A matching command with fresh_on_completion: true runs again during wu:done, after branch reconciliation and before the landing transaction, even when ordinary gates reuse a valid wu:prep checkpoint
  5. Completion freshness is fail-closed: a failed or unavailable fresh command blocks completion, including a command whose normal preparation severity is warn. severity: off still disables it
  6. 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:

  1. gate:conditional (recommended, per-rule) — mirrors gate:co-change:

    pnpm gate:conditional --add --name db-verify \
      --trigger "supabase/migrations/**" \
      --trigger "supabase/schema.sql" \
      --command "pnpm db:verify" \
      --severity error \
      --guidance "Apply pending migrations locally before verifying database state."
    
    pnpm gate:conditional --list          # human-readable
    pnpm gate:conditional --list --json   # machine-readable
    
    pnpm gate:conditional --edit --name db-verify --severity warn
    pnpm gate:conditional --remove --name db-verify

    The name field is how --remove/--edit address a specific entry. It is optional in the underlying schema (existing unnamed entries continue to work) but required for CLI-managed entries.

  2. config:set --json-value — writes the whole array verbatim when you need a shape the flags above don’t cover:

    pnpm config:set --key software_delivery.gates.conditional_commands \
      --json-value '[{"name":"db-verify","trigger_patterns":["supabase/migrations/**"],"command":"pnpm db:verify","severity":"error","fresh_on_completion":true}]'

Both paths validate against ConditionalCommandConfigSchema and commit atomically via micro-worktree.

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.

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.

For common languages, use a preset to get sensible defaults:

software_delivery:
  gates:
    execution:
      preset: 'python'
      # Override specific commands
      lint: 'mypy . && ruff check .'

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.

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-tests or --full-coverage;
  • test-runner configuration changes;
  • unsafe or missing scope, unavailable change detection, or untracked code; and
  • a missing, blank, or full-equivalent test_incremental command.

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.

Prep-evidence reuse is default-on and is not a substitute for the test-plan safety rules above. Opt out with:

local_prep:
  reuse_evidence: false

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.

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.unit can 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_incremental regression flow instead of trusting the declared-only result.
PresetFormatLintTypecheckTest
nodeprettier --check .eslint .tsc --noEmitnpm test
pythonruff format --check .ruff check .mypy .pytest
gogolangci-lint fmt --diff --enable gofmtgolangci-lint rungo vet ./...go test
rustcargo fmt --checkcargo clippycargo checkcargo test
dotnetdotnet format --verifydotnet build-dotnet test
javamvn spotless:checkmvn checkstyle:checkmvn compile -DskipTestsmvn test
java-gradle./gradlew spotlessCheck./gradlew checkstyleMain./gradlew compileJava./gradlew test
rubyrubocoprubocop-rspec
phpphp-cs-fixerphpstan-phpunit

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:

[gates] Dependency isolation preflight failed; refusing to execute gates:
  - <checkout-a>/packages/app/node_modules/@hellmai/lumenflow-host -> <checkout-b>/packages/host (workspace dependency resolves outside the active checkout)

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.

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.

# Run all gates
pnpm gates

# Output
> Format check... pass
> Lint check... pass
> Type check... pass
> Test suite... pass
> All gates passed!

If any gate fails, wu:prep fails and you fix issues in the worktree before completion.

For migration verification failures, the expected fix is:

  1. Apply the pending migrations using your project’s normal process
  2. Re-run the configured verification command manually if needed
  3. 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:

<git-common-dir>/lumenflow-generated-audits/<wu-id>/
  <rebase-or-cleanup>-<source-head>-<audit-sha256>/
    skip-gates-audit.ndjson
    appended.ndjson
    retention.json

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.

FlagDescription
--docs-onlyRun only docs-related gates (format, spec-linter, backlog-sync; skip lint/typecheck/test)
--full-testsForce one full test_full execution instead of scoped or incremental tests
--full-lintRun full lint pass instead of scoped lint

The setup command can be a string or an object with execution options:

software_delivery:
  gates:
    execution:
      setup:
        command: 'dotnet restore'
        timeout: 300000 # 5 minutes
        continueOnError: false
software_delivery:
  gates:
    execution:
      preset: 'node'
      # Or custom:
      setup: 'pnpm install --frozen-lockfile'
      format: 'pnpm prettier --check .'
      lint: 'pnpm eslint . --max-warnings 0'
      typecheck: 'pnpm tsc --noEmit'
      test: 'pnpm vitest run'
software_delivery:
  gates:
    execution:
      preset: 'python'
      # Or custom:
      setup: 'pip install -e ".[dev]"'
      format: 'ruff format --check .'
      lint: 'ruff check . && mypy .'
      test: 'pytest -v'
software_delivery:
  gates:
    execution:
      preset: 'dotnet'
      # Or custom:
      setup: 'dotnet restore'
      format: 'dotnet format --verify-no-changes'
      lint: 'dotnet build --no-restore -warnaserror'
      test: 'dotnet test --no-restore'
software_delivery:
  gates:
    execution:
      preset: 'go'
      # Or custom:
      format: 'golangci-lint fmt --diff --enable gofmt'
      lint: 'golangci-lint run'
      typecheck: 'go vet ./...'
      test: 'go test -v ./...'
software_delivery:
  gates:
    execution:
      preset: 'rust'
      format: 'cargo fmt --check'
      lint: 'cargo clippy -- -D warnings'
      typecheck: 'cargo check'
      test: 'cargo test'
software_delivery:
  gates:
    execution:
      preset: 'java'
      # Or custom (Maven):
      format: 'mvn spotless:check'
      lint: 'mvn checkstyle:check'
      typecheck: 'mvn compile -DskipTests'
      test: 'mvn test'
      # Or Gradle:
      # format: './gradlew spotlessCheck'
      # lint: './gradlew checkstyleMain'
      # typecheck: './gradlew compileJava'
      # test: './gradlew test'
software_delivery:
  gates:
    execution:
      preset: 'ruby'
      # Or custom:
      setup: 'bundle install'
      format: 'bundle exec rubocop --format simple --fail-level W'
      lint: 'bundle exec rubocop'
      test: 'bundle exec rspec'
software_delivery:
  gates:
    execution:
      preset: 'php'
      # Or custom:
      setup: 'composer install'
      format: 'vendor/bin/php-cs-fixer fix --dry-run --diff'
      lint: 'vendor/bin/phpstan analyse'
      test: 'vendor/bin/phpunit'

Each gate maps to a policy rule in the Software Delivery Pack:

GatePolicy IDTrigger
Format checksoftware-delivery.gate.formaton_completion
Lintsoftware-delivery.gate.linton_completion
Type checksoftware-delivery.gate.typecheckon_completion
Testsoftware-delivery.gate.teston_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.

When you wu:claim:

  • Worktree is created
  • Gates status is “pending”

Run pnpm gates frequently:

# Quick feedback loop
pnpm gates
# Fix issues
pnpm gates
# All green? Continue

When you wu:prep:

  1. Gates run automatically in the worktree
  2. If any fail, the WU stays in_progress until you fix and rerun wu:prep

When you wu:done:

  1. The WU merges to main
  2. The stamp is created
  3. The worktree is cleaned up
pnpm wu:prep --id <wu-id>

> Running gates...
> Format check... FAILED
>   utils/validation.ts - needs formatting
>
> Fix the issues above before completing.

Fix and retry:

pnpm prettier --write utils/validation.ts
pnpm wu:prep --id <wu-id>
# Gates pass, then complete in main:
pnpm -C /path/to/main wu:done --id <wu-id>

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.

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, plus test) 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.

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.

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.

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> and pnpm 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 with pnpm 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.
- config/shared.json
    landing order: <holder-wu-id> (claimed 2026-09-28T10:00:00.000Z) -> <wu-id> (claimed 2026-09-28T11:00:00.000Z, this WU)
    holder: <holder-wu-id> preps and lands first; <wu-id> waits.
Next: wait for <holder-wu-id> to land, then run pnpm wu:rebase --id <wu-id> and re-run pnpm wu:prep --id <wu-id>.

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.

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:

pnpm wu:done --id <wu-id> \
  --skip-gate lane-health \
  --reason "Pre-existing test failure in legacy module" \
  --fix-wu <wu-id>

See Constraints — Gates and Named Gate Skips for the gates that are immutable and can never be skipped this way.

Or in configuration:

software_delivery:
  gates:
    commands:
      test_full: 'pnpm test'
      test_incremental: '' # No incremental test command for this project

Use the LumenFlow Gates GitHub Action:

# .github/workflows/gates.yml
name: Gates
on: [pull_request]
jobs:
  gates:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hellmai/lumenflow/actions/lumenflow-gates@v4
        with:
          token: ${{ secrets.LUMENFLOW_TOKEN }}

The action reads your software_delivery.gates.execution config automatically. See GitHub Action docs for details.

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.