akdarrah
adam@akdarrah~/posts$cat one-session-per-pull-request.md

One session per pull request

5 min #ai#tools
contents · notes

# contents

# in this post

{}snippet-1 L1–1text {}factory-sessions-prompt.txt L1–17text ↓ download all code (.zip)

A couple of weeks ago I wrote about running the software factory in parallel. When one ticket turns into several pull requests, the main session plans them all once, then hands each PR to a background agent of its own. A table above the prompt shows every agent's progress.

I've used that every day since. It's great at the start of the work and gets in the way at the end.

Pull requests don't finish together

Say one session fans out into five pull requests. They all start together, but they don't finish together, and they don't finish in the same state. By the time they're all in review, one PR is ready to merge, one conflicts with main because a teammate merged first, one has a failing test, and two have review comments waiting.

I don't babysit my sessions in tabs. I use a small orchestration tool I built. It watches every Claude Code session on my machine and shows me the one thing that needs me next: a plan to approve, a question to answer, a PR to merge, or a PR to finish. When I press its fix button, it types into that session what's left on the PR (conflicts, failing CI, review threads), and the session does the work.

That only works if one PR has one session. With parallel mode, five PRs shared one parent session. The tool could see that five PRs needed different things, but there was only one session to steer. Asking the parent to fix one PR kept it busy, so the next PR's fix waited behind it. The work had been done in parallel, and now it was being finished one PR at a time.

The parent also had nothing left to do. Its two jobs were checking the ticket and planning the batch, and both were done long before the PRs came back. It was only a switchboard, passing my messages to the subagents and their answers back to me.

I could have made the parent smarter: track each subagent, route each fix to the right one, report every PR's state separately. But the session is already the thing my tools understand. Claude Code lists sessions, resumes them, and runs them in the background. An orchestration agent or a dashboard can track a session. A subagent buried inside one is hard to see.

What changed

The parent still does the first two steps once, for the whole batch: verify the ticket, and plan every PR in one plan that I approve once. The plan still has a collision table: every file two PRs both touch, and the order to merge them in.

Then, instead of starting subagents, it starts real sessions, one per PR:

text
1claude --bg -n "TICKET-123 Short title" "/factory <ticket url> --plan <plan file>"

Each session gets its own plan file, saved somewhere that outlives the parent: that PR's section of the plan, the collision table, and a pointer to every other PR in the batch. A session started with --plan skips the verify and plan steps and goes straight to build. Then the parent tells me which sessions it started, and stops.

From there each PR lives its own life. It has its own progress bar, it asks me its own questions, and when it's done, my orchestration tool shows it as its own card. If one PR conflicts and another fails CI, I press fix on both and they get fixed at the same time, each in its own session with its own context.

Some things didn't change, they just moved:

  • Two at a time. My laptop still can't run more than two of these builds at once. Every session starts right away, but before building it has to take one of two slot locks (a directory made with mkdir, which fails if it already exists). A session without a slot waits until one is free.
  • Locks for shared things. There's still one browser and one development database. A session takes a lock before using either one. These locks now work across sessions instead of across subagents, and that needed no change: a directory on disk doesn't care which process made it.
  • No more relaying questions. Before, a stuck subagent stopped, the parent asked me, and the parent passed my answer back. Now the session that's stuck just asks me itself.

One thing I had to fix in the locks. Before, a lock older than 45 minutes meant its holder had died. A slot is held for the whole build, review and QA, which takes hours, so its age means nothing. Now a slot counts as free when the session that took it is no longer running.

What I learned

Put the boundary where your tools already look. My tools think in sessions, so a unit of work that will need my attention later should be a session, not something hidden inside one.

Parallel work is easy to start and hard to finish. The planning and the fan-out are where the speed is. But everything ends with merge conflicts, CI, and review comments, and that part needs one handle per pull request.

The prompt

If you built parallel mode from the last post, paste this into Claude Code:

factory-sessions-prompt.txt
1Change parallel mode in my /factory skill so each pull request runs in its own Claude2session instead of as a subagent of the main session.3 41. The main session still verifies and plans the whole batch once, with a collision table5 and a merge order, and I approve it once.62. It then writes each PR's part of the plan, the collision table, and a pointer to the7 other PRs to a file that survives the main session ending (not a temp directory).83. For each PR, it runs from the repo checkout:9 claude --bg -n "<ticket> <short title>" "/factory <ticket url> --plan <that file>"10 then lists the sessions it started and stops. It doesn't follow them afterwards, and it11 never reports "done" itself.124. Teach /factory a new entry: when given --plan <file>, skip verify and plan, read the13 file, and run build through done for that one PR. Ask me directly when stuck.145. Keep my limit of 2 running at once with two slot locks (mkdir slot-1 / slot-2). A15 session takes one before building and releases it when done. A session with no free slot16 waits for one. A slot is free once the session that holds it is no longer running.176. Keep the browser and dev-database locks as they are.
share
view .md
↑↓ move↵ open` shellesc close
akdarrah0:posts1:archive2:about3:shell* esc close
adam@akdarrah~$help
  ls [posts|tags]   list things      cat <slug>   open a post
  grep <word>       search           art [scene]  draw something
  tree              the old bonsai   subscribe <email>