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) 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/*.yamlfile in the layer is rejected with aworkflow-not-allowedwarning. 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 asecurity.mdwould 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 defaultbuildAssets.location: repomode 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.
| Key | What 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 | Key | A 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 in | disable: [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.
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 | Key | Default | Effect |
|---|---|---|
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.