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:
@@ -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.
|
||||
Executable
+179
@@ -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()
|
||||
Reference in New Issue
Block a user