Skip to content

feat(loop): enforce driver one-shot contract — post-exit verify + debris classifier + tests (#111) - #118

Merged
robercano-ghbot merged 1 commit into
mainfrom
feat/issue-111-driver-oneshot-contract
Jul 15, 2026
Merged

feat(loop): enforce driver one-shot contract — post-exit verify + debris classifier + tests (#111)#118
robercano-ghbot merged 1 commit into
mainfrom
feat/issue-111-driver-oneshot-contract

Conversation

@robercano-ghbot

Copy link
Copy Markdown
Collaborator

What & why

First scoped slice of #111. The autonomous PR loop was wedged twice by drivers that ended their headless one-shot turn without a published result:

This slice adds post-exit verification and a debris CLASSIFIER (not a blind deleter) at the driver post-exit boundary in the daemon.

Changes (all within .claude/, module: harness)

  • Fail-fast spawn (issue pt 4): run_driver exports CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0 (overridable) so backgrounded work dies loudly instead of half-completing.
  • classify_debris <branch> <worktree_dir> (pt 2): deterministic, pure-git, network-free → empty | publishable | half-done (absent for a missing branch). Shares states with Loop: failure classification + crash-resume for dead workers (the recovery half of loop-health) #98's classifier.
  • verify_and_classify_post_exit (pt 1): for advance issue=N verdicts with a non-timeout rc, queries GitHub via bot-gh.sh for the issue's open PR; records pr=<n>; rewrites result=exit rc=0result=phantom when rc=0 but no PR exists. Offline/failed bot-gh.sh degrades to verify=skipped with zero destructive action.
  • Mechanical case A only: empty + no PR + feat/issue-N-* branch → delete branch + worktree (action=deleted). This is the only destructive path, reachable solely on the provably-empty state. publishable/half-done are never deleted (half-doneresumable).
  • Ledger honesty: still exactly one ledger line per run, now carrying result=… [pr=…] [debris=…] [action=…]; run_driver's return-code contract is preserved.

Tests

  • loop-event.test.sh (23→29): regression guards asserting the ADVANCE and FEEDBACK driver prompts contain the three one-shot contract clauses (foreground-only spawn, "do NOT end your turn until the work product exists on GitHub", delete-debris-on-failure) — guards hotfix d37e951.
  • loop-daemon.test.sh (22→47): behavioral CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0 assert; phantom (rc=0 + no PR → result=phantom); classify_debris unit tests (empty/publishable/half-done/absent, real git fixtures); Case A actual branch+worktree deletion; Case C safety (nothing deleted, branch survives, resumable); offline bot-gh.shverify=skipped.

Gates

.claude/self/gates.json: build / lint / test (checks.sh + smoke-fanout) all green. Reviewer lenses (correctness=opus, tests=sonnet, consensus=all) both APPROVE; the tests reviewer mutation-tested the Case A/C/phantom assertions to confirm they bite.

Deferred (follow-ups)

  • Case B automated publish execution (re-run test gate, push, open PR) — classifier already detects publishable; execution deferred.
  • Case C model-driven continuation driver / claude --resume — belongs to Loop: failure classification + crash-resume for dead workers (the recovery half of loop-health) #98 by the issue's scope note.
  • WIP-commit-at-milestones mandate in agent prompts (issue pt 5) — separate slice.
  • WIP-commit preservation for dirty half-done worktrees (currently recorded/resumable, never auto-committed).

Closes #111

…sifier (#111)

Two incident classes wedged the autonomous PR loop: a driver backgrounding
its orchestrator and ending its turn early left half-born feat/issue-N-*
branches with a phantom rc=0 ledger success (issues #91/#92), while a naive
"delete debris" fix risked destroying gates-green, unpushed work (recovered
manually as PR #117 for issue #107).

loop-daemon.sh: run_driver now forces
CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0 (overridable) on the driver spawn so a
backgrounded orchestrator fails fast instead of half-completing. After every
advance-verdict driver exits (skipping timeouts/spawn-errors),
verify_and_classify_post_exit queries an open PR for the issue via
bot-gh.sh, corrects a naive `result=exit rc=0` to `result=phantom rc=0` when
none exists, and calls the new pure-git classify_debris (empty /
publishable / half-done / absent) to decide whether cleanup is safe. Only
the provably `empty` + no-PR case is ever destructive (branch + worktree
deleted); publishable/half-done work is always left untouched. A failed/
offline bot-gh.sh degrades to `verify=skipped` and takes no action.

Adds 47 new/updated assertions across loop-daemon.test.sh (classifier unit
checks + full-fixture phantom/Case-A-delete/Case-C-safety/offline-degrade
scenarios) and loop-event.test.sh (regression guard for the one-shot
contract clauses in both the advance and feedback driver prompts).
@robercano-ghbot
robercano-ghbot merged commit b29f03c into main Jul 15, 2026
9 checks passed
@robercano-ghbot
robercano-ghbot deleted the feat/issue-111-driver-oneshot-contract branch July 15, 2026 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Loop: driver one-shot contract needs enforcement + tests — background orchestration produced phantom-success drivers and wedged the queue

2 participants