Docs

Per-repo config

Everything on the Configuration page is instance-wide: one operator, one overlay, one set of models and gates for every repo the bot watches. That breaks down as soon as one repo wants a cheaper model, one repo wants an approval gate nobody else needs, and one repo wants to sit out the nightly health cron.

So a managed repo may commit a .lastlight/ directory of its own. It overrides a bounded subset of config — only for runs against that repo — and it lands last in the precedence chain:

config/default.yaml  →  instance/config.yaml  →  env  →  <repo>/.lastlight/lastlight.yml
     (packaged)            (your overlay)        (env)          (the repo itself)
Only the default branch is ever read. Never a pull-request head, never the checkout the agent is working in. Without that rule a PR could commit a lastlight.yml that re-points the model, turns off the review workflow and drops the approval gates — of the very agent reviewing it.

Layout

The directory mirrors a deployment overlay exactly, so anything you already know about forking assets applies unchanged:

.lastlight/
├── lastlight.yml              # the config override
├── workflows/
│   └── prompts/*.md           # prompt overrides — never workflow YAML
├── skills/
│   └── <name>/SKILL.md        # skill overrides
└── agent-context/*.md         # ADDITIVE persona/rules files

Three rules are baked in, and the tooling tells you about each one:

  • No workflow YAML. A repo may retune what an agent is told, never which phases run, with which skills, under which GitHub permission profile, behind which approval gates. A workflows/*.yaml file in the layer is rejected with a workflow-not-allowed warning. Changing a workflow definition is an operator action — lastlight fork <workflow> into the instance overlay.
  • agent-context/ is additive only. A repo file whose name matches one the instance already provides (soul.md, rules.md, security.md) is ignored — otherwise committing a security.md would switch off the operator's security boundaries for every run against that repo. Add context under a repo-specific name instead, e.g. project-notes.md.
  • Everything else in .lastlight/ is simply not part of the layer. In the default buildAssets.location: repo mode the build workflow commits its handoff docs to .lastlight/<issue-key>/; those are left alone.

A worked example

Say acme/payments is a small, high-stakes service. It wants a stronger model for reviews, a cheap one for triage, a human gate before any agent-written code is pushed, no nightly health report, and a house style note in every agent session. That whole policy is one file plus one prompt:

.lastlight/
├── lastlight.yml
├── agent-context/payments-house-rules.md
└── workflows/prompts/review.md
# .lastlight/lastlight.yml

models:
  # A stronger reviewer for this repo only.
  review: anthropic/claude-opus-4-8
  # …and a cheap one for triage.
  triage: anthropic/claude-haiku-4-5

variants:
  review: high

approval:
  # Add-only: this repo demands a human before the executor pushes.
  post_architect: true

crons:
  disable: [repo-health]     # sit out the nightly health report
  enable:  [security-scan]   # …but opt in to this one even if it's off globally

disabled:
  workflows: [demo]          # this repo never wants the demo workflow

Validate it before you push — the check is offline and runs the same validators the server runs:

cd ~/code/payments
lastlight repo config validate
Validating /Users/you/code/payments/.lastlight

  agent-context
    ✓ agent-context/payments-house-rules.md
  config
    ✓ lastlight.yml
  prompt
    ✓ workflows/prompts/review.md

Effective config overrides
  approval.post_architect = true
  disabled.workflows = ["demo"]
  models.review = "anthropic/claude-opus-4-8"
  models.triage = "anthropic/claude-haiku-4-5"
  variants.review = "high"

✓ Everything in this layer is within the shipped default bounds.

Then commit it to your default branch — that is the only ref Last Light reads. The next run against the repo picks it up (within about a minute; see Caching).

lastlight repo fork scaffolds the layer for you — it copies a workflow's prompts and skills (never its YAML) into <git repo root>/.lastlight/, so you start from the real built-ins rather than a blank file. lastlight repo fork with no argument lists what's forkable.

What a repo may set

Only these keys, and only within the bounds the operator configures. Anything else is dropped with a warning — the run still proceeds.

KeyWhat it does
models Per-task model overrides, keyed by phase or helper name. Each value must be a provider/model string whose provider Last Light knows how to wire, and must satisfy the operator's repoConfig.allowedModels.
variants Per-task reasoning effort (minimal / medium / high / …), same key scheme.
approval Add-only. A repo may raise a gate for runs against itself; every false is dropped with a warning. A repo can never remove oversight the operator asked for.
disabled.workflows The repo opting itself out of a workflow. The dispatch is refused before any run is created.
crons { enable, disable } — which scheduled jobs this repo takes part in. See below.
disabled.crons Older spelling of crons.disable; still works, and is merged into it.
fix / dependencies / review The workflow policy blocks — retry budgets, major-bump auto-merge, and when a review runs. One-way: a repo may only ever be more conservative than the operator. See below.
notifications Which Slack channel this repo's weekly digest goes to. The one key with no clamp direction — a channel isn't more or less conservative. See below.

Arrays replace rather than append, exactly as they do between the packaged defaults and your overlay — so a repo's disabled.workflows is the whole list for that repo.

Notifications: routing, not policy

A repo can name the Slack channel its weekly digest goes to:

# .lastlight/lastlight.yml
notifications:
  slack:
    channel: C01ENGINEERING     # channel id, or "#eng-widgets"

This is the one repo-settable key with no more conservative direction — a channel name simply isn't a budget — so unlike the policy blocks below, the repo's answer wins outright. Two things make that safe, and neither is a bound:

  • The .lastlight/ layer is always read from the repo's default branch, never a pull request head. A PR cannot redirect the output of the agent reviewing it.
  • Slack won't deliver to a channel the bot hasn't been invited to. The worst a bad value achieves is a logged channel_not_found.

Setting channel: null explicitly means send me no digest, and beats anything the operator configured — that's different from leaving the key out, which means "no opinion, use whatever's configured".

Operators who want channel choice back remove notifications from repoConfig.allowKeys, and route repos themselves with the slack.repoChannels map instead.

Policy blocks: a repo may only be more careful

fix, dependencies and review aren't preferences — they're budgets and blast radius. Admitting them the way models is admitted would let any repo vote itself a bigger share of your money and your machines. So they follow the rule approval already established, generalised: a repo may only ever be more conservative than the operator.

A value that would loosen is simply dropped — and dropping is the clamp, because the operator's value is what's underneath, so the leaf resolves straight back to it. The repo is told why, with a policy-downgrade warning; the run carries on regardless.

# .lastlight/lastlight.yml — in a repo that wants to be careful

fix:
  maxAttempts: 1               # ✓ lower than the operator's 3 — accepted
  maxCostUsd: 10.0             # ✗ higher than the operator's 5.0 — dropped, stays 5.0
  gateTimeoutSeconds: 3600     # ✗ operator-only — dropped

dependencies:
  autoMergeMaxImpact: none     # ✓ never auto-merge a major in this repo

review:
  trigger: on-request          # ✓ this repo opts itself out of automatic reviews
  skipDraft: false             # ✗ add-only — dropped, drafts stay skipped
KeyA repo may…
fix.maxAttempts, fix.localIterations, fix.maxFlakyDeferrals lower them only.
fix.maxCostUsd lower the ceiling only — and never set null, which means "no ceiling" and is the loosest value there is.
fix.retryableClasses keep a subset of your list. Naming a class you don't retry would add a retryable failure mode, so it's dropped; retrying less is always fine. A name that isn't one of the five diagnosis classes at all is reported as a typo rather than as a policy decision — different problems, different fixes.
fix.escalateModelAfterAttempt, fix.gateTimeoutSeconds nothing — operator-only. One is spend, the other a shared-resource budget; neither is a "how careful is this repo" dial.
dependencies.autoMergeMaxImpact move it down the none < low < medium < high scale, all the way to none.
dependencies.requireSettledChecks turn it on. A repo can demand settled checks; it can't waive yours.
dependencies.minSettledChecks nothing — operator-only. Clamping it upward would weld the escape hatch shut for a repo with no CI at all, which could then only ever raise the number of checks it must show; that case is settled on the facts of the PR instead.
dependencies.auditComment turn it on. It isn't cosmetic: it's the record of a major version we auto-merged into that repo, so the party it would silence is the party being audited. Asking for one you don't require is always fine.
review.trigger move it down the on-request < after-checks < eager scale. The three modes are equally safe but not equally expensive — eager buys a full agent review per push, on your budget — and a repo opting itself out of automation is entirely its call.
review.requestLabel set it freely. Naming a label only ever adds an explicit, human-initiated route; yours keeps working alongside it, and both are honoured on the pull_request.labeled webhook.
review.postsCheck, review.skipDraft turn them on. A repo may ask for the check run and may skip drafts; it may not suppress a check a branch-protection rule is requiring, or force reviews onto drafts.
review.generatedPaths add patterns — its own generator output, say. A longer list suppresses more re-reviews, which is the conservative direction. Dropping one of yours buys the repo an extra agent run per lock-file bump on your budget, so an omitted pattern is put back.

Cron participation

The crons: block is valid at every layer, but it means something different depending on who wrote it:

Written indisable: [x]enable: [x]
the instance config / overlay cron x is off by default, everywhere no-op — a cron is on unless something disables it
a repo's .lastlight/ this repo drops out of x's fan-out this repo opts in, even when x is off by default

A name in both lists is disabled, at every layer — a cron that doesn't run is the safe reading of a contradictory config. Participation is resolved on each tick, so a repo's edit takes effect on the next run of the cron with no restart.

Because a repo can opt back in, crons.disable in your overlay is a default, not a lock. If you need a cron no repo can turn on, remove crons from repoConfig.allowKeys — that is the un-overridable kill switch.

Operator bounds

Everything above is gated by a repoConfig: block in your instance config. It ships enabled but inert — nothing changes for an existing deployment until a repo actually commits a .lastlight/. A repo can never widen these; only narrow itself within them.

# instance/config.yaml

repoConfig:
  enabled: true                  # false ignores every repo's .lastlight/ entirely
  allowKeys: [models, variants, crons, disabled.workflows, disabled.crons, approval,
              fix, dependencies, review]
  allowedModels: null            # null = any model whose provider you've configured;
                                 # a list restricts repos to exactly those model specs
  allowAssets: true              # false keeps lastlight.yml only — no repo prompts,
                                 # skills or agent-context
KeyDefaultEffect
repoConfig.enabled true Master switch. false and no repo's .lastlight/ is even fetched.
repoConfig.allowKeys the nine keys above Dotted config paths a repo may set. An entry admits itself and everything under it — models admits models.review, while disabled.workflows does not admit disabled.prompts. Narrow this to shrink the blast radius. Note that admitting fix / dependencies / review is not the same as trusting them: those blocks are clamped one-way on top (see above).
repoConfig.allowedModels null An exact-match list of model specs repos may select. null means any model whose provider/ prefix is one you've configured — a repo still can't invent a provider.
repoConfig.allowAssets true Whether a repo's prompt / skill / agent-context files are used at all.

When something is wrong

A repo's config file can never fail a run. Every problem degrades: invalid YAML drops the whole file, an out-of-bounds key drops just that key, a GitHub outage falls back to the last cached copy — and the run proceeds on the instance config. Each rejection is recorded against the run and shown by both repo config validate (offline) and repo config show (what the server actually applied), so a drop is never silent.

lastlight repo config show acme/payments
Effective config for acme/payments
  .lastlight/ from main@3f1c9ab, fetched 2026-07-31T09:12:04.117Z

Models
  default = "anthropic/claude-sonnet-4-6"  (default)
  review = "anthropic/claude-opus-4-8"  (repo)
  triage = "anthropic/claude-haiku-4-5"  (repo)

Approval gates
  post_architect = true  (repo)

Fix policy
  maxAttempts = 1  (repo)
  localIterations = 2  (default)
  maxCostUsd = 5  (default)

Dependencies policy
  autoMergeMaxImpact = "none"  (repo)
  requireSettledChecks = true  (default)

Repo assets
  ✓ agent-context/payments-house-rules.md
  ✓ workflows/prompts/review.md

Values marked (repo) came from this repo's .lastlight/.

The same view — merged config with per-value provenance (default / overlay / env / repo), the raw file the repo committed, its warnings and its assets — is a Config tab on each repo's page in the admin dashboard, policy blocks included.

Caching

The layer is fetched with the same GitHub App credentials as everything else, so private repos work, and cached on the Last Light host under $STATE_DIR/repo-config/<owner>/<repo>/. A cached layer is trusted for about a minute; after that the next fetch is conditional, so a cron fanning out over fifty repos costs fifty cheap "not modified" checks and no downloads. The layer is capped at 200 files / 2 MiB, and symlinks are rejected.

Just merged a .lastlight/ change and don't want to wait out the minute? lastlight repo config show <owner/repo> --refresh forces a re-read.

One thing that deliberately does not refresh: a run already in flight. If a run pauses at an approval gate, resuming it reuses the exact config it started with rather than re-reading the repo — otherwise an edit made while it was paused could send the second half of a build to a different model than the first.

Commands

lastlight repo fork                      # list what a repo may override
lastlight repo fork all                  # every workflow's prompts + skills + agent-context
lastlight repo fork <workflow>           # one workflow's prompts + skills (never its YAML)
lastlight repo fork agent-context [file] # persona files — ADDITIVE, rename before committing
lastlight repo fork classifier           # the base intent-classifier prompts
lastlight repo config validate           # check ./.lastlight/ offline; non-zero exit if rejected
lastlight repo config show <owner/repo>  # what a server actually applies, with provenance

These run inside your own code repo and write to <git repo root>/.lastlight/ — unlike lastlight fork, they never fall back to a server's instance/ overlay, and refuse to run outside a git repo. Full flags on the CLI page.