Configuration
Lerp reads one config file at the root of your project, checked in. The root is the directory holding that file, found by walking up from wherever lerp was run. It declares which Linear teams the repo serves, how to build a workspace, and the pipeline itself, runners and queues with their prompts. It holds no durable state, because Linear is the database. Parsing is strict, and an unknown key is an error.
TOML is the default format and what lerp init writes (lerp.toml).
YAML (lerp.yaml or lerp.yml) and JSON (lerp.json) are accepted as
well. Exactly one config file may exist at the repo root; having more
than one is refused at startup.
lerp init --team KEY writes it (see Quickstart).
lerp.example.toml is the stock pipeline in full,
with prompts you can read and argue with. Read it before you run it.
The shape of it
# Linear team keys this repo serves. One repo may serve several teams
# (the monorepo case); two repos may never claim the same team.
teams = ["LERP"]
# Commands that create and destroy a lane's workspace. Lerp invokes
# them with a unique lane/run identity and otherwise knows nothing
# about what they do. Environment isolation — ports, databases,
# containers — is this repo's problem, solved here.
provision = 'git worktree add --detach "$LERP_WORKSPACE" HEAD'
dispose = 'git worktree remove --force "$LERP_WORKSPACE"'
# A runner is an adapter to a coding-agent CLI (or a raw command template).
# The contract is the lowest common denominator: takes a prompt and a
# working directory, runs to exit, exit code means done or failed.
[runners.claude]
vendor = "claude"
# model = "opus"
# effort = "high"
# context = 200000
args = "--permission-mode bypassPermissions"
# A queue is a Linear status with instructions attached. Tickets
# sitting in `status` are picked up, run through `runner` with
# `prompt`, and moved to `on_success` on a clean exit — unless the
# agent already moved the ticket itself, which lerp respects.
[queues.plan]
status = "Planning"
prompt = "Read {{ticket}} and post a plan as a comment."
runner = "claude"
on_success = "Implementing"
[queues.implement]
status = "Implementing"
prompt = "Implement {{ticket}} per its plan comment. Open a PR."
runner = "claude"
on_success = "In Review"
# Optional. Where the ticket goes when the agent exits non-zero.
on_failure = "Needs Attention"# Linear team keys this repo serves. One repo may serve several teams
# (the monorepo case); two repos may never claim the same team.
teams:
- LERP
# Commands that create and destroy a lane's workspace. Lerp invokes
# them with a unique lane/run identity and otherwise knows nothing
# about what they do. Environment isolation — ports, databases,
# containers — is this repo's problem, solved here.
provision: git worktree add --detach "$LERP_WORKSPACE" HEAD
dispose: git worktree remove --force "$LERP_WORKSPACE"
# A runner is an adapter to a coding-agent CLI (or a raw command template).
# The contract is the lowest common denominator: takes a prompt and a
# working directory, runs to exit, exit code means done or failed.
runners:
claude:
vendor: claude
# model: opus
# effort: high
# context: 200000
args: --permission-mode bypassPermissions
# A queue is a Linear status with instructions attached. Tickets
# sitting in `status` are picked up, run through `runner` with
# `prompt`, and moved to `on_success` on a clean exit — unless the
# agent already moved the ticket itself, which lerp respects.
queues:
plan:
status: Planning
prompt: Read {{ticket}} and post a plan as a comment.
runner: claude
on_success: Implementing
implement:
status: Implementing
prompt: Implement {{ticket}} per its plan comment. Open a PR.
runner: claude
on_success: In Review
# Optional. Where the ticket goes when the agent exits non-zero.
on_failure: Needs Attention{
"teams": ["LERP"],
"provision": "git worktree add --detach \"$LERP_WORKSPACE\" HEAD",
"dispose": "git worktree remove --force \"$LERP_WORKSPACE\"",
"runners": {
"claude": {
"vendor": "claude",
"args": "--permission-mode bypassPermissions"
}
},
"queues": {
"plan": {
"status": "Planning",
"prompt": "Read {{ticket}} and post a plan as a comment.",
"runner": "claude",
"on_success": "Implementing"
},
"implement": {
"status": "Implementing",
"prompt": "Implement {{ticket}} per its plan comment. Open a PR.",
"runner": "claude",
"on_success": "In Review",
"on_failure": "Needs Attention"
}
}
}Those queue fields are the complete set, status, prompt, runner,
on_success, and optionally on_failure. There is no conditional,
template or DAG syntax, because topology lives on the
board.
Runners
A runner defines how lerp invokes a coding-agent CLI for a queue. There are two kinds.
Vendor adapters (vendor = "claude", "codex" or "antigravity")
package what a shell template cannot express. They supply the flags that
stream events live (--output-format stream-json --verbose for Claude
Code and Antigravity, --json for Codex), which is what the log pane
decodes into activity lines, token counts and sparklines. They also track
the session ejecting resumes. Claude Code takes a UUID
lerp mints up front, while Codex and Antigravity assign their own IDs,
which lerp reads back from the run’s stream log.
Command templates (command = "...") run the exact shell command you
write, via sh -c, keeping the contract at the lowest common
denominator, prompt and working directory in, exit code out. Reach for
one for an unsupported CLI, an agent inside a container or VM (docker exec ...), or a custom wrapper.
[runners.custom]
command = "my-agent --prompt {{prompt}} --dir {{workdir}} --session {{session}}"
resume = "cd {{workdir}} && my-agent --resume {{session}}"runners:
custom:
command: my-agent --prompt {{prompt}} --dir {{workdir}} --session {{session}}
resume: cd {{workdir}} && my-agent --resume {{session}}{
"runners": {
"custom": {
"command": "my-agent --prompt {{prompt}} --dir {{workdir}} --session {{session}}",
"resume": "cd {{workdir}} && my-agent --resume {{session}}"
}
}
}Runner configuration keys
| Key | Type | Kind | Description |
|---|---|---|---|
vendor |
string | vendor only | Names a built-in vendor adapter: "claude", "codex", or "antigravity". Exactly one of vendor or command must be set. |
model |
string | vendor only | Model override passed to the CLI (--model <model> on claude/codex/antigravity). Shell-quoted. |
effort |
string | vendor only | Reasoning effort override (--effort <effort> on claude/antigravity, -c model_reasoning_effort=<effort> on codex). Shell-quoted. |
args |
string | vendor only | Extra CLI flags appended verbatim and last to the command template. Unquoted shell text, and it overrides earlier flags on last-wins CLIs. |
context |
integer | vendor or command | The model’s context window in tokens (e.g. 200000). Display denominator for the token percentage on a running row, faint until 80% and then ⚠. 0 (default) means unset, and no percentage is shown. |
command |
string | command only | Raw shell command template executed via sh -c. Supports {{prompt}}, {{ticket}}, {{workdir}}, and {{session}}. Shell-quoted placeholders. |
resume |
string | command only | Resume command template handed to operator on eject. Supports {{session}}, {{ticket}}, {{workdir}}. |
Validation rules:
- Exactly one of
vendororcommandmust be set per runner. - Setting
model,effort, orargson a command runner is refused at startup. - Setting
resumeon a vendor runner is refused at startup (the adapter supplies its own resume template). - Setting a negative
contextis refused at startup.
Permission grants in checked-in config
No adapter lerp ships adds permission-skipping flags on its own. An
unattended agent that cannot edit files or run git, gh, and tests
fails at the first restricted tool call. To permit unattended execution,
write the grant explicitly into your runner’s args (or command):
- Claude Code:
args = "--permission-mode bypassPermissions" - Antigravity:
args = "--dangerously-skip-permissions"
Saying yes means the agent runs with your full user account. The worktree
that provision builds is a tidiness boundary, not a security sandbox,
and nothing stops an agent from reading ~/.ssh. Because the grant lives
in a checked-in file, adding it, reviewing it, or narrowing it with
--allowedTools is an ordinary code change. SECURITY.md
covers the rest, including who can trigger a run.
Workspace commands
Lerp runs provision before starting a runner and dispose when reaping
its lane. Each runs from the repo root, writes stdout and stderr to the
lane log, and receives:
LERP_LANE, the lane numberLERP_TICKET_ID, the Linear issue IDLERP_WORKSPACE, the workspace path
They inherit the rest of lerp’s environment minus one variable.
LINEAR_API_KEY is lerp’s own credential and never reaches a provision,
dispose or runner command, and stored OAuth tokens are never passed
either. A command that needs Linear carries its own credential.
If provisioning fails, lerp leaves the ticket untouched and does not start the runner. A disposal failure is recorded in the lane log but never keeps a lane occupied.
Queues
A queue connects a Linear status to a runner and a prompt.
| Field | Type | Description |
|---|---|---|
status |
string | The Linear status this queue watches. Must match a status on the team’s board. Each status may drive at most one queue. |
prompt |
string | Instructions passed to the runner. Plain text. |
runner |
string | Name of a runner defined under [runners]. |
on_success |
string | Linear status to move the ticket to on a clean exit (exit code 0), unless the agent already moved the ticket itself. |
on_failure |
string | Optional. Linear status to move the ticket to when the agent exits non-zero. |
Prompt placeholders
Prompts support four runtime placeholders:
{{ticket}}, the Linear ticket identifier (e.g.LERP-42). Also exported to runners asLERP_TICKET.{{status}}, this queue’s ownstatusfield.{{on_success}}, this queue’s ownon_successstatus.{{on_failure}}, this queue’s ownon_failurestatus. Referencing it when unset is a config error.
Name the ticket in your prompt. A prompt is shared across every ticket in
its queue, so one that never names {{ticket}} leaves the agent no way
to know which ticket it was started for. Name target statuses by
placeholder too, so “Move {{ticket}} to {{on_failure}}” follows a status
rename on its own.
The stock pipeline
lerp.example.toml ships one opinion. Lerp holds
none. The order below exists only in that config’s on_success
pointers, and rewriting them into a different shape needs no code change.
| Status | Who acts | Then |
|---|---|---|
| Backlog / Todo | you | promote a ticket into Planning, or into Implementing if it is small |
| Planning | agent | writes the plan into the ticket’s description, under ## Plan → Plan Review |
| Plan Review | you | read the plan, edit it where you disagree, then press p to promote: Implementing to build it, Planning to re-plan, Needs Attention to park it |
| Implementing | agent | reads its brief, commits, pushes, opens a draft PR with gh or adds to the ticket’s existing one, then reviews and fixes its own work until a round is clean or three rounds are up, then ends by leaving a verdict comment and marking the PR ready → In Review, or moves itself → Needs Attention |
| In Review | you | merge the PR, and Linear’s GitHub integration moves it to Done |
| Needs Attention | you | where failed runs and unsettled reviews park, with no queue watching it, so nothing retries it |
Which ticket enters where is the only routing decision, and it is made by moving a ticket, not by configuration.
Review happens inside Implementing
There is no review queue. A stage that hands work back draws a cycle, and bounding a cycle needs a round count, which is state outside Linear (SCOPE.md carries the argument). So Implementing reviews and fixes its own work inside one run, leaving each round’s findings as a review on the pull request and one verdict comment on the ticket.
The verdict is also the check on the review. The hop out of Implementing keys on the run’s exit code and the ticket’s status, never on a pull request, so the prompt requires a run to end either with a verdict and the PR marked ready, or in Needs Attention saying what stopped it. The comment goes on before the PR flips to ready, which keeps the run safe on a team that still has a mid-stage PR automation on.
Needs Attention holds what three rounds could not settle, and it is a loop rather than the end of the line. Say what you want on the pull request, promote the ticket back to Implementing, and the next run reads your unanswered threads as its brief, checks out the same branch, and adds commits. There is no re-run flag anywhere, only a ticket in a queue status with its description, comments and pull request as the state.
Two things it assumes
- Every agent CLI needs its own Linear access. Lerp passes the ticket identifier and nothing else, so the plan, the pull request and the verdict are all written by the agent. Install sets that up. Without it, a run silently fails to read its ticket.
- The stock runner wants a permission grant.
lerp initasks before including it and defaults to leaving it out. See permission grants above.