Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
6bd413a
fix(guard): refuse launch inside a linked worktree (BUG-014)
Jul 30, 2026
4fea218
merge v0.0.15-task-BUG-014
Jul 30, 2026
f9acb26
feat(report): count todos zeroed per instance, drop Claude loops timing
Jul 30, 2026
38c68e2
merge v0.0.15-task-ISSUE-015
Jul 30, 2026
b4d132e
feat(logging): send claude's TUI to fd 4 so a piped run logs only Cla…
Jul 30, 2026
f37d092
merge base v0.0.15 into ISSUE-018
Jul 30, 2026
f2240e0
merge v0.0.15-task-ISSUE-018
Jul 30, 2026
6576945
feat(session): name each claude session (<instance id>) <student acti…
Jul 30, 2026
75d575d
feat(report): report per-instance token consumption
Jul 30, 2026
dc6a89d
merge base v0.0.15 into ISSUE-019
Jul 30, 2026
89507b1
merge v0.0.15-task-ISSUE-019
Jul 30, 2026
f475b2f
Merge branch 'v0.0.15' into v0.0.15-task-ISSUE-016
Jul 30, 2026
1f21145
Merge branch 'v0.0.15' into v0.0.15-task-ISSUE-016
Jul 30, 2026
c57ffef
merge v0.0.15-task-ISSUE-016
Jul 30, 2026
5dc42d4
feat(zero.sh): collapse acquire/validate/re-check into one `claim` (I…
Jul 30, 2026
4da0b64
merge base v0.0.15 into ISSUE-020
Jul 30, 2026
16f9db8
merge v0.0.15-task-ISSUE-020
Jul 30, 2026
1d448a0
feat(report): print a fleet-wide TOTAL on the exit path
Jul 30, 2026
273a164
merge base v0.0.15 into ISSUE-017
Jul 30, 2026
ac96e2b
merge v0.0.15-task-ISSUE-017
Jul 30, 2026
ae8e3a5
feat(session): give each instance a short unique nickname
Jul 31, 2026
2a69e53
merge v0.0.15-task-ISSUE-021
Jul 31, 2026
96beb66
fix(report): print the fleet TOTAL block on solo runs, singular heading
Jul 31, 2026
662d176
merge v0.0.15-task-BUG-022
Jul 31, 2026
f475ea1
fix(dojo): reword three activities so a session name reads as a series
Jul 31, 2026
6c5adda
merge v0.0.15-task-BUG-023
Jul 31, 2026
905883a
chore: stop tracking issues/
Jul 31, 2026
3881bc2
chore(release): 0.0.15
Jul 31, 2026
26017d7
ci(tap): bump the Homebrew tap on a pushed v* tag, not on a release
Jul 31, 2026
a4aef8c
docs(readme): reword two lines
Jul 31, 2026
b1b95f5
chore: track todo.md at the repo root
Jul 31, 2026
cec0579
fix: refuse a zero-mode launch when the todo is untracked on the base…
Jul 31, 2026
6c5dd0f
merge v0.0.15-task-BUG-026
Jul 31, 2026
a022a6b
feat(report): name the instance next to its id in the stats header
Jul 31, 2026
0882608
merge v0.0.15-task-ISSUE-027
Jul 31, 2026
8642f56
chore(todo): file BUG-024 and BUG-025 for the macOS bash 3.2 failures
Jul 31, 2026
ac2ab34
fix: parse the zero prompt's heredoc outside the command substitution
Jul 31, 2026
7dbc7c8
merge v0.0.15-task-BUG-024
Jul 31, 2026
cf8c2f1
fix: source fd 4 from a dup of stdin instead of /dev/tty
Jul 31, 2026
d4846c8
merge v0.0.15-task-BUG-025
Jul 31, 2026
b0a8be7
docs: record BUG-026 in the changelog and land the branch's UI change…
Jul 31, 2026
50be789
docs: fix grammar throughout the readme
Jul 31, 2026
f5e7e4b
docs: add ISSUE-028 to the todo list
Jul 31, 2026
d7a0161
feat: show the script version below the usage heading in --help
Jul 31, 2026
616898d
merge v0.0.15-task-ISSUE-028
Jul 31, 2026
b3e5dd5
docs: record ISSUE-028 in the changelog
Jul 31, 2026
8368078
Add push in tap repo by default
Jul 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,41 @@ flock "$repo/.wtlock" git worktree remove --force "$wt" || fail "git work
git worktree prune || fail "git worktree prune failed"
ok "git worktree add/repair/remove/prune"

# 7. main-worktree root — git worktree list --porcelain | sed -n '1s/^worktree //p' (claudezero.sh:191)
# First porcelain entry must be the MAIN worktree even when read from inside a linked one,
# which is what makes the root guard refuse a launch in a leftover ../ts-* worktree (BUG-014).
main_root="$(cd "$repo" && pwd -P)"
[ "$(git -C "$repo" worktree list --porcelain | sed -n '1s/^worktree //p')" = "$main_root" ] \
|| fail "porcelain/sed main-root form failed from the main worktree"
wt2="$tmp/repo-task-2"
git -C "$repo" worktree add "$wt2" -b "$base-task-2" "$base" >/dev/null 2>&1 || fail "worktree add for root check failed"
[ "$(cd "$wt2" && git worktree list --porcelain | sed -n '1s/^worktree //p')" = "$main_root" ] \
|| fail "porcelain/sed answered the linked worktree instead of the main one"
git -C "$repo" worktree remove --force "$wt2" >/dev/null 2>&1
ok "git worktree list --porcelain | sed main-root"

# 8. token accounting — transcript_path sed (claudezero.sh:243) + the usage awk (claudezero.sh:304).
# The awk must dedupe by requestId and take the PARENT field on each line, never the
# usage.iterations[] copy or the cache_creation ephemeral leaves.
tp="$(printf '%s' '{"transcript_path": "/a b/c.jsonl"}' \
| sed -n 's/.*"transcript_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
[ "$tp" = "/a b/c.jsonl" ] || fail "transcript_path parse got '$tp'"
u='"input_tokens":10,"cache_creation_input_tokens":248,"cache_read_input_tokens":1000,"output_tokens":20,"cache_creation":{"ephemeral_5m_input_tokens":148,"ephemeral_1h_input_tokens":100},"iterations":[{"input_tokens":10,"output_tokens":20,"cache_read_input_tokens":1000,"cache_creation_input_tokens":248}]'
for _ in 1 2; do printf '{"requestId":"req_A","message":{"usage":{%s}}}\n' "$u"; done > "$tmp/t.jsonl"
sums="$(awk '
function num(key, s) {
if (!match($0, "\"" key "\":[0-9]+")) return 0
s = substr($0, RSTART, RLENGTH); sub(/.*:/, "", s); return s + 0
}
/"output_tokens":/ {
k = match($0, /"requestId":"[^"]+"/) ? substr($0, RSTART + 13, RLENGTH - 14) : "line" NR
if (k in seen) next
seen[k] = 1; n++
i += num("input_tokens"); o += num("output_tokens")
cc += num("cache_creation_input_tokens"); cr += num("cache_read_input_tokens")
}
END { if (n) printf "%d %d %d %d %d\n", i, o, cc, cr, i + o + cc + cr }' "$tmp/t.jsonl")"
[ "$sums" = "10 20 248 1000 1278" ] || fail "usage awk got '$sums' (want '10 20 248 1000 1278')"
ok "sed transcript_path / awk usage dedupe"

echo "SMOKE PASS ($(uname -s), bash $BASH_VERSION)"
10 changes: 7 additions & 3 deletions .github/workflows/bump-tap.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: Bump Homebrew tap

# On a published release, bump url + sha256 in the tap formula.
# On a pushed v* tag, bump url + sha256 in the tap formula.
# Wraps `brew bump-formula-pr`. Does not build bottles or touch this repo.
on:
release:
types: [published]
push:
tags: ['v*']

jobs:
bump:
Expand All @@ -16,4 +16,8 @@ jobs:
token: ${{ secrets.TAP_TOKEN }}
tap: IvanRublev/homebrew-tap
formula: claudezero
# No release payload on a tag push — name the tag and commit explicitly.
tag: ${{ github.ref_name }}
revision: ${{ github.sha }}
# Opens a PR on the tap by default. Add `push: true` to commit direct.
push: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@

sources/
probity.config.js
issues/
59 changes: 59 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,65 @@ All notable changes to ClaudeZero are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project uses
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.0.15] — 2026-07-31

### Added

- Fleet-wide `TOTAL` block on every exit path, summing todos, todo time and
tokens across every instance of the run. A crashed peer's landed work still
counts.
- Per-instance token consumption in the execution-stats report: a headline total
plus the input / output / cache-creation / cache-read breakdown. No dollar
figure — there is no first-party rate source, and a hardcoded table would
print confidently wrong money after any model launch.
- Per-instance count of todos zeroed, credited at the same point as the
ownership time so the two always agree.
- The `-h`/`--help` screen states the script version on its own line below the
`usage:` heading, so a bug report can quote the release without opening the
script or starting a run to read the launch banner (ISSUE-028).
- Each `claude` session is named `(<instance id>) <nick> · <activity>`, visible
in the prompt box, `/resume` picker and terminal title. The nickname is a
short word no live peer holds, so you can say "kill kit" instead of reading
eight hex characters off a terminal title. Names survive context restarts.

### Changed

- `claude`'s TUI goes to fd 4, so `claudezero.sh 2>&1 | tee run.log` captures
only ClaudeZero's own output instead of every TUI redraw. Falls back to
stdout when there is no redirect or stdin is not a terminal. `-h` documents
the Ctrl+C-safe pipe form.
- The zero prompt's acquire / validate / re-check steps collapse into one
`claim` call, so a failed validation no longer leaves a claim behind until a
peer steals it.
- The report heading is "execution stats", not "execution time" — a token block
is not a duration. The aggregate Claude-loops timing is gone.
- The execution-stats header carries the instance nickname next to its id
(`instance a1b2c3d4 · moss`), so a report in a scrollback matches the terminal
title it came from without reading eight hex characters.

### Fixed

- Launching inside a leftover task worktree is refused. It previously passed the
root guard and took a peer's claim branch as the base, letting two instances
claim the same todo and landing merges in the peer's in-flight branch
(BUG-014).
- The `TOTAL` block prints on solo runs, with a singular heading. Suppressing it
made a correct one-instance total indistinguishable from a fleet total that
matched no files because the base slug was not what you thought.
- Three dojo activities no longer claim a single unit of work, which read as a
ceiling the instance does not have.
- The script parses under bash 3.2, the `/bin/bash` macOS ships. The zero
prompt's heredoc sat inside a command substitution, which bash 3.2 cannot
parse, so every macOS run died before doing anything (BUG-024).
- A piped run on macOS reaches the prompt instead of killing `claude` at startup
with `EINVAL … kqueue`. fd 4 is a dup of stdin, not a fresh open of `/dev/tty`
— a descriptor from the clone device cannot be registered with kqueue
(BUG-025).
- A zero-mode launch whose todo file is not tracked on the base branch is
refused at startup. Untracked — never added, or gitignored — means every merge
is refused, so the run would burn tokens on work that can never land
(BUG-026).

## [0.0.14] — 2026-07-26

First public release. Base version — prior `0.0.x` iterations were pre-public
Expand Down
58 changes: 42 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
<h2 align="center">Todo-list sensei for Claude Code. Zeros your list.</h2>
<h4 align="center">
Loops claude until every todo is done, committed, and checked off.<br>
Restarting it on a fresh context before rot.<br>
Restarts it on a fresh context before rot.<br>
Spawn many instances to parallelize.
</h4>

<p align="center">
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License: MIT" /></a>
</p>

Runs [`claude`](https://claude.com/product/claude-code) on a predefined prompt in a loop on the given todo list file. Makes it complete, commit, and check off each todo. The session stays interactive, so you can add prompts and make choices as it runs. Launches `claude` in permission auto mode by default, restarts it on a fresh context before rot sets in.
Runs [`claude`](https://claude.com/product/claude-code) on a predefined prompt in a loop on the given todo list file. Makes it complete, commit, and check off each todo. The session stays interactive, so you can add prompts and make choices as it runs. Launches `claude` in auto permission mode by default, and restarts it on a fresh context before rot sets in.

## Contents

Expand All @@ -31,21 +31,22 @@ Runs [`claude`](https://claude.com/product/claude-code) on a predefined prompt i
## What it does

- **Guides Claude to zero a todo list unattended** — one task at a time until all are completed, committed, and checkmarked.
- **Beats context rot** — session Stop hook SIGTERMs `claude` near ~80% of the context window and restarts clean. Fresh context, no quality decay.
- **Beats context rot** — session Stop hook SIGTERMs `claude` at ~80% of the context window and restarts clean. Fresh context, no quality decay.
- **Parallel by default** — run many instances at once; they coordinate via git worktrees, each claiming todos the others haven't taken.
- **Safe merges** — cross-instance merge-back serialized through `flock`; no races, no corrupted base.
- **Crash resilient** — if a `claude` crashes or is killed mid-task, its half-done work isn't lost. The next instance to come by — a peer switching to its next task, or the same loop restarted — reclaims the branch, finishes it, and merges it back. Work only counts as done once it lands on the base branch and its box is checked there.
- **Crash resilient** — if `claude` crashes or is killed mid-task, its half-done work isn't lost. The next instance to come by — a peer switching to its next task, or the same loop restarted — reclaims the branch, finishes it, and merges it back. Work only counts as done once it lands on the base branch and its box is checked there.
- **Named sessions** — every `claude` session is named `(<instance id>) <nick> · <activity>`, shown in the prompt box, the `/resume` picker, and the terminal title. The nickname is a short word no live peer holds, so you can say "kill moss" instead of reading eight hex characters. Both id and nick head the instance's report, and survive context restarts.
- **Ctrl+C window** — 5s pause between runs to stop cleanly.

## Quickstart

Change to your repo root with a todo-list file, and make sure the working tree is in a clean state (commit or stash any changes). Then run `claudezero` pointing to your todo-list:
Change to your repo root with a todo-list file, and make sure the working tree is in a clean state (commit or stash any changes) and the todo file itself is committed on that branch. Then run `claudezero` pointing to your todo-list:

```
claudezero todo.md
```

Run that command in multiple parallel terminals to zero todos faster.
Run that command in multiple parallel terminals to work through the todos faster.

> ⚠️ ClaudeZero runs `claude` **unattended with permissions auto-approved** and **commits on its own** to the branch you launch it on. Only ever point it at a todo file you wrote or reviewed, on a branch with a clean, committed tree — git is your only undo.

Expand All @@ -56,7 +57,7 @@ Representative zero-mode run (agent output between the markers elided):
```console
$ claudezero todo.md

❄ ClaudeZero 0.0.14
❄ ClaudeZero 0.0.15
zero mode · base master · fork → implement → commit → merge

… claude works a task: forks a worktree, implements, commits, merges, ticks its box …
Expand All @@ -65,14 +66,25 @@ $ claudezero todo.md

… fresh context, next task …

❄ execution time (instance a1b2c3d4)
Todos: 12m 30s
Claude loops: 41m 02s
❄ execution stats (instance a1b2c3d4 · moss)
Todos: 12m 30s · 5 completed
ClaudeZero run loop: 48m 15s

Tokens: 5.8M Total
in 2.1k · out 84.3k · cache write 312k · cache read 5.4M

-----------------------------------------------
❄ TOTAL (3 instances)
Todos: 41m 12s · 14 completed

Tokens: 17.4M Total
in 6.3k · out 251.9k · cache write 903k · cache read 16.2M

❄ ClaudeZero surveys the frozen field, and is proud.
```

The `TOTAL` block sums every instance of the run on this base branch — a crashed peer counts too, since its merged work did land. It prints on solo runs as well, with a singular heading. `ClaudeZero run loop` stays per-instance: parallel wall times overlap, so their sum is not a duration anything took.

## Install (for the Claude coding agent)

Supported on **macOS and Linux** (the script is bash-3.2-safe, so stock macOS `bash` works).
Expand Down Expand Up @@ -107,6 +119,10 @@ Supported on **macOS and Linux** (the script is bash-3.2-safe, so stock macOS `b

Both prerequisites are guard-checked at startup; the script exits with a clear message if either is missing.

**Transcript-schema contract.** The token figures in the execution-stats report are a second coupling to Claude Code internals, alongside the state file above. ClaudeZero's own Stop hook records each session's `transcript_path` (a field of the hook payload it already parses `session_id` from), and after `claude` exits the loop reads those session JSONL transcripts and sums the `message.usage` fields of every assistant line: `input_tokens`, `output_tokens`, `cache_creation_input_tokens`, `cache_read_input_tokens` — the four categories Anthropic bills separately. One API request is written as several transcript lines, one per content block, each repeating the same `usage` object verbatim, so records are **deduped by the line's `requestId`**, and only the first (parent) match of each field name on a line is taken: `usage.iterations[]` repeats all four names one level down, and `usage.cache_creation` carries the `ephemeral_5m`/`ephemeral_1h` leaves that already sum into the parent. Transcripts are only ever read, and no schema change can fail a run — any parse miss prints `Tokens: n/a` and the run continues.

Two limits: **subagent tokens are invisible** — a session that used the Agent tool writes no `isSidechain` usage lines, so anything the task prompt spawns is missing from the totals, and the size of the under-count is not measurable from inside; and the figures are **per instance run, for this repo only**, unlike whole-machine tools such as `ccusage`, whose denominator is every Claude Code session on the box.

## Todo file format

GitHub-style Markdown checkboxes, one task per line. Each line carries a **unique id** as the first whitespace-delimited token right after the checkbox — it names the task's branch and worktree:
Expand All @@ -118,7 +134,7 @@ GitHub-style Markdown checkboxes, one task per line. Each line carries a **uniqu
- [x] SMTH-140 Set up CI pipeline

```
- [ ] EXAMPLE-1 illustrative example, not task
- [ ] EXAMPLE-1 An illustrative example, not a task
```
`````

Expand All @@ -139,7 +155,7 @@ The kata is a strict per-iteration algorithm every instance runs:
2. Judge independence by evidence — blocked only if the body quotably consumes an *unchecked* task's output; adjacency is not a dependency.
3. Claim & re-check — one task per git worktree (branch = claim), then guard against a peer who already landed it.
4. Implement & check off — scoped to that task, tick only its box, commit.
5. Merge serially — on a conflict or over-check, self-heal once, else stop and hand off to user rather than corrupt the base.
5. Merge serially — on a conflict or over-check, self-heal once, else stop and hand off to the user rather than corrupt the base.
6. Repeat — next id; when every box is checked, announce done and stop.

### Closing the loop
Expand Down Expand Up @@ -186,12 +202,22 @@ claudezero -h

**`-l` loop prompt** — skips zeroing entirely when set. Instead of walking a todo file, it loops `claude` on one literal prompt you supply, restarting on fresh context each time. Any `todo-file-path` you pass is ignored. This mode creates no worktree branches, so run just one instance at a time.

**`CLAUDEZERO_MAX_LOOPS`** — cap the number of context reset iterations. Unset or `0` loops forever (until Ctrl+C); set `>0` to exit the script after N restarts of `claude`.
**`CLAUDEZERO_MAX_LOOPS`** — cap the number of context-reset iterations. Unset or `0` loops forever (until Ctrl+C); set `>0` to exit the script after N restarts of `claude`.

```sh
CLAUDEZERO_MAX_LOOPS=3 claudezero todo.md
```

### Logging a run

`claude`'s TUI is written to fd 4, which stays on the terminal, so a pipe captures only ClaudeZero's own `❄` reports instead of every TUI redraw:

```sh
claudezero todo.md 2>&1 | { trap '' INT; tee ../run.log; }
```

The `trap` keeps `tee` alive through Ctrl+C, so the final report and the `TOTAL` block land in the file. Without a redirect — or when stdin is not a terminal — the TUI falls back to stdout as before.

## Cleanup

<!--
Expand All @@ -203,7 +229,7 @@ the human to run them themselves.
-->
Normal exits tidy up after themselves. But a crash, a `kill`, or `Ctrl+C` mid-task can leave a claim branch and its worktree behind — by design, so the next run can reclaim and finish them. These leftovers are exactly what crash-recovery reattaches to, so only remove them once you've **stopped every instance** and are done zeroing the unchecked todos.

Easiest is to let Claude Code walk the cleanup and confirm each removal with you. From the repo root:
The easiest way is to let Claude Code walk the cleanup and confirm each removal with you. From the repo root:

```sh
claude "ClaudeZero left stray git worktrees and branches behind. Clean them up
Expand All @@ -217,7 +243,7 @@ discards work; (4) after removals, run 'git worktree prune'. Do nothing
destructive without my explicit confirmation."
```

Prefer to do it by hand:
To do it by hand:

```sh
git worktree list # find ../ts-<base>-task-<id>-<hex>
Expand All @@ -232,7 +258,7 @@ Only delete a branch whose work you've already merged or intend to throw away.

## Tests

End-to-end tests live in [TEST.md](TEST.md) — written to be executed by an LLM agent. Point the coding agent at the file and it runs all scenarios autonomously `claude --permission-mode auto "execute TEST.md and return a report"`.
End-to-end tests live in [TEST.md](TEST.md) — written to be executed by an LLM agent. Point the coding agent at the file and it runs all scenarios autonomously: `claude --permission-mode auto "execute TEST.md and return a report"`.

## Security

Expand Down
Loading
Loading