Docs

Slack integration

Slack integration is optional but recommended. Last Light uses three independent Slack features that you can enable one at a time.

  1. Chat — the bot listens for messages and @mentions, classifies intent, and runs an in-process @earendil-works/pi-ai chat loop to reply. Every Slack thread becomes a growing, DB-backed conversation; the harness rehydrates the message history from the messaging_messages SQLite table on every turn, so context stays coherent across hours or days. That transcript covers the whole thread, not just its chat turns — when the classifier sends a message to a workflow instead (a question to answer, a request to build), the question and the workflow's reply are recorded too, so a follow-up like "can you summarise that?" still knows what was said. Events arrive over the HTTP Events API by default (reliable, retried delivery); Socket Mode is available as a dev fallback.
  2. The weekly repo digest — a Monday-morning summary, per repository, of what happened and what Last Light did about it. Posted to a channel each repo chooses. See below.
  3. OAuth dashboard login — the admin dashboard offers "Login with Slack" via OpenID Connect. You can restrict login to one workspace so only your team can see execution history.

Create the Slack app

Go to api.slack.com/apps and click Create New AppFrom scratch.

Enable chat (HTTP Events API)

In the app's settings, do the following:

  1. OAuth & Permissions → add Bot Token Scopes:
    • app_mentions:read
    • chat:write
    • im:history, im:read, im:write
    • channels:history, groups:history
    • commands (if you want /approve, /reject slash commands)
  2. Install to Workspace. After install, copy the Bot User OAuth Token (xoxb-...) — that's SLACK_BOT_TOKEN.
  3. Basic Information → copy the Signing Secret — that's SLACK_SIGNING_SECRET. Deploy the harness with this set first (the Request URL below must already answer).
  4. Event Subscriptions → enable, set the Request URL to https://your-host/webhooks/slack (Slack verifies it with a one-time challenge), then subscribe to bot events:
    • message.im — direct messages
    • app_mention — @mentions in channels
    (Don't also subscribe message.channels — a channel @mention would then arrive twice, as both a message and an app_mention.)
  5. Slash Commands (optional) → create /approve, /reject, /status, /reset pointing at https://your-host/....

Add the tokens to .env (or instance/secrets/.env in production):

SLACK_BOT_TOKEN=xoxb-...
SLACK_SIGNING_SECRET=...        # enables webhook mode (the default)

Socket Mode (dev fallback). Without a public URL, set SLACK_MODE=socket and enable Socket Mode in the app (App-Level Token with connections:writeSLACK_APP_TOKEN=xapp-...). Mode is auto-detected when SLACK_MODE is unset: webhook if SLACK_SIGNING_SECRET is present, otherwise socket. Socket Mode is at-most-once and can drop messages under bursts, so prefer webhooks in production.

Optional restrictions and routing:

SLACK_ALLOWED_USERS=U01ABC,U02DEF  # Comma-separated user IDs allowed to interact
SLACK_DELIVERY_CHANNEL=C01XYZ      # Channel for cron health reports

Restart the harness. You should see a [slack] Connected log line, and DM'ing the bot should get a reply. Every conversation shows up on the dashboard's Chat Sessions tab.

The chat skill is intentionally not overridden with a small/cheap model in LASTLIGHT_MODELS — small models tend to refuse tool calls on the assumption they lack permission, creating false "I can't do that" replies. Leave it on the default mid/large-tier model.

The weekly repo digest

Once a week (Monday 09:00 by default), Last Light posts a per-repository summary to Slack: what happened in the repo, and what it did about it.

acme/widgets — week to 9 Aug

Review reliability dominated the week: three fixes closed the last
stranding modes and the digest stopped burying itself in preview cards.
Dependency work stayed quiet.

Merged (7)
• #342 Stop Slack unfurling every PR link — @cliftonc   ← links to GitHub
• #338 Reclaim a pod so a retry can proceed (closes #331)
• #337 Log a failed phase at error level
• …and 4 more · plus 6 bot PRs

New issues (4)
• #345 Digest says little about what went on in the repo
• #344 k8s retry loses its PVC

Closed issues (5)
• #329 Review sweep spend loop
• …and 1 more · 3 by merged PRs above

Repo
• 7 PRs merged, 3 opened
• 5 issues closed, 4 opened
• 9 PRs open (2 awaiting review)
• Oldest unreviewed: #412 Refactor the loader — open 9 days

Last Light
• 14 runs — 12 ok, 2 failed
• reviewed 6 · fixed CI on 3
• $4.12 across 40 phases

The numbers and the lists are computed in code, from the GitHub API and Last Light's own database — not written by a model. Only the opening summary is generated, from those already-settled facts, and if that call fails the digest still goes out without it. Set digest.narrative: false in your overlay to skip it entirely.

Two details worth knowing. An issue that a listed merged pull request closed is shown once, hanging off that PR, rather than twice — and only when the timestamps agree that the merge is what closed it, since GitHub reports every linked issue whether or not the merge closed it. And pull requests opened by bots are folded into a count (plus 6 bot PRs) so a week of dependency bumps can't push the human work out of the list.

Sizes are tunable: digest.listItems caps each of the three content lists (default 8), digest.maxItems caps the waiting-on-a-human list (default 5), and digest.detailItems bounds how many items' text the summariser reads (default 25).

Pick a channel

The digest does nothing until a channel resolves for a repo — no channel, no post, no cost. There are three ways to name one, most specific first.

1. The repo chooses. A managed repo names its own channel in its .lastlight/lastlight.yml:

notifications:
  slack:
    channel: C01ENGINEERING

2. You route it. Map repos to channels in your overlay's config.yaml:

slack:
  repoChannels:
    acme/widgets: C01ENGINEERING
    acme/gadgets: C02PLATFORM

3. One channel for everything. Set SLACK_DELIVERY_CHANNEL in .env and every repo without a more specific answer lands there.

Invite the bot to the channel. Slack won't deliver to a channel the app isn't a member of — the post fails with not_in_channel, which is logged and skipped rather than retried.

To turn the digest off entirely, add repo-digest to crons.disable. To change the schedule, use the Crons tab in the dashboard or lastlight cron. To see it now rather than on Monday:

lastlight cron trigger repo-digest

Enable OAuth dashboard login

The dashboard can optionally let your team log in with Slack via OpenID Connect. Back in the Slack app settings:

  1. OAuth & Permissions → User Token Scopes → add openid, email, profile. (These are user scopes, separate from the bot scopes above.)
  2. Redirect URLs → add exactly: https://your-host/admin/api/oauth/slack/callback (or http://localhost:8644/... for local dev).
  3. Basic Information → copy the Client ID and Client Secret.

Add the following to .env:

SLACK_OAUTH_CLIENT_ID=...
SLACK_OAUTH_CLIENT_SECRET=...
SLACK_OAUTH_REDIRECT_URI=https://your-host/admin/api/oauth/slack/callback
SLACK_ALLOWED_WORKSPACE=T01ABCDEF   # Optional: restrict to one team_id or domain

With these set, the dashboard login page shows a "Login with Slack" button alongside the password login (or instead of it, if you don't set ADMIN_PASSWORD).