|
| 1 | +# Week Two with loop-worktree: PR Babysitter, one fix at a time |
| 2 | + |
| 3 | +**Pattern:** PR Babysitter |
| 4 | +**Tool:** loop-worktree (paired with loop-context) |
| 5 | + |
| 6 | +## What I was doing |
| 7 | + |
| 8 | +I was running a small PR Babysitter loop: watch a PR, try a fix in isolation, |
| 9 | +verify it, and only escalate to a human if the fix doesn't land. The part I |
| 10 | +wanted to get right this week was making retries safe — if attempt #1 fails, |
| 11 | +attempt #2 shouldn't be fighting attempt #1 for the same branch. |
| 12 | + |
| 13 | +## The workflow |
| 14 | + |
| 15 | +For each fix attempt, I create an isolated worktree keyed to the run: |
| 16 | + |
| 17 | +``` |
| 18 | +npx @cobusgreyling/loop-worktree create --run-id pr-42-fix-1 --pattern pr-babysitter |
| 19 | +``` |
| 20 | + |
| 21 | +That gives me a fresh checkout under `.loop-worktrees/pr-42-fix-1` on its own |
| 22 | +branch (`loop/pr-42-fix-1`), tracked in `.loop-worktrees/manifest.json` — so |
| 23 | +nothing touches the branch another attempt might still be using. |
| 24 | + |
| 25 | +Once the verifier rejects an attempt, I mark it rather than deleting it |
| 26 | +immediately — that keeps the audit trail intact: |
| 27 | + |
| 28 | +``` |
| 29 | +npx @cobusgreyling/loop-worktree mark --run-id pr-42-fix-1 --status rejected |
| 30 | +``` |
| 31 | + |
| 32 | +`list` gives me a quick read on what's currently tracked: |
| 33 | + |
| 34 | +``` |
| 35 | +npx @cobusgreyling/loop-worktree list |
| 36 | +``` |
| 37 | + |
| 38 | +And periodically I sweep anything rejected or escalated: |
| 39 | + |
| 40 | +``` |
| 41 | +npx @cobusgreyling/loop-worktree cleanup --status rejected --older-than 24h |
| 42 | +``` |
| 43 | + |
| 44 | +## Pairing with loop-context --check |
| 45 | + |
| 46 | +Before letting the loop retry again, I run the ledger through |
| 47 | +`loop-context --check` so a stuck fix doesn't just keep burning attempts: |
| 48 | + |
| 49 | +``` |
| 50 | +npx @cobusgreyling/loop-context --check --ledger run.json --stagnation 3 --json |
| 51 | +``` |
| 52 | + |
| 53 | +With three identical failures in a row it stayed green (`shouldContinue: true`), |
| 54 | +but on the fourth repeat of the same error it correctly flipped to |
| 55 | +`escalate: true` with exit code 2 — which is the signal I use to stop the loop |
| 56 | +and hand the PR back to a human instead of trying a fifth time. |
| 57 | + |
| 58 | +## The surprise |
| 59 | + |
| 60 | +The gotcha that cost me the most time: `--check`'s stagnation trigger only |
| 61 | +recognizes attempts with `"outcome": "failure"` exactly. I'd first logged |
| 62 | +attempts as `"outcome": "fail"` — `--summary` and `--status` happily counted |
| 63 | +those as failures and showed the repeated error, so everything *looked* |
| 64 | +right, but `--check` never escalated no matter how many times the same error |
| 65 | +repeated. Nothing errors out to tell you the value was wrong; the breaker |
| 66 | +just quietly stays at "continue." Since the whole point of pairing |
| 67 | +loop-worktree with loop-context is to stop a bad retry loop before it wastes |
| 68 | +a worktree (and tokens) on a fix that isn't working, a silently-inert circuit |
| 69 | +breaker is exactly the failure mode you don't want. |
| 70 | + |
| 71 | +Separately, a smaller but good surprise: `cleanup --older-than 24h` refused |
| 72 | +to remove a worktree I'd just marked rejected seconds earlier — it only acts |
| 73 | +once the attempt is actually old enough (or you pass `--force`). That's the |
| 74 | +right default; I just didn't expect it on first try and thought the command |
| 75 | +had silently no-op'd. |
| 76 | + |
| 77 | +## What I'd change |
| 78 | + |
| 79 | +I'd want `--check` to validate the `outcome` field against the documented |
| 80 | +enum and fail loudly (or at least warn) on an unrecognized value, instead of |
| 81 | +silently treating it as a no-op for the stagnation/no-progress triggers while |
| 82 | +`--summary` keeps counting it as a failure. Right now the two commands can |
| 83 | +disagree about the same ledger without telling you. |
0 commit comments