Getting started

What do I need to run Last Light?

Three things:

  1. An API key for your chosen model — OPENAI_API_KEY, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY (Last Light runs on any model the pi coding agent supports — Claude, GPT, Gemini, Llama, and more; one OpenRouter key reaches most of them through a single endpoint)
  2. A GitHub App with Issues, PRs, and Contents permissions
  3. Node.js 20+. Docker is required only when LASTLIGHT_SANDBOX=docker; the default gondolin sandbox uses HVF/KVM with no Docker dependency.

See the Get Started guide for step-by-step instructions.

Which LLM does it use?

The agent runtime is agentic-pi (workflow phases) plus @earendil-works/pi-ai (in-process chat). It runs on any model the pi coding agent supports — Claude, GPT, Gemini, Llama, and more, as openai/…, anthropic/… or openrouter/<vendor>/<model> strings. The default model is anthropic/claude-sonnet-4-6, configurable via the LASTLIGHT_MODEL environment variable.

Per-phase overrides go through LASTLIGHT_MODELS as JSON — e.g. {"chat":"openai/gpt-5.1-mini","architect":"openai/gpt-5.5"}. Set OPENAI_API_KEY, ANTHROPIC_API_KEY, and/or OPENROUTER_API_KEY to match — one OpenRouter key covers most providers. Legacy OPENCODE_* env vars are still read as fallbacks.

Does it work with GitHub Enterprise?

Not yet tested, but it should work with GitHub Enterprise Cloud since it uses the same API. GitHub Enterprise Server (self-hosted) would need base URL configuration in the GitHub tooling layer — contributions welcome.

How it works

How does the bot authenticate with GitHub?

Last Light authenticates as a GitHub App, not a personal account. It generates short-lived installation tokens (expire in ~1 hour) using your App's private key. Every action — comments, labels, PR reviews — shows up as coming from the bot, not you.

The harness handles token generation, refresh, and per-workflow downscoping automatically (see permission profiles below). For git operations inside sandboxes, a credential file is synced before each command.

What's the difference between webhook mode and cron mode?

Webhooks react in real-time — GitHub sends events (issue opened, PR created, comment posted) to your endpoint, and the agent handles them immediately. Requires a publicly reachable URL.

Cron polls on a schedule — the agent checks for new issues, PRs, and stale items periodically (e.g. every 15 minutes). No public URL needed, but responses are delayed.

Use one or the other — not both. Webhooks are better for responsive repos; cron is simpler to deploy.

Can the bot actually fix bugs and open PRs?

Yes. When a maintainer @mentions the bot on an issue, it clones the repo into an isolated sandbox (a gondolin micro-VM by default, or a Docker container if LASTLIGHT_SANDBOX=docker), reads the code, makes changes, runs tests, and opens a PR. It's not magic — it works best for well-scoped issues with clear descriptions.

The bot always links back to the original issue and posts validation results so you can review before merging.

What happens when it doesn't know what to do?

It does nothing. Silence is built into the design — the agent only acts on events it has explicit rules for. Unknown event types, ambiguous requests, and bot-generated events are all ignored. For build requests, if it can't push code, it posts a detailed comment explaining what it tried and what needs human attention.

What's the difference between Sandbox Sessions and Chat Sessions?

Sandbox sessions are agent runs that executed inside a sandbox — triage, PR review, build, health report, anything driven by a YAML workflow. Each phase runs agentic-pi run in a gondolin micro-VM (or Docker container when LASTLIGHT_SANDBOX=docker); events are streamed into a Claude-SDK-style JSONL envelope on disk and listed on the dashboard's Sandbox Sessions tab.

Chat sessions are conversations from Slack threads. They run an in-process @earendil-works/pi-ai chat loop inside the harness — one pi-ai conversation per Slack thread, history rehydrated from the messaging_messages SQLite table on every turn, so context holds for hours or days. Every turn is persisted (executions table) and one thread becomes one growing, coherent JSONL. They live on the dashboard's Chat Sessions tab.

In short: sandbox sessions are one-shot workflow runs; chat sessions are multi-turn conversations glued to a Slack thread.

How do approval gates work?

A workflow phase can declare approval_gate: post_architect (or any other gate name). If that gate is enabled via the APPROVAL_GATES env var, the runner pauses after that phase, persists the run state, writes a row to the workflow_approvals table, and waits for a human decision.

You resolve gates three ways: commenting @last-light approve or @last-light reject on the triggering GitHub issue; running /approve / /reject from Slack; or clicking the button on the dashboard. The run resumes exactly where it stopped — no phases re-execute.

Because gates are opt-in per environment, you can ship workflows with gates pre-declared and enable them only in production.

Where do the build handoff docs live — and can I keep them out of the target repo?

During a build, each phase writes a handoff doc — architect-plan.md, status.md, executor-summary.md, reviewer-verdict.md, guardrails-report.md, and the explore-* docs. By default (buildAssets.location: repo) these are committed into the target repo under .lastlight/<issueKey>/ and ride the working branch.

If you'd rather not commit them, set buildAssets.location: server (or the LASTLIGHT_BUILD_ASSETS=server env var). The docs are then externalized to the Last Light host — written under $STATE_DIR/build-assets/<owner>/<repo>/<issueKey>/*.md (override the root with BUILD_ASSETS_DIR), injected into each phase from outside the repo and harvested back after. PR-body links point at the dashboard's Artifacts view instead of GitHub blob URLs, and the admin API serves them read-only at /admin/api/artifacts. Nothing is committed into the target repo.

How do permission profiles downscope the GitHub token?

The GitHub App itself has broad permissions (contents read/write, issues read/write, PRs read/write). But every workflow declares a profile — one of read, issues-write, review-write, or repo-write. Before spawning a sandbox, the harness mints an installation token that only has the scopes that profile needs.

The practical effect: a triage run literally cannot push code, a review run cannot open issues, etc. Even if a prompt got compromised, the token handed into the sandbox doesn't have the write permission in the first place.

Customization

How do I change what the bot does?

Edit the YAML in workflows/. Every behavior is a workflow — build.yaml, issue-triage.yaml, pr-review.yaml, cron-health.yaml, etc. A workflow is a list of phases; each phase runs an agent with either an inline prompt (prompt: prompts/architect.md) or a named skill (skill: assure-guardrails).

To change an existing behavior, edit its workflow file or its referenced prompt under workflows/prompts/ or its SKILL.md under skills/. To add a new behavior, drop a new YAML file into workflows/ — the harness picks it up automatically. No TypeScript needed.

Can I add my own skills?

Yes. Create a new directory under skills/ with a SKILL.md file — name, description, and tags in the frontmatter, instructions in the body. Skills are plain Markdown that the agent follows as instructions. The agent discovers new skills automatically.

How do I add more repos?

Just install the GitHub App on additional repos — no code changes needed. The bot handles all installed repos automatically.

Can a single repo have its own settings?

Yes — a managed repo can commit a .lastlight/ directory that overrides a bounded subset of config for runs against itself: models and reasoning effort, extra approval gates, which scheduled jobs it takes part in, and its own prompts, skills and agent-context files. Same on-disk shape as the deployment overlay, so lastlight repo fork scaffolds it from the real built-ins and lastlight repo config validate checks it offline before you push.

It is always read from the repo's default branch, never a pull-request head — so a PR can't reconfigure the agent reviewing it. A repo can never contribute workflow YAML (phases, permission profiles and gates stay yours), can only add approval gates rather than remove them, and can only add agent-context files rather than shadow your rules.md/security.md. What's on offer at all is bounded by a repoConfig: block you control, and the whole feature can be switched off with repoConfig.enabled: false. See Per-repo config.

Operations

How much does it cost to run?

The main cost is model API usage on whichever model you've configured. Each issue triage or PR review is roughly one agent session. For a repo with a few events per day, expect modest API costs. Build requests (cloning, editing, testing) use more tokens but are less frequent.

Set OPENAI_API_KEY, ANTHROPIC_API_KEY, and/or OPENROUTER_API_KEY to match your LASTLIGHT_MODEL and costs depend on usage. Cheap models like anthropic/claude-haiku-4-5-20251001 or openrouter/google/gemini-2.5-flash can be routed to high-volume phases (triage, screen) via LASTLIGHT_MODELS to keep the bill down. OpenRouter adds a small per-token markup over going direct, but in exchange you get a single key + dashboard for every model.

The agent itself is free and open-source.

What if the model provider goes down?

If the configured model provider is unavailable, agent sessions will fail and the error is logged. GitHub webhook events are retried by GitHub automatically if your endpoint doesn't respond, so the work will be picked up when service returns. Because pi-ai is provider-agnostic, you can flip LASTLIGHT_MODEL to a different provider during an outage. The execution database tracks all attempts for visibility.

How do I tell the bot to leave one issue or PR alone?

Apply the hold label — lastlight-ignore by default. Last Light then stops acting on that issue or pull request entirely: no triage, no review, no fix, no merge, and no comment. It outranks every other rule except "we couldn't read the pull request at all", including a direct @last-light … request — which gets exactly one reply naming the label, so a direct instruction is refused rather than silently ignored.

Remove the label and the bot resumes. Nothing is persisted either way, which is the point: it is a live precondition re-read on every event, not a stored verdict you have to clear. GitHub already requires triage permission to change a label, so it needs no separate permission model. Rename it with LASTLIGHT_HOLD_LABEL (overlay key hold.label) — but the packaged prompts create the default name, so a rename means creating the label by hand or forking that prompt.

Don't use requires-human for this. That one is a notification the bot writes when it gives up; nothing reads it, and taking it off now means the opposite — "have another go".

Last Light gave up on my PR. How do I get it going again?

When the attempt or cost budget for a PR runs out, Last Light labels it requires-human, comments once explaining what it tried, and stops. Four things un-stick it, and they all do exactly the same thing — re-arm the attempt counter and the cost window for a fresh full budget:

  • Push a commit to the branch (anyone's but the bot's).
  • Comment @last-light retry, optionally with a reason: @last-light retry the arm64 runner was flaky. The reason is recorded and handed to the next attempt.
  • Remove the requires-human label.
  • Run lastlight pr retry owner/repo#42 [reason] from a terminal — the only one that dispatches the stuck workflow immediately rather than waiting for the next event.

A retry keeps what the earlier attempts learned; a push wipes it, because a push changed the code. Retries are unbounded — each buys a full window — so it is a real spending decision, and each one is recorded (who asked, when, why) on the run that follows. Nothing you ask for overrides the hold label, a run already working that PR, a fork PR, a red base branch, or a PR the harness could not read.

Is there a risk of the bot going rogue?

The bot can only do what its GitHub App permissions allow. It can't delete repos, manage org settings, or access repos where it's not installed. Build requests only trigger when a maintainer explicitly @mentions the bot. All actions are logged as session trajectories in logs/.

For extra safety: install the App with read-only Contents permission if you don't want the bot pushing code, and only grant write access to Issues and PRs.