feat: checkpoint-and-resume so the turn cap stops discarding completed work #356
No reviewers
Labels
No labels
Agent
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No assignees
2 participants
Notifications
Total time spent: 1 minute 33 seconds
Due date
igor
1 minute 33 seconds
No due date set.
Dependencies
No dependencies set
Reference
joshtronic/igor!356
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/checkpoint-resume"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The problem
--max-turns 100is meant to bound a tick's runtime (no single issue runs for hours and starves the loop). But the ship-safety gate discarded the entire worktree on the nonzero max-turns exit — throwing away real, commit-worthy work and re-running large tasks from zero, only to hit the cap again next tick (igor#351, #329, #334). The cap was forbidding work from ever finishing, not just bounding runtime.The design: checkpoint-and-resume (tier-1 issue work)
After the claude run, the worktree's disposition is one of three (pure logic in
lib/checkpoint.sh):commitcheckpointmax_turnsresult event, no unrestoredgit stashdiscardA
checkpoint:WIP:) PR the review + merge loops skip;exit 0) drops theWIP:prefix and writes a clean title/body → shadow review picks it up.Guardrails
max_turnsresult event with no unrestored stash earns a checkpoint; a real crash still discards (the porksicle#114 invariant). A crash mid-resume preserves the prior (clean) checkpoint and re-queues.CHECKPOINT_MAX=8, tracked as<!-- agent-checkpoints=N -->in the PR body. Past it, the issue is escalated to the human withStatus/Blocked("too big — split it"), so a non-converging task can't monopolize the loop. Both checkpoints and crash-mid-resumes count against it.WIP: … checkpointcommits so a completed multi-checkpoint task isn't blocked on history it didn't choose. The 400-line cap is orthogonal and still applies to the whole diff — a task that's both turn-expensive and >400 net lines will checkpoint through the turns then block at finalize on scope, which is the correct "human, split this" signal.The cap stays hardcoded at 100 — it's now a per-tick budget, not a hard ceiling on total work.
Touch points
lib/checkpoint.sh(new, pure) — disposition,max_turnsstream detection, WIP-title + counter helpers, budget.bin/tick.sh— resume detection at claim; worktree-from-branch on resume; resume preamble; disposition routing; checkpoint push + draft-PR; WIP-drop finalize; discovery gate resumes a WIP (not skip); review tick skips WIP; scope-cap excludes checkpoint commits.lib/forgejo.sh—bot_prs_for_issuereturnstitle; newforgejo_edit_pr.lib/automerge.sh— excludes WIP drafts.CLAUDE.md— documents the new subsystem.Testing
bin/test-checkpoint.sh— 46 assertions (disposition matrix incl. the stash guard,max_turnsdetection, WIP round-trip, body counter, budget, the exact discovery-gate/resume jq filters,forgejo_edit_prpayload).make test+shellcheck(warning+) clean; SC2015 count unchanged from baseline.Review notes
This edits the harness's own core tick flow and self-deploys from master in ~1 min, so it's a human-review-and-merge change — I built it, I'm not merging it. Suggest a read of the disposition matrix + the resume lifecycle first. The riskiest bits are the exit-handling rewrite (
tick.shdisposition block) and the discovery-gate/resume detection; both are covered by the exact jq filters in the test.🤖 Review —
COMMENT(automated)CI for
756de410: successSummary: Adds turn-cap checkpoint-and-resume: a clean
max_turnscut-off now snapshots WIP as a draftWIP:PR and resumes next tick instead of discarding the worktree. New purelib/checkpoint.sh+ 46-assertion unit test, with the git/forgejo side effects wired intobin/tick.sh. CI is green, the checklist matches the diff, no security issues, and I found no clear logic defect. But this is a >400-line change to the harness's core tick flow that self-deploys from master, and its most load-bearing seams can't be verified from the diff alone — that belongs in front of a human (which the author themselves states). Handing it over rather than approving.Things a human must confirm before merge:
The stream file path is the crux and I can't verify it.
CLAUDE_STREAM="$(dirname "$CLAUDE_LOG")/claude-stream.jsonl"(tick.sh ~line 4085) — but the diff only showsCLAUDE_LOG=".agent/claude-output.log"; the claude invocation that would produceclaude-stream.jsonlisn't in the diff. If that file isn't actually written at that path,checkpoint_hit_turn_capalways returns false, every turn cap silently falls back todiscard, and the entire feature is a no-op — with no failing test to catch it (the unit tests feed hand-built fixtures, not a real run). Confirm the live claude run emits stream-json to exactly that path.Integration is unavoidably untested.
checkpoint.shis pure and well-covered, but the risky part — disposition routing, resume worktree carve, push/--force-with-lease, draft-PR open/finalize, budget escalation — lives intick.shand has no end-to-end coverage. The jq filters are pinned in tests, the control flow around them is not.Crash-mid-resume drops that session's committed work (by design, verify it's intended). In the
discard && IS_RESUMEbranch (tick.sh ~line 4110) the code assumes "HEAD == origin/$BRANCH, nothing to push." If Claude made real commits during the resume and then crashed, those commits are on local HEAD, never pushed, and lost when the worktree is pruned — only the prior checkpoint survives. This matches the stated "crash mid-resume preserves the prior checkpoint" guardrail and is bounded by the budget, so it won't loop, but it is a real (intentional) progress loss worth a conscious sign-off.WIP → ready transition depends on Forgejo re-deriving draft status from the title. Finalize edits the title to strip
WIP:via the issues PATCH endpoint (tick.sh ~line 4325). Whether Forgejo re-clears the draft/mergeable state on a title edit through that endpoint (vs. the PR endpoint) is the hinge on which review+merge actually pick the PR back up. Worth a quick confirm against the deployed Forgejo version.Minor:
lib/automerge.sh: the explanatory comment sits just above thedone < <(...)line rather than near the filter it describes — cosmetic.No blocking defect found, but too much of the correctness rests on facts not visible in the diff for me to sign off. Recommend a human review-and-merge, as the author intended.
Independent review by the harness on
claude-opus-4-8(effort: high). The human reviewer is requested once Igor has reviewed; a human still merges.