Last month I wrote about how I use coding agents to build things. This week I pointed the same setup at someone else's codebase. Rowboat (rowboatlabs/rowboat, 17.5k stars) had its good first issues picked clean, so I had to work for it a little.
What I shipped
Three pull requests in about a day:
PR #1031: the sidebar Meetings subtitle showed an all-day event next week as if it happened today, masking a timed event that was sooner. Root cause was a sort that put all-day events first regardless of date, duplicated in two components. Fix: sort purely by start time, and give future all-day events a date qualifier so they never read as today.
PR #1032: in child mode, if the spawned rowboat-server died, the app just logged it and the UI went stale. Now it respawns with exponential backoff (1s to 16s, capped at 30s), gives up after five consecutive failures with a clear message, and never respawns on intentional shutdowns. The interesting part was the state swap: the RPC forwarder caches a ready promise, so a respawn has to swap that promise or the app keeps talking to a dead process.
PR #1033: a show-pairing CLI mode for the headless server. Before: SSH in, cat the server key file, squint. After: one command prints pairing URLs, the access code, and the exact payload the mobile app scans, optionally as a terminal QR. Zero new dependencies.
How the work actually went
Each one followed the same loop: audit the issue, verify nobody was actually working on it, post a short claim comment, let Claude Code write the code on my machine, then review the diff myself before pushing.
The review step caught real things. On PR #1033, my agent had branched from the previous PR's branch, which would have dragged an unrelated commit into the pull request. Caught it during diff review, cherry-picked the commit onto a clean main, pushed. Ten seconds of checking saved a maintainer from untangling my mess.
The part that was not coding
While picking issues I kept finding ghosts. Seven issues around server hardening were all claimed by one person in a single day, ten days before I showed up. None of them had a pull request. The claims sat there like reservations at a restaurant that never orders.
So I audited instead of grabbing blindly. Two of those issues turned out to be already fixed in main: the Host header allowlist and the header-first WebSocket auth had both landed in a merged hardening PR. The issues were just never closed. I left comments with file names and line references so maintainers could verify in one click.
One of those comments was also a self-correction. I claimed the WebSocket issue, then checked the code before writing any, found the fix already in, and posted the correction with the evidence. Claiming an issue and then silently dropping it is the ghost behavior I was criticizing. The difference between me and the ghosts is that I posted why.
What the numbers say
- Issues picked clean at the attractive end: dozens of candidates, most already claimed or covered by open PRs
- Claims with zero work behind them: at least seven in my target area alone
- PRs that carried exactly one clean commit each: three of three
- Reviews from maintainers so far: zero, they are presumably asleep, it was 2am in their timezone too
What I would tell someone starting this
Do not pick issues by labels. Labels are where everyone goes and where the ghosts live. Pick by reading the code and finding the gap between what the issue says and what main actually does. Sometimes the gap is the contribution.
And review your agent's diffs like a hostile senior engineer. The agent that writes good code will also quietly branch from the wrong place. It is still your name on the PR.
Top comments (9)
The review step has one more thing in it, in the PR you call the interesting one.
scheduleRespawndoesrespawnAttempts += 1and then returns whenrespawnAttempts > MAX_RESPAWN_ATTEMPTS, so the reachable attempts are 1 through 5 and the index isMath.min(attempt - 1, 5), which only ever lands on 0 through 4.RESPAWN_DELAYS_MSends16_000, 30_000, and that last entry is never read. TheMath.minclamp guarding it cannot fire either.So the ladder really is 1s to 16s, as your summary says, and the 30s cap is not a cap, it is a dead array element. Harmless right now, which is why it survives review: the two constants are independent, and the first person who raises
MAX_RESPAWN_ATTEMPTSto 6 gets a silent extra retry at a delay nobody looked at. Either drop the entry or define the max asRESPAWN_DELAYS_MS.length, so they cannot drift apart in the first place.This is a sharper read than my summary, and you caught the part I glossed over: the dead 30s entry and the clamp that can never fire. My article stopped at the ladder being 1s to 16s, you explained why it stays that way and when that silently changes.
Of your two fixes I would take defining the max as RESPAWN_DELAYS_MS.length. Dropping the entry fixes today; tying the two constants together fixes the drift class. The failure you describe (someone raises MAX to 6 and gets a retry at a delay nobody reviewed) is exactly the kind of bug that survives because both constants look correct in isolation.
If a maintainer picks this up from the thread, the one-line change plus a test asserting delay[attempt - 1] exists for every reachable attempt would close it permanently. Thanks for reading the code instead of the prose.
The branch mistake is the perfect example because the code can be correct while the contribution is still wrong. Agents optimize the visible task; maintainers absorb repository state, issue ownership, branch ancestry, and review cost. Your “audit before claiming” loop is arguably the more valuable contribution than the three patches.
I would turn it into a pre-push gate: clean base, one intended commit, no unrelated files, issue still open, and a human-readable reason for every changed file.
Your gate list matches the checklist I now run by hand, with one humbling addition: the branch mistake happened at the claim step, not the push step. The repo state was clean, the commit was single-purpose, and the contribution was still wrong because the bug was already fixed upstream before I wrote a line. So the gate belongs before claiming, not just before pushing: issue still open, fix not already in main, then your five items.
The reason-per-changed-file idea is the piece I had not formalized. If I cannot write one honest sentence about why a file changed, that file is scope creep wearing a commit message. Automating that check as a script is on my list, and your list is the spec.
The respawn-promise swap is the part I'd want tested hardest, not the backoff ladder. When the server dies mid-request, whatever RPC calls already awaited the OLD ready promise are holding a reference to something that already resolved to a dead process. Swapping the promise for new callers doesn't retroactively fix the ones already past that await. Did those get an explicit rejection and a retry, or do they just hang until some other timeout catches them?
Good catch, and you found the actual gap. Honest answer: no explicit rejection path for calls already past that await. When the child dies the JSON-RPC pipe breaks, so most in-flight calls fail fast with a transport error and the caller sees the failure, but that is incidental to the pipe closing, not a designed rejection, and anything mid-flight on a buffered write has no guarantee at all. The swap itself only reassigns the cached ready promise for new callers, so old awaiters are on their own.
Your framing is the right spec for the follow-up: on crash, reject the cached promise (or wrap it so in-flight callers get a typed RPCError), retry idempotent calls against the new child, and surface non-idempotent ones to the UI instead of silently re-driving them. Keeping this comment as the acceptance criteria for that PR.
Someone else's codebase is the honest benchmark for an agent setup, and good-first-issues being picked clean makes it honest twice - no scaffolded task, no head start. The thing your own project never tests is orientation cost: on your own code the agent benefits from YOUR context leaking into every prompt, because you already know where the bodies are buried. On a foreign repo the prompt has to carry the whole map, and the gap between "agent that edits my code" and "agent that contributes upstream" is almost entirely that map-making. The maintainer's side matters too: a PR from an agent-assisted stranger gets read differently than one from a known contributor, so the bar shifts from "does it work" to "is it obviously correct to someone with thirty seconds." Three PRs in 24 hours is speed; the interesting number is how many survive review without a rewrite request.
You named the metric I should have led with. The current honest number: 3 submitted, 0 reviews, 0 merge requests, 0 rewrites requested. Survival rate undefined until a maintainer reads them, and I will report it either way, including if the answer is zero.
Your orientation-cost point matches what the build log showed. Every prompt for the foreign repo had to carry the whole map: where validation lives, which files are generated, what the tests actually assert. On my own projects that context leaks in for free, which is exactly why it never tested anything. The gap between editing my code and contributing upstream was almost entirely map-making, and the PRs stand or fall on how much of that map I got right without noticing.
When the first review lands, the diff between what the maintainer asks and what the map said becomes the real dataset. That follow-up post writes itself, whichever way it goes.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.