From 3163bd26f0a0d8ab308d325c2320ccf5686122b7 Mon Sep 17 00:00:00 2001 From: naps62-yolo Date: Sat, 25 Jul 2026 13:08:41 +0000 Subject: [PATCH] feat(nightshift): long autonomous build skill with limit backoff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_019SSXDKsik8yYrezStVaDXt --- skills/nightshift/SKILL.md | 121 ++++++++++++++++++++ skills/nightshift/usage-window.py | 179 ++++++++++++++++++++++++++++++ 2 files changed, 300 insertions(+) create mode 100644 skills/nightshift/SKILL.md create mode 100755 skills/nightshift/usage-window.py diff --git a/skills/nightshift/SKILL.md b/skills/nightshift/SKILL.md new file mode 100644 index 0000000..20de138 --- /dev/null +++ b/skills/nightshift/SKILL.md @@ -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 /nightshift/usage-window.py # human +python3 /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. diff --git a/skills/nightshift/usage-window.py b/skills/nightshift/usage-window.py new file mode 100755 index 0000000..267f39b --- /dev/null +++ b/skills/nightshift/usage-window.py @@ -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()