Batch harness — drain mode
Status: Beta. Single-commission haft harness run is the
trustworthy operator path. Drain mode (--drain --concurrency N) and
workspace_patch_auto_on_pass auto-apply landed in v7.x and have been
validated end-to-end on docs-class commissions. Treat them as Beta on
production-code commissions; review the diff or use
workspace_patch_manual when in doubt.
What this is
The harness lets you commission work from DecisionRecord artifacts
and run it under a real codex agent in an isolated workspace. Drain mode keeps
the runtime alive while runnable commissions remain, runs up to N agents in
parallel, blocks lockset-overlapping claims, and exits cleanly when the queue is
empty.
Per-commission delivery policy is the apply-authority gate.
Commissions tagged workspace_patch_auto_on_pass get their workspace
diff auto-applied to the project checkout as a discrete revertable commit when
they reach terminal+pass. Manual commissions (default) leave the
diff in the workspace clone awaiting haft harness apply.
Why batch instead of one-shot
A planning session typically produces 10–50 commissions. Running them sequentially with manual approval each time is the same friction as the self-written Ralph loop people resort to. Drain mode is the canonical operator path: queue the batch, walk away, wake up to terminal commissions with their per-policy outcomes already applied or held for review.
The workflow
DecisionRecord
↓ (operator decides via /h-decide or haft_decision MCP)
WorkCommission <-- delivery_policy declared here
↓ (haft commission create-from-decision OR haft harness run --decision)
Drain <-- haft harness run --drain --concurrency N
↓ (parallel codex sessions; lockset enforcement, AutonomyEnvelope at create/preflight/execute)
Terminal verdict
↓
— pass + workspace_patch_auto_on_pass → auto-applied to checkout
— pass + workspace_patch_manual → diff in workspace, awaits haft harness apply
— blocked / failed → stays for operator decision (requeue / cancel)
Quickstart
Assumes you already have one or more active DecisionRecords.
1. Create commissions
Auto-select all active decisions that don't already have commissions and inspect the generated plan:
haft harness run --prepare-only
# Reads .haft/decisions/*.md, writes .haft/plans/<auto-id>.yaml,
# creates one WorkCommission per decision, stops before runtime starts.
Or commission a specific decision with an explicit policy override:
haft commission create-from-decision dec-20260428-issue-66-rest-105a0416 \
--delivery-policy workspace_patch_auto_on_pass
For low-level / scripted workflows, the same surface is reachable through
the MCP haft_commission tool:
haft_commission(
action="create_from_decision",
decision_ref="dec-...",
repo_ref="local:myproject",
base_sha="...",
target_branch="dev",
allowed_paths=["internal/cli/", "internal/artifact/note.go"],
forbidden_paths=[".haft/", "go.mod"],
delivery_policy="workspace_patch_auto_on_pass"
)
2. Drain the queue
haft harness run --drain --concurrency 4 \
--force-skip-specs "overnight: docs polish + small refactors"
Drain reads the haft store every poll cycle. While runnable commissions exist
it claims up to N of them in parallel under independent
codex app-server sessions. When all observed commissions reach a
terminal state, drain exits with status 0. --drain requires the
operator stream — do not combine with --detach.
Operator stdout receives one line per terminal auto-apply attempt:
auto_apply_succeeded: commission=wc-20260429-61c7a2ee files=1
auto_apply_failed: commission=wc-... reason="..."
Manual commissions and blocked/failed ones stay queued for operator review.
3. Inspect what landed
haft commission list --selector all # everything, including terminals
haft harness result wc-... # per-commission verdict + diff stat + next action
haft harness tail wc-... --follow # stream the tail of an active or recently-terminal run
4. Operator recovery
The harness never deletes a commission. Stale, blocked, or no-longer-relevant commissions move via:
haft commission requeue wc-... --reason "<why this is still wanted>"
haft commission cancel wc-... --reason "<why this is no longer wanted>"
Use cases
Use case A: Overnight docs & mechanical refactors
Tag every commission workspace_patch_auto_on_pass. Cover all
docs files, formatting, comment cleanups, mechanical lint fixes,
test-coverage padding, etc. Wake up to a stack of small commits, each tied
to a specific DecisionRecord for traceability.
haft commission create-from-decision dec-docs-cleanup-... \
--delivery-policy workspace_patch_auto_on_pass \
--allowed-path docs/ --allowed-path README.md
haft commission create-from-decision dec-formatter-pass-... \
--delivery-policy workspace_patch_auto_on_pass \
--allowed-path internal/
haft harness run --drain --concurrency 4
Use case B: Mixed batch with human-in-loop on risky paths
Default to workspace_patch_manual. Opt-in to auto-apply only
for low-risk commissions:
haft commission create-from-decision dec-trivial-cleanup-... \
--delivery-policy workspace_patch_auto_on_pass
haft commission create-from-decision dec-touches-payment-flow-... \
# default workspace_patch_manual
haft harness run --drain --concurrency 2
# Auto-applies first commission, holds the second for review.
Use case C: Single commission baseline (no drain)
Sometimes you just want one commission to run end-to-end and stop. Drain is opt-in:
haft harness run dec-...
# Streams progress, exits when the commission reaches terminal,
# leaves the diff in the workspace clone for `haft harness apply`.
Invariants and guard surfaces
- Lockset overlap blocks parallel claims. Two commissions
that touch overlapping
allowed_pathscannot run in parallel. Open-Sleigh intake silently skips conflicting candidates and returns to them on the next poll cycle. - AutonomyEnvelope evaluates at create / preflight / execute,
NOT at apply. A missing envelope snapshot does not block
auto-apply on terminal+pass. An explicitly blocked envelope
decision still keeps the manual path even on
workspace_patch_auto_on_pass, because that represents a concrete operator decision rather than a missing snapshot. - Per-commission apply is a discrete revertable git operation.
No batch squash, no force-push, no merge automation. Each
auto_apply_succeededline corresponds to exactly one commit affecting only declaredallowed_paths. - No remote operations from drain. No
git push, no PR creation, no comments, no external webhooks. Drain stays local. - Stale-lease age cap. Claims older than the configured cap
(default 24h, override via
OPEN_SLEIGH_STALE_LEASE_MAX_AGE_S) are skipped at intake with typedlease_too_oldreason and surfaced inhaft harness statusfor explicit operator action. - Default policy stays manual.
workspace_patch_auto_on_passis per-commission opt-in only. There is no global toggle that flips every commission to auto-apply. - Single-commission behavior is unchanged. Running
haft harness runwithout--drainbehaves exactly as v7.0 did.
Known rough edges (Beta caveats)
-
Codex workspace clones do not pre-cache Elixir Hex deps
(
deps/,_build/). Cross-language commissions that includecd open-sleigh && mix testin theirevidence_requirementswill skip that step inside the agent's measure phase. Operators should re-run mix tests on the project checkout after apply. -
haft commission listdefaults to--selector runnable; use--selector allto see terminal commissions for audit. - Open-Sleigh resumes claimed leases on runtime restart with no age cap on the runtime side; the intake-level cap is the only stale-lease boundary today.
-
auto_applyis wired into drain mode only. Single-commission runs (haft harness runwithout--drain) still require manualhaft harness applyeven onworkspace_patch_auto_on_pass; this is a deliberate conservative choice and may change in a later v7.x slice.
Troubleshooting
"no runnable WorkCommissions" on a fresh drain start
Open-Sleigh's commission-source intake claims runnable commissions on its
own poll loop. If the runtime was already running and immediately consumed
the queue, your CLI invocation may race with it and report empty. Check
haft harness status — if the runtime is active
with non-zero claimed, your commissions are running.
Commission terminal but checkout unchanged
Three common reasons:
-
delivery_policyisworkspace_patch_manual(default). The diff is held in the workspace clone — apply withhaft harness apply <commission-id>. -
delivery_policyisworkspace_patch_auto_on_passbut the commission was run under single-commission mode (no--drain). Auto-apply is wired into drain only today; same remedy —haft harness apply. -
AutonomyEnvelope explicitly blocked the apply gate. Inspect with
haft commission show <id>and look fordelivery_decision.reason=autonomy_envelope_blocked. Resolve by updating the envelope, or apply manually after operator review.
Commission ended in blocked_policy with no_commission_mutation
The agent passed evidence but produced an empty diff in the execute phase. Open-Sleigh's pre-mutation scope guard catches this: a passing measure without product-file edits implies the commission either had no real work to do, or the agent misunderstood the frame. Inspect the workspace clone:
cd ~/.open-sleigh/workspaces/<commission-id> && git status
Decide between haft commission cancel (truly nothing to do) or
haft commission requeue after sharpening the
DecisionRecord's frame.
Related docs
- Spec onboarding (v7) — required state before non-tactical commissions are allowed
haft check— CI-facing rollup that includes commission readiness- Installation — Open-Sleigh runtime + codex prerequisites