Docs

Autonomous mode

In autonomous mode, an issue moves through Last Light on its own. Once triage marks it ready, the harness picks it up, builds it, and parks the pull request where a human will see it. Nobody has to type @last-light build. Labels on the issue record how far it has got. The dashboard's Board shows every issue in the pipeline as a card that you can drag, retry, or approve.

The pipeline ships switched off, and it starts cautious even once you switch it on. Two approval gates are on by default, and a human merges every pull request. You loosen those settings yourself, one at a time, until the pipeline runs as hands-off as you want.

The Board tab of the Last Light dashboard: four columns — Ready for agent, Agent building, Ready for human and Agent blocked. Two issues from nearform/lastlight-test-repo sit in Ready for human, each with a green SUCCEEDED band, a link to the open pull request that closes it, and its labels.
The Board. Each column is a label in the pipeline. Both issues have been through a successful build, and each now has an open pull request that closes it, waiting for a human.

How an issue moves through it

The pipeline has one stage, build, and four labels. Each label becomes a column on the Board:

ready-for-agent ──▶ agent-building ──▶ ready-for-human   (succeeded)
                                   └─▶ agent-blocked     (failed, cancelled, out of budget)
  1. Triage marks it ready. The issue-triage workflow adds ready-for-agent to issues that are fully specified. A maintainer can also add the label by hand.
  2. The label starts a build. The issues.labeled webhook dispatches a build run. A backstop cron (pick-up-ready-issues, every 20 minutes) catches any issue whose label event was never delivered.
  3. The label moves at dispatch, before the run starts. The harness swaps ready-for-agent for agent-building first. The issue then acts as its own lock: a redelivered webhook or the next sweep tick no longer sees an entry to pick up.
  4. The run ends on a terminal label. A success moves the issue to ready-for-human, and the pull request that closes it appears as a link on the card. A failure or a cancel moves it to agent-blocked, with the reason shown on the card.

Turning it on

Autonomy is configured by the operator in the overlay's config.yaml. The whole pipeline is already in the shipped defaults, so opting a repository in takes one line:

# instance/config.yaml
autonomy:
  repos:
    - acme/web
    - acme/api

Names must match exactly, with no globs. Matching is plain string equality, so acme/* is a repo name that will never exist. The restriction is deliberate: if a pattern could enable a whole org, any new repo would get an agent and a budget the moment somebody created it, before anyone had reviewed it.

A repository can't opt itself in. Autonomy spends the operator's model budget, so there is no autonomy key in per-repo config. If a repo could list itself, anyone who could commit a file could spend the operator's money. A repo can opt out: disabled.workflows: [build] in its .lastlight/lastlight.yml turns the pipeline off for the whole repo. The hold label (lastlight-ignore) turns it off for a single issue.

These are the full defaults you are layering over:

autonomy:
  repos: []                      # ships inert: no repo, no pipeline
  stages:
    build:
      enter: ready-for-agent       # the label that starts the stage
      running: agent-building      # applied at dispatch
      on_success: ready-for-human  # a human reviews, and merges, the PR
      on_failure: agent-blocked    # the run stopped; a human reads why
      workflow: build
      gates:
        post_architect: true       # pause after the plan
        post_reviewer: true        # pause after the independent review
      on_merge: none               # none | auto | auto-low-impact
  budget:
    maxConcurrentBuilds: 2         # across every repo, at any instant
    maxBuildsPerRepoPerDay: 3      # per repo, per UTC day
    dailyUsd: 25                   # deployment-wide model spend per day
    repoDailyUsd: 10               # one repo's share of it

Overlay config merges into these defaults key by key. You only set the keys you want to change. Labels are renamed in the same way; the Board takes its column titles from whatever labels you configure.

Gates and merging: from supervised to hands-off

By default an autonomous build stops twice for a human, then stops a third time at the merge button.

  • post_architect pauses once the architect has written its plan, before any code is written.
  • post_reviewer pauses once the independent reviewer has finished, before the pull request is finalised.
  • on_merge: none leaves the pull request on ready-for-human for a human to merge.

You can resolve a gate from the card on the Board, from the dashboard's run view, by commenting @last-light approve on the issue, or from Slack. The run then resumes exactly where it paused.

Stage gates can only add gates. Only true values take effect, so a stage can switch on a gate the workflow left off, but it can never switch off one that your APPROVAL_GATES setting enabled. The result is that an autonomous build pauses at post_architect, while an @last-light build on the same repo does not, unless your environment enables that gate for every build.

on_merge decides what happens to the pull request:

ValueBehaviour
none The default. The pull request waits on ready-for-human and a human merges it.
auto The build agent turns on GitHub auto-merge for the pull request it opened. CI and your branch protection still decide whether and when it lands. The agent never merges the pull request itself.
auto-low-impact Not implemented yet. There is no impact signal for a feature pull request today. The value is accepted and recorded, and it behaves like none.

For a fully hands-off pipeline, from triage label to merged pull request:

# instance/config.yaml
autonomy:
  repos:
    - acme/web
  stages:
    build:
      gates:
        post_architect: false
        post_reviewer: false
      on_merge: auto

Setting a gate to false only removes the stage's own gate. If APPROVAL_GATES in your environment names post_architect or post_reviewer, those gates still pause every build, autonomous or not.

Branch protection becomes the last human decision. With both gates off and on_merge: auto, nothing stops for a person. Once a pull request passes the checks you require, it merges. Only do this on a repo where required status checks, and ideally a required review, would stop a change you don't want. Start with one repo and a low budget.

Safety limits

Every autonomous dispatch passes through one build dispatch gate, whichever way it arrived: the label webhook, the backstop sweep, the Board, or the API. The gate checks, in this order:

  1. The hold label. An issue carrying lastlight-ignore is skipped.
  2. The allow-list. A repo not on autonomy.repos is skipped.
  3. Already built. If the bot re-applied the entry label to an issue it has already built, the gate skips it. This is what stops a label loop. If a human re-applied the label, it counts as an explicit retry and the build runs again.
  4. Already running. An issue with a build queued, running or paused is skipped. Two agents can't share one build workspace.
  5. The budget, in this order:
    • maxConcurrentBuilds: autonomous builds in flight across the deployment. A build paused at a gate doesn't count.
    • maxBuildsPerRepoPerDay: builds started today, whatever their outcome. A failed build still cost money.
    • dailyUsd and repoDailyUsd: model spend today, for the deployment and for the repo.

Every comparison is >=, so a configured 0 refuses everything. That is how you stop the pipeline without un-configuring it. The day-based limits reset at midnight UTC, the same day boundary the dashboard's stats use.

How loudly a refusal is reported depends on how long it will last. A concurrency skip clears within minutes, so it only writes a log line. A repo's daily quota writes an autonomy.skip row in the activity log. An exhausted spend budget also moves the issue to agent-blocked and posts one comment on the issue. The comment names the limit, when it resets, and how to re-arm. No limit is enforced mid-run: all checks happen before a build starts.

The Board

The dashboard's Board tab is the pipeline's control surface. It has four columns per stage, one for each label, in pipeline order. Its universe is the autonomy allow-list, so a deployment with autonomy.repos: [] shows an empty board and makes no GitHub reads at all.

  • Cards are issues. Each card shows the issue's latest run, the phase that run is actually in, any pending approval, the failure reason if it failed, a short excerpt of the issue, and the pull requests that close it.
  • Dragging a card moves work. Dropping a card on a stage's entry or running column starts a build through the same dispatch gate, as you, the logged-in human. Dropping it on a terminal column only moves the label. If the gate refuses, for example because the budget is spent or a build is already running, the card shows the reason.
  • The card menu lists what's possible right now. Actions include approve or reject a pending gate, cancel a run, retry (resume the failed run from the phase that failed), Rebuild, and Unblock & rebuild (start again from the entry column). The server works out which actions are enabled and why, so the Board never offers anything the gate would refuse.
  • Filters. Needs you narrows the cards to the ones paused at an approval gate. Unstaged opens a drawer of open issues that carry no stage label yet, so you can drag them in.
  • It updates live. A server-sent change signal refetches the board whenever a label moves or a run changes phase. GitHub reads are batched and cached, so an open Board costs about sixty GitHub requests an hour for twenty repos, however many people are watching.

Further reading