Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
71e1f27
Update readme.
Aug 2, 2026
2ff3f8e
test: close the bash 3.2 and fd 4 coverage gaps (BUG 024, BUG 025)
Aug 2, 2026
be0409f
fix: make an issue's acceptance criteria the definition of done in th…
Aug 2, 2026
adf0bba
Bump version, add tasks
Aug 2, 2026
487419e
fix: background claude and trap TERM so a hang still lands at the clo…
Aug 2, 2026
912c305
merge v0.0.16-task-BUG-029
Aug 2, 2026
a7b3bf7
feat: report claude's exit code and add an opt-in --debug-file captur…
Aug 2, 2026
453ea71
merge v0.0.16-task-ISSUE-030
Aug 2, 2026
ebbcf68
Add ISSUE-031 and ISSUE-032 tasks
Aug 2, 2026
c8472d6
update subtitles
Aug 2, 2026
ac51f7f
update readme
Aug 2, 2026
ae0aa3d
feat: zero one task per claude session and wait for the next claimabl…
Aug 2, 2026
701a83e
merge v0.0.16-task-ISSUE-031
Aug 2, 2026
ff49af9
feat: kill a hung claude with a CLAUDEZERO_WATCHDOG timer (ISSUE-032)
Aug 2, 2026
ebf2206
merge v0.0.16-task-ISSUE-032
Aug 2, 2026
b9baecf
feat: link gitignored spec dirs into task worktrees and pace the wait…
Aug 3, 2026
8fb4a73
feat: name CLAUDEZERO_LINK on the --help screen (ISSUE-033)
Aug 3, 2026
27aa5af
chore: check off ISSUE-031 after verifying its acceptance criteria (I…
Aug 3, 2026
4e58ba4
merge v0.0.16-task-ISSUE-031
Aug 3, 2026
cc3f907
feat: sample claude's CPU time for the watchdog and name the knob in …
Aug 3, 2026
90abd70
merge v0.0.16-task-ISSUE-032
Aug 3, 2026
1cfe009
Update changelog
Aug 3, 2026
0f7a54b
fix: recheck kill -0 before the watchdog's SIGKILL escalation
Aug 3, 2026
887d917
test: harden TEST.md against reaching outside its own sandbox
Aug 3, 2026
daf1d55
fix: prefer CLAUDE_PID over bare ancestor-name walk in term_owner/fin…
Aug 3, 2026
63e2bd6
test: guard Scenario G's real Stop hook invocation against killing th…
Aug 3, 2026
24f7e2b
test: fix grun() PATH bug, expand scenario list, split Scenario G
Aug 3, 2026
80182ac
docs: changelog entries for the watchdog kill-0 recheck and CLAUDE_PI…
Aug 3, 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
13 changes: 13 additions & 0 deletions .github/smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,17 @@ sums="$(awk '
[ "$sums" = "10 20 248 1000 1278" ] || fail "usage awk got '$sums' (want '10 20 248 1000 1278')"
ok "sed transcript_path / awk usage dedupe"

# 9. CLAUDEZERO_LINK symlink — plain POSIX `ln -s target link` (claudezero.sh link_ignored).
# BSD and GNU ln agree on the two-argument form and diverge on -r/-f/-n, which is why none are
# used. The guard is `[ ! -e ] && [ ! -L ]`: -e FOLLOWS the link, so a dangling link reads as
# absent and an unguarded ln would die "File exists".
ldir="$tmp/linksrc"; mkdir -p "$ldir"; echo "criterion" > "$ldir/spec.md"
ln -s "$ldir" "$tmp/link" || fail "ln -s <dir> <name> failed"
[ -L "$tmp/link" ] || fail "-L did not see the symlink"
[ "$(cat "$tmp/link/spec.md")" = criterion ] || fail "read through symlink failed"
ln -s "$tmp/nowhere" "$tmp/dangling" || fail "ln -s to a missing target failed"
[ -L "$tmp/dangling" ] || fail "-L did not see the dangling link"
if [ -e "$tmp/dangling" ]; then fail "-e followed a dangling link (guard would misfire)"; fi
ok "ln -s / -L / -e guard"

echo "SMOKE PASS ($(uname -s), bash $BASH_VERSION)"
68 changes: 68 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,74 @@ 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.16] — 2026-08-03

### Added

- `CLAUDEZERO_WATCHDOG` kills a `claude` that has stopped making progress
mid-turn, so a wedged API call or a hung tool costs one timeout instead of the
whole overnight run. Progress is claude's own cumulative CPU time, so a long
honest run is never killed — only one that has stopped working. SIGTERM first,
SIGKILL after a 10s grace. Default `15m`; `0` disables it. The kill prints its
own `❄ watchdog · no progress from claude for …` line, so it is never mistaken
for the ordinary context-full restart (ISSUE-032).
- `CLAUDEZERO_LINK` — comma-separated top-level names symlinked from the repo
root into every task worktree, unset by default. A worktree checks out tracked
files only, so a gitignored spec directory a todo line points at is simply not
there: the session falls back to the one-line title and reports done against
it. Through a link the session reads the acceptance criteria and ticks them in
the real file. The whole list is validated at startup and a bad entry (empty,
containing `/`, or missing at the repo root) refuses the run — a typo costs the
launch, not the tasks (ISSUE-033).
- `CLAUDEZERO_DEBUG` adds `--debug-file .git/debug-<base>-<instance>-<loop>.log`
to the `claude` invocation, so a repeat of a rare startup flake leaves a trace
instead of a bare hang. Off by default — a diagnostic opt-in, not a standing
cost on every run. Unset, the claude argv is unchanged (ISSUE-030).
- An `Environment:` block on the `-h`/`--help` screen naming `CLAUDEZERO_WATCHDOG`
and `CLAUDEZERO_LINK` with their formats and defaults, so neither knob has to be
found by reading the script (ISSUE-032, ISSUE-033).
- A waiting line while every unchecked task is peer-held: a spinner with an
elapsed clock and the held count on a terminal, a periodic line every 20s when
stdout is a log or a pipe (ISSUE-031).

### Changed

- A `claude` session zeroes exactly one task and exits; the wait for the next
claimable task lives in `claudezero.sh` instead of inside claude. `/loop` is
gone from the zero prompt, and the Stop hook now ends the session at every turn
end rather than only when the context bucket crosses its threshold. Every task
therefore starts on a context isolated from the task before it (~20% fewer
tokens on a working instance), and an instance with nothing to claim launches
no `claude` at all instead of re-sending a startup context per probe.
`-l/--loopprompt` is unaffected (ISSUE-031).
- Both "claude exited after N runs" lines — `CLAUDEZERO_MAX_LOOPS`-reached and
restarting — carry claude's real exit code: `claude exited with code %s after
%s runs · …`. A hang is visible without re-deriving it from timing, and it is
one line per event, not two (ISSUE-030).
- A run stopped by SIGTERM exits `143` (128+15), so a supervisor can tell
"terminated" from "finished". Plain `timeout` callers still see its own `124`;
use `timeout --preserve-status` to observe the 143 (BUG-029).

### Fixed

- A SIGTERM arriving while `claude` hangs no longer kills the run silently.
`claude` ran as a foreground child, and bash defers every trap until a
foreground child exits — so with a hung child no handler could run, the
follow-up SIGKILL ended the process mid-wait, and the run dropped its
`❄ execution stats` report, the fleet `❄ TOTAL`, and the EXIT trap that clears
the instance liveness marker. `claude` is now backgrounded and reaped with a
re-entrant `wait`, and a trapped TERM forwards to claude and breaks to the
existing closer, so every exit path lands at the closing report (BUG-029).
- The watchdog's SIGKILL escalation rechecks `kill -0` before firing: if the
earlier SIGTERM already reaped `claude`, the OS can recycle that pid during
the grace sleep, and a blind SIGKILL would land on whatever unrelated process
holds it next.
- `term_owner`/`find_owner` prefer the inherited `CLAUDE_PID` over the bare
ancestor-name walk: the walk matched any process named `claude` up to 8 hops
with no check it was this session's own, so a live `claude` sitting in the
ancestry for an unrelated reason (a nested Task agent, this tool being
dogfood-tested from inside a real session) got SIGTERMed instead.

## [0.0.15] — 2026-07-31

### Added
Expand Down
17 changes: 17 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ Thanks for helping to improve ClaudeZero. Even snow leopards sharpen their claws
2. **Make your change.** Keep the diff surgical — match the existing style in
`claudezero.sh`. Update the README and `TEST.md` if behavior changes.

**`claudezero.sh` must parse under bash 3.2** — macOS ships it as `/bin/bash`,
and a parse error there kills the script before line one runs. Avoid:

```sh
prompt="$(cat <<'EOF' # here-doc inside $( … ) — bash 3.2 cannot parse it
...
EOF
)"

IFS= read -r -d '' prompt <<'EOF' || : # use this instead
...
EOF
```

`TEST.md` Scenario S guards it: S3a scans for the construct on any host,
S3b parses with a real bash 3.x. Locally: `/bin/bash -n claudezero.sh`.

3. **Keep the CI scripts in sync with `claudezero.sh`.** Two scripts under
`.github/` mirror details of `claudezero.sh` and drift silently if you don't
update them:
Expand Down
51 changes: 40 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

<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>
Restarts it on a fresh context before rot.<br>
Spawn many instances to parallelize.
Loops Claude until every todo is done, committed, and checked off.<br>
Restarts the coding session on a fresh context before rot.<br>
You can spawn multiple instances to parallelize.<br><br>
It's for practical <a href="#loop-engineering">Loop engineering</a>.<br>
</h4>

<p align="center">
Expand All @@ -32,6 +33,7 @@ Runs [`claude`](https://claude.com/product/claude-code) on a predefined prompt i

- **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` at ~80% of the context window and restarts clean. Fresh context, no quality decay.
- **One task per session** — `claude` exits once it has zeroed a single todo and the script restarts it, so every task runs on a context isolated from the task before it, which cuts token spend (~20% on a working instance). An instance that has nothing to claim waits in the shell without launching `claude` at all, spending nothing.
- **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 `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.
Expand All @@ -40,13 +42,19 @@ Runs [`claude`](https://claude.com/product/claude-code) on a predefined prompt i

## 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) and the todo file itself is committed on that branch. Then run `claudezero` pointing to your todo-list:
```
brew install IvanRublev/tap/claudezero
```

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 work through the todos faster.
You can 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 @@ -62,7 +70,7 @@ $ claudezero todo.md

… claude works a task: forks a worktree, implements, commits, merges, ticks its box …

❄ claude exited after 1 runs · restarting in 5s · press Ctrl+C to stop
❄ claude exited with code 0 after 1 runs · restarting in 5s · press Ctrl+C to stop

… fresh context, next task …

Expand Down Expand Up @@ -142,21 +150,29 @@ GitHub-style Markdown checkboxes, one task per line. Each line carries a **uniqu

## Loop engineering

[Loop engineering](https://claude.com/blog/getting-started-with-loops) shapes an agent's iteration cycle so it gets *better* across turns, not just runs once. Two halves:
[Loop engineering](https://claude.com/blog/getting-started-with-loops) shapes an agent's iteration cycle so it gets *better* across turns, not just runs once. It is the outermost of three nested levels — each one only works because the one under it holds:

1. **Spec** — what to build. Expected outputs, constraints, acceptance criteria, done-conditions. The contract everything downstream enforces. Without it the layers above have nothing to check against. Here: the todo file, one task per line, each referencing a separate issue file with the details of the specification.
2. **Harness** — how to keep the agent on the spec, in two directions. *Feedforward* guides steer before it acts (`CLAUDE.md`, conventions, templates); *feedback* sensors catch after (tests, linters, type checks, review). Feedback alone repeats the same mistakes; feedforward alone never proves it worked. Here: the per-iteration algorithm below, plus whatever guides and checks your repo already has.
3. **Loop** — who does the prompting. The harness on a timer: self-triggering runs, isolated worktrees, subagents that verify and feed back. You stop prompting turn by turn and start designing the thing that prompts itself. Here: ClaudeZero with `--taskprompt` instruction on how to learn by prompting itself.

Levels 1 and 2 are yours; ClaudeZero supports level 3. Together they steer: when a mistake recurs, you don't only fix the code, you sharpen the spec and/or the harness, and the loop needs you less each pass due to the learning instruction.

Loop engineering has two halves:

1. Mechanics — a durable loop over external state; disposable runs that restart before context rots.
2. Learning — each turn carries a lesson forward, so the agent stops repeating mistakes.

ClaudeZero owns the mechanics and leaves the learning to you. It drills the *form* precisely — how to claim, zero, commit, and check off a todo without collision or rot. You bring the *material* — what this codebase's tasks should teach. Sensei drills the kata; you bring the fight.

The kata is a strict per-iteration algorithm every instance runs:
The kata is a strict algorithm every instance runs, one task per `claude` session:

1. Find & validate — collect tasks; a missing or duplicate id stops the loop.
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 the user rather than corrupt the base.
6. Repeat — next id; when every box is checked, announce done and stop.
6. End the session — the shell starts a fresh one for the next task, or waits without spending a token while peers hold the rest; when every box is checked, announce done and stop.

### Closing the loop

Expand All @@ -167,10 +183,11 @@ ClaudeZero never writes `CLAUDE.md` — the harness stays learning-agnostic, so
Bake a reflection step into the task prompt; the lesson lands in a committed `CLAUDE.md`, survives the restart, and reaches peers after their next merge:

```sh
claudezero todo.md -t 'Implement the task following your setup.
claudezero todo.md --taskprompt 'Implement the task following your setup.
When done, if you learned something that will help future tasks — a gotcha, a
project convention, a command that worked — append one concise bullet under a
"## Learnings" heading in CLAUDE.md, and include that edit in the task commit.'
"## Learnings" heading in the project CLAUDE.md, and include that edit
in the task commit.'
```

Now "the leopard remembers what the last winter taught him."
Expand Down Expand Up @@ -208,6 +225,18 @@ claudezero -h
CLAUDEZERO_MAX_LOOPS=3 claudezero todo.md
```

**`CLAUDEZERO_WATCHDOG`** — how long one `claude` may make no progress before it is killed. Default `15m`; accepts plain seconds or an `s`/`m`/`h` suffix (`900`, `90s`, `15m`, `1h`), and `0` disables it. A `claude` that stops making progress never exits, so without the timer the loop parks on it forever — no restart, no report, and nothing left to stop but the whole run. Progress is measured as `claude`'s own cumulative CPU time, not wall clock: one that is thinking, streaming, or running tools burns CPU and keeps resetting the window however long the task takes, while one blocked on a dead socket burns none. When the timer fires it says so on its own console line, then `SIGTERM`s `claude` (the same signal the Stop hook uses, so the restart path is the usual one) and escalates to `SIGKILL` 10s later. The task worktree survives the kill: the next session reclaims it through the crash-recovery path.

```sh
CLAUDEZERO_WATCHDOG=45m claudezero todo.md
```

**`CLAUDEZERO_LINK`** — comma-separated top-level names symlinked from the repo root into every task worktree. Unset by default. A worktree is a checkout of tracked files only, so anything gitignored is absent there: if your todo lines point at spec files you keep in another git repository — `issues/ISSUE-031.md` holding the acceptance criteria for `- [ ] ISSUE-031 …` — the session never sees them and works from the one-line title alone. Listing the directory here links it in, so the criteria are readable and a tick lands in the real file rather than in a copy the worktree removal deletes. Each linked name is added to `.git/info/exclude`, so it stays out of the session's `git add -A` and out of this repository.

```sh
CLAUDEZERO_LINK=issues 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:
Expand Down
Loading
Loading