feat(nightshift): long autonomous build skill with limit backoff

Takes an issue, ticket or plain description and works it for hours as an
architect: freeze the interface contract first, then delegate implementation
to subagents against frozen signatures, review, commit every milestone.

Checks the rolling 5-hour window before each dispatch and parks rather than
letting agents die mid-task. The usage script sums every Claude Code session
on the machine, and is explicit that it is a floor — it cannot see other
machines, claude.ai, or direct API use — so the reactive backstop of an
agent dying on a limit error is documented as the stronger signal.

Encodes what worked in practice: disjoint file ownership per agent, asking
agents to report disagreement (their pushback was the highest-value output),
sabotage-testing every safety net, and asking what a passing check is
structurally unable to see.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SSXDKsik8yYrezStVaDXt
This commit is contained in:
2026-07-25 13:08:41 +00:00
parent 423747b08d
commit 3163bd26f0
2 changed files with 300 additions and 0 deletions
+121
View File
@@ -0,0 +1,121 @@
---
name: nightshift
description: "Long-running autonomous build. Take an issue, ticket or plain description and work it for hours as an architect: decide the high-level shape, delegate implementation to subagents, review what lands, commit often. Backs off before hitting the 5-hour limit. Built for running while the user is asleep or away."
user-invocable: true
args:
- name: input
description: "A Linear issue ID (e.g. ERN-347), a Gitea issue number (e.g. #23), a GitHub issue URL, or a plain-text description of what to build"
required: false
---
# Nightshift — long autonomous build
For work measured in hours, not minutes, with nobody watching. You are the **architect**: you decide structure, freeze interfaces, delegate implementation to subagents, review what comes back, and commit. You write specs and reviews; subagents write most of the code.
Use when: a large feature or whole subsystem, an overnight run, "keep going until X works".
Don't use when: the task is one or two files (`/yolo`), or needs a PR review loop (`/work`, `/land`).
**First:** read `linear-common/COMMON.md` (sibling skill, same skills root) for tracker config and worktree setup.
## 1. Resolve the input
- **Linear ID / Gitea number / GitHub URL** — fetch it, read the whole thread including comments. Comments usually hold the real constraints.
- **Plain text** — that IS the spec. Do not go looking for a ticket.
- **Nothing** — pick the next unblocked task per COMMON.md. If that's ambiguous, ask *before* starting; a long run in the wrong direction is the most expensive failure mode here.
Set up a worktree per COMMON.md. Long runs and dirty main branches do not mix.
## 2. Phase zero: write the contract before any code
The single highest-leverage thing in this whole flow. Before delegating anything:
1. **Write the design to `docs/ARCHITECTURE.md`** (or the project's equivalent) — what's being built, the layer boundaries, and *what you rejected and why*. Rejected options are the most valuable part later, when someone wonders whether to revisit them.
2. **Author the type/interface contract yourself.** Every module lands as a **compiling stub**: full signatures, full doc comments explaining the contract, bodies that throw. Nobody's parallel work should be blocked on inventing a boundary somebody else also needs.
3. **Put every magic number in one tuning file**, with the reasoning next to each.
4. Commit that. It's the frozen surface everything else builds against.
Then fan out. Agents implementing against frozen signatures can run in parallel without racing to define the same types three ways.
## 3. The milestone loop
Repeat until done or stopped:
1. **Check the limit** (see below). Park if close.
2. **Assess** — run the project's gate (typecheck + lint + tests). Read what landed since last time.
3. **Decide** — one architectural decision, written down. If nothing needs deciding, you are done; say so.
4. **Decompose** into subtasks with **disjoint file ownership**.
5. **Delegate** — subagents, in parallel where files don't collide.
6. **Review** what returns. Their reports are the product as much as the code.
7. **Commit and push.** Every milestone. Non-negotiable — see "assume you will be killed".
## 4. Limit discipline
The 5-hour window is real and an agent dying mid-task loses its work. Check at **every milestone**, before dispatching:
```bash
python3 <skills-root>/nightshift/usage-window.py # human
python3 <skills-root>/nightshift/usage-window.py --json # machine
```
It sums billable tokens across **every Claude Code session on this machine** — all projects, not just yours, since parallel sessions in other repos share the same window.
**It is a floor, never a ceiling.** Nothing local states the real quota, and the script cannot see Claude Code on another machine, claude.ai web usage, or direct API calls. Real usage is whatever it reports *plus* however much the account is being used elsewhere. If the user works across several machines, treat a comfortable reading with suspicion and take a wider margin.
Because of that gap, the reactive backstop matters as much as the estimate: **an agent that dies on a limit error is telling you the truth this script only guesses at.** Believe it immediately, and park — do not retry into the wall.
How to act on it:
- **Window looks heavy** (well into millions, and climbing fast across recent milestones): finish what's running, commit, push, and **park**. Do not dispatch new agents.
- **Parking** = `ScheduleWakeup` for the time `oldestAgesOutInSeconds` reports, plus a margin. Sleeping until the window loosens is strictly better than having three agents killed halfway through their tasks.
- **Before parking, always**: commit, push, and write the current state and the next intended step into the log. The run must be resumable by a different session that has none of your context.
- **If an agent dies on a limit error anyway**: do not immediately retry. Check the tree still passes the gate, commit whatever is green with a message stating plainly that it is **unverified** and what was left half-done, then park.
- Never *silently* burn the window to zero. If the user is asleep, they will wake to a stalled run and no explanation.
Scale the check to the work: a run doing small mechanical tasks needs it rarely; a run fanning out three heavy agents per milestone needs it every time.
## 5. Delegating well
What actually works, learned the hard way:
- **Disjoint files, stated explicitly.** Name the files each agent owns and the files it must not touch, including which other agents are live. Overlap produces lost work and confusing merges.
- **Give context and constraints, not procedures.** Tell them the invariant that must hold and why; let them design. The best results come from agents that understood the *reason* and then improved on the instruction.
- **Ask for disagreement, explicitly.** "Report anything you think I got wrong" produces the highest-value output in this whole flow. Subagents repeatedly find that a spec is wrong, a tuning knob is dead, an interface is frame-coupled. Treat a pushback as a finding, not friction.
- **Demand verification the task can actually support.** "Tests pass" is not enough for anything a human will look at or listen to. Require a screenshot, a measured number, a browser run. Say plainly when something can only be verified by a human.
- **Model choice**: strongest model for design-heavy or feel-critical work; a cheaper one is fine for mechanical, well-specified changes.
- Instruct them to commit and push their own work when it's coherent, so a killed agent loses less.
## 6. Reviewing what lands
You are the only thing standing between a green test suite and a bad codebase.
- **Fix interfaces while they have zero call sites.** The cheapest moment an interface will ever be wrong is before anything uses it. If a signature is awkward now, it will be awkward in forty places tomorrow.
- **Ask what the verifier structurally cannot see.** This finds the bugs nothing else does. A browser test drives synthetic key events, so it cannot notice a keyboard has no numpad. A sim fuzzer that never runs the server cannot see a bug in joining. When something is green, ask what class of failure that check is blind to.
- **Sabotage-test the safety nets.** Break the thing a check guards, confirm the check fails, restore. A check nobody has watched fail is not yet a check. Apply this to every invariant, harness and lint rule you add.
- **Measure instead of guessing**, and beware the single metric. A number moving the wrong way can be a *good* sign with the right denominator; check the thing you actually care about, not its proxy.
- **Reject plausible-but-wrong designs even when tests pass.** Agents make reasonable decisions that are wrong for the domain. That is your job to catch.
## 7. The log is a deliverable
Keep two documents:
- **`docs/ARCHITECTURE.md`** — the map. What exists now, and why. Must describe the code that *is there*, not the code you imagined at the start. Re-audit it against source periodically; a stale architecture doc is worse than none, because it gets trusted.
- **`docs/ROADMAP.md`** (or a build log) — the honest narrative. Every decision, and **every time you were wrong**. Record corrections in place rather than quietly editing them away. If you claimed evidence you did not have, say so where you claimed it.
This is what makes an overnight run reviewable by a human who slept through it. Include what is **not** done and what only a human can judge.
## 8. Assume you will be killed
Limits, crashes, closed laptops. Therefore:
- Commit and push at every milestone, and whenever the tree is green.
- Never leave the only copy of anything in an agent's context.
- A commit of unverified work is fine **if the message says so**. A commit that implies verification that never happened is not.
- Leave the next step written down, in the repo.
## 9. Stopping
Stop when the goal is met, when what remains needs a human decision, or when further iterations cannot make progress. Say plainly what is done, what is untested, and what needs the user.
Do not invent work to stay busy. A run that ends with an honest "the rest is yours to judge" is a successful run.
**Never treat silence as approval.** If you asked the user something and got no reply, do not pick for them. Do decision-independent work, or park and say you are blocked. The exception is the autonomy this skill was started with: proceeding through the *work itself* is the point — it's the questions you raised that must not be self-answered.
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""Estimate token usage inside the rolling 5-hour limit window.
The real quota is server-side and nothing local states it. What IS local is
every assistant message Claude Code has written, with a timestamp and a usage
block, in ~/.claude/projects/**/*.jsonl. Summing those over a rolling window
gives a usable proxy: not the quota, but a consistent measure of how hard the
window has been worked, plus when the oldest tokens in it age out.
SCOPE — read this before trusting the number:
Counted: every Claude Code session on THIS machine, all projects, not
just the session asking. Parallel sessions in other repos land
in the same window and this picks them up.
NOT counted: Claude Code on any other machine, claude.ai web usage, direct
API calls, and anything else that never writes a transcript here.
So the figure is a floor, never a ceiling. Real usage is this or higher, and
the gap is however much the account is being used elsewhere. Size your margin
accordingly: if the user works on several machines, treat a "comfortable"
reading with suspicion.
Treat the number as a trend, not a truth. It is here so a long autonomous run
can back off *before* an agent dies mid-task, rather than discovering the limit
by being killed by it. The reactive backstop still matters: an agent that dies
on a limit error is telling you the truth this script only estimates.
Usage:
usage-window.py # human summary
usage-window.py --json # machine readable
usage-window.py --hours 5 # window size (default 5)
"""
from __future__ import annotations
import argparse
import json
import os
from datetime import datetime, timedelta, timezone
from pathlib import Path
PROJECTS = Path.home() / ".claude" / "projects"
def parse_ts(value: str | None) -> datetime | None:
if not value:
return None
try:
return datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError:
return None
def billable(usage: dict) -> int:
"""Tokens that plausibly count against a quota.
Cache reads are excluded: they are the cheap path and counting them would
make a long cached session look far heavier than it is. Cache *creation* is
included, because writing the cache is real work.
"""
return (
int(usage.get("input_tokens") or 0)
+ int(usage.get("cache_creation_input_tokens") or 0)
+ int(usage.get("output_tokens") or 0)
)
def collect(hours: float) -> dict:
now = datetime.now(timezone.utc)
cutoff = now - timedelta(hours=hours)
total = 0
output = 0
messages = 0
oldest: datetime | None = None
newest: datetime | None = None
per_project: dict[str, int] = {}
if not PROJECTS.is_dir():
return {
"error": f"no transcript directory at {PROJECTS}",
"windowHours": hours,
}
for path in PROJECTS.glob("*/*.jsonl"):
# Cheap skip: a file untouched since the cutoff cannot contribute.
try:
if datetime.fromtimestamp(path.stat().st_mtime, timezone.utc) < cutoff:
continue
except OSError:
continue
project = path.parent.name
try:
with path.open(encoding="utf-8", errors="replace") as handle:
for line in handle:
if '"usage"' not in line:
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
stamp = parse_ts(entry.get("timestamp"))
if stamp is None or stamp < cutoff:
continue
usage = (entry.get("message") or {}).get("usage")
if not isinstance(usage, dict):
continue
tokens = billable(usage)
if tokens <= 0:
continue
total += tokens
output += int(usage.get("output_tokens") or 0)
messages += 1
per_project[project] = per_project.get(project, 0) + tokens
oldest = stamp if oldest is None or stamp < oldest else oldest
newest = stamp if newest is None or stamp > newest else newest
except OSError:
continue
# When the oldest tokens in the window age out. This is the soonest the
# window measurably loosens; it is NOT a quota reset time, which only the
# server knows.
ages_out_in = None
if oldest is not None:
ages_out_in = max(0, int(((oldest + timedelta(hours=hours)) - now).total_seconds()))
return {
"windowHours": hours,
"tokens": total,
"outputTokens": output,
"messages": messages,
"projects": dict(sorted(per_project.items(), key=lambda kv: -kv[1])),
"oldestInWindow": oldest.isoformat() if oldest else None,
"newestInWindow": newest.isoformat() if newest else None,
"oldestAgesOutInSeconds": ages_out_in,
"now": now.isoformat(),
}
def human(report: dict) -> str:
if "error" in report:
return f"usage: unavailable ({report['error']})"
lines = [
f"rolling {report['windowHours']}h window — all local Claude Code sessions",
f" tokens {report['tokens']:,} ({report['outputTokens']:,} output) "
f"over {report['messages']:,} messages",
]
if report["oldestAgesOutInSeconds"] is not None:
secs = report["oldestAgesOutInSeconds"]
lines.append(f" oldest entry ages out in {secs // 60}m{secs % 60:02d}s")
top = list(report["projects"].items())[:3]
if top:
lines.append(" busiest: " + ", ".join(f"{k} {v:,}" for k, v in top))
lines.append(f" sessions: {len(report['projects'])} project(s) contributing")
lines.append(
" FLOOR, not a quota: misses other machines, claude.ai and direct API use"
)
return "\n".join(lines)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--hours", type=float, default=5.0)
parser.add_argument("--json", action="store_true")
args = parser.parse_args()
report = collect(args.hours)
if args.json:
print(json.dumps(report, indent=2))
else:
print(human(report))
if __name__ == "__main__":
main()