Skip to content

Lanes

Lanes are parallel work streams that organize work by domain and prevent overload through WIP (Work-in-Progress) limits. At the kernel level, lanes also define scope boundaries — the scope intersection algorithm uses lane code_paths as one of four permission levels that determine what an agent can access.

Without lanes:

  • Everything competes for attention
  • Context switching kills productivity
  • Bottlenecks form unpredictably

With lanes:

  • Work is organized by domain
  • Each lane has focused attention
  • WIP limits prevent overload

Lane design is an explicit lifecycle process:

unconfigured -> draft -> locked

lumenflow init bootstraps tooling and sets lane lifecycle to unconfigured. Before creating delivery WUs, run:

pnpm lane:setup
pnpm lane:validate
pnpm lane:lock

Check current state any time:

pnpm lane:status

Lanes use a Parent: Sublane naming convention. Create and edit them through the lifecycle commands so kernel scopes and delivery code paths remain synchronized:

pnpm lane:create \
  --name "Framework: Core Lifecycle" \
  --wip-limit 2 \
  --add-path "packages/@lumenflow/kernel/**" \
  --add-path "packages/@lumenflow/runtime/**"

pnpm lane:edit \
  --name "Framework: Core Lifecycle" \
  --add-path "packages/@lumenflow/packs/**"

lane:edit writes both places a lane is described — the workspace lane scopes and the delivery code paths — under one validation and one commit. If either rejects the edit, neither is changed.

ParentPurpose
FrameworkCore libraries, CLI, memory, agents
OperationsInfrastructure, CI/CD, tooling
ContentDocumentation, guides
ExperienceUI components, pages

WIP (Work-in-Progress) limits control how many active WUs a lane can have simultaneously. The default is WIP=1, but lanes can be configured with higher limits when work is safely parallelizable.

  • WIP=1 (default): One active WU per lane. Prevents context switching and merge conflicts.
  • WIP=2+: Multiple active WUs allowed. Use when code paths are disjoint and non-overlapping. Requires a wip_justification explaining why parallelization is safe.

A raise above WIP=1 is refused unless the lane already carries a justification or the same command records one:

pnpm lane:edit \
  --name "Framework: Core Lifecycle" \
  --wip-limit 2 \
  --wip-justification "kernel and runtime work targets disjoint packages"

Parallel work across lanes: Each lane operates independently. Even with WIP=1 per lane, you achieve concurrency by working in multiple lanes simultaneously.

The lock_policy field controls how blocked WUs affect lane availability:

software_delivery:
  lanes:
    definitions:
      - name: 'Content: Documentation'
        wip_limit: 1
        lock_policy: active
        code_paths:
          - 'docs/**'
PolicyBehavior
allDefault. in_progress and blocked WUs count toward WIP and retain the lane lock.
activeOnly in_progress WUs count toward WIP. Blocking a WU releases its lane lock and WIP occupancy.
noneLane-lock acquisition and WIP occupancy checks are disabled.

Atomic lane locking, WIP occupancy, and code-path overlap queues are separate controls. A lane-lock refusal names the lock owner and current lock_policy; it is not evidence that the WIP preflight made the same decision.

  • --force affects only the WIP preflight. It cannot overwrite an existing atomic lane lock, so rerunning the same claim with --force does not resolve a lock refusal.
  • --wait-for-lane queues WUs behind detected code-path overlap blockers. It runs before lock acquisition and does not wait for an already-held lane lock.
  • Under the default all policy, blocking the owner does not free the lane. Under active, a legitimate lifecycle block releases both the lock and WIP occupancy. Under none, no lock should be acquired.

Inspect the owner with pnpm wu:status --id WU-XXX. If the owner is genuinely abandoned, inspect the lock with pnpm wu:unlock-lane --lane "<lane>" --status, then follow the command it prints. The audited recovery surface requires a reason:

pnpm wu:unlock-lane --lane "<lane>" --reason "Confirmed abandoned owner WU-XXX"
  • all (default): High-conflict lanes where blocked work may resume soon
  • active: Low-conflict lanes like documentation where blocked work is unlikely to cause merge conflicts
  • none: Experimental. Use only for lanes with guaranteed non-overlapping work

Start with lock_policy: active on low-conflict lanes like Content: Documentation:

  1. Documentation WUs are low-conflict (different files, few dependencies)
  2. Blocked docs WUs rarely resume immediately
  3. Easy rollback: change active back to all in config

If lock_policy: active causes issues:

  1. Review the current definition with pnpm lane:list --json and update the governed lane policy through the workspace configuration process.
  2. Inspect pnpm wu:unlock-lane --lane "<lane>" --status.
  3. For a genuinely stale lock, run pnpm wu:unlock-lane --lane "<lane>" --reason "Lock policy rollback cleanup".

Lane locks are file-based locks (.lumenflow/locks/<lane-kebab>.lock) that enforce WIP limits. For WIP=1 lanes, a single lock file is used. For WIP=2+ lanes with lock_policy: none, lock checking is bypassed entirely. The lock lifecycle has three key phases:

When wu:claim runs, it creates a lock file atomically using the wx (write-exclusive) flag. The lock file contains metadata including the WU ID, timestamp, agent session, and the PID of the claiming process.

Because wu:claim is a short-lived CLI process, the PID stored in the lock becomes invalid as soon as the claim completes. This is expected behavior — the lock persists on disk regardless of whether the original process is still running. The lock remains valid until explicitly released by wu:done or forcibly cleared.

Locks are released in three ways:

  1. Normal release: wu:done removes the lock after merging
  2. Stale zombie auto-clear: If a lock is both stale (older than 2 hours) AND the PID is no longer running, subsequent wu:claim calls auto-clear it. This handles genuinely abandoned locks from crashed processes.
  3. Manual unlock: inspect pnpm wu:unlock-lane --lane "<lane>" --status and the owner with pnpm wu:status --id WU-XXX before using the audited unlock. A recent dead claim PID is expected and is not sufficient to remove the lock without --force.

A “zombie lock” is one where the PID that created it is no longer running. However, a dead PID alone does NOT trigger auto-clearing. Since wu:claim exits immediately after creating the worktree, every lock will have a dead PID shortly after creation.

Auto-clearing requires BOTH conditions:

  • The lock PID is no longer running (zombie)
  • The lock is older than 2 hours (stale)

This prevents a second wu:claim from stealing a lane that was legitimately claimed by a recently exited wu:claim process.

High WIP (anti-pattern):
→ Multiple things started, none finished
→ Context switching overhead
→ Long cycle times
→ Merge conflicts

WIP=1 (LumenFlow default):
→ Finish one thing
→ Start the next
→ Steady flow
→ Clean merges

Lane code_paths serve a dual purpose: they organize work AND enforce security boundaries through scope intersection.

When a tool call executes, the kernel computes:

effective_scope = workspace ∩ lane ∩ task ∩ tool

The lane’s code_paths act as the second narrowing layer. An agent working in Services: API (paths: services/api/**) cannot write to apps/web/** — the intersection is empty, and the kernel denies the call.

When claiming a WU, specify the full Parent: Sublane format:

pnpm wu:claim --id WU-042 --lane "Experience: UI"

The WU spec should already have a lane assignment:

# WU-042.yaml
id: WU-042
title: Add email validation
lane: 'Experience: UI' # ← Full Parent: Sublane format

LumenFlow suggests lanes based on the authoritative lane definitions in workspace.yaml:

software_delivery:
  lanes:
    definitions:
      - name: 'Services: API'
        code_paths:
          - 'services/api/**'

If a WU’s code_paths match configured lane code_paths, the lane is suggested automatically by wu:infer-lane.

A WU should belong to one lane. If work spans multiple domains:

  1. Split the WU – One WU per lane
  2. Choose primary – Assign to the most affected lane

Instead of one WU: ❌ “Add user preferences (UI + API + DB)”

Split into: ✅ WU-100: “Add preferences API” (lane: Framework: Core) ✅ WU-101: “Add preferences UI” (lane: Experience: UI) ✅ WU-102: “Add preferences schema” (lane: Operations: Infrastructure)

The status file shows lane state:

# Status

## 🔧 In Progress

- [WU-101 — Add preferences UI](wu/WU-101.yaml) (lane: Experience: UI)
- [WU-100 — Add preferences API](wu/WU-100.yaml) (lane: Framework: Core)

## 🚀 Ready

- [WU-103 — Add dark mode toggle](wu/WU-103.yaml) (lane: Experience: UI) ← blocked (lane occupied)
- [WU-104 — Add export feature](wu/WU-104.yaml) (lane: Operations: Infrastructure) ← can claim

## ⛔ Blocked

(none)

Note: WU-103 cannot be claimed because Experience: UI lane already has WU-101 in progress.