Skip to content

Gates

Gates are automated quality checks that must pass before a WU can be completed. They replace manual code review with consistent, automated enforcement.

Traditional review:

  • Human bottleneck (waiting for reviewers)
  • Inconsistent (different reviewers, different standards)
  • Slow feedback (review happens after code is written)

Gates:

  • Instant (run automatically)
  • Consistent (same checks every time)
  • Fast feedback (run locally before pushing)

Define your gate commands in workspace.yaml under software_delivery.gates:

# workspace.yaml
version: '2.0'

software_delivery:
  gates:
    execution:
      setup: 'pnpm install'
      format: 'pnpm format:check'
      lint: 'pnpm lint'
      typecheck: 'pnpm typecheck'
      test: 'pnpm test'

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 from worktrees in the same repository share a FIFO execution lock. The default capacity is 2, allowing two isolated worktree runs while keeping host use bounded. Repositories whose gate commands write shared outputs can opt down to strict serialization:

software_delivery:
  gates:
    execution_lock:
      concurrency: 1

concurrency must be an integer from 1 through 16. When all slots are occupied, later runs remain queued in request order. Queue diagnostics report the configured capacity, active holder identities, and the current WU’s queue position.

Fresh lifecycle CLI builds do not occupy this gate semaphore. They 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.

The repository semaphore limits host load, but it is not the authority for shared build output. Every gate run that may write build artifacts also acquires a capacity-one lock under ~/.lumenflow/locks/, keyed by the canonical build-output root that run writes. Two runs whose roots are disjoint proceed concurrently; two runs that share a root queue instead of interleaving. The lock remains held for the complete build-producing run, not one gate at a time, preventing another run from replacing artifacts between a build and a later test.

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 lock at ~/.lumenflow/locks/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, and co-change. Their existing parallel pre-pass is unchanged. 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.

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 []
  • 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.

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 src/lib/testimonials.ts or src/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 gate for completion review. 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 lumenflow-cloud or any hosted 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 (WU-3982) 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 .lumenflow/artifacts/delivery-review/<WU-ID>.json. 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 review found sufficient delivery evidence
  • PARTIAL means the review completed with uncertainty or lower-severity findings; it warns by default and blocks when software_delivery.gates.delivery_review.block_partial is true
  • FAIL means the review found blocking gaps or risks and gates fail

The native review inspects the current WU spec, changed files, acceptance criteria, and delivery risks. It is intentionally separate from wu:verify, which keeps its existing lifecycle meaning.

Define pattern-triggered commands alongside your standard gates:

software_delivery:
  gates:
    execution:
      format: 'pnpm format:check'
      lint: 'pnpm lint'
      typecheck: 'pnpm typecheck'
      test: 'pnpm test'

    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 (escape hatch) — 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.

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

Prep-evidence reuse is default-off and is not a substitute for the test-plan safety rules above. Enable the fast path only with both settings:

local_prep:
  reuse_evidence: true
  report_all: 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, report-all, and high-risk prep run gates fresh; --full-tests also forces full test execution. Snapshot isolation remains OFF: reuse does not pin or execute an immutable workspace snapshot.

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
gogofmt -l .golangci-lintgo vet ./...go test
rustcargo fmt --checkcargo clippycargo checkcargo test
dotnetdotnet format --verifydotnet build-dotnet test
javaspotless:checkcheckstylemvn compilemvn 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:
  - /repo/worktrees/wu-a/packages/app/node_modules/@hellmai/lumenflow-host -> /repo/worktrees/wu-b/packages/@lumenflow/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.

# 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-XXX
FlagDescription
--docs-onlyRun only docs-related gates (skip format/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

Commands can be strings or objects with options:

software_delivery:
  gates:
    execution:
      format: 'dotnet format --verify-no-changes'
      test:
        command: 'dotnet test --no-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: 'test -z "$(gofmt -l .)"'
      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-042

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

Fix and retry:

pnpm prettier --write src/utils/validation.ts
pnpm wu:prep --id WU-042
# Gates pass, then complete from main:
cd /path/to/main && pnpm wu:done --id WU-042

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.

Branch-vs-main comparison artifacts (WU-3836)

Section titled “Branch-vs-main comparison artifacts (WU-3836)”

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 at .lumenflow/artifacts/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.

To reproduce concurrent attribution deterministically, run the filesystem-barrier fixture repeatedly:

pnpm vitest run packages/@lumenflow/cli/src/__tests__/lifecycle-concurrency.test.ts \
  -t "overlaps two prep processes beyond a barrier"

Pre-existing, introduced, or undeterminable (WU-3981)

Section titled “Pre-existing, introduced, or undeterminable (WU-3981)”

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 the repository owner’s 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.

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-042 \
  --skip-gate lane-health \
  --reason "Pre-existing test failure in legacy module" \
  --fix-wu WU-150

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:
    execution:
      format: 'pnpm format:check'
      lint: 'pnpm lint'
      # No typecheck or test 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.