Engineering·

Two human touches

Why this pipeline exists

Most of my ideas don't happen at a desk. They happen in transit: the commute to work, the way home, any stretch of getting from A to B. Sometimes it's an idea for a completely new application, sometimes a single feature for something that already exists, sometimes just a bug I noticed and want gone. The routine is always the same: capture it before it evaporates. In practice that means writing with Claude or Claude Code from my phone and pushing the result into a Linear issue, somewhere more durable than my short-term memory.

For the bigger ideas I want a sparring partner before anything is written down. Depending on the idea that's OpenSpec's explore mode, a thinking partner that digs through the actual codebase and maps the problem before any solution is proposed, or a skill whose entire job is to grill me: attack the idea until only the parts that survive the questioning are left standing. Sometimes both. For a bugfix, neither. Either way, by the end of the ride the idea is an issue with a goal, a scope, and acceptance criteria a reviewer could check.

Capturing ideas was never the problem. The state of my Linear before the pipeline: roughly two dozen real issues filed since mid-May. Six done. The oldest open one from May 17. In two mornings in early June I planned an entire second tool, a personal agent for experimenting with certain libraries and features, in nine tidy issues. Not one of them has moved since. The CMS deployment issue from May is the one this very blog eventually stopped waiting for.

The bottleneck was never the thinking. Implementation only happens at a desk, in the leftover hours, competing with everything else. And since AI can write code, spending those hours typing feels like waste. What I care about is the input, a well-specified idea, and the output: a reviewed, merged pull request. As long as the code between those two points stays clean and sticks to the architecture, I don't need to be the one producing it. That last condition is the entire design problem. "Agent writes code, I skim the diff" isn't a process. It's a risk that feels like progress.

Architecture

The pipeline is shaped like a small team, with agents in the seats and a human at exactly two points:

The pipeline: two human gates at the ends, two trigger hops in dashed boxes (the Cloudflare Worker that fires the implementer, the GitHub Action that fires the reviewer), and two reviewers converging in the middle

Gate 1: issue approval. The commute conversation, formalized. When an idea converges, a skill drafts the Linear issue and shows it to me. I approve, edit, or drop it. That one confirmation is the gate. On approval the issue gets an agent:ready label, and a tiny Cloudflare Worker turns "label added" into "routine, go".

Implementation. Routine A, a Claude Code routine, picks up the issue, re-verifies the labels itself (a forged trigger is a no-op), implements exactly the issue's scope on a claude/ branch, and opens a draft PR. If the issue is too ambiguous to build without guessing, it builds nothing and posts its questions instead.

Review. Two reviewers, neither of them me. CodeRabbit reviews the diff automatically. Routine B, a second routine with reviewer instructions and a rubric checked into the repo, reviews the changes and reconciles every CodeRabbit finding with an explicit disposition: agree, dispute, or escalate. While critical or high findings remain it pushes fixes, capped at three rounds. It ends with a verdict either way, CONVERGED or NOT CONVERGED with whatever is left named for the human, marks the PR ready, and puts a needs:human label on the Linear issue.

Gate 2: merge. I read a diff two reviewers have already argued about, with the argument on the record. Nothing in the pipeline can merge. That stays human, always.

The guardrails matter more than the happy path:

  • No agent merges. Ever. The final authority is the person who can be held responsible.
  • Agents can't push to any branch that isn't claude/-prefixed. Protected branches enforce linear history and PRs for humans and agents alike.
  • Secrets live with humans, never with agents.
  • Unauthenticated or stale triggers do nothing: the worker verifies webhook signatures, and the routine re-checks the issue's labels itself before writing a line.
  • No silent endings. Every run must end in success, a defined quiet no-op, or a loud failure that messages me. "It just didn't run and nobody noticed" is not an allowed outcome.
  • Bounded loops. Three review rounds, then a human, even if findings remain. A clean review is the goal. A guaranteed stop is the rule.

The first day: August 2, 2026

Everything above was theory until August 2. This is the log of that first day, with times and screenshots.

The moving parts:

  • Linear holds the issues. Labels drive the state machine.
  • linear-relay, a small Cloudflare Worker, turns Linear webhooks into routine starts.
  • Routine A (implementer) and Routine B (reviewer) run as Claude Code routines, Anthropic's hosted-agent platform.
  • CodeRabbit reviews every PR.
  • GitHub hosts the PRs, plus a GitHub Action that joins partway through.

One thing predates the log: in the early afternoon, before the label wiring was live, I had fired the implementer by hand as a trial run. It hit a wall and behaved exactly right. Its session could reach this repo but not the second repo holding the writeups it was told to migrate, so it built everything else, marked its PR partial and blocked on the missing content, wrote out the exact steps needed to finish, and refused to invent the rest. That PR was closed, the access gap was fixed, and the evening's first real run took the issue from the top.

Run one, 18:02: build the blog

I flip agent:ready on the issue that builds this blog. The worker fires one second later.

Cloudflare worker log: linear-relay POST OK, one second after the label flip

Ten minutes after that a draft PR exists: renderer, types, two of my CTF writeups migrated with their images copied locally. The implementer even caught its own bug on the way, a React prop leaking into the DOM, found by rendering a probe page instead of just asserting the code looked fine.

Routine A mid-run: fixing a prop leak it caught itself, then lint and build

Routine A's transcript: draft PR is open, Linear updated

Routine A's closing report: what shipped, and three judgment calls documented instead of guessed

With the draft PR open, the implementer's job is done and the reviewers take over. CodeRabbit shows up on time and finds a real bug: the date formatter would let a calendar-invalid date like February 29 of a non-leap year silently roll forward and display the wrong day. So far, exactly the workflow I had in mind.

CodeRabbit starting its review of the pull request

CodeRabbit's finding on dates.ts: reject invalid calendar dates before formatting

Failure: the reviewer trigger never fires

At this point the reviewer is supposed to wake on the PR. It doesn't. The facts:

  • Routine B uses Claude Code's native GitHub trigger for routines, configured with exactly the filters its docs describe. The GitHub app is installed and confirmed working.
  • Three qualifying events arrived that evening. Zero runs started. Not once, not late, not partially.
  • No error, no log line, no run marked failed. Nothing, which is the worst possible signal, because nothing has no stack trace.
  • The docs admit that events over an hourly cap get silently dropped, but three events in one evening is nowhere near any cap, so that is one more problem, not an explanation.
  • There is an open bug about exactly this. The cause stayed out of my reach.

Pipeline B's routine configuration: the native trigger on Pull request Opened, set up as documented, and the only run it ever lists arrived through the API instead

While digging into the failure I recorded the platform's other rough edges from the same day:

  • Runs appear in the dashboard well after they start, so confirming that something fired means refreshing and waiting.
  • The model picker displayed one model while a different one ran.
  • My own agents can't fire the routines I created. The API refuses account credentials, so even an agent acting as me can't start a routine on its own. Firing works only with a dedicated token, generated per routine in that same dashboard.

It's a research preview, so issues are to be expected. The lesson I kept is about my own design: never let the fanciest link in the chain be the one you can't inspect. Every other hop in this pipeline is a webhook or a workflow with readable logs. The one black box was the one that failed silently.

Fix: take the trigger back

The fix already existed once. Earlier that afternoon I had opened a PR with almost this exact Action. Its whole reason to exist was an assumption: that routines could not be triggered from GitHub directly. CodeRabbit left a trivial nitpick on that PR asking me to check whether the assumption still held, because the docs described a native GitHub trigger. The nitpick was right: the trigger existed, and I had built a workflow around the belief that it didn't. I closed the PR unmerged and configured the native trigger instead. Configuration seemed to beat code.

Two hours later the configuration proved it doesn't deliver events, and the Action came back: a small workflow that fires the reviewer over its API on every pipeline PR. The native trigger stays configured as a fallback for the day it decides to work. One last nudge: the Action only fires when a PR opens, and run one's PR had been open for nearly two hours by then, so I closed and reopened it to hand the Action the event it was waiting for.

The replacement trigger: the Fire Pipeline B workflow run, eleven seconds, green

19:55: the review

The reviewer finally wakes, and earns its keep:

  • Rubric review, plus its own verification of the build and the exported pages.
  • A disposition on every CodeRabbit thread: all six findings acknowledged, none of them critical.
  • Verdict CONVERGED, zero fix rounds needed.
  • The date bug it judged real but latent, since both live posts carry valid dates. Instead of touching code it named the gap in its verdict for me, and CodeRabbit conceded the severity in-thread.

Routine B's run summary: CONVERGED, zero fix rounds, and its independent verification list

The disposition thread: the reviewer bounds the severity, CodeRabbit agrees and offers a follow-up issue

The hand-off then hit a limit I hadn't predicted. Its last step, formally requesting a review from me through GitHub, was rejected: "Review cannot be requested from pull request author." Every routine acts under my account. The PR's author was my account, via the implementer. As far as GitHub is concerned, my pipeline is one person, and it cannot ask itself for a review. Instead of ending silently on that, the routine wrote exactly what failed into the thread and pinged me directly.

The hand-off comment reporting the 422: review cannot be requested from the pull request author

Gate 2

I let Claude fix the non-critical date finding by typing "@claude fix it" under the thread. That summons the one actor not on the pipeline's roster: the Claude responder, which answers @claude mentions on any PR in this repo, independent of both routines. It shipped the exact guard CodeRabbit had proposed, in two and a half minutes. I fixed one CSS margin no agent could know was ugly, read the argument end to end, and merged.

Label flip to merged: two hours and forty-four minutes, most of it trigger repair.

My contribution to the code that day: at-claude fix it

The responder's report: date guard shipped, spec docs tightened, under three minutes

Linear hands the issue back: In Review, needs:human

The pull request, ready to merge

Run two, 20:47: publish the blog

Unhide the blog, add the nav entry. Label, PR, review, verdict, my queue: eight minutes, thirty-nine seconds. Zero interventions.

The day's best exchange is buried inside it: the reviewer disputed a CodeRabbit finding (regression tests, in a repo with no test framework, for a nav entry) and CodeRabbit didn't just back down. It recorded a learning, a stored rule it will apply to every future review of this repo, to stop asking. My reviewers train each other now.

A third run fired later that evening: the pipeline patching its own last gap, teaching the merge to close the Linear issue behind it. Its PR is still sitting at Gate 2 as I write this, which is the system working as designed: agents fill the queue, and the human empties it on human time.

Takeaways

Own your triggers. Prefer boring, inspectable links: webhooks, workflows, log lines. When you must use platform magic, verify it through the API, not the dashboard. Twice that day the dashboard showed me a configuration that was supposed to work but did not.

Honest failure is the killer feature. The best moments of the day were not clean successes. The trial run's partial PR that shipped half the feature and said so, refusing to invent the content it couldn't reach. A reviewer reporting a 422 instead of ending silently. If you build an agent pipeline, spend your time on how runs are allowed to end.

Two reviewers argue, one reviewer nods. The dispute-and-concede exchange produced a better outcome than either reviewer alone, and the outcome outlives the argument: the recorded learning changes how every future PR on this repo gets reviewed.

The pipeline is one person. Every agent acts as my account. It cannot request my review, and every approval is, in GitHub's eyes, me approving myself. Fine for a personal site. I don't think I would leave it that way for something with real stakes.

Humans still see what agents don't. Both times I stood at Gate 2 that day, I caught something no agent flagged: an off-center image, an overlapping nav bar. The gates aren't ceremony. They are where the things only a human notices get caught.

Two human touches: the approved idea with its label at the start, and the review and merge at the end.


Disclosure: this post was itself drafted through the pipeline it describes, and reached you through the same gate: a human read it, corrected what the agents got wrong, and merged it.