lastlight
v0.10.4
Published
GitHub repository maintenance agent — Agent SDK harness
Maintainers
Readme
An AI agent that maintains GitHub repositories: triaging issues, reviewing PRs, monitoring repo health, and building features through an Architect → Executor → Reviewer development cycle.
Built on agentic-pi (workflow phases) and @earendil-works/pi-ai (in-process chat) with a lightweight TypeScript harness for webhook ingestion, cron scheduling, and process management. Provider-agnostic — point LASTLIGHT_MODEL at any provider/model pi-ai supports (defaults to anthropic/claude-sonnet-4-6).
Production Setup (Clean Server)
The fastest way to go from a bare server to a running Last Light instance:
npx lastlight setupThe setup wizard walks you through:
- GitHub App — enter your App ID, Installation ID, and PEM key path
- Domain & TLS — optional Caddy config for automatic HTTPS
- Managed repositories — the
owner/repolist the bot operates on - Model provider + API key — pick from any of pi-ai's 15+ supported
providers (Anthropic, OpenAI, Google Gemini, Mistral, Groq, Cerebras, xAI,
Hugging Face, Moonshot, NVIDIA, Fireworks, Together, DeepSeek, Z.AI,
Kimi for Coding, MiniMax, OpenRouter), then enter the model id and the
matching API key. See
src/providers.tsfor the full registry. - Webhook secret — auto-generated if you don't have one
- Slack — optional bot token and app token for Slack integration
- Admin dashboard — optional password protection
It scaffolds your private deployment overlay at instance/ — writing
instance/config.yaml (your managed repos), instance/secrets/.env, and copying
your PEM to instance/secrets/app.pem (mode 600) — then offers to build and start
the Docker stack. When it's done you have a running instance ready to receive
webhooks. Everything deployment-specific lives in instance/, which is mounted
read-only and never baked into the image; edit it and docker compose restart agent
to apply (no rebuild). See Deployment overlay for the model.
Requires: Node.js 20+, Docker, and a GitHub App already created (see Create a GitHub App below).
For a Docker-free production install (systemd unit, gondolin sandbox), see Native deploy below.
Quick Start (Local Dev)
Prerequisites
- Node.js 20+
- Docker Desktop (or compatible) — only needed for
LASTLIGHT_SANDBOX=docker; gondolin runs without it on macOS/Linux - A GitHub App (see Create a GitHub App below)
- An API key for whichever provider your chosen
LASTLIGHT_MODELuses. The wizard surfaces pi-ai's 15+ providers — seesrc/providers.tsfor the full registry (e.g.ANTHROPIC_API_KEYfor anthropic/…,OPENAI_API_KEYfor openai/…,GROQ_API_KEYfor groq/…,GEMINI_API_KEYfor google/…,OPENROUTER_API_KEYfor the openrouter/… aggregator).
Setup
git clone https://github.com/nearform/lastlight.git
cd lastlight
npm installCopy and edit the environment file:
cp .env.example .envFill in the required values in .env:
# GitHub App (required)
GITHUB_APP_ID=123456
GITHUB_APP_PRIVATE_KEY_PATH=./your-app.private-key.pem
GITHUB_APP_INSTALLATION_ID=789012
# Webhook secret (required for webhook mode)
WEBHOOK_SECRET=your-secret-here
# Model + provider — the wizard surfaces pi-ai's 15+ providers. The
# registry lives in src/providers.ts; pick any `provider/model` it lists.
LASTLIGHT_MODEL=anthropic/claude-sonnet-4-6
ANTHROPIC_API_KEY=sk-ant-...
# OPENAI_API_KEY=sk-...
# OPENROUTER_API_KEY=sk-or-...
# GROQ_API_KEY=gsk_... GEMINI_API_KEY=AIza... HF_TOKEN=hf_...
# XAI_API_KEY=...
# ZAI_API_KEY=... MISTRAL_API_KEY=... FIREWORKS_API_KEY=...
# Sandbox backend (default: gondolin; alternatives: docker, none)
# LASTLIGHT_SANDBOX=gondolinRun
npm run dev runs the harness on your host. Sandbox mode is selected by LASTLIGHT_SANDBOX:
gondolin(default) — agentic-pi spawns a per-phase QEMU micro-VM in-process. Uses HVF on macOS, KVM on Linux. No Docker needed.docker— agentic-pi runs inside a per-phase sibling Docker container (thelastlight-sandbox:latestimage). Requires Docker. Useful for prod-like smoke testing.none— agent runs in-process on your host with no isolation. Dev only — never in production.
The dev script is explicitly safe with your personal config:
| | Touched? |
|---|---|
| ~/.gitconfig (your identity, credential helper) | ❌ skipped (LASTLIGHT_LOCAL_DEV=1) |
| ./data/agent-sessions/ | ✅ project-local; shim envelope jsonls for the dashboard live here |
| ./data/sandbox-data/ | ✅ project-local bind-mount when using LASTLIGHT_SANDBOX=docker |
| ./data/lastlight.db, ./data/sandboxes/, ./data/logs/ | ✅ project-local state, gitignored |
If you want the Docker sandbox mode locally, build the image once first:
docker compose --profile build-only build sandboxThen run the harness (server + dashboard, with hot reload):
npm run dev # both server and dashboard, concurrent
npm run dev:server # server only
npm run dev:dashboard # dashboard onlyBoth server scripts call scripts/dev-local.sh, which:
- Verifies the sandbox image exists when
LASTLIGHT_SANDBOX=docker - Copies your
GITHUB_APP_PRIVATE_KEY_PATHinto./data/sandbox-data/secrets/app.pem(mode 600) so the sandbox can authenticate to GitHub - Sets
LASTLIGHT_LOCAL_DEV=1,STATE_DIR=./data,LASTLIGHT_SESSIONS_DIR=./data/agent-sessions - Starts the harness with
tsx watch src/index.ts
Triggering work via the CLI
The CLI talks to the running server — it does not execute agents directly. Start the server first, then in another terminal:
# Cheap, safe defaults — single agent invocation
npx tsx src/cli.ts owner/repo#42 # triage that one issue
npx tsx src/cli.ts https://github.com/owner/repo/issues/42 # same, full URL form
npx tsx src/cli.ts https://github.com/owner/repo/pull/99 # review that one PR
npx tsx src/cli.ts triage owner/repo # scan repo for new issues to triage
npx tsx src/cli.ts review owner/repo # scan repo for PRs to review
npx tsx src/cli.ts health owner/repo # weekly health report
# Expensive, opt-in — full Architect → Executor → Reviewer → PR cycle
npx tsx src/cli.ts build owner/repo#42
npx tsx src/cli.ts build https://github.com/owner/repo/issues/42The default for a single-issue/PR shorthand is the cheap action (triage or review). Build cycles require the explicit build subcommand to opt in.
Authentication
pi-ai picks credentials from the provider env vars the harness forwards (the full listed set lives in src/providers.ts — Anthropic / OpenAI / OpenRouter / Google / Mistral / Groq / Cerebras / xAI / HuggingFace / Moonshot / NVIDIA / Fireworks / Together / DeepSeek / Z.AI / Kimi / MiniMax). The harness forwards them into each sandbox container (or VM) so workflow runs can reach the API.
Docker Deployment
The docker-compose stack is useful when you want a single docker compose up -d deploy. For gondolin (and a smaller deployment surface area), prefer the native systemd deploy instead.
Build and Run
Build both the harness image and the sandbox image. The sandbox image is what the harness spawns per phase when LASTLIGHT_SANDBOX=docker. The sandbox service is under the build-only profile so it is never started — it is only built.
docker compose build agent
docker compose --profile build-only build sandbox
docker compose up -d agentSet LASTLIGHT_SANDBOX=docker in your .env. (Inside the harness container, gondolin's QEMU path isn't available unless you do the nested-virt setup yourself — the docker-sandbox path is the practical default for Docker deployments.)
Deployment overlay
Everything specific to your deployment — managed repos, model/route/approval
overrides, agent-context, secrets — lives in a single instance/ folder
next to docker-compose.yml. It's mounted read-only at /app/instance
(LASTLIGHT_OVERLAY_DIR=/app/instance) and is never committed to the public
repo or baked into the image, so it's the natural home for a private config
repo.
instance/
config.yaml # overlay config — merged over the public config/default.yaml
agent-context/*.md # (optional) persona/rules overrides, merged by filename
workflows/*.yaml # (optional) add or replace workflows by logical name
skills/<name>/SKILL.md # (optional) skill overrides
secrets/ # host-only, gitignored: .env + GitHub App *.pem
.env
app.pemBoth npx lastlight setup (the config wizard) and lastlight server setup
scaffold this for you and then offer to version it as a private repo —
git init + an initial commit, then gh repo create … --private --push when
the GitHub CLI is authenticated, or the exact git/GitHub commands to run by hand
otherwise. server setup also lets you point at an existing overlay repo to
clone instead. To do it fully by hand:
mkdir -p instance/secrets
cp deploy/.env.production.example instance/secrets/.env # then fill it in
cp your-app.private-key.pem instance/secrets/app.pem
chmod 600 instance/secrets/.env instance/secrets/app.pem
# instance/config.yaml — at minimum your managed repos:
printf 'managedRepos:\n - your-org/repo-one\n' > instance/config.yamlBoth the agent and caddy services read instance/secrets/.env via
env_file, and the entrypoint also sources it inside the container — so no
repo-root .env is needed. Merge rules: maps (models, variants, routes,
approval) deep-merge over the public defaults; arrays (managedRepos,
disabled.*) replace; environment variables override both. Overlay files are
read at startup — edit and docker compose restart agent to apply, no rebuild.
The dashboard Config tab shows Default / Overlay / Merged (non-secret) config
(secret-looking keys are redacted, so a stray secret in config.yaml won't leak).
Startup is fail-fast: if LASTLIGHT_OVERLAY_DIR is set but the folder is
missing or empty (the common "forgot to populate instance/" case), or a cron
targets a missing workflow, or a phase's prompt/skill can't resolve, the harness
exits 78 with a clear message instead of booting a broken instance.
Expose Webhooks
To receive GitHub webhooks, the server needs to be publicly reachable. The included Caddy config handles HTTPS — set DOMAIN in instance/secrets/.env:
# In instance/secrets/.env:
# DOMAIN=lastlight.example.com
# Start both agent and caddy
docker compose up -dOr use ngrok / Cloudflare Tunnel for testing.
State & Monitoring
All persistent state lives in a single Docker volume (agent-data), mounted at /app/data:
data/
lastlight.db # SQLite: executions, workflow_runs, approvals, messaging sessions
agent-sessions/ # Dashboard JSONL envelope store (written by event-shim.ts)
projects/-app/*.jsonl # Chat sessions (one per Slack thread)
projects/-home-agent-workspace/*.jsonl # Sandbox-mode workflow sessions
sandboxes/ # Cloned repos per task (gondolin or docker)
sandbox-data/ # Shared volume mounted into docker-mode sandboxes
logs/ # Structured logs
secrets/app.pem # GitHub App PEM (mode 600) for sandbox accessMount this volume or bind-mount the directory for monitoring tools to access session logs and the execution database.
Trigger Work via CLI
With the container running:
# Health check
curl http://localhost:8644/health
# Trigger a build cycle
npx tsx src/cli.ts https://github.com/owner/repo/issues/42
# Trigger triage
npx tsx src/cli.ts triage owner/repoNative (systemd) Deploy
For a Linux production host with KVM available (/dev/kvm), the native deploy runs the harness directly under systemd and uses gondolin for sandboxing — no Docker required.
See deploy/native/README.md for the full runbook. The short version:
git clone https://github.com/nearform/lastlight.git /opt/lastlight
cd /opt/lastlight
# (optional) install -m 0600 -o root /path/to/app.pem /etc/lastlight/app.pem
sudo bash deploy/native/install.sh # scaffolds /etc/lastlight/lastlight.env
sudo $EDITOR /etc/lastlight/lastlight.env # fill in secrets
sudo bash deploy/native/install.sh # second run: starts the serviceRe-deploys: git pull && sudo bash deploy/native/install.sh (idempotent — rebuilds and restarts the service).
Required: the host kernel must expose /dev/kvm (bare-metal Linux or KVM-enabled VM). Hetzner Cloud, Cloud Run, Fly Machines (without --vm-cpu-class shared), and most managed container hosts do not expose nested virt — see agentic-pi's SPIKE-gondolin.md for the full constraint matrix.
If KVM isn't available, fall back to the Docker deploy above with LASTLIGHT_SANDBOX=docker.
Setup Details
1. Create a GitHub App
- Go to https://github.com/settings/apps/new
- Fill in:
- Name: your bot name (appears on comments/PRs with a
[bot]badge) - Homepage URL: your repo URL
- Webhook URL:
https://your-domain:8644/webhooks/github(or leave blank for now) - Webhook Secret: a random string (same as
WEBHOOK_SECRETin.env)
- Name: your bot name (appears on comments/PRs with a
- Set permissions:
- Issues: Read & Write
- Pull Requests: Read & Write
- Contents: Read & Write
- Checks: Read & Write (post the
last-light/reviewcheck; receive its "Re-run" requests) - Metadata: Read
- Subscribe to events:
Issues,Pull request,Issue comment,Check run,Check suite(the last two enable the GitHub "Re-run checks" buttons to re-trigger a review) - Click Create GitHub App
- Click Generate a private key — save the
.pemfile into the project directory - Note the App ID from the app settings page
- Click Install App → install on your repos
- Note the Installation ID from the URL:
github.com/settings/installations/{ID}
2. Environment Variables
Legacy OPENCODE_* names are still read as fallbacks for the corresponding LASTLIGHT_* names, so existing .env files from the OpenCode era keep working.
| Variable | Required | Description |
|----------|----------|-------------|
| GITHUB_APP_ID | Yes | GitHub App ID |
| GITHUB_APP_PRIVATE_KEY_PATH | Yes | Path to .pem file |
| GITHUB_APP_INSTALLATION_ID | Yes | Installation ID |
| WEBHOOK_SECRET | Yes | GitHub webhook signature secret |
| OPENAI_API_KEY | One of | API key for an openai/… model |
| ANTHROPIC_API_KEY | One of | API key for an anthropic/… model |
| OPENROUTER_API_KEY | One of | API key for the openrouter/… aggregator |
| GEMINI_API_KEY | One of | API key for a google/… model |
| MISTRAL_API_KEY | One of | API key for mistral/… |
| GROQ_API_KEY | One of | API key for groq/… |
| CEREBRAS_API_KEY | One of | API key for cerebras/… |
| XAI_API_KEY | One of | API key for xai/… (Grok) |
| HF_TOKEN | One of | API key for huggingface/… |
| MOONSHOT_API_KEY | One of | API key for moonshotai/… (Kimi) |
| NVIDIA_API_KEY | One of | API key for nvidia/… |
| FIREWORKS_API_KEY | One of | API key for fireworks/… |
| TOGETHER_API_KEY | One of | API key for together/… |
| DEEPSEEK_API_KEY | One of | API key for deepseek/… |
| ZAI_API_KEY | One of | API key for zai/… (GLM) |
| KIMI_API_KEY | One of | API key for kimi-coding/… |
| MINIMAX_API_KEY | One of | API key for minimax/… |
| … or any other provider/model whose key is forwarded by src/providers.ts | | The wizard surfaces the registered set; see src/providers.ts for the full list. |
| LASTLIGHT_OVERLAY_DIR | No | Trusted deployment overlay directory (the docker-compose stack mounts instance/ here as /app/instance). Startup loads config/default.yaml, optional $LASTLIGHT_OVERLAY_DIR/config.yaml, then env overrides; overlay assets under workflows/, workflows/prompts/, skills/, and agent-context/ replace built-ins. Secrets live in $LASTLIGHT_OVERLAY_DIR/secrets/. Restart required after changes. See Deployment overlay. |
| LASTLIGHT_MODEL | No | Default model (default: anthropic/claude-sonnet-4-6). Legacy: OPENCODE_MODEL. |
| LASTLIGHT_MODELS | No | Per-task model overrides as JSON, e.g. {"chat":"openai/gpt-5.1-mini","architect":"openai/gpt-5.5"}. Legacy: OPENCODE_MODELS. |
| LASTLIGHT_THINKING | No | Reasoning-effort default (off | minimal | low | medium | high | xhigh). pi-ai translates per-provider. Legacy: OPENCODE_VARIANT. |
| LASTLIGHT_THINKINGS | No | Per-task thinking-level overrides as JSON, e.g. {"architect":"high","reviewer":"high","triage":"minimal"}. Legacy: OPENCODE_VARIANTS. |
| LASTLIGHT_SANDBOX | No | Workflow sandbox backend: gondolin (default) | docker | none. |
| LASTLIGHT_OTEL_ENABLED | No | Enable OpenTelemetry export (default: false). Standard OTEL_* env vars alone do not enable telemetry. |
| LASTLIGHT_OTEL_SERVICE_NAME | No | OTEL service name (default: lastlight; falls back to OTEL_SERVICE_NAME). |
| LASTLIGHT_OTEL_INCLUDE_CONTENT | No | Include prompts/message/tool-result content in telemetry (default: false; sensitive, use carefully). |
| LASTLIGHT_OTEL_FORWARD_TO_SANDBOX | No | Forward sandbox telemetry to the backend (default: true). On the docker backend this routes through an in-network OTEL collector; on gondolin/none it forwards allowlisted OTEL_* env vars directly. |
| LASTLIGHT_OTEL_STRICT | No | Throw on OTEL initialization/export setup failure instead of warn-and-continue (default: false). |
| LASTLIGHT_OTEL_COLLECTOR_HOSTS | No | Comma-separated collector hostnames added to the strict sandbox egress allowlist. Used only by the gondolin backend (the docker backend reaches its collector internally and ignores this). |
| OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_*_ENDPOINT | No | Standard OTLP HTTP collector endpoints. Used by the harness directly, and by the in-network collector as its re-export target on the docker backend. |
| OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_*_HEADERS | No | Standard OTLP headers; secret/env-only and never shown in public config. |
| OTEL_SERVICE_NAME, OTEL_RESOURCE_ATTRIBUTES | No | Standard OTEL resource configuration. |
| LASTLIGHT_SESSIONS_DIR | No | Where the dashboard reads sessions (default: $STATE_DIR/agent-sessions). |
| PORT / WEBHOOK_PORT | No | Webhook listener port (default: 8644) |
| STATE_DIR | No | Persistent state directory (default: ./data) |
| DB_PATH | No | SQLite path (default: $STATE_DIR/lastlight.db) |
| MAX_TURNS | No | Reserved (kept for API stability) |
| BOT_LOGIN | No | Bot login name for self-event filtering (default: last-light[bot]) |
| LASTLIGHT_LOCAL_DEV | No | Set to 1 on dev machines to skip git config --global writes from git-auth.ts. The installation token still reaches sandboxes via GIT_TOKEN. |
| SANDBOX_DATA_VOLUME | No | Used only when LASTLIGHT_SANDBOX=docker. Either a Docker named volume (default: lastlight_agent-data) or a host path (/, ./, ../, ~) to bind-mount as /data inside each sandbox. Local dev uses ./data/sandbox-data. |
OpenTelemetry export
OpenTelemetry is disabled by default. Set LASTLIGHT_OTEL_ENABLED=true and configure standard OTEL exporter env vars such as OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, and OTEL_RESOURCE_ATTRIBUTES to export harness spans/metrics for workflow runs, phases, agent executions, PI event streams, and chat turns.
By default Last Light exports metadata only: workflow/phase names, repo, sandbox backend, model, success/stop reason, timing, tokens, and cost. Prompt text, message content, tool arguments, and tool outputs are redacted unless LASTLIGHT_OTEL_INCLUDE_CONTENT=true; that opt-in can export sensitive data and should only be used with a trusted collector.
When LASTLIGHT_OTEL_FORWARD_TO_SANDBOX=true (default), agentic-pi workflow sandboxes emit their own telemetry too. How it reaches the backend depends on the sandbox backend:
docker(production): sandboxes export OTLP to an in-network OTEL collector (aotel-collectorcompose service on thesandbox-egressnetwork, reached by a fixed internal IP). That collector re-exports to your real backend over its own outbound network leg. This means the sandbox only ever dials one fixed internal endpoint — collectors on any port or scheme (e.g.https://collector:4318) work without special egress rules, and the backend endpoint and auth headers (OTEL_EXPORTER_OTLP_HEADERS) stay host-side and are never forwarded into the untrusted sandbox. The collector cannot be redirected by sandbox traffic, so it adds no SSRF/exfil surface.gondolin/none:agentic-piruns in the harness process and already inherits the harness OTEL SDK. AllowlistedOTEL_*env vars are forwarded into the sandbox shell env directly;gondolinadds collector hosts (parsed fromOTEL_EXPORTER_OTLP_ENDPOINT, signal-specific endpoint env vars, andLASTLIGHT_OTEL_COLLECTOR_HOSTS) to its egress allowlist. Private/internal metadata hosts remain blocked.
Set LASTLIGHT_OTEL_FORWARD_TO_SANDBOX=false to disable sandbox telemetry entirely and keep collector endpoints/headers in the harness only.
3. Managed Repositories
The public config/default.yaml ships an empty managedRepos list — set
yours in the overlay (instance/config.yaml), which replaces the list wholesale:
managedRepos:
- your-org/repo-one
- your-org/repo-twoWebhooks for repos not in this list are filtered at the connector level; Slack/CLI commands targeting unmanaged repos are rejected. Install the GitHub App on each.
4. Customize Behaviour
Put deployment-specific changes in your overlay (instance/) instead of editing
packaged files — that keeps your config out of the public repo and applies on a
restart (no rebuild). See Deployment overlay for the full
model and layout.
instance/
config.yaml # non-secret config overrides only
workflows/*.yaml # add/replace workflows by logical `name`
workflows/prompts/*.md # prompt overrides/fallbacks
skills/<name>/SKILL.md # skill overrides/fallbacks
agent-context/*.md # merged by filename; overlay replaces built-ins
secrets/ # host-only, gitignored: .env + *.pemThe dashboard Config tab shows Default / Overlay / Merged non-secret config.
Overlay files are read at startup only; docker compose restart agent after changes.
| What | Where |
|------|-------|
| Managed repos, routes, models, variants, approvals, disables | overlay instance/config.yaml (over config/default.yaml) |
| Bot personality & communication style | agent-context/soul.md or overlay instance/agent-context/ |
| Operational rules, review guidelines, triage rules | agent-context/rules.md or overlay instance/agent-context/ |
| Skill definitions | skills/*/SKILL.md or overlay instance/skills/ |
| Workflow phases (Architect/Executor/Reviewer/PR) | workflows/*.yaml + workflows/prompts/ or overlay equivalents |
| Cron job schedules | workflows/cron-*.yaml or overlay workflows |
Claude Code skills
Last Light ships a Claude Code plugin that teaches Claude Code how to install, configure and operate Last Light for you. Install the skills, then in a Claude Code session just ask — e.g. "set up a Last Light server", "connect my CLI to my server", "fork the build workflow into my overlay", or "scaffold a Last Light evals workspace".
| Skill | Use it when you want to… |
|-------|--------------------------|
| lastlight-server | Install & configure a Last Light server (agent + docker stack). |
| lastlight-client | Point the lastlight CLI at a server and log in. |
| lastlight-overlay | Create a deployment overlay and fork workflows/prompts/skills/persona. |
| lastlight-evals | Scaffold & run a Last Light Evals workspace (datasets, models, comparisons). |
Install them with the CLI (version-matched to the installed lastlight, works
offline — uses the claude plugin marketplace when present, else copies the
skills into ~/.claude/skills):
lastlight skills install # → ~/.claude/skills (user scope)
lastlight skills install --scope project # → ./.claude/skills (this repo only)
lastlight skills list # show bundled skills + where they're installed
lastlight skills uninstallOr register the marketplace directly from a checkout of this repo:
claude plugin marketplace add ./
claude plugin install lastlight@lastlight-skillsThe skills live under plugins/lastlight/ (manifest in .claude-plugin/). These
are Claude Code skills — distinct from Last Light's internal sandbox skills in
the top-level skills/ dir.
Architecture
┌─────────────────────────────────────────┐
│ Connector Layer │
│ GitHub Webhook │ Slack Socket Mode │
│ ↓ │ ↓ │
│ Event Normalizer (EventEnvelope) │
└────────────────┬────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Core Engine │
│ Event Router (deterministic) │
│ ↓ │
│ Workflow Runner (YAML phases) │
│ - Sandbox: `agentic-pi run` per phase │
│ in a gondolin VM or docker container │
│ - Chat: in-process pi-ai loop │
│ one session per messaging thread │
│ ↓ │
│ Sandboxes (git clone per task) │
│ Cron Scheduler (health reports) │
│ State DB (SQLite execution log) │
└─────────────────────────────────────────┘How Events Flow
- GitHub webhook → connector verifies signature, filters noise (bot events, edits, labels), normalizes to
EventEnvelope - Router maps event type to skill deterministically (no LLM in the routing loop):
issue.opened→issue-triagepr.opened→pr-reviewcomment.createdwith@last-lightfrom a maintainer → routed by intent classifier (build / explore / question / triage / review / security / verify / qa-test / demo)
- Workflow runner loads the matching YAML, dispatches each phase to
executeAgent(src/engine/agent-executor.ts, which invokes agentic-pi) or, for chat,ChatRunner(src/engine/chat-runner.ts, in-process pi-ai) - Build workflow runs a multi-phase cycle:
- Phase 1: Architect — read-only analysis, writes plan to
.lastlight/issue-N/architect-plan.md - Phase 2: Executor — TDD implementation following the plan
- Phase 3: Reviewer — independent verification (no shared context with executor)
- Phase 4: Fix loop (up to 2 cycles if reviewer requests changes)
- Phase 5: Create PR
- Phase 1: Architect — read-only analysis, writes plan to
Cron
When webhooks are enabled, only the weekly reports (health + security) run on cron (issue/PR events arrive in real-time via webhooks). Without webhooks, triage and PR review also run on cron.
| Job | Schedule | Condition | |-----|----------|-----------| | Triage new issues | Every 15 min | Only without webhooks | | Check PRs for review | Every 30 min | Only without webhooks | | Weekly health report | Mondays 9am | Always | | Weekly security scan | Mondays 10am | Always |
Project Structure
lastlight/
src/
index.ts # Server entry point
cli.ts # CLI client (talks to server)
config.ts # Config loader (.env)
connectors/
types.ts # Connector + EventEnvelope interfaces
github-webhook.ts # GitHub webhook connector (Hono)
index.ts # Connector registry
engine/
router.ts # Deterministic event → skill routing
agent-executor.ts # Workflow phase runner: invokes agentic-pi
# (gondolin / docker / none backends)
chat-runner.ts # In-process pi-ai chat loop; one session per
# Slack/Discord thread, rehydrated from DB
chat.ts # Chat skill (delegates to ChatRunner)
github-tools.ts # Read-only GitHub tools surfaced to chat
event-shim.ts # Translates agentic-pi events → Claude-SDK
# envelope jsonl for the dashboard reader
profiles.ts # ExecutorConfig / ExecutionResult types +
# GITHUB_PERMISSION_PROFILES + loadAgentContext
llm.ts # One-shot LLM helper for screen + classifier
screen.ts # Prompt-injection screener
classifier.ts # Intent classifier (build / explore / triage / …)
git-auth.ts # GitHub App git credential setup
github.ts # Harness-side Octokit client (comments, etc.)
workflows/ # YAML workflow runner (see src/workflows/CLAUDE.md)
sandbox/ # Per-task workspace + docker-sandbox lifecycle
cron/
scheduler.ts # Cron with overlap protection
jobs.ts # Cron job registry
admin/ # Dashboard API (Hono) + session readers
state/
db.ts # SQLite execution tracking
agent-context/
soul.md # Bot personality, principles, communication style
rules.md # Operational rules, managed repos, review guidelines
skills/
github-orchestrator/ # Central build cycle coordinator
issue-triage/ # Issue labeling and triage
pr-review/ # Structured PR review
repo-health/ # Health reports
github/ # GitHub API workflow skills
software-development/ # Dev skills (architect, TDD, debugging)
workflows/ # YAML workflow definitions
build.yaml # Architect → Executor → Reviewer → PR
issue-triage.yaml # Label/triage an issue
issue-comment.yaml # Handle an @mention on an issue
pr-review.yaml # Structured PR review
pr-fix.yaml # Apply review feedback to a PR
pr-comment.yaml # Handle an @mention on a PR
explore.yaml # Shape an idea into a spec (web search)
answer.yaml # Research and answer a question
verify.yaml # Confirm/refute a claim by running code
qa-test.yaml # Browser/QA test pass
security-review.yaml # Security scan
security-feedback.yaml # Apply security review feedback
repo-health.yaml # Health report
demo.yaml # Recorded demo run
cron-*.yaml # Cron-kind triggers (triage, review, health, security)
prompts/ # Per-phase prompt templates
deploy/
entrypoint.sh # Docker entrypoint (harness container)
sandbox-entrypoint.sh # Sandbox container entrypoint (docker-sandbox mode)
native/ # Native (systemd) deploy artifacts
lastlight.service # systemd unit
install.sh # Idempotent provision + redeploy script
lastlight.env.example # Env template for /etc/lastlight/lastlight.env
README.md # Native-deploy operator runbook
Dockerfile # Harness image (test-only; prod uses native deploy)
sandbox.Dockerfile # Sandbox image for LASTLIGHT_SANDBOX=docker
docker-compose.yml
Caddyfile # Reverse proxy for HTTPSTroubleshooting
npm i -g lastlight prints deprecation warnings
npm warn deprecated [email protected]: No longer maintained …
npm warn deprecated [email protected]: Use your platform's native DOMException …These are harmless transitive deprecations, not Last Light deps:
prebuild-install is pulled by better-sqlite3 (its native-binary fetcher), and
node-domexception by @google/genai's node-fetch polyfill chain. They don't
affect the install — the package works. A future thin-CLI package split (so a
CLI-only install skips the server's native/AI deps) is the only clean removal.
Server won't start
# Check .env is loaded
npm run dev:server
# Look for "Required environment variable not set" errorsnpm run dev says the sandbox image is missing (docker-sandbox mode)
docker compose --profile build-only build sandboxWorkflow run fails with exit 127: sh: 1: agentic-pi: not found
The sandbox image needs agentic-pi baked in. Rebuild it:
docker compose --profile build-only build sandboxIf you're on an old lastlight-sandbox:latest, this picks up the install step that the current sandbox.Dockerfile performs.
Workflow hangs forever with no output (gondolin mode on a host without KVM)
Gondolin requires /dev/kvm on Linux or HVF on macOS. Inside a container with no nested virt, VM.create() succeeds but the first vm.exec() hangs. Symptoms: phase shows "running" indefinitely with no JSONL events after sandbox_status.
Either switch to LASTLIGHT_SANDBOX=docker (sibling containers via socket) or move the harness to a KVM-capable host. Full analysis: agentic-pi/SPIKE-gondolin.md.
Agent run fails with a quota / billing error
The runtime surfaces upstream errors as error_api with the verbatim provider message in the executions row:
- OpenAI: "Quota exceeded. Check your plan and billing details." → top up at https://platform.openai.com/account/billing
- Anthropic: "Credit balance is too low" → top up at https://console.anthropic.com
Chat replies fail with error_error
Check the agent logs for the underlying error — common causes:
- Wrong key for the chat model.
LASTLIGHT_MODELS={"chat":"anthropic/…"}but noANTHROPIC_API_KEYset. Fix the override or add the key. - Model id typo. Watch for
[config] Model: …in startup logs to confirm what's actually loaded.
Webhooks not arriving
# Check health endpoint
curl http://localhost:8644/health
# Test with a fake POST (should return 401 — invalid signature)
curl -X POST http://localhost:8644/webhooks/github -d '{}'
# Check Docker port mapping
docker compose ps