Bundle
nightshift
Run an agent unattended for hours and get a morning brief that is an audit, not a summary. Signs a manifest before the work, holds the run to it, and reports the difference.
- Source
- liyixuan201211
- License
- MIT
- Updated
- Updated 4 hours ago
Readme
# nightshift
**Run an agent unattended for hours — and get a morning brief that is an audit, not a summary.**
Sign a manifest before the work: what it is for, which commands prove it worked,
what must never run, and what it is allowed to spend. Then wrap each mutating
command in a checkpoint that rolls itself back when verification fails. In the
morning, get a brief generated from the run's own journal — what changed, what
was verified, **what was not**, and the exact command to undo the night.
```bash
dsh plugin --profile web add github:liyixuan201211/nightshift
```
No install needed to try it:
```bash
npx --yes github:liyixuan201211/nightshift --help
```
## The problem this is for
The tools this belongs to each answer one question about one moment —
`ctx-budget` what it will cost, `blastradius` what it would destroy, `deadend`
whether this failed before, `mcp-cap` what a server can do, `skillnotary` what a
skill may do, `rewind` how to get it back. All of them are point-in-time and
stateless.
An unattended run is neither. Once the person leaves, nobody is standing at
those checkpoints — and the failures that matter over hours are the quiet ones.
Drift: the objective changing, the budget passing, a step failing silently, a
loop spinning. No single-point tool is watching for those.
**nightshift is the layer that watches.** It is also the generalisation of
something the family already does by hand — `ctx-budget`'s own bundle notes that
it reads a `tools/list` dump that `mcp-cap` already produced.
## The one idea
> **The manifest is a contract you sign while you are still awake. The brief is
> the audit of that contract.**
```bash
nightshift init --root ~/work \
--objective "finish the auth refactor and get the suite green" \
--deliverable "npm test passes" \
--accept "tests=npm test" \
--accept "types=npx tsc --noEmit" \
--forbid "git push" \
--command "npm run build"
nightshift preflight --root ~/work # may I start?
nightshift step --root ~/work --verify "npm test" -- npm run build
nightshift acceptance --root ~/work
nightshift brief --root ~/work
```
Only two of the declared things are enforceable, and that is the point:
`acceptance[]` is a list of **shell commands**, so "done" is an exit code rather
than a claim; `forbidden[]` is matched **before execution**, so a rule is a gate
rather than a post-mortem. An objective with no acceptance checks produces a
brief that says `unclear` and means it.
## What the brief contains
The brief is a **rendering of the journal**, never a summary written from
memory. That direction is what makes it worth reading:
- a claim with no event behind it cannot appear
- a declared acceptance check that never ran is reported as **not run**, not omitted
- a step whose success was never verified is reported as **unverified** — a
different fact from "passed"
- **every action the gate refused** is listed, because a report of what a run
*did* cannot show whether the gate ever fired
- tools that were unavailable are named, with the count (`5/6 family tools`)
- the baseline snapshot id and the exact `rewind undo` line to reverse the night
Its bottom line is one word: `verified`, `failed`, `incomplete`, `recovered`,
`nothing-done`, `refused`, or `unclear`.
## The gate sits next to the action, not the session
`preflight` judges the commands a run **declares**. But a step can run anything,
and an unattended run has hours in which to change its mind. So the same
judgement runs again at every execution:
1. **forbidden patterns** — a standing instruction, matched first and never
routable-around
2. **irreversibility class** — priced by `blastradius`, read from the **exit
code** (`lost` → terminal, `costly` → recoverable, `safe` → two-way,
anything else → unknown)
3. **curfew** — after `--curfew-minutes`, no *new* one-way action may start
(two-way actions stay allowed: refusing to save a file at 6am would be a worse
failure than the one a curfew prevents)
4. **class policy** — the manifest's own knobs
A refusal means the command **did not run** and nothing was changed.
### A real disagreement between two of the tools
`blastradius` calls this `lost`:
```json
{ "recoverability": "lost",
"why": "not inside a git repository, so nothing can restore it",
"samples": ["data.txt"], "verdict": "lost" }
```
It means *git* cannot restore it. But `rewind` exists precisely to restore files
**git never tracked**, with or without git — so for a path inside the protected
root, the checkpoint taken moments later does restore it.
Trusting either tool alone is wrong: believing `blastradius` refuses nearly every
real refactor; believing the snapshot treats `rm -rf ~/something-else` as
recoverable when the snapshot never covered it. So nightshift uses
`blastradius`'s own `samples` to decide:
| at-risk paths | result |
|---|---|
| all inside the protected root | downgraded to `one_way_recoverable` — warned, allowed |
| any outside the root | stays `one_way_terminal` — **refused** |
| findings name no paths | stays `one_way_terminal` — **refused** |
The last row is deliberate: **being unable to tell is never an upgrade.**
## Refusing to start is a feature
`preflight` exits `4` and starts nothing when a run would be unrecoverable. The
usual cause is that `rewind` is unavailable — **an unattended run with no way
back is the one thing this refuses outright.** Fix the cause; there is no flag
that gets past it.
## Exit codes are the contract
| Exit | Meaning |
|---|---|
| `0` | ok |
| `1` | error |
| `2` | usage error |
| `3` | attention — a preflight warning, or a step that failed and was rolled back |
| `4` | refused — nothing was done, on purpose |
| `5` | degraded — it ran, but some checks could not be performed |
| `6` | nothing — there was nothing to do |
`-q` suppresses output and never the exit code.
## Notes on the seams with its siblings
- **`--exclude .nightshift` is not optional.** nightshift keeps its journal
inside the directory `rewind` protects, so every `rewind` call passes the
exclude. Without it the journal would dirty its own snapshots and `rewind
status` would never be clean. (Tested.)
- **Exit codes are translated, not trusted.** `3` means OVER_BUDGET in
ctx-budget, LOST in blastradius, DRIFT in deadend, CHANGED in mcp-cap, and
"the tree differs" in rewind. Each tool's dialect is data in `src/tools.js`,
overridable per run, and `nightshift tools` prints the table.
- **`unverifiable` is not `ok`.** `blastradius` exits 5 for any command outside
its vocabulary, and its JSON says `"verdict": "none"` when it means *"I have no
analysis for you"*. Reading that string as "no risk" waves through
`curl … | sh`, so nightshift maps on the **exit code** and treats the verdict
as corroboration only.
- **"Not priced" is recorded as `info`, never as a pass.** `blastradius` only
understands destructive commands, so pricing `touch` legitimately returns "I
cannot tell you". Escalating that to a warning every time would train the
operator to ignore warnings — so it is visible in the brief without raising the
verdict. (`onUnpriced`, default `ignore`.)
## Honest limits
- **It does not decide what to do.** It holds a run to what was already decided.
- **It is not a scheduler.** It does not wake up; something drives it.
- **It is not a sandbox.** It predicts with `blastradius` and recovers with
`rewind`; it does not isolate.
- **It fetches nothing.** There is no `npx` fallback unless `--allow-npx` is
passed. Resolution is local: `--tools-dir`, `$NIGHTSHIFT_TOOLS_DIR`, the
configured search path, or `PATH`.
- **The brief is only as good as the acceptance checks.** No oracle, no
verification — and the brief will say so rather than implying otherwise.
## Design
See [DESIGN.md](./DESIGN.md) for the architecture, the exit-code normalisation
table, and the reasoning behind the ordering rule that a refused run must not
leave a snapshot behind.
## License
MIT
Install
dsh plugin --profile web add github:liyixuan201211/nightshift
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install nightshift from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.