Everything in Req2Prod hinges on a single artifact in the middle: the pull request. Two quite different things can open one, and from that point on nothing downstream can tell them apart. This page follows a change the whole way — from the moment you type a requirement, through the agents that challenge it and build it, to the review loop that gates it and the deploy that puts it live.
The shape of it
One entry point is the product: you describe a feature, and a product manager, an architect and an engineer turn it into code. The other is you in a Claude Code session, building the tool itself. They converge on the same artifact and are held to the same gate.
build_feature() only ever opens a pull request — it never pushes to main and never merges — so the engineer's work arrives exactly like a human-authored branch.The seam also marks where you change roles. Before the pull request you are in the loop: the product manager and architect challenge your requirement, hand back open questions, and nothing proceeds until you have answered them. After it you are deliberately out of the loop: the reviewer, the fixer and the arbiter converge or escalate among themselves, and you are only ever informed of the outcome.
That is the position the whole system takes. You are the authority on what to build, and explicitly not the judge of how it was built.
Before the pull request
The front half is a conversation, not a form. You type a feature request, and challenge_requirement() sends the entire thread — your original idea plus every answer you have given since — to the product manager and the architect. They re-run fresh against that whole accumulated conversation every time rather than relying on agent memory, so picking a session back up days later challenges from the same context.
build_feature() re-checks both flags itself before running — defense in depth, not the primary gate.The loop is the point. Neither agent gives you a plan and stops; they come back with what they still need to know, and the Push to Software Engineer button does not exist until both have set ready_for_development. It also disappears the moment anything new is said, so a stale verdict can't be pushed twice.
Once you press it, software_engineer builds — and it can stop and ask the product manager a functional question or the architect a technical one mid-build rather than guessing. It works inside a throwaway clone of the repo that is deleted afterward regardless of outcome. That isolation was learned the hard way: the file and git tools used to run wherever the process happened to be, which in production is the directory serving live traffic. One malformed write left streamlit_app.py containing the word "test" and the live server parked on a half-built branch — a full outage from a single bad tool call.
Once the pull request exists
From here the path is identical whichever half produced the branch. The pipeline is not one sequence: it contains three jobs, and which of them runs depends entirely on which event fired — code_review and deploy never execute in the same run. The one that always runs is resolve_backend, which decides api versus subscription in a single place so the runner choice and the backend handed to the scripts can never disagree.
Nothing in this file enforces review before deploy — there is no needs: linking the two jobs. What guarantees a change was reviewed before it reaches production is main's branch protection requiring an approving review. GitHub refuses the merge, so the push event that would trigger a deploy never happens. The ordering lives in your repo settings, not in the YAML.
One caveat the boundary hides: it is only temporary in api mode. Flip AGENT_BACKEND to subscription and those jobs relocate to your laptop, which persists — which is why the deploy job's cleanup step matters. resolve_backend also enforces a security boundary: a fork's PR silently falls back to api no matter what the toggle says, because subscription would otherwise run an outsider's code on your machine.
When the event is a pull request
GitHub is only the trigger and the gate here — it does no reviewing itself. The workflow checks out the actual branch rather than the merge ref, because the fix agent needs somewhere real to push, then pipes gh pr diff into a file and runs one Python module.
code_review_runner.py is the orchestrator, and it is plain Python with no AI in it. It owns the review and fix loop, all git operations, and the final review and merge calls. The agents never touch git: pr_fix_agent only edits files in the working tree, and the runner commits everything it changed exactly once at the very end. Pushing mid-loop would fire a fresh synchronize event and spawn a second, racing copy of the same job.
Each agent returns a typed result rather than prose, which is what lets the plain-Python driver branch on result.passed and verdict.safe_to_merge instead of parsing text. If code_reviewer passes, the runner approves and squash-merges immediately. If it returns findings, those exact findings go to pr_fix_agent, which edits the named files; the runner then re-diffs against origin/main and re-reviews the new diff. If it still hasn't converged, pr_arbiter makes a genuine independent call: merge it, or leave the PR open and email you a plain-language explanation.
The review is submitted as MarcoAIagent rather than the workflow's default token, because GitHub won't let a PR's author approve its own PR — the reviewer has to be a different account. The default token is read-only anyway, so it couldn't push fixes even if it were allowed to approve.
When it's a merge or a manual run
The deploy job genuinely is one long ordered sequence, and its whole purpose is reaching out of the throwaway VM to a machine that isn't throwaway.
The snapshot step near the top is what makes the bottom half work: it records production's current commit before anything changes, and hands that value to rollback_agent as PREVIOUS_COMMIT. Without it there would be nothing to roll back to.
The restart is conditional — it only fires if the diff touched .py files or requirements.txt — and when it does, dependencies are installed into the server's persistent venv, not just the runner's throwaway one. That step exists because its absence once restarted the app straight into a ModuleNotFoundError. Installing a new dependency on the runner only affects the box that is about to be deleted.
Two neighbours
deploy-to-prod.yml is a separate, manual-only deploy that predates the merge; the pipeline's own deploy job largely supersedes it. devops-agent.yml watches both of them and fires automatically when a deploy fails: devops_agent reads that run's logs, applies a fix, pushes it, and re-triggers the workflow.
It ignores pull request failures, since the review path already has pr_arbiter as its escape hatch. It also carries two guards against runaway cost — it stops and emails you if the last commit on main was already one of its own fixes, or if it has fired too many times in a short window.
Collapses the whole SDLC — analysis, build, review, release — into one autonomous, auditable flow.
The review loop runs a fix agent, then an independent arbiter decides; failed prod tests auto-rollback or invoke DevOps.
Each agent runs its own Claude tier from a model registry. The sole human step is requirement clarification.