Skip to content

WU Prep Workflow

Completing a Work Unit is a two-step process. This guide explains the workflow.

The previous single-step wu:done ran gates on main, which could cause issues with worktree isolation. The new workflow:

  1. wu:prep runs gates in the worktree (where your changes are)
  2. wu:done does merge and cleanup from main

This ensures gates validate your actual changes, not stale main code.

  1. Work in your worktree

    Complete your implementation, write tests, commit changes.

  2. Run wu:prep from worktree

    pnpm wu:prep --id <wu-id>

    This command:

    • Validates you’re in a worktree
    • Runs quality gates
    • Prints a copy-paste instruction for the next step
  3. Run wu:done in main (copy-paste)

    Copy the command printed by wu:prep. pnpm -C runs wu:done in the main checkout without moving your shell, and the line is written for your shell:

    pnpm -C /path/to/main wu:done --id <wu-id>

    On Windows the path is printed with backslashes and quoted so the same line runs in PowerShell and cmd.exe, for example pnpm -C "C:\Users\First Last\repo" wu:done --id <wu-id>.

    This command:

    • Validates you’re in main checkout
    • Fast-forward merges your changes
    • Creates completion stamp
    • Runs configured external work-item close-out/readback hooks after successful local completion
    • Removes the worktree

If a commit does invalidate the checkpoint, wu:done prints both the prepared commit and the current commit before rerunning gates. The fallback retains the WU identity and replays named gate skips recorded at prep. After any intentional fix or notes edit, the normal recovery remains: commit, rerun wu:prep, then run the printed wu:done command without another commit.

wu:done skips gates when the checkpoint wu:prep wrote is still valid. The key is the normalized diff hash of your branch against its merge-base with origin/main, within the checkpoint TTL (gates.checkpoint_reuse_ttl_minutes, default 1440). Main moving is not part of the key: another WU landing, a metadata-only commit, or a rebase onto unrelated changes still reuses the checkpoint.

wu:done refuses reuse, and prints the overlapping paths, only when files that changed on main since prep overlap a file your branch touches or a declared gate input (tests.*, scoped test paths, pnpm-lock.yaml, workspace.yaml). A wu:done retry after a failed landing on an identical diff reuses the passing evidence of its own earlier gate run. Every decision (reused, refused with a reason code, or none when no checkpoint existed) is recorded in the local flow log so the reuse rate is measurable.

When another WU lands a change to a file yours touches while wu:done is running its gates, wu:done rebases onto the new target and re-runs the gates on the rebased tree before it writes the done commit (up to three rounds). Its own metadata commit, the ledger, and changes to files your WU does not touch never trigger a re-run. --no-auto-rebase opts out.

Refusal or taskSanctioned recovery
Generated manifest or metadata is dirtyRun the repository formatter, review the generated diff, and commit it before prep.
--allow-todo has no notes justificationRun pnpm wu:edit --id <wu-id> --notes "allow-todo: <justification>"; the edit creates a commit, so rerun prep afterward.
Several external PBIs/work items existCover every declared item in acceptance criteria. Leave configured provider close-out and readback to successful wu:done.
Contract completeness failsUpdate the complete named surface: schema, manifest or registry, implementation, tests, and affected docs.
The worktree or main checkout is dirtyInspect status, commit only intended worktree changes, preserve unrelated user changes, and follow the governed recovery printed by the command.
An attestation or artifact is staleRerun its documented producer against the current commit; do not hand-edit its JSON or skip an unrelated producing gate.
Prep succeedsTreat prep and pnpm -C <main> wu:done --id <wu-id> as one uninterrupted landing operation.

Prepares a WU for completion by running gates in the worktree.

pnpm wu:prep --id <wu-id> [--docs-only] [--full-tests]
FlagDescription
--idWU ID (required)
--docs-onlyRun docs-only gates (format, spec-linter)
--full-testsDisable tests.unit scoping and run the default incremental/full test gate behavior

When the active preset supports path-scoped execution, wu:prep uses the current WU’s tests.unit entries to scope the test gate.

Prep-evidence reuse is default-on. Opt out with:

local_prep:
  reuse_evidence: false

Only an identical eligible low- or medium-risk plan can reuse passed evidence. Missing, malformed, stale, cross-workspace, or mismatched evidence runs gates fresh. --full-tests and high-risk prep run gates fresh; --full-tests also forces full test execution. report_all (default true) does not exclude reuse — it is already one of the hashed execution-plan inputs. Snapshot isolation remains OFF: reuse does not pin or execute an immutable workspace snapshot.

For presets that do not support path-scoped execution, such as dotnet, wu:prep falls back to the configured default test gate flow instead of trying a JavaScript-specific scoped runner.

Sensitive-data evidence is committed by wu:prep

Section titled “Sensitive-data evidence is committed by wu:prep”

When a WU’s code_paths include a sensitive-data candidate (a path segment such as auth, credentials, pii or user-data), wu:prep evaluates the branch diff and records the decision on the WU record as escalation_evidence. It commits that change as chore(<wu>): record wu:prep sensitive-data evidence before the gates run. The worktree is clean when prep returns, so wu:done needs no manual commit in between.

  • escalation_triggers is set at wu:claim, so a sensitive path added later (a wu:edit that widens code_paths, which a claimed WU records as a pending edit until wu:done) is seen only here. wu:prep evaluates the effective code_paths and, when a decision escalates, adds sensitive_data to escalation_triggers in the same commit, so wu:escalate and wu:status show it. wu:done also fails closed on any recorded escalating decision, so the landing needs wu:escalate --resolve even if the trigger were missing. A WU whose every decision is diff evidence with no escalation stays cleared. wu:edit on a ready WU adds the trigger at edit time.
  • A re-run over identical evidence changes nothing and makes no commit. A WU with no sensitive-data candidate records nothing and commits nothing.
  • The commit holds the WU record and the lifecycle ledger only. If the record has any other uncommitted change, wu:prep stops before the gates and names it: commit or revert that change, then rerun.
  • A handoff receipt presented to wu:prep is bound to the committed branch diff, which this commit changes. wu:prep re-checks the receipt right after the commit and stops before the gates if it no longer matches. Run wu:brief again from the claim-holder session, then rerun wu:prep; the rerun commits nothing. A claim-holder or same-user run uses no receipt.
  • Human resolution still applies. A sensitive-data claim sets requires_human_escalation, and that flag is always respected, so resolve it as described under Escalation Resolution before wu:done.

wu:prep verifies that every declared code_paths entry covers at least one changed file. Replace a complete scope with explicit repeatable values:

pnpm wu:edit --id <wu-id> --replace-code-paths \
  --code-paths "src/app/[orgId]/admin/**" \
  --code-paths "src/shared/**"

Bracketed framework route segments such as [orgId] are matched literally when that segment exists in the changed path. Ordinary glob character classes keep their normal meaning, so [ab] still matches either a or b when no literal [ab] segment is present.

The same matcher is authoritative during wu:claim overlap checks and wu:done completion validation. Claim also completes all blocking overlap, mode, and branch checks before it records an ownership advisory or persists claim state, a session, a worktree, or a branch. A rejected preflight therefore does not leave a claim or ownership-advisory event behind; a validated claim records its applicable ownership advisory exactly once, inside the same commit as the claim itself.

From wu:claim to your first edit, the shared origin main receives exactly one commit and one push. The claim commit carries the WU record, the generated backlog and status views, the claim event, and (for an initiative WU that was not delegated to you) the ownership-advisory audit event. wu:brief records its evidence on your lane branch, not on main, so it adds nothing to main. If another checkout pushed to main first, the claim is regenerated from the new tip and pushed again (up to eight attempts with jittered back-off) rather than rebased, because two claims rewrite the same generated backlog lines. Claiming different WUs from two machines therefore both succeed, and claiming the same WU yields exactly one winner.

When software_delivery.gates.delivery_review.enabled: true, wu:prep can also run the native delivery_review gate. Auto-run is controlled globally by software_delivery.gates.delivery_review.auto_run.

Must be run from worktree - errors if run from main.

delivery_review is a Software Delivery pack feature, not a vendor overlay. Enforcement is registered from the global gate config and behaves the same for Claude, Codex, Cursor, Windsurf, Cline, Aider, custom agents, and hosted cloud workers.

wu:prep auto-runs delivery_review only when:

  • software_delivery.gates.delivery_review.enabled is true
  • software_delivery.gates.delivery_review.auto_run is true
  • the WU type does not match software_delivery.gates.delivery_review.skip_types

Direct gate execution with pnpm gates uses the same global config, but does not require auto_run: true.

wu:prep records the ids of the gates it executed, and a digest of the gate configuration that selected them, in its checkpoint. wu:done reuses the checkpoint only when every gate it requires was executed and the digest is unchanged (refusal code gate-set-mismatch). With auto_run: false, prep does not run delivery_review, so wu:done runs it instead of skipping it; with auto_run: true the checkpoint is reused as before.

When a verifier_command is configured, software_delivery.gates.delivery_review.verifier_command_mode (default always) decides whether wu:prep runs it unconditionally or only when native evidence is insufficient (on-insufficient-evidence) — see the Gates Reference for the full semantics.

Legacy client-scoped software_delivery.agents.clients.<client>.features.delivery_review blocks are accepted for one release and emit a migration warning. They may still be used for adapter UX or prompt hints, but they are not the core enforcement switch.

The review writes a stable artifact at delivery-review/<wu-id>.json in the workspace artifacts with a portable verdict contract:

  • PASS passes this evidence gate; it does not certify semantic correctness
  • PARTIAL warns by default, and blocks when delivery_review.block_partial is true
  • FAIL blocks wu:prep

The result is designed for any host or product to consume later. It does not require a hosted control plane, does not assume a specific agent vendor, and does not rename or overload wu:verify. See the Gates Reference for the catalogue entry, skippability default, and artifact contract.

Completes a WU by merging and cleaning up.

pnpm wu:done --id <wu-id>
FlagDescription
--idWU ID (required)
--skip-gate <name>Skip one explicitly named skippable gate (repeatable; requires —reason and —fix-wu)

Must be run from main - errors if run from worktree.

When rebasing combines append-only state conflicts with a conflict in the active WU spec, wu:done applies the append-only resolver, then invokes the governed metadata resolver for any remaining paths before checking the index. It follows the same order after each git rebase --continue. The metadata resolver is limited to governed files, including the active WU spec; source conflicts, another WU’s spec, malformed metadata, and unresolved conflict artifacts still refuse completion.

If a completion attempt stops after its worktree recorded done, keep that worktree and its evidence, then rerun the ordinary pnpm wu:done --id <wu-id> recovery route. Recovery accepts only the same claim generation with an in_progress canonical WU and event ledger paired with a done worktree WU and event ledger. The authenticated session, owner, branch, registered worktree, and path must still match. Inconsistent document and event pairs, generic binding calls, and terminal readback remain strict.

During recovery, the materialized worktree WU YAML supplies lifecycle status. A pending specification snapshot still supplies effective spec edits such as title and code_paths, but its status cannot manufacture a recovery state or replace a materialized done status. The reader leaves the pending snapshot and ledger unchanged, and the existing claim attestation and document/event pair checks still decide whether recovery is allowed.

For an unpublished local completion that wu:done identifies as a zombie, the configured main-sync check runs before the recovery squash. The retry status is prepared in memory and persisted only by the normal metadata transaction with its matching event projection. If sync or pre-commit work fails, the preserved document and ledger remain paired; after a successful squash, retry rollback uses a snapshot taken from the post-squash pair. Ordinary completion attempts keep their original pre-write rollback snapshot.

If a later brief or checkpoint commit follows the unpublished completion, wu:done proves that exact completion and replays each descendant commit onto its active parent in an isolated candidate. It preserves later source and event changes, removing only the completion event bound to the old tuple. A remote completion, ambiguous history, replay conflict, dirty worktree, or changed lane ref stops recovery without moving the caller’s branch. After replay, the command rereads the complete active WU document and replays its pending spec entries over it (each entry adds only its declared, source-allowed fields) before creating a fresh completion commit. The strict done validator remains in force.

wu:done keeps the shared merge lock for the landing only (rebase, fast-forward merge, push and the completion stamp); everything else runs before it or after it is released:

  • Cleanup never waits for the lock. Automatic state cleanup (cleanup.trigger: on_done) cleans ignored signals and memory in place and only touches the lock when tracked events need archiving, then tries it once without waiting. A held lock prints cleanup=skipped reason=merge-lock-held and the next landing does the work.
  • No worktree rebuild in the landing. The metadata commit’s pre-commit hook uses the main checkout’s CLI build, so a WU that changes the CLI no longer rebuilds its own worktree dist inside the lock.
  • Post-land sweep. After the lock is released, wu:done starts pnpm main:sweep --post-land detached and does not wait for it or for its result; the sweep reports through its own signals.
  • Archived done logs stay readable. .logs/wu-done-archive/<WU>/gates-latest.log always points at the archived log next to it.

External systems are side effects, so LumenFlow treats them as lifecycle hooks instead of worker instructions. A worker may see external_work_items in wu:brief, but those items are context only. The safe sequence is:

  1. wu:claim
  2. wu:brief
  3. implementation
  4. verification / wu:prep
  5. local wu:done
  6. external close-out adapter
  7. readback adapter
  8. audit trail

If wu:prep fails, wu:done fails, or branch-pr mode only creates a PR, close-out does not run. When it does run, audit events are appended to the external-closeout.ndjson telemetry log.

Configure providers through config:set:

pnpm config:set --key software_delivery.external_work_items.enabled --value true
pnpm config:set --key software_delivery.external_work_items.providers.hub --json-value '{
  "closeout": {
    "kind": "command",
    "command": "pbis",
    "args": ["done", "{{item_id}}", "--status", "{{desired_status}}"]
  },
  "readback": {
    "kind": "command",
    "command": "pbis",
    "args": ["show", "{{item_id}}", "--json"],
    "status_json_path": "state.status"
  },
  "review": {
    "verdict_statuses": ["Pass", "Fail"],
    "worker_may_set_verdict": false
  },
  "rollback_guidance": "Reopen the item or move it out of review if readback diverges."
}'

Adapters may be command or api. Template variables available to adapters are {{wu_id}}, {{provider_id}}, {{item_id}}, {{desired_status}}, and {{lifecycle_intent}}.

# Old workflow (single step)
cd /path/to/main
pnpm wu:done --id <wu-id>  # Ran gates on main
# New workflow (two steps)
# Step 1: From worktree
pnpm wu:prep --id <wu-id>

# Step 2: Runs in main (copy-paste from wu:prep output, with no intervening commit)
pnpm -C /path/to/main wu:done --id <wu-id>

If a WU has escalation_triggers (e.g., sensitive_data, security_p0), wu:done blocks until the escalation is resolved. Use wu:escalate to check status or resolve:

# Check escalation status
pnpm wu:escalate --id <wu-id>

# The agent requests a human-escalation approval (never resolves its own escalation)
pnpm approval:request --type human-escalation --wu <wu-id> --triggers sensitive_data --summary "<what changed>"

# A human, in a shell with no agent session, approves; this runs wu:escalate --resolve --approval-id
pnpm approval:review --approve <approval_id>

Locally, human escalation raises the cost of self-resolution and leaves an audit trail. It is not an enforced boundary against an agent that runs with your own user authority.

wu:escalate --resolve refuses when it detects an agent session in the reviewing process, and it needs --approval-id naming a human-escalation approval. approval:request records the requester from the checkout’s git user.email (--requester-id cannot assert another identity) together with the requester’s agent-session evidence, and approval:review records the reviewer. The chain runs wu:escalate, then records the approval approved (locally and on the control plane), then runs wu:done, because wu:done refuses a pending approval; a re-run after a failed wu:done does not record it twice. approval:request --context cannot set the reserved requester_agent key. The resolver is always the checkout’s git user.email (--resolver is a cross-check only).

Separation rule: a review is refused when the reviewing process shows an agent marker or the requester’s agent session id. The email comparison is an additional refusal only when the requester had no agent-session evidence, so a human reviewing an agent’s request on the same git email passes (the single-identity developer case), and a human-requested approval reviewed by the same email does not.

The WU records escalation_resolution_evidence (approval id, requester, source, and a sha256 digest binding them to the resolver and time). A request file alone is never accepted. At wu:done the approval is re-verified: a local approval record must be approved, belong to this WU, have type human-escalation, and have a reviewer who is a user and not the requester (unless the requester had agent evidence). A control-plane approval (one posted by approval:request to the control plane, recorded with source: control-plane) is fetched by id and must show status approved, this WU, type human-escalation and a user reviewer. An unreachable or missing approval is refused. A resolution that omits escalation_resolved_triggers is never current. A local approval record must be present in the checkout where wu:done runs.

--resolve is idempotent. After wu:release --keep-branch, wu:recover --action reset, or a re-claim, run the same resolve command again. LumenFlow reads the active worktree’s WU as the completion authority and reconciles an existing resolution without creating an empty commit. No force flag or manual YAML edit is required.

For a claimed worktree WU, the resolution is committed on the lane branch and published to origin/<lane-branch>. origin/main receives it when wu:done lands the lane. If another checkout moved the remote lane branch, the resolution commit is replayed onto the new tip and pushed again. If every attempt loses, the command fails and names the rerun. A rerun publishes the existing resolution instead of resolving again. If the local lane was rewritten (for example by wu:rebase, which does not push a worktree-mode lane), nothing is force-pushed. The resolution stays committed on the local lane branch, and wu:done lands it. For a WU without a claimed worktree, the resolution is published to origin/main through the governed micro-worktree push, which rebases and retries. A rerun checks the origin/main copy and publishes an earlier resolution that never reached it.

wu:done evaluates the approval gate against the same WU document it reports reading, the worktree copy. A resolution recorded there passes, even when main’s copy or an earlier pending wu:edit or wu:rebase snapshot lacks it.

FlagDescription
--idWU ID (required)
--resolveResolve the escalation (refused in an agent session, except with --delegated)
--approval-idThe human-escalation approval to resolve under (required with --resolve unless --delegated)
--resolverCross-check only: must equal this checkout’s git user.email
--delegatedResolve as a different agent seat, only when the trusted escalation.mode is delegated (see below)

After resolving, wu:done will succeed for escalation-triggered WUs.

Who resolves an escalation is a choice in the trusted policy, software_delivery.escalation.mode: human (the default flow above), delegated (an independent agent seat runs pnpm wu:escalate --id <wu-id> --resolve --delegated after a zero-findings review and green gates on the exact tip; it is recorded as delegated-agent, never as a human, and a commit after the review invalidates it) or off (triggers are recorded but never gate wu:done; agents decide and the landed WU records escalation_advisory). Delegated resolution is separation of duties plus independent review, and like the human route it is advisory against an agent running with your own user authority. See Escalation modes to enable a mode.

No. If you run wu:done from main without running wu:prep first, gates won’t have run in the worktree where your changes are. The previous behavior of running gates on main has been removed.

Fix the issues in your worktree, then run wu:prep again. The workflow prevents partial completions.

Do I still need --full-tests for .NET repos?

Section titled “Do I still need --full-tests for .NET repos?”

Usually no. For presets such as dotnet, wu:prep already falls back to the configured default test flow when path-scoped execution is unsupported. Use --full-tests when you want to force the default incremental/full flow even for presets that support scoped execution.

Named --skip-gate <name> requests remain available for explicitly skippable gates with matching structured failure evidence on main. They require --reason and --fix-wu.

What if gates fail on code I didn’t change?

Section titled “What if gates fail on code I didn’t change?”

wu:prep probes the named gate against main. When structured evidence proves the same failure is pre-existing, you may request that specific skippable gate:

pnpm -C /path/to/main wu:done --id <wu-id> --skip-gate <name> --reason "pre-existing on main" --fix-wu <other-wu-id>

New failures introduced by your changes remain blocking and must be fixed first. See Troubleshooting: Pre-existing gate failures for the full verification workflow.