Skip to content

Gates Reference

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

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

The standard catalogue ships in software_delivery.gates. Defaults below show the out-of-the-box skippable value. Per-repo overrides via software_delivery.gates.overrides.<gate_id> take precedence only for mutable policy gates; the safety-critical deny-set is immutable. When set, software_delivery.gates.commands.lint and .format run in their respective gates; when unset, the framework keeps its incremental ESLint and Prettier defaults. Go and Java presets use single commands accepted by the gate command schema. Java init selects Maven commands by default or the named java-gradle preset when gradlew exists.

Gateskippable (default)Lifecycle surfaceNotes
format:checkfalseprep + doneConfigured format command, or Prettier on changed files when unset, with the official Prettier content cache (--cache --cache-strategy content) on the incremental argv. Safety-critical. Missing script blocks in default mode.
lintfalseprep + doneConfigured lint command, or ESLint when unset. Safety-critical. Content cache retained on both the incremental and the full-run path — see Incremental typecheck and lint.
typecheckfalseprep + doneEveryday prep/done runs software_delivery.gates.commands.typecheck_incremental (changed packages only, or the Turborepo-derived default); the release path runs gates.commands.typecheck (changed packages + dependents); else full-workspace tsc --noEmit. Safety-critical. Fail-closed cache validation — see Incremental typecheck and lint and Everyday vs release typecheck scope.
spec:linterfalseprep + doneWU YAML spec validity. Safety-critical.
co-changefalseprep + doneRequired co-edits per software_delivery.gates.co_change. Safety-critical.
claim-validationfalseprep + doneBranch / lane / WU lock invariants. Safety-critical.
unread-directed-signalfalseprep + doneDirected inbox signals must be triaged before completion. Safety-critical.
invariantsfalseprep + doneRepo-defined hard invariants (paths, status, schema). Safety-critical.
safety-critical-testfalseprep + doneRuns on every WU by default. Narrow with applicability_paths, do not flip skippable.
testfalseprep + doneThe everyday test gate: runs software_delivery.gates.commands.test_incremental (or the resolved declared/scoped plan). Unconditionally registered on every prep/done. Grouped with safety-critical-test as a test-evidence gate and reuses its shared plan result within one gate run rather than executing twice; has no configured skip remedy — see Skip semantics.
delivery_reviewfalseprep + doneNative completion review gate. Enabled by software_delivery.gates.delivery_review.enabled; wu:prep auto-runs it when auto_run: true. Writes .lumenflow/artifacts/delivery-review/<wu-id>.json; FAIL blocks, PARTIAL warns, PASS passes.
docs-currencyfalseprep + doneA WU that changes product code must touch a configured docs surface, carry docs: n/a — <reason> in its notes, or be a documentation WU. Fails on silence, and on decision-record problems (ADR index parity, implemented ADRs left Proposed); see docs-currency. Blocking, not skippable by default, not in the immutable set.
tdd-diff-evidencetrueprep + doneTDD ratchet against baseline. Allow-listed.
migration-verifytrueprep + doneDB migration up/down dry-run. Allow-listed.
lane-healthtrueprep + doneLane wip-limit / coverage check. Allow-listed.
buildtrueprep (opt-in)local_prep.build.enabled=true. prereq_strategy: 'fail'.
integration-testtrueprep (opt-in)local_prep.integration_test.enabled=true.
e2e-smoketrueprep (opt-in)local_prep.e2e_smoke.enabled=true. Auto not-applicable when no Playwright config.
test-over-deletiontrueprepAdvisory heuristic: flags WUs whose diff deletes .test.ts(x) whose describe() symbol is not present in the WU’s code_paths. Skip with --skip-gate test-over-deletion --reason ... --fix-wu ....
monolithic-file-contentiontrueprepAdvisory heuristic: flags files with observed ownership in 2+ in-progress WUs’ code_paths including the current WU. The current WU’s own paths come from its branch record at HEAD (a wu:edit drop is not phantom-contended against origin/main). A co-claimant that declares a file is counted only when its reachable lane branch actually diffs that file; an empty branch diff logs a no-change note and does not block. Unclaimed ready placeholders are ignored. Overlap on ordinary source is advisory: the gate passes and names the other WU and its owner; only the unmergeable class (software_delivery.gates.unmergeable_paths: generated outputs, lockfiles, ledgers, baselines) keeps the failure. Declarers of one such file get one landing order: the holder (the declarer whose claim comes first in the tracked wu-events.jsonl ledger) preps; every other declarer waits, told the holder, the order with claim times and the next step. Two in-progress WUs that share a file are never both refused.
prod-migration-drifttrueCI (opt-in)Advisory CI gate: prod DB more than threshold (default 5) migrations behind main’s migrations/ directory. Auto-skips not-applicable until software_delivery.gates.overrides.prod-migration-drift.connection_string_env_var is configured.
cross-cutting-ratchetstrueprep + doneRuns cheap, branch-scoped delta checks (file-size, console.* count, path-literal rules) and the ratchet suites whose declared scan paths intersect the diff on every code-gate run. The static phase of wu:prep runs the suites whose declared scans= paths intersect the diff; every suite still runs in the post-landing pnpm main:sweep --post-land and in the release. Auto-skips outside worktrees and in repos without the suites.
parity-drifttruerelease gate planRuns cross-package parity/drift suites (MCP, conductor-sdk, integration-tests, CLI flag, public docs), exempt from changed-file scoping, on the full gate plan the release runs. In a worktree, same NEW-vs-main triage as cross-cutting-ratchets; on the main checkout every subset runs and any failure blocks. Auto-skips in repos without the suites.
coveragetrueoperator onlyRegisters only for pnpm gates --full-coverage or --only-gate coverage; no lifecycle or release path enforces it.

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

lumenflow init scaffolds echo-style stub scripts so a fresh project can run pnpm gates and wu:done before any tooling exists. A gate whose script is an unmodified scaffold stub checked nothing, so the gates runner reports it as not-configured and never as passed: in the console summary ([NOT-CONFIGURED]), the discriminated state summary, the gate_attempt ledger (a skip with planMode: not-configured and the reason) and the wu:prep gate counts. This covers lint, typecheck and format:check (the latter only while prettier is not installed, when the stub exits with guidance). A script you edited, or a command set in software_delivery.gates.commands, is your own and is never reported this way. spec:linter is not a stub: init wires it to the real wu-validate --all.

Policy per gate: software_delivery.gates.overrides.<gate>.not_configured is warn (default) or fail. The default keeps fresh-project onboarding working while showing the state; fail fails the run, for example pnpm config:set software_delivery.gates.overrides.lint.not_configured=fail.

wu:done runs the cos:gates step only when the project defines a non-stub cos:gates script. With a stub or no script it logs COS gates not configured, never COS gates passed, and continues under the default policy (overrides.cos:gates.not_configured=fail refuses). With a real script the blocking behaviour, the skip flag and its audit row are unchanged.

Each gate run produces one of seven GateResult discriminated states (the seventh, not-configured, is described above). Glyphs and labels are stable and surfaced verbatim by wu:prep, wu:done, and the audit NDJSON source field.

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

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

ApplicabilityPreconditionsskippableprereq_strategyResult
not-applicable———skipped-auto-not-applicable
applicablesatisfied——run → passed or failed
applicablemissingfalse—blocked-missing-prereq (BLOCK)
applicablemissingtrue'auto-skip'skipped-auto-missing-prereq
applicablemissingtrue'fail'failed

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

On a claimed WU, wu:edit records scope (code_paths, tests, acceptance criteria, notes) as pending spec edits that materialise only at wu:done, so the worktree YAML keeps the old values. The native and contextual review read the same effective pending spec that wu:prep and wu:done use (readEffectivePendingWU) for the declared scope, the acceptance criteria and the contextual staleness snapshot. A path removed by a pending wu:edit is no longer required, and a path it added must be covered by the diff. A contextual result is still refused as stale when the effective spec changes during analysis, for example because a new wu:edit lands mid-review.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Two budgets, two questions:

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

The budget is resolved from the run’s weight class, read through the classifyGateRunWeightClass classifier on the resolved execution plan. The class only selects a budget: gate runs take no admission lock, so no class changes how much of the machine a run gets.

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

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

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

Measured, not chosen. The shipped numbers are derived from measured gate_attempt ledger data. The rule applied to every class: at least twice the p95, and never below the observed green maximum. Only a green run ever reaches the budget, because a failed gate exits wu:prep first, which is why the green maximum is the floor. A full run is the costliest plan by construction, so full gets a larger budget.

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

Gate execution only. Gate runs are never queued for admission; the only wait a run can have is the summed delay of the memory start guard in enforce mode. That delay is subtracted and reported separately as the admission wait: waiting on a busy host is contention, not a lifecycle regression, and charging it to the budget would make your own parallelism look like the thing the budget exists to catch. The span is taken from a monotonic clock, so a wall-clock correction cannot trip it.

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

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

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

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

The waiver’s scope is narrow and fixed:

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

reuse_evidence is default-on. A workspace opts OUT with:

software_delivery:
  gates:
    local_prep:
      reuse_evidence: false

Only an identical eligible low- or medium-risk plan can reuse a passed evidence record. It is bound to source and declared-test content, dependencies, toolchain, main snapshot, policy, authority, applicability, and the exact execution plan. Missing, malformed, stale, cross-workspace, or mismatched evidence runs gates fresh. --full-tests and high-risk prep run gates fresh; --full-tests also forces full test execution. report_all (default true) does NOT exclude reuse — its value is already one of the hashed execution-plan inputs, so a cached record only ever replays a run whose report_all setting matches. Snapshot isolation remains OFF: reuse does not pin or execute an immutable workspace snapshot.

The green wu:prep checkpoint can also be reused by wu:done after a commit that only appends lifecycle state. Its diff hash, changed-file fingerprint, and rebase overlap check share the same lifecycle-state exclusion: .lumenflow/state/** and .lumenflow/meta-rebase-audit.ndjson. Posting a worktree PREP END or READY signal therefore does not force gates to run again. Rows that wu:prep itself appends to .lumenflow/skip-gates-audit.ndjson are lifecycle audit appends, not author changes: committing only a strict append of well-formed rows leaves the diff hash unchanged, and wu:edit and wu:escalate --resolve --delegated accept an unstaged append as a clean tree. A rewrite, truncation, deletion, malformed row or any other path still counts as a change and refuses. Changes to source, declared tests, configuration, lockfiles, WU authority, or gate ratchet baselines remain gate inputs and can require fresh gates. A rebase also reruns gates when main changed a branch-touched file or a declared gate input. The lifecycle handoff receipt continues to bind the full branch diff.

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

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

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

software_delivery:
  gates:
    throughput_baseline:
      path: .lumenflow/throughput-baseline.json
      tolerance_percent: 25
      tolerance_floor_hours: 0.5
FieldDefaultEffect
path.lumenflow/throughput-baseline.jsonRepository-relative path to the recorded baseline document.
tolerance_percent25How far a metric may move in its bad direction before it is a regression.
tolerance_floor_hours0.5Absolute floor (hours) applied to the hour-valued claim-to-done series’ threshold, so a zero baseline is not compared against zero.
MetricDirectionSource
gate_run_p50_mslower is betterMedian of the per-day gate-attempt p50s in the window
gate_run_p90_mslower is betterMedian of the per-day gate-attempt p90s in the window
warm_path_prep_mslower is betterElapsed time of a warm, evidence-reusing prep
first_attempt_landing_share_percenthigher is betterShare of landed WUs with no failed gate attempt

Record it — never hand-write it:

pnpm throughput:baseline:record

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

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

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

The statistic is the median of per-day values, because the worst day in a window describes the noisiest afternoon rather than the lifecycle, and a gate that blocks on noise gets routed around.

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

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

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

software_delivery.gates.local_prep.build_scope controls this:

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

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

wu:prep classifies a failing gate as introduced-by-branch vs pre-existing-on-main by re-running it against an isolated main-snapshot probe — the “gate-main comparison” subprocess. On a large monorepo test suite this subprocess can exceed Node’s default heap and crash with FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory.

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

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

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

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

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

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

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

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

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

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

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

Both fast-paths share one comparison: branch-count <= main-count classifies pre-existing-on-main; any non-finite input or a branch-caused increase returns no classification and the normal probe-based path runs unchanged.

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

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

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

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

Changed-file lists and the Windows command-line limit

Section titled “Changed-file lists and the Windows command-line limit”

The incremental format:check and lint gates, the format-failure guidance and the path-scoped test runs pass file lists to the package manager in argv form. When pnpm resolves to a .cmd shim on Windows (an npm-global or corepack install), that command runs through cmd.exe, which reads at most 8,191 characters. Each list is therefore split into chunks sized against the real planned command, and the chunks run one after another:

  • a gate passes only when every chunk passes;
  • the lint warning ceiling applies to the total across chunks, not to each chunk;
  • the scoped test runs become more partitions, each with its own report;
  • a single path too long for any command line fails the gate with WindowsCommandLineTooLongError instead of cmd.exe’s “The command line is too long.”.

On macOS, Linux and a Windows host whose pnpm is a native .exe, a normal WU’s list fits in one chunk, so these gates run exactly as before.

Everyday wu:prep/wu:done and the full gate plan run different typecheck commands — the same everyday/release split the test gate already uses for the related-plan cross-check and the parity suites. LUMENFLOW_INCLUDE_RELEASE_ONLY_GATES=1 selects the full plan for a pnpm gates run (for example in your own release CI).

PathConfig keySelection
Everydaygates.commands.typecheck_incrementalchanged packages only
Releasegates.commands.typecheckchanged packages + dependents
  • Everyday integrity is the package you touched, whole-program. A one-file change still typechecks that entire package (TypeScript has no honest single-file mode), comments included. A public-API break that only surfaces in a dependent is a release catch, the same class as the related-plan cross-check.
  • Unconfigured default. When typecheck_incremental is unset on a Turborepo host whose turbo.json declares a typecheck task (root or <pkg>#typecheck), the gate derives pnpm exec turbo run typecheck --filter=[<integration ref>] from software_delivery.git (origin/main unless defaultRemote/mainBranch say otherwise). Any other host falls through to typecheck, then to the full default. A release never narrows to changed packages: with only typecheck_incremental configured, the release path runs the full default.
  • Every fail-closed fallback still applies to whichever command was selected — main branch, main snapshot, root-owned change, unresolvable changed-file set, lockfile parse problem. None of them is release-only, and typecheck stays never-skippable. The incremental format:check argv passes the official Prettier --cache --cache-strategy content flags the root format:check script already used, so a changed file whose content Prettier already judged is not re-formatted; the check itself is never skipped. For consumers, the node gate preset carries the official ESLint and Prettier content caches (--cache --cache-strategy content); the tsc --noEmit preset deliberately stays without --incremental, because its build-info file would live outside the packages/** purge glob the fail-closed cache validation above controls.

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

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

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

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

The section above only sized the gate-main comparison subprocess. Under report-all mode the top-level wu:prep / wu:done / gates process itself — the one running that whole gate loop — could still exhaust the Node default heap and crash with the same FATAL ERROR: Reached heap limit.

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

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

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

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

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

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

Two things stay true regardless of report_all:

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

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

Setting report_all: false restores the earlier first-failure behaviour: wu:prep stops at the first failing gate, exactly as before.

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

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

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

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

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

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

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

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

Gate runs take no admission lock (below), and that never weakens build-output serialization. After root keying, parallelism on one machine is bounded only by declared shared roots and the memory start guard, never by a machine-wide count.

The lock is built on the owned-lock primitive and is the one exclusion left in the gate path. It guards correctness, not compute, and never refuses on capacity. Waiting is bounded by the holder’s liveness, not by a clock: it ends when the holder is proven dead (process id plus process start time, on Linux, macOS and Windows), when the holder releases, or when the gate deadline or an abort fires. There is no fixed default ceiling. A holder on another host, or one whose liveness cannot be proven, is treated as alive and is never stolen. A dead top-level holder whose delegated child is still writing keeps custody until that child exits. A lock directory left by an older engine is honoured the same way, so a mixed fleet never runs two writers on one root.

SettingDefaultMeaning
LUMENFLOW_BUILD_OUTPUT_WAIT_MSnoneOpt-in ceiling in milliseconds for one wait. Unset or 0 means no ceiling. The workspace key is added separately.

While a gate waits it prints the holder (pid, host, owner, work unit, held-for time) every 15 seconds, with each setting and its source (default, workspace, env). When the ceiling, the gate deadline or an abort ends a wait, the error names the live holder and the override, and the waiter’s queue record is removed. A run that cannot key a single root prints the machine-wide degrade once.

A gate run (pnpm gates, and the gates wu:prep and wu:done run) takes no admission lock. It is never queued, weighted, ticketed or refused because of capacity, and the operating system schedules CPU across concurrent runs. Ten runs started at once all start at once. This replaces the weighted admission against a measured host ceiling that 6.2.0 introduced.

The one start-time control is the memory start guard. Before each gate step spawns, it compares live available memory, less what running peer steps have not yet used, less a floor, with the step’s expected peak. In observe mode (the default) it records the would-be decision and never waits. In enforce mode it delays a step that does not fit for at most the defer budget, then starts it. Nothing refuses.

FieldTypeDefaultMeaning
software_delivery.gates.resource_guard.memory.modeobserve | enforce | offobserveEnv LUMENFLOW_START_GUARD overrides. off is the kill switch.
software_delivery.gates.resource_guard.memory.defer_budget_msnumber300000Longest one step is delayed in enforce mode; 0 disables delay. Env LUMENFLOW_START_GUARD_DEFER_BUDGET_MS overrides.
software_delivery.gates.resource_guard.memory.floor_bytesnumber1 GiBAvailable memory always left for the operating system. Env LUMENFLOW_START_GUARD_FLOOR_BYTES overrides.
software_delivery.gates.resources.runners.<name>.valuenumber—Declared runner value, passed to the gate command unchanged.

The environment beats workspace.yaml, which beats the default. Each guard decision is recorded on the gate’s row in the gate-attempt ledger (start_guard, peak_rss_bytes); gate runs no longer write a gate-admission row.

Worker counts. A gate run exports LUMENFLOW_GATE_GRANTED_WORKERS to every gate subprocess: the largest value declared for a parallelism-controlling gates.resources.runners flag (a worker, thread, concurrency, parallel, jobs or pool flag), otherwise the host’s os.availableParallelism(). A tool configured by file rather than by flag can follow it. A test runner started inside a gate sees it and caps its workers to it.

What still serializes. The build-output writer lock (above) is held per build-producing gate, and the repository-wide merge lock still serializes landings. software_delivery.gates.execution_lock blocks still parse but bound nothing: test runs, builds and release cuts take no admission pool, so a hung holder of the legacy pool can never delay them. Each wrapper keeps a local bound taken from the host’s os.availableParallelism() (which honours the process’s CPU affinity), or from the gate’s exported worker budget inside a gate.

Run scope. A run’s scope is still classified from its resolved repository content and selected gates. Classification compares committed, staged, unstaged, and untracked paths with the integration base; WU code_paths describe expected coverage and do not stand in for the execution diff. Canonical lifecycle metadata and the repository-root .gitignore alone do not make a content-free WU code work. If the integration base cannot be resolved, scope resolution fails closed. A docs-only content plan runs the docs-only gates even when the caller did not explicitly narrow the gate list.

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

PropertyValue
Default skippabletrue (allow-listed for --skip-gate)
Lifecycle surfacewu:prep
ApplicabilityThe WU diff includes deleted .test.ts / .test.tsx files.
PreconditionEach deleted test’s describe() / describe.each() symbol must still be present in one of the WU’s code_paths (i.e. the test is being moved, not silently lost).
Output on failLists each unrecognised symbol, the deleted file, and a remediation hint.
Overridepnpm config:set software_delivery.gates.overrides.test-over-deletion.skippable=false to make the gate strict.
Skippnpm wu:prep --id <wu-id> --skip-gate test-over-deletion --reason "..." --fix-wu <other-wu-id>

Effective scope. The gate reads the WU’s code_paths from its effective spec: the record on disk plus any pending wu:edit changes not yet written by wu:done. It uses the same reader as the other lifecycle commands. If the WU is known but that read fails, the gate fails with TEST_OVER_DELETION_SCOPE_UNREADABLE instead of passing.

Deleted with its source. A test deleted together with source deleted in its mirrored directory (src/__tests__/<dir> beside src/<dir>, or a package __tests__/<dir> beside src/<dir>) counts as in scope.

Moved tests. A flagged describe() still passes if the test was moved within the same branch. The rule is strict:

  • The destination is a describe() with the same title (whitespace normalised) in a test file this branch added or changed.
  • That block must be new in this branch. If any test file the branch changed already had that title at the merge base, the move does not count.
  • Each deleted block needs its own new block. If the number of deleted and new blocks with that title differ, the gate fails.
  • Each new block has at least as many expect( calls of its own as the deleted block. Calls inside nested describe() blocks, comments and strings do not count.

Renames count as a delete plus an add, so they follow the same rule. The gate prints every move it accepts (old path → new path, title, assertion counts) and the reason for every move it rejects.

PropertyValue
Default skippabletrue (allow-listed for --skip-gate)
Lifecycle surfacewu:prep
ApplicabilityAlways (every wu:prep).
PreconditionA file in the current WU’s code_paths (read from that WU’s branch record at HEAD; origin/main is fallback only if the branch record is unreadable) appears in another claimed / in-progress WU’s code_paths (those still from the integration target) and that co-claimant’s reachable lane branch carries a non-empty diff on the file. A declared-but-untouched path is not contention. Unclaimed ready placeholders are ignored. An unreachable co-claimant branch still fails closed. A co-claimant whose YAML status at the integration target is terminal (done, cancelled) is excluded even when the ledger’s own status projection is stale (this YAML-status check is independent of the ledger’s own status projection).
Landing orderAmong the in-progress declarers of a contended file, the holder is the WU whose latest claim event comes first in the tracked wu-events.jsonl ledger at the integration target. That order is set by push, so it does not depend on any machine’s clock. The holder passes; every other declarer is a waiter. A later claim never takes the file from a holder that has already prepped, and a WU that is released and claimed again goes to the back. The existing clears still apply to a waiter (a co-claimant branch that never changes the file; a recorded overlap authorisation with disjoint hunks). Every declarer computes the same order and the earliest claim holds every file it shares, so two in-progress WUs that share a file are never both refused.
Output on failA waiter that stays refused is told the holder, the full landing order with claim times, and the next step: wait for the holder to land, then pnpm wu:rebase --id <your WU> and pnpm wu:prep --id <your WU> again (or pnpm wu:edit --id <your WU> --remove-code-paths <file> if the WU does not need the file). pnpm wu:edit --code-paths and pnpm wu:claim --force-overlap print the same holder and order when the contention is created; those checks never block.
Overridepnpm config:set software_delivery.gates.overrides.monolithic-file-contention.skippable=false to make the gate strict.
Skippnpm wu:prep --id <wu-id> --skip-gate monolithic-file-contention --reason "coordinated landing with co-claimant" --fix-wu <other-wu-id> (rarely needed now that a waiter has a landing order; keep the reason specific to the audited decision)

Advisory versus blocking. A hotspot on ordinary source never fails the gate: it prints monolithic-file-contention (advisory): <file> is also declared by <other-wu-id> (owner <assigned_to>) and the claim order. Only a hotspot on the unmergeable class (software_delivery.gates.unmergeable_paths, glob patterns, a non-empty list replaces the built-in lockfile, generated-output, ledger and baseline defaults) keeps the landing order and the failure below. The second WU to land resolves any conflict at pnpm wu:rebase or the wu:done auto-rebase, and wu:done re-runs the gates on the rebased tree when the rebase brings in changes to files the WU touches.

A failed contention result remains blocking. It is explicitly non-comparable against an isolated main snapshot because that worktree lacks the current WU context used to evaluate contention. The classifier keeps native branch output when captured and otherwise reports that the output was unavailable. It does not create or prepare that snapshot worktree, install dependencies, build, or infer that main passes. This removes incidental main install/build diagnosis from this advisory guard; ordinary build and gate policy remains in force. The existing explicitly authorized, audited coordination skip route through wu:prep and wu:done is unchanged.

When a claimed worktree WU narrows code_paths, wu:edit publishes a strict, non-empty subset to canonical main so overlap checks can stop reserving the removed paths. The publication verifies the active claim and caller, and runs the normal metadata transaction and push checks. Expansions, path remapping, stale claims, ownership mismatches, and failed pushes do not release paths; other pending WU edits remain local until wu:done.

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

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

Documentation currency is an acceptance criterion of every work unit, so wu:prep checks it and wu:done checks it again. The gate is invisible to compliant work and fails on silence: the WU diff changes product code, and none of the following holds. It also fails on decision-record problems (see below), which apply to every checkout.

  1. The diff touches a configured docs surface.
  2. The WU notes carry docs: n/a — <reason> with a non-empty reason.
  3. The WU type is documentation.

A diff with no product code never triggers it. Product code means non-test source under a configured product path; test files, lockfiles, generated output, prose (.md, .mdx), workspace state and the WU records themselves are not product code, and a WU-record edit never counts as a docs surface either.

Declaration. Put docs: n/a — <reason> in the notes when nothing user- or operator-visible changed. The label may be docs, docs parity, documentation or documentation parity in any case, and after n/a any separator works (a dash, :, ,, ;, a parenthesis, or none) as long as a real reason follows, so docs: n/a (test-only realignment) and docs: n/a, internal rename both pass. A reason that starts with unless or if, or a label inside quotes, is not a declaration. The reason must say something: an empty reason, the unfilled or quoted <reason> placeholder, or just n/a, tbd or todo is refused with a message that says so.

pnpm wu:edit --id <wu-id> --notes "docs: n/a — internal rename, no operator-visible change"

The gate reads the effective notes, including a wu:edit that is still pending until wu:done. wu:done re-reads them even when it reuses a wu:prep gate checkpoint, because a checkpoint binds the diff and not the notes.

Failure text. It lists the product files, the configured docs surfaces and the exact declaration syntax, so the fix is one of two edits:

docs-currency: this WU changes product code, but no documentation surface changed and the notes carry no docs declaration.
  Product files changed (1): src/feature.ts
  Do ONE of the following:
  1. Update documentation in this WU. Docs surfaces: docs/**, ...
  2. If nothing user- or operator-visible changed, declare it with a reason:
       pnpm wu:edit --id <wu-id> --notes "docs: n/a — <reason>"

Configuration. The layout is configuration. Set it with pnpm config:set, never by editing workspace.yaml; a non-empty list replaces the built-in default for that list.

pnpm config:set --key software_delivery.gates.docs_currency.product_paths --json-value '["src/**"]'
pnpm config:set --key software_delivery.gates.docs_currency.docs_surfaces --json-value '["handbook/**","docs/**"]'

The examples use POSIX quoting; pnpm config:set --help prints the JSON form for your own shell. Built-in defaults apply until you set a list.

Where it runs. The gate reads only the WU diff and notes on your checkout. It never builds or compares a main snapshot, so a repository with no build script gets the plain documentation refusal rather than a snapshot error.

Decision records. The same gate checks the decision-record files in decisions_dir. Every <adr-id>-<title>.md file needs one row in decisions_index (default README.md inside decisions_dir), and no number may be used twice. A missing index with records present also fails. A WU that lists a decision record in its implements_adrs (for example implements_adrs: ['<adr-id>']) fails at wu:prep and wu:done while that ADR’s status line reads Proposed with no reason after it. A reason on the status line passes, for example Proposed (blocked on <reason>); so does any final status such as Accepted. The index rows stay hand-maintained, and the gate reports drift. Set the keys with pnpm config:set:

pnpm config:set --key software_delivery.gates.docs_currency.decisions_dir --json-value '"adr"'
pnpm config:set --key software_delivery.gates.docs_currency.decisions_index --json-value '"adr/INDEX.md"'

Skippability. The gate is blocking and not skippable by default, because the declaration is the compliant route. It is not in the immutable safety-critical set: it is a policy gate. A WU with no docs impact passes by declaring docs: n/a — <reason>; an explicit --skip-gate docs-currency is refused at wu:prep because the failure is introduced by the branch, and wu:done ignores a done-time skip. A declared or documentation WU needs no diff at all. Otherwise, if the diff cannot be read (for example no fetched integration branch or a shallow clone), the gate fails with a message saying so rather than passing. Cost. One diff read and one record read, once at wu:prep and once at wu:done.

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

A WU whose changed files and declared tests.unit do not connect to a paired parity test in a different package can land without that test ever running. This mechanism closes that gap.

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

Repos may register their own additional always-on suites, but cannot remove or empty the framework defaults — the same deny-set posture the immutable gate names have:

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

Scoped test execution and full-suite fallbacks

Section titled “Scoped test execution and full-suite fallbacks”

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

wu:prep resolves tests.unit from the WU’s effective spec — the on-disk WU YAML with its pending wu:edit/wu:rebase/wu:recover entries replayed over it, if any — the same effective document wu:done already scopes from. Each pending entry adds only the fields it declared and its source may change (wu:edit spec fields; the wu:rebase baseline; the wu:recover session); lifecycle fields apply only from an entry of the current claim generation, except that a legacy entry or record with no claim generation keeps the earlier behaviour. The claim generation, pinned target and provenance always come from the record. A wu:edit --test-paths-unit on a claimed worktree only appends a pending ledger snapshot; it never rewrites the WU YAML until wu:done materializes it.

A declared tests.unit scope also runs every release test when the branch diff or the WU’s code_paths touch a release module or its test folder: each release*.test.ts, and each test that imports a release module (for example licence-check.test.ts). wu:prep and wu:done join them in the same place, so both run the same tests. A WU with no declared tests.unit, or one run with --full-tests, is unchanged.

When a branch changes a managed template source or its committed template, prep and done also run a targeted template-parity check. It uses the sync manifest and conversion, checks only the changed pairs, and reports stale, missing or orphaned templates with repair guidance. Both halves of each selected pair are gate inputs, so a counterpart changed on the integration branch forces revalidation. This applies to code and documentation plans, including an empty tests.unit; it does not change the normal full-suite fallback. Unrelated documentation does not select the check.

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

Fallback triggerLog line
Unscopable tests.unit paths, or a preset without direct path-scoped execution (e.g. dotnet)Running the FULL test suite (slow): <detail>
Untracked code files present in the worktreeUntracked code files detected
A test-config change (package manifest key that can affect tests, lockfile, tsconfig, *.config.*)Test config changes detected (<file or key>) - running full test suite
The changed-file list is unavailableChanged file list unavailable - running full test suite
No test_incremental configured (or test_incremental equals test_full)No incremental test command configured, running full suite
--full-tests passed explicitlyFull tests requested - running full test suite
Running on the main branchOn main branch - running full test suite

A package.json change forces a full run only when its keys can affect tests

Section titled “A package.json change forces a full run only when its keys can affect tests”

The gate compares each changed package.json at the diff base (the merge base, read through git) with the candidate. It forces the full suite only when a changed key can affect tests: a dependency block (dependencies, devDependencies, peerDependencies, optionalDependencies), overrides, resolutions, pnpm, workspaces, type, exports, imports, main, module, engines, packageManager, or a test-related script. A script is test-related when it is named test, test:*, pretest or posttest, or when a configured gate test command (test_full, test_incremental, test_docs_only) invokes it by name, directly or through another script. The log line names the deciding key, for example Test config changes detected (package.json: scripts.test:unit) - running full test suite. A change limited to other scripts or to metadata (description, keywords, repository, homepage, bugs, author, contributors, license, funding) stays scoped. Anything unparseable, a newly added or deleted manifest, or an unrecognised top-level key forces the full run. Lockfiles and the other config files are unchanged.

Documentation-only work units run no unit suite

Section titled “Documentation-only work units run no unit suite”

A docs-only run whose code_paths are all documentation paths (the same classifiers that decide docs-only mode, including a docs directory holding only documentation) and whose effective tests.unit is empty never falls back to the full suite. The docs gates still run, and the log says docs-only mode: every code_path is documentation and no tests.unit is declared; running no unit test suite (docs gates still run). A docs-only WU that declares tests.unit runs exactly those tests, and --full-tests still forces the full suite. A WU with any non-documentation code_path keeps the standard test plan. wu:prep and wu:done both take tests.unit from the effective pending spec, so a governed wu:edit --test-paths-unit on a claimed WU takes effect at the next prep without a commit, and wu:done runs the same scope.

  • Declare tests.unit on every WU — an empty or missing declaration is the most common cause of an unscoped run.
  • Set software_delivery.gates.commands.test_incremental to a genuinely scoped command (one that accepts a changed-file list and runs only matching tests), not an alias for the full suite.
  • Keep the worktree free of untracked code files before wu:prep — an untracked source file cannot be safely excluded from the full run because its coverage is otherwise unverifiable.

Deferred-only declarations never silently widen

Section titled “Deferred-only declarations never silently widen”

A WU may declare a real-process timing suite (for example gates-parallel-execution.test.ts) directly in tests.unit. Those suites cost 300–500+ seconds each and belong to their own landing-time harness gate, never to a per-change scoped run — the test gate filters them out of the scoped plan before building any command.

When every declared tests.unit path is one of these deferred suites, the gate resolves a deferred-only plan: a bounded, evidenced outcome, not the generic “cannot scope this WU’s paths” case above. By default it reports the deferral — naming each deferred path and the landing-time harness gate that covers it — and passes without running test_incremental or test_full. Earlier releases fell through to the same table above and silently ran the full test_incremental command.

Widening beyond the declared tests.unit on a deferred-only WU is opt-in, schema-validated workspace policy: software_delivery.gates.widen_on_deferred_only_tests (boolean, default false). Set it to true to restore the earlier fallback behaviour; the gate always prints which value it applied and why. Every gate_attempt ledger event for the test gate now records plan_mode (for example deferred-only) and plan_reason (the printed evidence), schema-additive fields absent on every gate and on earlier records.

The scoped test gate runs only the effective tests.unit, so a changed test that is not declared never runs in wu:prep, and a changed source file that is not declared is invisible to co-change and to the audit record. wu:prep therefore refuses, in its reality preflight (rule R009), when the branch diff against the merge base contains a source or test file that is covered by neither the effective code_paths nor the effective tests.unit. “Effective” is the same record the test gate reads: the claim’s pending wu:edit overlay on the WU record, so a file declared by a pending edit counts at once.

What counts:

  • A declared directory in code_paths (a trailing slash) or a glob covers every file beneath it.
  • Exempt: lifecycle-managed files (the .lumenflow state ledgers and stamps, the WU and initiative records, generated backlog.md and status.md), lockfiles and dist output.
  • Advisory only: documentation, changesets and template files. The docs-currency gate already governs documentation, so requiring every docs file in code_paths would be noise. They are still listed as a warning.
  • Deleted files: a declared code_paths entry, or an exact declared automated test file, that the branch deletes in its committed diff (present at the merge base, absent at the tip) is declared, so the deletion needs no extra edit, and the existence checks do not ask for the file. A glob or directory test declaration is never exempt by deletion, so declare the exact test files. A declared path that never existed, or that is absent without a committed deletion, still fails the existence checks, and a deleted file that is not declared still refuses.

The refusal lists each file and prints the exact command that declares it. Source files go to --code-paths and test files go to --test-paths-unit, so the test runs in prep:

pnpm wu:edit --id <wu-id> --code-paths src/extra.ts --test-paths-unit src/__tests__/extra.test.ts

This is a preflight refusal, not a skippable gate: it has no gate id, so --skip-gate does not apply and no configuration can relax it. wu:done and the standalone wu:preflight keep the warning.

One rule keeps a test-gate run from paying for the same work twice:

  • The related cross-check excludes declared tests. When the release plan cross-checks changed files the declaration does not cover (see “Everyday vs release typecheck scope” above for the everyday/release split), it never selects a test file already in the declared tests.unit set, neither a declared file nor a file inside a declared directory. The exclusion is applied before the selection bound, so a declared file never uses up one of its slots. When every covering test was already declared, the gate prints that they already ran and does not report a zero-match finding. The vitest related fallback receives the same set as --exclude flags. Real-git suites stay in the cross-check: they are coverage.

A declared set that spans several packages runs as one group per package, one group after another. A failure in any group still fails the gate and names that group’s paths.

Test-gate evidence capture-integrity classification

Section titled “Test-gate evidence capture-integrity classification”

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

CodeMeaning
output-unavailableThe gate process’s original output was not captured at all.
empty-outputThe gate process produced no output.
truncated-outputCaptured output exceeded the evidence capture bound.
capture-integrity-failureThe gate exited non-zero, but the captured output contains zero FAIL <file> > <test> markers anywhere (see below).
malformed-outputOutput contains failure markers, but none of them parse into a clean file/test identity.
partial-outputOutput contains a mix of reconciled and unreconciled signals (fatal channels, mismatched Vitest/Turbo summaries, unscoped failures).
foreign-output-noiseEvery parsed FAIL identity is proven outside every known file or directory selection for the executed test plan (for example, an isolated nested self-test fixture). A plausible match to a selected scope uses the general evidence pipeline; noise classification never masks a selected failure.

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

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

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

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

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

partial-output reconciliation under concurrent-runner contention

Section titled “partial-output reconciliation under concurrent-runner contention”

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

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

foreign-output-noise: nested self-test fixtures are not this run’s failures

Section titled “foreign-output-noise: nested self-test fixtures are not this run’s failures”

A scoped tests.unit selection can include a self-test that starts an inner Vitest process against an intentionally failing fixture. The inner process must use its own reporter and JSON output path, and its live stdout/stderr must not be teed into the outer gate capture.

For known direct, partitioned, or file-explicit related plans, the gate can preserve executedTestFileSelections: one group per known executed plan, pairing that runner’s checkout-relative working directory (empty string for checkout root) with its selected file or directory scope paths. For file-explicit related plans, scopedTestPaths is changed-source attribution; actual argv supplies the executed selection. Any unknown subplan in a combined execution omits known-selection metadata for the whole cohort. A directory scope does not prove every file beneath it ran, and cwd does not always identify which runner emitted an unlabeled FAIL. The legacy flattened executedTestFilePaths field remains for compatibility but has no runner context.

The fast path classifies foreign noise only when every parsed FAIL identity is proven outside every known selection. If 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. If a failure plausibly matches a selected scope, it also uses that pipeline, so a real selected failure is not masked. Ambiguous, unresolved, and out-of-checkout paths use the general pipeline. Full, dynamic, fallback, unknown, and zero-match executions omit known-selection metadata. Runner labels and general failure identity normalization remain unchanged; raw WU tests.unit is not execution evidence.

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

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

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

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

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

Experimental, not yet active. No engine reads this section yet. The schema ships ahead of the embedded gate runner so the contract can be reviewed. Because the section is strict, an invalid key under software_delivery.gates.embedded fails the configuration parse for the whole workspace, so leave the section out unless you are testing the schema. A workspace without it is unaffected.

software_delivery.gates.embedded is the typed policy section for running gates against a working copy that has no claim and no remote and is written by an untrusted workload. When the embedded runner ships, policy will be read only from a trusted commit, never from the working copy. Every field defaults to the fail-closed value.

FieldMeaningDefault
package_managerThe only package manager embedded runs usenone; absence refuses an embedded run
reusable_gatesGates whose result depends only on hashed inputsempty: nothing is reusable
required_gatesGates that may never report not-applicableempty
env_allowlistEnvironment variable names passed to commands and hashedempty
toolchain_probesVersion commands (id, argv) run once per runempty
base_aware_commandsCommands that consume the base themselvesempty: no command is base-aware
selection_outputsWhere a runner reports its selection count (gate_id, path)empty
gate_domainsPath patterns each gate observes, keyed by gate idempty: every path
docs_only_patternsPaths that may select the docs-only planempty: docs-only is off
tool_cache_pathsPaths removed from the sandbox view.eslintcache, *.tsbuildinfo, node_modules/.cache
declared_outputsPer-gate output files the engine may read, keyed by gate idempty
policy_filesExtra policy files written over the engine-private viewempty

Rules enforced when the configuration is parsed:

  • Unknown keys, and gate ids outside the embedded gate allowlist (invariants, format:check, lint, typecheck, test, co-change, conditional_commands, build, integration-test, e2e-smoke, coverage), are refused.
  • Path and pattern values use a character allow-list: A-Z a-z 0-9 . _ @ + - /, plus * and ? in pattern fields (tool_cache_paths, docs_only_patterns, gate_domains). ** is a whole segment of stars. Brace syntax ({a,b}, ranges), character classes, extended globs, backslashes, colons, spaces, controls and non-ASCII characters are not supported and are refused. Write a brace list as separate entries (**/*.ts and **/*.tsx, not **/*.{ts,tsx}). To match a literal path that contains (, ), [, ], a space or a non-ASCII character, match that character with ? or * (for example app/?marketing?/** or src/routes/?slug?/+page.svelte).
  • Every path is repository-relative. A segment may not be empty, .. or start with .., end in a dot or a space, be a Windows device name (CON, NUL, COM1 and so on, with any extension), name git metadata under any platform alias, or be a glob that could match .. (so .* is refused). A path may not start with /.
  • policy_files, declared_outputs and selection_outputs[].path must be literal paths with no * or ?.
  • gate_domains and declared_outputs lists must not be empty, and every list refuses duplicates (paths are compared after normalisation and without case, and env_allowlist names without case).
  • A selection_outputs path must also be listed in that gate’s declared_outputs.
  • reusable_gates needs at least one toolchain_probes entry.
  • A tool_cache_paths list you set is merged with the defaults and never replaces them.

Set a field with pnpm config:set --key software_delivery.gates.embedded.package_manager --value pnpm, or a structured field with --json-value.

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

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