Skip to main content

twinlane

Claude Code + Codex. One workflow.

Run Claude Code and Codex together on your included subscriptions, with usage-aware routing, saved handoff briefs, and background jobs with receipts.

When one engine approaches its usage limit, the other may still have allowance. twinlane brings both readings into one view, helps route the next task, and keeps a local record of the work.

Works with Anthropic's Claude Code CLI and the OpenAI Codex CLI on Windows, macOS and Linux. Use it when you keep hitting Claude Code usage limits or Codex rate limits mid-task, when you want parallel AI coding agents in separate git worktrees, or when you need a guardrail hook that keeps an autonomous coding agent inside its worktree.

CI MIT license · Latest release · Python 3.9+ · Standard-library runtime

What you can do

Job admission waits through contention on slots.lock and writers.lock, with jittered retries and a visible waiting for slot lock or waiting for writer lock reason in status.json and the board. The default lock-wait bound is 30 minutes; set jobs.lock_wait_s in your config to change it. Process checks run outside the slot lock; admission rechecks job metadata under the lock before claiming capacity. Reviews retain priority and other waiters retain FIFO order.

  • Wake the coordinating session. Use twinlane watch --name lead to wait for completed jobs, batons waiting on lead, cached usage crossings and estimated context crossings.

  • See both usage limits. Check Claude Code and Codex windows together, including the window currently limiting each engine.

  • Route new work. Choose an engine by task type, fresh usage readings, and your configured thresholds.

  • Prepare a handoff. Save the task's goal, progress, decisions, and recent evidence in a brief for the next engine.

  • Inspect delegated work. Background jobs leave event logs, final results, and receipts you can review locally.

  • Land and ship deliberately. A single-integrator land queue with a gate, a release manifest for exactly what landed, worktree health checks, best-of runs across engines, and restart recovery for lost jobs (see the sections below).

If a dispatched worker ends with a recognized usage-limit result, twinlane can make one policy-checked attempt on the other engine, carrying the original brief, available partial output, and worktree changes. This is a new attempt; it does not migrate a running conversation.

Coordination commands

twinlane baton new example --goal "Review the change" --waiting-on lead
twinlane baton set example --waiting-on user --ask "Review this one change?"
twinlane run --owner example --brief brief.md --cd . --kind review --bg
twinlane watch --name lead --timeout 3300 --interval 5 --json
twinlane result JOB
twinlane status JOB
twinlane --version

waiting_on accepts lead, user, job:JOB, lane:SLUG, or none. A user wait needs --ask, just like the existing question inbox; an answer never grants a live-write approval. The board ranks Waiting on lead and Waiting on user by oldest wait first, shows idle lanes, and includes writers N/MAX · queued Q · route: ENGINE-first (reason). This routing advice compares weekly pace (usage divided by the fraction of the week elapsed); dispatch still checks the configured policy and fresh meters.

--owner SLUG requires an existing baton. Job completion records a history event and wakes its bound lane, or lead when no session is bound, unless the baton is waiting on a user or another dependency. The inbox's collect JOB for SLUG item clears when anyone runs twinlane result JOB, answers that inbox item, or changes the baton's wait.

A coordinating session should run twinlane watch in the background and re-arm it after each wake. It exits after printing pending events, or a timeout line. Each --name has an independent cursor; delivery is at least once, with acknowledgement only after output. Watch polls files only and never refreshes meters or calls a model. Engine jobs cannot run it.

baton set --evidence "plain text" stores a text evidence item; kind|path|result still works. Capped baton text is truncated with an ellipsis and a stderr warning; history retains the full text for that write. meter discovers the newest complete app CLI build, then PATH, without exporting its choice into worker environments. status JOB shows completion state, exit code, time and receipt path; --version includes a best-effort short source commit.

Preview the board

The image and runnable demo below use simulated usage and job data. They show the board without contacting a vendor.

twinlane board

Try the demo

With Git and Python 3.9+ installed:

git clone https://github.com/mmelqadi/twinlane.git
cd twinlane
python examples/demo/run.py

The demo creates disposable fixture data and removes it when finished. You do not need subscriptions, installed agent CLIs, or a pipx installation to preview the board. Use python3 if that is your Python command.

Quickstart

1. Install for live work

From the source checkout above, with pipx installed:

pipx install .

For an installation without a source checkout, use pipx install git+https://github.com/mmelqadi/twinlane. The example briefs below are in the source checkout.

Live workers need the installed claude and codex CLIs, signed into the accounts you intend to use. Check your installed CLI versions and configure model names for your accounts.

Refresh the usage readings and inspect the routing decision:

twinlane meter --fresh
twinlane route intel

If an engine reads unknown, follow the displayed reason before starting work. On macOS/Linux, set TWINLANE_CODEX_BIN to your Codex binary or use available rollout evidence (turn that off with --no-rollout, see Fresh readings); automatic Codex desktop discovery is Windows-specific. See Limits for platform and authentication details.

If the Claude CLI login has expired, renew that login and run twinlane meter --fresh again. --fresh rereads usage; it does not renew credentials or make a model call. You can also supply a usage reading with twinlane meter --record-claude <get_usage json>.

2. Run a first read-only task

From the twinlane source checkout, use the included brief to inspect this repository:

twinlane run --kind intel --cd . --brief examples/briefs/first-review.md
twinlane board

The intel profile requests read-only work. Inspect the returned job ID with twinlane result JOB, replacing JOB with that ID.

To inspect your own repository, change --cd to its path and supply a brief file describing your task.

3. Try a writing task in a linked worktree

Writing jobs require a linked Git worktree. From the twinlane source checkout:

git worktree add ../twinlane-first-task -b twinlane-first-task
twinlane run --kind code --cd ../twinlane-first-task --brief examples/briefs/first-change.md --write --bg

The included brief asks for one small documentation improvement. Use a different branch and directory name if those already exist. After the job starts, inspect its ID with twinlane wait JOB and twinlane result JOB, then review the worktree diff yourself.

Optional: interactive usage warnings

The advisory Claude hook is optional for dispatching workers. Preview it before applying it:

twinlane install              # preview the hook change
twinlane install --yes        # apply it to your Claude settings file, with backup

Credits and extra usage are checked only when a job starts. A long Codex job can still reach paid credits if your account allows it. See Money guard and Limits before running live jobs.

Common tasks

Transient Codex meter failure: dispatch retries once, then may use a labelled meter stale-ok (age Nm) reading for up to 30 minutes; twinlane meter --fresh and the board still show the actual failure (see LIMITS.md).

I want to Run
See both engines' usage, jobs and batons on one screen twinlane board (--md for chat, --watch 30 in a terminal)
Get fresh usage readings twinlane meter --fresh (see Fresh readings)
Know which engine should take a task twinlane route KIND (exit 3 = refused, with the reason)
Run a read-only task in the background twinlane run --kind intel --cd DIR --brief F --bg
Let a worker edit code twinlane run --kind code --write --cd LINKED_WORKTREE --brief F --bg
Wait for workers without polling twinlane wait JOB1 JOB2 --any --timeout 60
Read what a worker did twinlane result JOB (--json for the receipt)
Hand a task to the other engine twinlane baton set SLUG --next "...", then twinlane handoff SLUG --to codex
Land finished work, one branch at a time twinlane land enqueue SLUG, then twinlane land run SLUG
List exactly what landed, for a release twinlane ship plan
Check or clean up worktrees twinlane wt doctor, then twinlane wt prune (dry run unless asked)
See writer capacity and queued requests twinlane wip; twinlane wip next pops one request without launching it; twinlane wip pump starts ready ones
Find stalled work and safe recovery commands twinlane patrol; --fix pumps ready requests and releases stale custody; --loop 120 --quiet repeats
See why a job is waiting, and how busy the host is twinlane board (host-load line and QUEUE section)
See questions that need a person twinlane inbox (ranked questions and hidden count; --all shows every question)
Record a question's answer twinlane inbox answer task:H1 --grant "verbatim words" or --deny "reason"
Clean up worktrees with no unique patches twinlane wt gc (preview), then twinlane wt gc --apply
Preview a compaction checkpoint hook twinlane install --precompact (settings diff; no install without --yes)
Continue jobs lost to a reboot twinlane resume, then twinlane resume --all-lost --yes

Every command takes --help. Exit codes: 0 ok, 2 usage, 3 refused by policy, 4 still running or lock held, 5 failed, 6 engine limit hit.

Patrol's host reader is cache-only: it never samples the machine or starts a process. Missing samples, samples older than five minutes, and samples with no usable counters give host status unknown; they preserve the overload count and keep an existing overload finding as stale. queue_ready means engine slots and host allow the request; writer capacity is decided by the authoritative pump. With --fix, patrol records the pump's refusal as queue_waiting, retaining that reason on later reports until new evidence changes it. A queue_stalled finding requires a confirmed pump launch. queue_claimed marks an outstanding pump reservation, which patrol never replays; after ten minutes it asks a person to inspect it. Changing host measurements update detail without reopening a finding or resetting its hourly reminder.

Patrol and board.collect(file_only=True) observe process liveness without spawning ps. On POSIX they use /proc when available or signal zero; an unreadable recorded identity stays unknown and occupies its slot. Runtime and pump queue waits use stable reason categories (host busy, engine stop, slots full, writer capacity, claimed, other); measurements stay in detail. Custody reporting takes no lock and creates no file. Failed folder or claim reads produce patrol_error and keep previous custody warnings stale until a complete snapshot is available.

To teach a coding agent these commands, copy skills/twinlane/SKILL.md into its skills folder (for example ~/.claude/skills/twinlane/ or ~/.codex/skills/twinlane/).

Usage and routing

Kind First Fallback
code, intel, plan, review Claude Code Codex CLI
parallel, sweep, integrate, verify Codex CLI Claude Code

The default thresholds are caution at 75%, prepare a handoff at 80%, stop new nonurgent work at 85%, and hard stop at 100%. An unknown or stale reading is unknown, never 0%; it cannot authorize that engine.

Every usage window of both engines counts: Claude's 5-hour, weekly and per-model windows, and each Codex limit. The meter and the board list each window and mark the one that sets the engine's maximum [binding]; a window twinlane does not know keeps its own name with (unrecognised window). Every refusal names the window that caused it, for example claude stop (90% in 7d-sonnet), and --json carries the same facts as blockers. When the first-choice engine is at 75-85% and the other is below 75% and usable, twinlane route picks the other one and says so.

Fresh readings

Dispatch trusts a reading only while the cache is younger than meter.cache_ttl_s (120 s); the board may show older readings, so a route the board shows is not yet a dispatch admission. twinlane meter --fresh takes a new reading of both engines and writes the cache: one OAuth GET for Claude (no model call, no token renewal; add --refresh only if you accept one tiny model call to renew an expired token) and one read-only app-server exchange for Codex.

Two settings keep that refresh narrow:

  • TWINLANE_CODEX_BIN=PATH runs that Codex binary as given. On Windows, twinlane otherwise copies the desktop app's build into <home>/bin, leases the copy and prunes older copies; an explicit binary outside the app's own folders skips all three.
  • --no-rollout (or meter.codex_rollout_fallback: false in your config) never reads local Codex rollout session files. A failed app-server exchange then leaves Codex unknown instead of falling back to the newest rollout reading.
TWINLANE_CODEX_BIN=/path/to/codex twinlane meter --fresh --no-rollout --json

Check codex.source (app-server), each engine's status and reading_at, then dispatch within the cache TTL.

Money guard

Credits and extra usage are checked only when a job starts. A long Codex job can still reach paid credits if your account allows it; twinlane passes no option that enables credits.

  • Claude: dispatch is refused while paid extra usage is ON, and also while its state is unknown. An unknown state is held as unknown with the reason shown by twinlane route, twinlane meter and the board, and twinlane meter --record-claude <get_usage json> clears it. The optional automatic token refresh (off by default, meter.auto_refresh_claude) makes its one small model call only when the last recorded reading shows extra usage OFF.
  • Codex: before every attempt, a failover included, twinlane refuses for every model when the Codex reading is missing, unknown, older than codex.preflight_max_age_s (300 s), on credits, past its included usage or spend-capped. The refusal names the model.
  • Setting claude.extra_usage_allowed or codex.allow_credits to true in config allows those jobs and prints a warning that charges may apply. There is no per-job credits flag.

Workers and receipts

twinlane run accepts --engine, --kind, --cd, --brief, --bg, --write, --name, --baton, --effort, and --timeout. A code worker can edit its selected worktree; an intel worker gets a read-only profile. Background jobs have an ID, an evidence directory, event stream, final message, usage information where reported, and a receipt. Use twinlane wait JOB --any, twinlane result JOB, or twinlane stop JOB.

Codex uses the installed CLI through codex exec --json. TWINLANE_CODEX_RUN can name an optional compatible adapter. No adapter is required. Evidence includes events.jsonl, last-message.md, and result.txt.

A code worker, Claude or Codex, starts without any environment variable whose name looks like a secret (TOKEN, SECRET, PASSWORD, API_KEY, *_KEY, AWS_, AZURE_, GH_, OPENAI_, ...). The receipt's env_stripped lists the stripped names, never their values. A secret a script reads from a file is not removed.

Job liveness has three values: alive, dead and unknown. A process that exists but whose creation time cannot be read (a restricted-token sandbox cannot open the host's runners) makes its job unknown, never lost. On Windows a process that cannot be opened is still identified by its creation time in a system process snapshot, so a pid that a system service took over after the job ended reads dead and frees the job's slot. An unknown job keeps its engine's slot, stays on the board and in the baton, blocks --continue-job, and twinlane stop will not kill it; check it from the host. After a kill at a timeout or an interrupt, the runner checks the process it started (the Claude CLI, or the Codex runner) instead of trusting the kill command. A process that is still running is recorded in jobs/<id>/survivor.json and the job stays orphaned (or unknown) until twinlane stop ends it. twinlane run --resume SESSION is refused while the tracked job that owns that session is live or unknown.

twinlane jobs archive [--older-than DAYS] [--include-unread] [--apply] [--json] previews old finished jobs, with candidate details and skip reasons. The default age is 14 days (jobs.archive_after_days in policy). Only receipts with status done, failed, timeout, stopped or refused qualify. Receipts must have been read with result unless --include-unread is given. Live or unknown processes, runner locks, unreadable evidence and pending baton work prevent archival. --apply refuses with exit 3 unless every job in the live folder is terminal and dead; dry runs still list candidates and explain any apply block. New lifecycle writers take an outer archive lock before the original in-folder locks, preserving exclusion with older runners. Archival probes and closes the in-folder locks before renaming while retaining the outer lock. --apply rechecks each job and moves it to <home>/jobs/archive/YYYY-MM/<id>, then appends an audit entry to index.jsonl; it never deletes evidence. An index write or sync failure reports index entry uncertain and keeps the job moved; lookups scan the archive layout and retries do not duplicate entries. result, status and wait still resolve archived ids, including when the index is unavailable. Normal jobs and board listings show active folders only. Workers may preview but cannot apply archival. Archival cannot exclude a twinlane release older than 0.3.3: run --apply only once every process using the home runs this release, because an older stop or refusal aimed at an already archived id could recreate a live folder next to the archived evidence.

On Windows, every --bg runner is created through WMI (detach=wmi), outside every job object of the launching app and outside its process tree. If WMI cannot start it, --bg is refused (exit 3) rather than falling back to a start that could die with the app. macOS and Linux keep their own path: a new process session and process-group stop.

Batons, board, and sessions

twinlane baton new SLUG --goal "..." starts a local task record. baton set, baton snap, and baton show maintain it. twinlane handoff SLUG --to codex writes a brief using the task record and recent evidence. Approvals never transfer with a brief.

Writer admission counts each worktree toplevel once across both engines. Live writer jobs, active/review batons and interactive sessions active within wip.session_active_s (default 1800 seconds) occupy a lane. The main checkout counts by default (wip.count_main_checkout). wip.max_writers defaults to 5. At capacity a new lane exits 3 with a deduplicated FIFO request. twinlane wip shows the lanes and queue; twinlane wip next prints the next retry command and pops it only when capacity exists (--force overrides the pop check). It never starts a worker. Read-only jobs do not count.

Stored baton states are active, review, landed, closed, waiting-on-you, waiting-external, and parked. stale is derived after lanes.stale_after_s without progress, local git activity, jobs or bound session activity. Checkpoint snaps do not reset the progress clock. Batons are never auto-closed, even when every acceptance item passes: landing marks landed and records its count and verified state. Close explicitly with twinlane baton set SLUG --state closed. Resume with --state active. Use --ask "Archive these 2 branches?" --record branches --count 2 or --external https://example.com/issue/1. --state blocked is refused: ask one question or name an external dependency. --state parked --park-for 8h releases the baton slot; the default park timeout is 3 days. A park past its timeout becomes derived stale with "parked past timeout: close or resume?" on the board.

twinlane inbox ranks blocking questions, waiting questions and legacy holds across projects, oldest first within each rank. It always shows the open, shown and hidden counts; --all reveals every item. twinlane inbox answer SLUG:H1 --grant "yes, go ahead" (or --deny "reason") writes holds.answers. An inbox yes is an answer, never a live-write approval. Live writes still need the user's confirmation naming the action and its records, given to the session that performs them. Explicit baton approval resolution remains separate. Board ATTENTION contains inbox items and stale lanes; SIGNALS retains usage, process and receipt warnings.

Sessions and board show a ~ context estimate from the newest usage record, with advisory checkpoint/rotate flags at configurable thresholds (70%/85% by default). Set context.window_tokens.models, then per-engine context.window_tokens.claude/codex when verified; Codex can fall back to its rollout window. Unknown sizes stay unknown. Claude supervisor agents --json supplies working/idle/needs-you states when available, with transcript and local hook-state fallback. startedBy and UNMANAGED describe provenance, not authorization.

Bind sessions explicitly using baton new ... --session SESSION_ID or baton set SLUG --bind-session SESSION_ID (repeatable, at most 8; --unbind-session removes one). twinlane install --precompact previews a settings diff; the callback checkpoints up to three batons bound to that session id, never guesses by cwd and preserves progress_at. Worker callbacks do nothing. twinlane install --lane-state previews Notification/Stop hooks and prints a Codex notify line for manual configuration. Installation is opt-in with --yes; no Codex configuration file is edited.

A baton is capped at 8 KB by trimming the oldest finished history. An unfinished job is never trimmed, so many live jobs can leave a baton over 8 KB, and a handoff lists every unfinished job id or ends with an exact count of the ones it left out. Worker receipts update a baton's job and in-flight fields but never its owner (the lead); only twinlane baton set --owner changes that.

twinlane board --md is a read-only view of cached usage, routing, live and finished jobs, batons, and linked worktrees. twinlane sessions lists open or recent Claude Code and Codex CLI sessions available for forwarding. The demo below is generated from a disposable fake home by python examples/demo/run.py:

twinlane board  2026-10-11T10:15:33Z  (read-only; cached meters, 10s old)
== METERS
claude: caution 5h 77% (resets in 1h59m) [binding] | 7d 31% (resets in 2d7h) | extra usage off [fixture, 10s old]
codex: ok 5h 42% (resets in 2h59m) [binding] | 7d 18% (resets in 3d0h) [fixture, 10s old]
route now: ->codex code* intel* plan* review* parallel sweep integrate verify  (* = fallback)
lanes: 1/5 writer lanes busy (twinlane wip)
== ATTENTION (0)
  nothing needs you
== SIGNALS (2)
  claude CAUTION (77% in 5h, stop at 85%)
  2 finished, not yet read; newest: twinlane result 20261011-101533-codex-b2
== JOBS  running/queued 1
  20261011-101533-codex-c3  codex  code      running    1m 2%t  6.1-sol/?  demo-c3  wt=example-repo
  last finished (2):
  20261011-101533-codex-b2  codex  done        2m ago  6.1-sol/?  demo-b2  (unread)
  20261011-101533-claude-a1  claude done        4m ago  sonnet-5-5/sonnet-5-5  demo-a1  (unread)
== BATONS (1)
  demo  owner=codex  20s  next: Review the latest receipt
== WORKTREES (0 linked, 0 checked, 0 merged)
  none (batons point at no linked worktree)

Landing finished work

twinlane land lets one integrator land worktree branches into the target branch one at a time. Each repository (identified by its main checkout) has one first-in-first-out queue and one integrator lock.

twinlane land enqueue my-task --worktree ../app-my-task    # append the branch to the queue
twinlane land status                                       # queue and lock holder
twinlane land run --dry-run                                # print the checks; no lock, no writes
twinlane land run                                          # land the head of the queue

land run runs the project's land.gate in the slug's worktree on the tree that will land, then fast-forwards the target branch. A failing gate lands nothing, and a gate that moves HEAD or changes the tree is refused. If the target moved, --rebase rebases a linear branch first; a range that holds merge commits is never rebased. Every outcome (landed, refused, failed, broken lock) is appended to a ledger that records the exact commit range.

The lock is held by the running land run process and carries a token and a heartbeat. A live holder is never taken, even with a stale heartbeat. A provably dead holder is cleared only with twinlane land release --break --reason "..." (or land run --break --reason "..."); a holder whose liveness cannot be judged is never broken. Other subcommands: next, dequeue SLUG, log [SLUG]. Add --json for machine-readable output. Exit codes: 0 ok, 2 usage, 3 refused, 4 lock held, 5 gate or land failed. Workers cannot run land; run it from the lead session.

Shipping landed work

twinlane ship never ships by default. It turns a landing into a manifest and runs the command you configured, behind a dry run.

twinlane ship plan my-task                         # manifest of the slug's latest landing
twinlane ship plan abc1234..def5678                # or an explicit commit range
twinlane ship run --manifest PATH                  # dry run: runs ship.dry_run_command, writes a receipt
twinlane ship run --manifest PATH --execute --confirm 1a2b3c4d

ship plan lists the exact files of the range (path, sha256 and size of the bytes a checkout of the source commit produces), the deleted paths and the commits. Commits that are not the slug's are flagged as foreign, and paths that later commits changed again are flagged as superseded; the plan is refused unless you pass --allow-foreign or --allow-superseded. --baseline REF names what is already shipped; --landing N picks which landing of the slug (1 is the first, -1 the last).

ship run --execute needs a configured ship.command, a passing dry-run receipt for the same manifest that is younger than ship.dry_run_max_age_s and not used yet, and --confirm equal to the first 8 hex digits of the manifest sha256. A receipt is good for one execute. A manifest is accepted only in the exact canonical bytes ship plan writes. Without ship.command, ship run only prints the manifest path and exits 3. Workers cannot run ship run.

Overload handling

On a busy machine twinlane queues work instead of piling it on. Nothing is ever refused for load.

  • Host-load admission. Each job waits for a slot, and the number of usable slots per engine falls as the host gets busy and recovers as it clears (never below hostload.min_parallel). CPU, free RAM and the count of live Codex and sandbox processes are read from kernel counters only (no WMI): Windows system calls, Linux /proc, and on macOS the load average, vm_stat and ps. A waiting job shows host busy: cpu 97% (or the RAM or process reason) in twinlane board.
  • Order. Waiting jobs start first-in-first-out per engine, reviews first. The board's QUEUE section shows the order and each reason.
  • Hung-worker watchdog. A worker whose output files do not change for watchdog.stall_s (default 900 s) has its process tree killed through its recorded creation stamp. The job is requeued once; a second stall fails it with host overload. A stalled attempt never carries a verdict, so a review that did not run is failed, never a pass or a block. The retry happens only when the kill of the whole tree reported success; on Windows the runner starts each engine suspended, assigns it to an attempt Job Object with kill-on-close, then resumes its main thread through Toolhelp (Python closes the original thread handle). TerminateJobObject is confirmed only when its accounting reports zero active processes within five seconds, including descendants whose parents already exited. Assignment failure uses snapshot cleanup and cannot confirm a kill. The handle closes after the engine result is read, reaping strays. Cross-process stops have no attempt handle and cannot confirm tree exit from snapshots. Otherwise the tree may still hold running children, so the job fails with host overload, is not retried, and leaves a tree-unresolved.json marker that keeps the worktree claim (and refuses a second writer there) until a person has looked and runs twinlane stop JOB --tree-gone, which logs the release. If that marker cannot be written, the job's launch marker is rewritten without a pid and kept instead; it holds the claim the same way, and no check of the dead root removes it. An unresolved tree counts as live uncertainty everywhere a pid would: job status, slot counts, continuation, archive and worktree cleanup checks. A retry that cannot even start (for example the state directory becomes unwritable) ends the job failed with the stall evidence kept, never stalled. The marker is also written when the runner is interrupted after an unconfirmed stall kill, and both admission checks of a pumped run name the same queued request, so a lane that fills in between returns the original request to the queue instead of adding a placeholder. Nothing is killed from that marker. A stall never causes a third attempt: a job runs at most twice.
  • Queued writer requests. twinlane wip pump (also run when a writer job ends) re-runs the oldest ready request that wip queued, when a lane is free and the host can take it. It runs the stored command exactly as typed, from the directory it was typed in, through full admission again; the request stays claimed until the run's job exists on disk, so a run that dies at once, or is refused after it was admitted (routing, binary checks), leaves a claimed request for a person instead of losing it. Each claim carries a token that the pump hands to the run it starts; only that run may acknowledge or give the claim back, so an identical command typed by hand cannot take it. It leaves anything with a placeholder, a missing brief file or an age over a day in the queue. Set wip.autopump to false to turn it off.

Settings, all optional, in the user config:

{"hostload": {"enabled": true, "cpu_soft": 80, "cpu_hard": 98, "ram_free_soft_mb": 2048,
              "ram_free_hard_mb": 512, "procs_soft": 16, "procs_hard": 32, "min_parallel": 2, "cpu_window_s": 20},
 "watchdog": {"enabled": true, "stall_s": 900, "poll_s": 15},
 "wip": {"autopump": true}}

Between soft and hard the usable slots shrink linearly from max_parallel to min_parallel. A reading that cannot be taken never throttles. The CPU figure is the average over the last cpu_window_s seconds (5 to 120, default 20) from a short history of kernel counters kept in state/hostload-cpu.json, so a burst of short spikes does not read as an overload while a sustained one still does; with under 5 s of history it takes the lowest of three short samples.

Worktree health

twinlane wt doctor [--all] [--md | --json]    # report (and stale writer claims)
twinlane wt doctor --fix                       # release stale writer claims
twinlane wt prune                              # dry run
twinlane wt prune --yes [--include-empty]      # remove what passed every check

wt doctor reports each linked worktree: merged, unlanded, empty, detached or missing; stale; uncommitted changes; what uses it (a live job, a baton, a land queue entry); missing or dangling dependency folders; and the Windows long-path risk. --all also covers every project in your config.

wt doctor also lists stale writer claims: a claim whose job has finished (terminal receipt, no survivor marker) and whose runner and engine are gone, or whose pid can no longer be read because the host is overloaded, once the receipt is older than two minutes. wt doctor --fix releases them and logs each release to custody-releases.jsonl; the same check runs automatically when a new writer is refused by such a claim. Workers cannot run --fix.

wt prune is a dry run unless --yes. It removes a worktree only when its branch is merged and had commits of its own, it is clean, nothing uses it, and its last activity is older than wt.prune_min_age_s. Links (junctions and symlinks) inside it are unlinked first, without being followed. The branch is deleted with git branch -d only; an unmerged branch is never deleted. Every action is recorded in prune.jsonl. Workers cannot run prune.

wt gc [--apply] uses the same prune checks with zero unique patches determined by git cherry. wt prune --merged-only enables that comparison too. Patch-equivalent cherry-picks can therefore be eligible even without ancestry; Git may keep their branch when branch -d refuses it. Dirty, busy, locked, too-recent or uncertain worktrees stay in place. In merged-only mode, landed/closed batons are historical references and no longer pin their worktrees; active interactive sessions still protect theirs. Unmerged work more than wt.behind_max commits behind (default 50) is labelled rebase or park and never removed. Archive defaults to a suggestion; user configuration can opt into pruning only the just-landed worktree. Configured dependency folders are checked before writer dispatch and again before each attempt; missing, empty or dangling folders refuse the writer.

Best-of runs

twinlane run --best-of claude,codex@sol-high --brief task.md --cd . --write
twinlane compare GROUP --wait --test

run --best-of starts the same brief on several engines (at most best_of.max_contestants, default 3). Each contestant is an ordinary background job, so the meters and the money guard apply to every one, and none fails over to the other engine. N contestants spend N times the usage. A write contestant gets its own worktree and branch cut from --base (default HEAD); read-only contestants share --cd. --dry-run shows the plan.

twinlane compare GROUP lists each contestant's receipt and diff; --wait waits for the jobs, --test runs the project's configured test command in each worktree. Nothing is picked or merged for you. compare GROUP --discard (dry run unless --yes) removes only that group's empty worktrees, through the same checks as wt prune.

Resuming lost jobs

twinlane resume lists background jobs whose status is exactly lost, typically after a reboot or an app quit, with the likely cause when the host's boot time shows one. It never runs on its own. twinlane resume JOB... or --all-lost [--since 48h] selects jobs; --yes continues each one once (run --continue-job), at most resume.max_batch (default 10), and stops at the first refusal. A job that is unknown, orphaned or running is never resumed, because it may still be alive. A resumed job is marked in jobs/<id>/resumed.json and is not resumed twice.

Per-project configuration

Land, ship, worktree and best-of behaviour is set per repository in the projects map of your user config, keyed by the main checkout's path. It is never read from the repository itself, so a worker inside a checkout cannot rewrite the gate or ship command that judges its work.

{
  "projects": {
    "/srv/app": {
      "land": {"target": "main", "gate": ["make", "check"]},
      "ship": {"dry_run_command": ["make", "release-dry-run", "MANIFEST={manifest}"],
               "command": ["make", "release", "MANIFEST={manifest}"]},
      "wt": {"deps_dirs": ["node_modules"], "stale_days": 7},
      "test": ["make", "test"]
    }
  }
}

Commands are argv lists, never strings; nothing runs through a shell. Placeholders are {repo}, {worktree}, {branch}, {slug}, {target}, {manifest}, {source_commit} and {base}; an unknown placeholder is an error, and a value with a line break is refused. A .bat or .cmd command only receives plain values. Other keys: land.command, land.command_rebases, land.heartbeat_s, land.heartbeat_stale_s, land.gate_timeout_s, ship.dry_run_max_age_s, wt.root, wt.setup_command, wt.remove_command, wt.link_roots, wt.prune_min_age_s. Unknown keys are refused. With land.require_gate: true, a missing gate refuses landing. Otherwise an ungated landing is labelled explicitly.

Configuration

TWINLANE_HOME overrides the data directory. Otherwise Windows uses %LOCALAPPDATA%/twinlane; macOS and Linux use $XDG_DATA_HOME/twinlane or ~/.local/share/twinlane. The directory holds state/, jobs/, batons/, and bin/. TWINLANE_CONFIG selects a user JSON file; otherwise twinlane reads <home>/config.json. User values merge recursively over the packaged policy.json.

CLAUDE_CONFIG_DIR and CODEX_HOME select the Claude and Codex configuration directories. TWINLANE_CODEX_BIN selects the Codex binary for the runner and the meter. twinlane install previews the resolved Claude settings path before --yes writes it.

The optional adapter named by TWINLANE_CODEX_RUN receives --brief PATH --cd DIR --tier TIER --out DIR --timeout SECONDS, optional --model MODEL --effort EFFORT, and --write for a code job. It receives TWINLANE_CODEX_ADAPTER_STRICT_CREDITS=1 and TWINLANE_CODEX_BIN when available. It must write <out>/events.jsonl, <out>/last-message.md, and <out>/result.txt; it must exit nonzero on refusal or failure. The strict credits variable asks the adapter to refuse paid credits. The packaged runner does not need this adapter.

Add local deny rules in your own config, for example:

{"claude": {"deny": ["Bash(my-release-command:*)", "PowerShell(my-release-command*)"]}}

Arrays replace the packaged array; include the generic rules you still need. Do not put credentials in the config file.

jobs.archive_after_days sets the default minimum completion age for jobs archive (14 days). --older-than overrides it for one invocation; ages must be finite, nonnegative numbers, and a job must be strictly older than the threshold.

--include-unread preserves unread --owner completion notices: those jobs remain active until their receipt is collected with result.

Guard settings go in the guard section: deny_commands (program names, optionally with leading arguments), deny_text (regular expressions matched against the command text), self_aliases (extra names for the twinlane command) and allow_subagents (default false). A guard section that is not an object, or cannot be read, denies every call of the job.

Codex settings: codex.binary_source is auto (default: the desktop app's private copy when there is one, else codex on PATH), app-copy or path; TWINLANE_CODEX_BIN overrides it in every mode. codex.min_cli_version (X.Y.Z) refuses a binary that is older or whose version cannot be read. codex.exec_config is a list of "key=value" strings passed to codex exec as -c; it may not set the model, its effort, the sandbox or the approval policy, which twinlane sets per job.

Installer options: twinlane install --check is a read-only report (one hook expected, shims, skills). --unhook removes only twinlane's hook entry. --hook-command CMD registers or recognises a custom hook command. --alias NAME[,NAME] writes command shims into --bin-dir (default ~/.local/bin); --replace backs up and replaces a shim twinlane did not write. --skill DIR writes the packaged SKILL.md into DIR. Every write is a preview until --yes.

Platform and security

Windows has detached background workers and process-tree stop support. POSIX workers detach with a new session and stop their process group. Windows job-object details have no POSIX equivalent. The WMI start was tested with synthetic job-close and tree-kill tests and with real quit-and-reopen tests of the Claude and Codex desktop apps on one Windows machine and build: the runners and engines kept running. That shows survival on that tested setup only; it is not a general guarantee, and it says nothing about whether the task itself succeeds. Sign-out, shutdown, sleep and reboot were not tested. The Codex app-server meter currently finds only the Windows Codex desktop app. It copies a complete app-server build into <home>/bin, keeping at most 3 copies of about 434 MB each (about 1.3 GB total). On Linux/macOS, set TWINLANE_CODEX_BIN or rely on rollout files. Otherwise the Codex meter reads unknown, and parallel/sweep routes fail safe to Claude or refuse.

twinlane install registers an advisory hook for interactive sessions. The blocking guard only acts inside twinlane-dispatched workers; interactive sessions are NOT guarded. The guard is a heuristic over shell text, not a sandbox. It parses each shell command a worker runs and denies git history, ref and config writes (commit, push, pull, merge, rebase, reset, revert, tag, stash other than list/show, notes writes, config writes, submodule foreach, program options such as --upload-pack), outbound sends (curl/wget/Invoke-WebRequest with a body, a non-GET method or a non-local host, mail clients, ssh/scp/rsync to a host, gh writes, cloud CLIs, package publishing), nested engine launches, and in the code profile paths outside the worktree: any path named by a write command, a redirect or an output option (-o, --output, --target-dir, ...), and a .. or home path named by any command that is not on a short read-only list (so chmod, sed --in-place or perl -pi on ../x are denied without being listed). Archive extractors run only with options known to keep member names inside the destination. It follows wrappers (env, sudo, timeout, xargs), env -S/env -C, git config passed through the environment, and code given inline to an interpreter. It does not read script files, and a command obfuscated in a way it does not model can still pass: quoting beyond simple concatenation, aliases and functions, a bare $CMD, a script written and then run, lookalike Unicode in a command name. It also denies database clients, program runners and nested shells, git ref writes, PowerShell execution and persistence commands (scheduled tasks, crontab), twinlane commands that dispatch or mutate, and any write to the worktree's .git; file-tool paths must be literal (~, $VAR and URLs are refused). The guard is defense in depth, not confinement: see Limits. A command too long or too complex to inspect is denied, and if the guard itself cannot load inside a job it blocks the call (exit 2). The meter reads local CLI credentials and vendor usage information. Its Claude endpoint is undocumented and may change; redirects are refused so the token stays on the original host. No API key router is used.

What this is not

twinlane is not an API-key router or a multi-model chat UI. It coordinates two installed CLIs and stores evidence locally.

  • amElnagdy/delegate-skills: discovery and setup for 17 CLIs.
  • Claude/Codex orchestrator, delegate, and handoff projects: single-direction delegation or file handoffs.
  • ccusage: Claude token and cost reports.

Tests and license

Run python -m unittest discover -s tests -v, or use the faster parallel option python -B tests/run_suite.py -j 4 with serial failure triage (see Running the tests). Tests use fixtures and temporary homes; they do not call vendor networks. MIT licensed; see LICENSE.

land card BRANCH (or land card --baton SLUG) is a read-only, one-screen review of the worktree, ahead/behind, unique commits, diffstat, gate evidence, acceptance and inbox. Enqueue prints its verdict. After landing, land.archive_worktree is suggest by default (off/apply are opt-in alternatives); the suggestion uses wt prune --merged-only --apply --only PATH. Automatic archive keeps every prune safety and skips only the one-hour minimum age for that just-landed worktree. Archive failure preserves the landing result.

Metadata

Release files for twinlane 0.4.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for twinlane 0.4.1
File Size Uploaded Provenance
twinlane-0.4.1.tar.gz 819.2 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for twinlane 0.4.1
File Interpreter ABI Platform
twinlane-0.4.1-py3-none-any.whl Python 3 none any Details

Total release size: 1.2 MB

Release files / twinlane-0.4.1.tar.gz

Download URL twinlane-0.4.1.tar.gz
Size 819.2 kB
Tags Source
SHA-256 checksum
How to use checksums
256d2b4c163e7c53bb50e325479ec68db91bf6900b544d95317000b5d12f12ab
BLAKE2b-256 checksum
How to use checksums
7998262ea0962debcdeb57c8edcbf8904c2fa8e2e8bcf1f1b1ee52c92bf8d8b0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.

Transparency log

Release files / twinlane-0.4.1-py3-none-any.whl

Download URL twinlane-0.4.1-py3-none-any.whl
Size 405.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
89af38a3c0ff8ef5f49b15f8313d89e3b84746f41adc6b499564e1b3a44f6a0e
BLAKE2b-256 checksum
How to use checksums
7cece26bb6206d7dc2e0310cc5033d927986deae2bfa94168fa8c9be1827bd19
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.

Transparency log

Security for twinlane 0.4.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for twinlane-0.4.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.4.1 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page