Nightshift lands a committed plan while nobody is watching: one task per pull
request, each judged by the verifier, a second model, and CI before it merges.
It runs from loop/land.sh on this Mac at 02:00, because the crate only builds
here and the repository’s plan has no branch protection to lean on. In the
morning you read a journal and a few merged pull requests, or one draft PR that
says where it stopped.
What a night looks like
loop/land.shreadsloop/configfor the plan, checks the kill switch (repository variableLANDING_STATEmust berun), and finds the first# Task Nthat has not landed. Landed means a merge commit onmainnames the branchland/<plan>-tN; nothing is stored anywhere else.- In its own worktree (
../ambient-nightshift) it branches fromorigin/main, extracts the task’s section withloop/task-brief, and runs a budget-cappedclaude -pin auto mode withloop/PROMPT.md. The generator commits and never pushes. Only the repository’s own.claude/settings.jsonloads, so the allow rules and the two hooks under.claude/hooks/are the whole permission surface. Its commits are unsigned: signing goes through the 1Password SSH agent, which does not answer while the app is locked, and at 02:00 it is. The merge commit is GitHub’s and the PR is the audit trail. scripts/checkmust end withCHECK OK. Then a read-onlyclaude -pwithloop/SKEPTIC.mdreads the diff and the task and ends withVERDICT: OKorVERDICT: REFUTED.- A red verifier or a refutation goes back to the generator once. A second
failure pushes the branch as a draft PR labelled
land:blockedwith the evidence in its body, and the night stops. - Otherwise the branch is pushed, a PR titled
[task N] …opens with the labelland, and./loop/merge-pr.sh --staywaits for CI and merges it. CI red turns the PR into a blocked draft. - Repeat until
MAXtasks have landed, the deadline passes, or something stops it.
scripts/quality runs after the verifier and is the accuracy half of the gate.
It builds the LibriSpeech fixture (cached after the first night), scores
Ambient’s own recording path over it, and compares each speaker’s word error
rate and the total against the committed quality/wer.json. In the same pass
it scores an Earnings-21 calls fixture the same way against quality/calls.json,
so a change that helps read audiobooks and hurts phone calls cannot merge, and
it scores the AMI diarization fixture and compares each meeting’s diarization
error rate against quality/der.json for the 300 s clips and
quality/der-full.json for the same meetings uncut. Last it runs
scripts/memory, which replays a whole 25-minute AMI meeting through the
recording path and fails if the peak physical footprint is over its budget
(see Measurements). Its last line carries all
five: QUALITY OK clean=<wer> calls=<wer> der=<der> der_full=<der> memory=<MiB>
when no row has drifted up by more than half a point of WER or a point of DER
and the footprint is within budget, or QUALITY BASELINE clean=<wer> calls=<wer> der=<der> der_full=<der> memory=<MiB> the first time a baseline
file is missing, when it writes that file instead; a regression prints the
offending rows and exits non-zero, and a task that improves a number records it
with scripts/quality --update, so the diff to quality/wer.json,
quality/calls.json, quality/der.json or quality/der-full.json is the
evidence in the pull request.
It runs only once CHECK_CMD in loop/config says so, which is a change a
person commits: the hooks deny a task’s own commit that stages loop/. The
script is the gate and that line is the switch, and a plan can only deliver the
gate. The line to commit is
: "${CHECK_CMD:=scripts/check && scripts/quality && echo 'CHECK OK'}"run_check in loop/land.sh asks that the whole command’s last line be
exactly CHECK OK, and a green quality run ends with
QUALITY OK clean=<wer> calls=<wer> der=<der> der_full=<der> memory=<MiB>
instead, so the trailing echo is what makes the pair a verifier: it runs only
when both halves passed, and without it every green night reads as red.
The same commit adds "Bash(scripts/quality:*)" and "Bash(./scripts/quality:*)" to the
allow list in .claude/settings.json, beside the scripts/check entries: the generator runs
CHECK_CMD before every commit, and a command the allow list does not name is denied whole.
Tasks land in plan order, and one task must merge before the next starts. An open PR from a killed run is picked up on the next start; a blocked or human-closed PR on any task stops the plan until someone acts.
Reading the morning
~/.local/state/nightshift/ambient/journal.mdis one line per event, and every stop says why:STOP: frozen,STOP: task 3 is blocked,STOP: done: 2 task(s) landed tonight.- Each task’s run has a directory beside it,
YYYY-MM-DD-tN/, holding the brief, every generator and skeptic transcript, the verifier log, and the merge log. gh pr list --label land:blockedis what needs a human. Read the PR body: the generator’s report, the skeptic’s findings and the verifier’s tail are all there.
What can stop it, and who can
| Stop | Set by | Cleared by |
|---|---|---|
LANDING_STATE is not run |
gh variable set LANDING_STATE --body frozen |
setting it back to run |
land:blocked draft PR on a task |
the loop, on a second red or a refutation | a human fixing or closing the PR |
| A closed, unmerged PR on a task | a human | labelling that PR land:retry (run it again from scratch) or removing the task from the plan |
MAX tasks, or DEADLINE |
loop/config |
the next night |
| Budget | --max-budget-usd on each claude -p |
nothing; a task that runs out of money produces no commits and the night stops |
The generator cannot lift any of these. .claude/hooks/no-route-around-ci.mjs
denies gh pr merge, --admin, gh workflow, gh variable set, force
pushes, pushes to main, --no-verify, and any commit that stages a workflow
file. .claude/hooks/tests-are-readonly.mjs denies a commit that removes more
test markers than it adds or deletes a test file. Both run before the
permission check, in every permission mode, and both fail open: the merge gate
is still behind them.
Cost
One task spends at most (GEN_BUDGET + SKEPTIC_BUDGET) × (REPAIR_ROUNDS + 1)
dollars on the API, which with the defaults in loop/config is $10, and a
night is capped at MAX tasks. CI cost is the usual per-PR run.
Setting it up
-
Write the plan: a markdown file with
# Task Nheadings, each section self-contained enough to implement without asking anything. Commit it and pointPLANinloop/configat it. -
Try one task in daylight from the terminal and watch it:
gh variable set LANDING_STATE --body run MAX=1 loop/land.sh gh variable set LANDING_STATE --body frozenloop/land.sh --dry-runsays which task it would pick and exits. -
Install the schedule:
sed -e "s|__REPO__|$PWD|g" -e "s|__HOME__|$HOME|g" -e "s|__NAME__|ambient|g" \ -e "s|__PATH__|$PATH|g" loop/launchd.plist \ > ~/Library/LaunchAgents/dev.nightshift.ambient.plist launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/dev.nightshift.ambient.plist launchctl kickstart gui/$(id -u)/dev.nightshift.ambient # run it now, onceA free proof that the launchd environment works: add
MAXset to0underEnvironmentVariablesin the installed plist, kickstart, and readlaunchd.login the state directory. It should fetch, check the switch, and stop with0 task(s) landedin a few seconds. Remove the key and reload before the night.The Mac must be logged in:
claudeandghuse this user’s credentials, andcaffeinate -ionly holds off idle sleep. -
Set
LANDING_STATEtorunthe evening you want it to work.
Why it is shaped this way
Every piece exists because of a way unattended agents are known to go wrong. The verifier is quiet because a verbose one eats the context of the model reading it. The skeptic is a second, read-only model because a generator grading its own work is the one thing every study of this finds unreliable. Tests are read-only because deleting the failing test is the shortest path to green. State lives only in git and GitHub because a ledger file is a second source of truth that drifts from the first. Merging is CI’s decision, not the loop’s, and the loop runs under budgets and a deadline because a loop that can retry forever will.
The loop is deliberately narrow: one plan, one task at a time, one repair. The things it cannot do, such as write its own plan, split a task, or decide a test is wrong, are the things a human does at breakfast.