863435915c
Co-authored-by: claude <claude@jpaul.io> Co-committed-by: claude <claude@jpaul.io>
149 lines
15 KiB
Markdown
149 lines
15 KiB
Markdown
<!--
|
|
Suggested title: Half Your Teammates Aren't Human (and the Loop Doesn't Care)
|
|
Alt title: One Loop, Any Contributor: How Issues, Branches, and PRs Become Agent Safety
|
|
Slug: the-workflow-collaboration-humans-and-agents
|
|
Meta description: The full coordination loop: issue, branch, PR, review, merge, issue
|
|
closed, was never really about humans. It's the harness that lets you
|
|
safely accept work from an agent. Here's how to run it.
|
|
Tags: AI, developer workflow, git, pull requests, code review, agents, collaboration
|
|
-->
|
|
|
|
# Half Your Teammates Aren't Human (and the Loop Doesn't Care)
|
|
|
|
A few posts back we filed an issue. Last post we opened a pull request and learned to review a diff we didn't write. Both of those are real, useful skills on their own, but they've been sitting in your toolbox as separate tools, and that's not how a team actually uses them.
|
|
|
|
So here's the thing I want you to see in this post, because once you see it you can't un-see it: there's *one loop* that connects all of it, and **nothing in that loop says the contributor has to be a person.**
|
|
|
|
That's not a cute observation. It's the most useful property of the whole system right now. The exact tooling you learned to coordinate human teammates turns out to be the tooling that lets you safely put an agent to work. Same loop. Same gate. Same rules. Let me walk you through it, and then point at the spot where some of the "contributors" running through it are machines, and it doesn't matter one bit.
|
|
|
|
(New here? This is part of [The Workflow](https://git.jpaul.io/justin/ai-workflow-course), a free course about the engineering scaffolding around AI coding. You can read this one standalone, but if "file an issue" or "open a PR" feels fuzzy, the earlier posts have you covered.)
|
|
|
|
## Two loops, not one
|
|
|
|
Way back, you learned the **inner loop**: edit, `git diff`, commit, repeat. That loop lives on your disk and it's yours alone. It's how *you* (or your agent) make progress in a working session. Nobody else sees it while it's happening.
|
|
|
|
This post is about the **outer loop**, the one the *team* sees:
|
|
|
|
```
|
|
issue → branch → implementation → pull request → review → merge → issue closed
|
|
```
|
|
|
|
Every one of those stations is something you've already met as a separate skill. The issue says *what* to do. The branch isolates the *attempt*. The PR makes the attempt *reviewable*. The review is the *judgment*. The merge is the *commitment*. Closing the issue is the *receipt*.
|
|
|
|
The reason to finally assemble these into a single loop, instead of keeping them as a pile of separate git tricks, is that the *handoffs between stations* are where collaboration actually happens. And where it breaks. Skip the issue and you get work nobody asked for. Skip the branch and changes land straight on `main` with no net. Skip the review and "done" means "merged," not "correct." The stations matter, but the seams between them matter more.
|
|
|
|
[insert a screenshot referencing the seven-station loop diagram (issue → branch → implementation → PR → review → merge → closed) here]
|
|
|
|
## The loop, station by station
|
|
|
|
Let's run it for real, on the little `tasks-app` the course carries the whole way through. The feature: add a `clear-done` command that removes every completed task. Deliberately small; the point is to practice the *loop*, not the code.
|
|
|
|
**1. The issue is the contract.** Before any code, there's a statement of intent with a number on it (`#42`). It exists so "what we're doing and why" lives somewhere durable and shared, not in one person's head or one chat session that'll evaporate. You assign it to whoever's taking it: a person, or an agent.
|
|
|
|
**2. The branch is the workspace.** You never implement on `main`. You cut a branch named for the work, and the convention is to make it traceable:
|
|
|
|
```bash
|
|
git switch -c 42-clear-done-command # branch off main and switch to it
|
|
```
|
|
|
|
That name does more than it looks like. Months from now, `git branch` and your host's branch list become a map of *what's in flight*, and the issue number ties each branch back to its contract.
|
|
|
|
**3. Implementation is the inner loop.** This is the edit/diff/commit rhythm you already have: you, or an agent, making commits on the branch. Nothing new here. The branch keeps it isolated, so however bold the change gets, `main` stays untouched until the loop says otherwise.
|
|
|
|
```bash
|
|
git push -u origin 42-clear-done-command # publish the branch so others (and the host) can see it
|
|
```
|
|
|
|
**4. The pull request makes it reviewable.** Opening a PR says "this branch is ready to be considered for `main`." It bundles the diff, a description, and a discussion thread into one reviewable unit. And (this is the load-bearing part) it's where you link back to the issue so the loop can close itself (more on that in a second).
|
|
|
|
**5. Review is the judgment gate.** Someone who isn't the author reads the diff for correctness *and plausibility*. For AI-generated diffs this gate is doing more work than it used to: the code compiles, reads cleanly, and is still wrong in a way only review catches. Approve, request changes, or comment.
|
|
|
|
**6. Merge is the commitment.** Approved, the PR merges into `main`. Squash, merge-commit, rebase: pick one; the effect is the same. The branch's work is now part of the shared trunk. Delete the branch after; its job is done.
|
|
|
|
**7. The issue closes itself.** If you linked the PR correctly, merging closes the issue automatically. Nobody touches the issue; the merge writes the receipt. That quiet *click* of the whole loop landing is the thing the lab makes you actually feel.
|
|
|
|
## The one line that closes the loop for free
|
|
|
|
Here's the mechanic behind station 7. Put a **closing keyword** in the PR description:
|
|
|
|
```
|
|
Closes #42
|
|
```
|
|
|
|
`Closes`, `Fixes`, and `Resolves` (and their variants) all work on the major hosts: GitHub, GitLab, Gitea/Forgejo, Bitbucket. When the PR merges **into the default branch**, the host closes the referenced issue and cross-links the two so each points at the other. One line in the PR body buys you a self-closing loop *and* a permanent trail from "why we did this" (issue) → "what we did" (PR/diff) → "when it landed" (merge).
|
|
|
|
A plain `#42` with no keyword *links* the two but does **not** close on merge. That's useful for "related to" references; just know the difference, because the keyword is the load-bearing part.
|
|
|
|
And that trail is the real prize. Six months from now someone asks "why does `clear-done` exist?", and that someone might be an agent reading the repo as durable memory. The answer is one click away: issue → PR → diff → merge. You built that trail for free by typing one line.
|
|
|
|
## Branch or fork? It's just push access
|
|
|
|
Two ways a contributor gets work in front of the team, and the deciding question is dead simple: **can you push to the repo?**
|
|
|
|
- **You have push access → branch in the repo.** The normal case for a team on a shared repo, and everything above assumes it. PRs go branch → `main` inside one repo.
|
|
- **You don't have push access → fork, then PR from the fork.** The open-source / outside-contributor case. You clone the repo into your *own* copy (a fork), push branches there, and open a PR *across repos*.
|
|
|
|
```bash
|
|
# Forked-contributor flow (no push access to upstream):
|
|
# 1. Fork upstream/repo -> you now own you/repo (one click on the host)
|
|
# 2. git clone https://host/you/repo
|
|
# 3. git switch -c my-fix ; ...commit...
|
|
# 4. git push -u origin my-fix # origin = your fork, which you CAN push to
|
|
# 5. Open a PR from you/repo:my-fix -> upstream/repo:main
|
|
```
|
|
|
|
For most of what you do (repos you control) **branches are the default, forks are the exception.** And here's where the AI angle sneaks in early: an agent you run on your own repo branches like any teammate. An agent contributing to a project it *doesn't* own forks like any outside contributor. The rule doesn't change for machines.
|
|
|
|
## Who's allowed to push (and making the server enforce it)
|
|
|
|
"Never commit directly to `main`" started life as a personal discipline. On a shared repo it becomes an *enforced* rule, and that enforcement is the half of collaboration nobody mentions until it bites.
|
|
|
|
**Roles.** Hosts hand out access in tiers: read (clone, comment), then write (push branches, open PRs), then maintain/admin (settings, protections, force-merge). A contributor only needs *write* to run the whole loop above. Give out the least that lets someone do their job: the same least-privilege instinct you already have for production systems.
|
|
|
|
**Protected branches** are the enforcement. You mark `main` as protected and the host *refuses* direct pushes to it: the only way in is a PR. You can layer rules: require a PR, require a review approval, restrict who can merge. Turning these on converts "we agreed not to push to `main`" into "the server won't let you."
|
|
|
|
Don't skip this in the lab, because *feeling* the server say no is the whole point:
|
|
|
|
```bash
|
|
git switch main
|
|
echo "# direct edit" >> README.md
|
|
git commit -am "try to push straight to main"
|
|
git push # expect: remote REJECTS the push to a protected branch
|
|
git reset --hard HEAD~1 # undo the local commit; we'll do it the right way
|
|
```
|
|
|
|
For a solo learner this can feel like bureaucracy. But it's exactly the guardrail that makes it safe to add a contributor you trust *less than fully*, including a machine one. Hold that thought, because it's the whole point of the next section.
|
|
|
|
## The contributor who isn't human
|
|
|
|
Okay. Re-read that loop (issue, branch, implementation, PR, review, merge) and notice what's *not* in it: any requirement that the contributor be a person. That's not an oversight. It's the most useful thing about the entire system right now.
|
|
|
|
**An agent is a contributor with a branch.** You hand it an issue. It cuts a branch, implements, opens a PR: exactly the loop above. A human reviews that PR on the same gate used for any teammate. The agent never touches `main`; the protected-branch rules and the review gate apply to it *identically*. This is *why* the loop is worth assembling as a loop: it's the harness that lets you accept work from a contributor whose judgment you don't fully trust yet. Which is the exact profile of an agent.
|
|
|
|
In the lab you run the loop a second time and let the agent be the contributor. There's one honest snag worth calling out, because it's a seam you'll feel: your editor-integrated AI edits files and runs local commands, but `git push` only *publishes a branch*: it does **not** open a PR, and the web UI you've been clicking can't be handed to a machine. So you either give the agent your host's CLI (`gh`, `glab`, `tea`) so it can run `gh pr create` itself, or you take the no-CLI fallback: let the agent branch, implement, commit, and push, and *you* open the PR. Either way, the agent drives the first five steps and **you stay the human at the merge.**
|
|
|
|
**Two agents at once? That's just two contributors needing branches.** The moment you run more than one agent, you've got the oldest collaboration problem there is: two workers who must not edit the same files in the same directory. Not a new problem, and it already has an answer: worktrees. Each agent gets its own working directory and its own branch, they work simultaneously, each opens its own PR, you review and merge them independently. Worktrees earned their own module precisely so this case would already be solved by the time you got here.
|
|
|
|
[insert a screenshot referencing two agents running in parallel worktrees, each with its own branch and PR, here]
|
|
|
|
**The merge stays human, for now.** An agent can do every step *up to* merge. The merge (the commitment to shared `main`) is where you stay in the loop, because review is judgment and judgment is the thing you haven't delegated yet. Later in the course we carefully, conditionally move that line. Today, the win is just being able to *picture* an agent doing the first five steps while you do the sixth, and not finding that the least bit exotic.
|
|
|
|
So here's the reframe to carry out of this post: **collaboration tooling was never really about humans.** It's about coordinating *contributors*: isolating their work, making it reviewable, controlling who commits it to the trunk. Those are exactly the guarantees you need to safely let an agent contribute. The team layer you just learned doubles as the agent-safety layer you'll lean on for the rest of the course. You're not learning collaboration *and then* learning to work with agents. They're the same skill.
|
|
|
|
## Where it breaks (because I always tell you this part)
|
|
|
|
- **Auto-close only fires on merge to the *default* branch.** Merge into a non-default branch and the issue stays open, by design. And keep the keyword in the *PR description* or a commit message; buried in a mid-thread comment it behaves differently across hosts.
|
|
- **The exact keyword set is host-specific.** `Closes/Fixes/Resolves` are the safe, widely-supported trio, but the full list and the cross-repo syntax (`owner/repo#42`) vary. When in doubt, mention-link and close by hand; the trail still exists.
|
|
- **Auto-closed is not the same as actually done.** Merging closes the issue *mechanically*. It says nothing about whether the work was correct; that was the review's job. If review was a rubber stamp, you just auto-closed an issue for broken code. The loop automates the bookkeeping, never the thinking.
|
|
- **Protected branches protect against accidents, not admins.** Most hosts let admins bypass protection, sometimes silently. And an account with push access (including a *bot* account you set up for an agent) is an attack surface and a blast radius. Scope machine accounts to the least they need.
|
|
- **Forks add friction.** Keeping a fork synced with a fast-moving upstream is ongoing work, and PRs from forks are deliberately limited by hosts (they often can't reach the upstream's CI secrets). For repos you own, prefer branches.
|
|
- **The diagram is the happy path.** Real PRs get change requests, need a rebase onto a moved `main`, or hit a merge conflict when two contributors touch the same lines: exactly the parallel-agent scenario worktrees mitigate but don't eliminate. The stations are fixed; the number of trips around them isn't.
|
|
|
|
## You're done when the loop feels like one motion
|
|
|
|
You're there when you can draw the seven stations from memory, state the branch-vs-fork rule in one sentence (push access → branch; no push access → fork), and, the real milestone, when "give the agent a branch and review its PR" feels *obvious* rather than novel. When the six tools collapse into one motion in your head, you've got it.
|
|
|
|
That's also the moment a quiet worry shows up: if an agent can run five of the six steps, what happens when a *bad* PR makes it all the way through review and lands on `main`? That's exactly where the next post goes: turning the *recovery* half of this safety net into its own discipline: cleanly reverting a merged change after the fact, without a panic.
|
|
|
|
Running the loop with an agent for the first time? Tell me where it got weird: the CLI hand-off, the parallel-worktrees thing, wherever it snagged. Drop it in the comments. I read them, and the rough edges you hit are what make the course better.
|