2684095e2f
Co-authored-by: claude <claude@jpaul.io> Co-committed-by: claude <claude@jpaul.io>
433 lines
24 KiB
Markdown
433 lines
24 KiB
Markdown
# Module 11 — Collaboration: Humans and Agents on One Repo
|
||
|
||
> **You now have every piece — issues, branches, PRs, review. This module wires them into one loop,
|
||
> and points out that half your "teammates" might not be human.** Once the loop runs the same way no
|
||
> matter who's pulling the work, an agent is just another contributor who needs a branch.
|
||
|
||
---
|
||
|
||
## Prerequisites
|
||
|
||
This is the synthesis module for Unit 2's collaboration arc. It assumes the whole chain up to here:
|
||
|
||
- **Module 2** — commits as checkpoints, and `git diff`/`git log` as the record everyone reads.
|
||
- **Module 6** — branches as isolated sandboxes; you make changes off `main`, not on it.
|
||
- **Module 7** — worktrees, so more than one branch (and more than one agent) can be live at once
|
||
without stepping on each other.
|
||
- **Module 8** — a remote on a git host (GitHub the default; a self-hosted forge if you took that
|
||
track), so there's a shared copy to collaborate around.
|
||
- **Module 9** — issues: the task layer that says *what* needs doing and *who* (human or agent) owns it.
|
||
- **Module 10** — pull/merge requests and the skill of reviewing a diff you didn't write.
|
||
|
||
Each of those taught one move. This module is the assembled motion. If you're missing one, the loop
|
||
still works, but a step will feel like a black box — go back and fill it in.
|
||
|
||
---
|
||
|
||
## Learning objectives
|
||
|
||
By the end of this module you can:
|
||
|
||
1. Run the full collaboration loop end to end — issue → branch → implementation → PR → review →
|
||
merge → issue auto-closed — and explain why each step exists.
|
||
2. Link a PR to an issue so the merge closes the issue automatically, and explain when that does and
|
||
doesn't fire.
|
||
3. Decide correctly between a **branch** and a **fork** based on whether you have push access.
|
||
4. Reason about **who's allowed to push**: roles, protected branches, and why "never commit to
|
||
`main`" stops being a personal habit and becomes an enforced rule.
|
||
5. Treat an agent as a contributor — give it a branch, route an issue to it, review its PR on the
|
||
same gate you'd use for a human — and know where a human has to stay in the loop.
|
||
|
||
---
|
||
|
||
## Key concepts
|
||
|
||
### Two loops, not one
|
||
|
||
Module 2 gave you the **inner loop**: edit, `git diff`, commit, repeat. That loop lives on your disk
|
||
and is yours alone. It's how *you* (or your agent) make progress in a working session.
|
||
|
||
This module is the **outer loop** — the one the *team* sees:
|
||
|
||
```
|
||
issue → branch → implementation → pull request → review → merge → issue closed
|
||
(M9) (M6) (inner loop, M2) (M10) (M10) (this module)
|
||
```
|
||
|
||
Everything you learned was a single station on this track. The reason to assemble them now — rather
|
||
than keep treating issues, branches, and PRs as separate skills — is that the *handoffs between
|
||
stations* are where collaboration actually happens, and where it breaks. 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. Skip a handoff and you get the
|
||
failure modes every team knows: work nobody asked for, changes that land straight on `main` with no
|
||
review, "done" issues for work that was never actually done.
|
||
|
||
The loop is worth internalizing as a loop because **it's the same loop regardless of who's doing the
|
||
work** — and increasingly, some of the workers are agents. Hold that thought; it's the whole point of
|
||
the module, and we'll come back to it.
|
||
|
||
### The loop, step by step
|
||
|
||
**1 — The issue (Module 9) is the contract.** Before any code, there's a statement of intent: a
|
||
title, a description of the desired behavior, maybe acceptance criteria. It has a number (`#42`) that
|
||
the rest of the loop will reference. The issue exists so that "what we're doing and why" lives
|
||
somewhere durable and shared — not in one person's head or one chat session that'll evaporate
|
||
(Module 1, Seam 2). Assign it to whoever's taking it: a person, or an agent.
|
||
|
||
**2 — The branch (Module 6) is the workspace.** You never implement on `main`. You cut a branch
|
||
named for the work — convention is something traceable like `42-clear-done-command` (the issue
|
||
number plus a slug). The name matters more than it looks: months later, `git branch` and the host's
|
||
branch list become a map of "what's in flight," and the issue number ties each branch back to its
|
||
contract.
|
||
|
||
```bash
|
||
git switch -c 42-clear-done-command # branch off main and switch to it
|
||
```
|
||
|
||
**3 — Implementation is the inner loop (Module 2).** This is where the actual editing happens —
|
||
you, or an agent, making commits on the branch. Nothing here is new; it's the edit/diff/commit
|
||
rhythm you already have. The branch keeps it isolated, so however bold the change, `main` is
|
||
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 (Module 10) 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. Crucially, **this is where you link back to the issue** (next section) so the loop
|
||
can close itself.
|
||
|
||
**5 — Review (Module 10) is the judgment gate.** Someone who isn't the author reads the diff for
|
||
correctness *and plausibility* — the skill Module 10 is built around. They approve, request changes,
|
||
or comment. 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.
|
||
|
||
**6 — Merge is the commitment.** Approved, the PR merges into `main`. Squash, merge-commit, or
|
||
rebase — your team picks 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 and its name lives on in the merge.
|
||
|
||
**7 — The issue closes — ideally by itself.** If you linked the PR correctly, merging closes the
|
||
issue automatically. The receipt is written without anyone touching the issue. That's the satisfying
|
||
*click* of the whole loop landing, and it's the concrete thing the lab makes you feel.
|
||
|
||
### Linking the PR to the issue (the auto-close)
|
||
|
||
The mechanic that makes step 7 free: put a **closing keyword** in the PR description. Most hosts —
|
||
GitHub, GitLab, Gitea/Forgejo, Bitbucket — recognize a common set:
|
||
|
||
```
|
||
Closes #42
|
||
```
|
||
|
||
`Closes`, `Fixes`, and `Resolves` (and their variants — `close/closed`, `fix/fixed`,
|
||
`resolve/resolved`) all work on the major hosts. When the PR merges **into the default branch**, the
|
||
host closes the referenced issue and cross-links the two so each shows the other. One line in the PR
|
||
body buys you a self-closing loop and a permanent trail from "why we did this" (issue) to "what we
|
||
did" (PR/diff) to "when it landed" (merge).
|
||
|
||
A plain mention without a keyword — just `#42` — *links* the two but does **not** close on merge.
|
||
That's useful too (for "related to" references), but know the difference: the keyword is load-bearing.
|
||
|
||
> **The trail is the point.** Six months later, someone — possibly an agent reading the repo as
|
||
> durable memory (Module 2) — asks "why does `clear-done` exist?" The answer is one click away:
|
||
> issue → PR → diff → merge. You built that trail for free by linking one line.
|
||
|
||
### Branch vs. fork: it comes down to push access
|
||
|
||
There are two ways a contributor gets their work in front of the team, and the deciding question is
|
||
simple: **can you push to the repo?**
|
||
|
||
- **You have push (write) access → branch in the repo.** This is the normal case for a team working
|
||
on a shared repo, and everything above assumes it. Your branch lives alongside everyone else's on
|
||
the same remote; PRs go branch → `main` within one repo.
|
||
- **You don't have push access → fork, then PR from the fork.** This is the open-source contribution
|
||
model and the "outside contributor" case. You clone the repo into your *own* copy (a fork), push
|
||
branches there, and open a PR *across repos* from `your-fork:branch` into `upstream:main`. The
|
||
maintainers review and merge; you never needed write access to their repo.
|
||
|
||
```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 this audience, working mostly on repos you control, **branches are the default and forks are the
|
||
exception** — you reach for a fork when contributing to something you don't own. The relevance to AI
|
||
work: 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
|
||
|
||
"Never commit directly to `main`" started as a personal discipline. On a shared repo it becomes an
|
||
*enforced* rule, and that enforcement is the other half of collaboration nobody mentions until it
|
||
bites.
|
||
|
||
**Roles.** Hosts assign access in tiers — typically read (clone, comment), then write/develop (push
|
||
branches, open PRs), then maintain/admin (manage settings, force-merge, change protections). A
|
||
contributor only needs *write* to do the whole loop above; admin is for the people running the repo.
|
||
Give out the least that lets someone do their job — the same least-privilege instinct you already
|
||
have for production systems.
|
||
|
||
**Protected branches.** This is the enforcement mechanism. You mark `main` (and any other shared
|
||
branch) as protected, and the host then *refuses* direct pushes to it. The only way in is a PR. You
|
||
can layer rules on top:
|
||
|
||
- **Require a pull request** — no direct pushes, full stop. The loop is mandatory, not optional.
|
||
- **Require a review approval** — at least one non-author approval before merge is allowed.
|
||
- **Restrict who can merge** — only certain roles can click the button.
|
||
|
||
Turning these on converts "we agreed not to push to `main`" into "the server won't let you." For a
|
||
solo learner this can feel like bureaucracy, but it's exactly the guardrail that makes it safe to add
|
||
contributors you trust *less than fully* — including machine ones. (Required **status checks** —
|
||
"CI must pass before merge" — are the same protected-branch feature, but they need CI to exist first;
|
||
that's Module 14. We'll come back and switch it on there.)
|
||
|
||
### The contributor who isn't human
|
||
|
||
Here's the synthesis the whole unit was building toward. Re-read the loop — issue, branch,
|
||
implementation, PR, review, merge — and notice that **nothing in it specifies that the contributor is
|
||
a person.** That's not an accident; it's the most useful property of the whole system right now.
|
||
|
||
- **An agent is a contributor with a branch.** You hand an agent an issue (Module 9 already framed
|
||
assignees as a mix of humans and agents). It cuts a branch, implements, and opens a PR — exactly
|
||
the loop above. A human reviews that PR on the same gate used for any teammate (Module 10). 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.
|
||
|
||
- **Two agents in parallel are just two contributors needing branches.** The moment you run more than
|
||
one agent at once, you have the classic collaboration problem — two workers who must not edit the
|
||
same files in the same working directory. That's not a new problem, and it already has an answer:
|
||
**worktrees (Module 7).** Each agent gets its own working directory and its own branch; they work
|
||
simultaneously, each opens its own PR, and you review and merge them independently. Worktrees
|
||
earned their module precisely so this case would already be solved by the time you got here.
|
||
|
||
- **The merge stays human (for now).** The agent can do every step *up to* merge. The merge — the
|
||
commitment to shared `main` — is where a human stays in the loop, because review is judgment and
|
||
judgment is the thing you haven't delegated yet. Unit 5 is about carefully, conditionally moving
|
||
that line; this module is where you should be able to *picture* an agent doing the first five steps
|
||
while you do the sixth.
|
||
|
||
The reframe to carry forward: **collaboration tooling was never really about humans.** It's about
|
||
coordinating *contributors* — isolating their work, making it reviewable, controlling who can commit
|
||
it to the trunk. Those guarantees are exactly what you need to safely let an agent contribute, which
|
||
is why the team layer you just learned doubles as the agent-safety layer you'll lean on for the rest
|
||
of the course.
|
||
|
||
---
|
||
|
||
## The AI angle
|
||
|
||
A generic "intro to team git" lesson ends at "branch, PR, review, merge — congrats, you can work on a
|
||
team." This module's reason to exist is that **the team you're coordinating now includes agents, and
|
||
the loop is what makes that safe.**
|
||
|
||
- **The loop is the harness for untrusted contributors — and an agent is one.** Branch isolation,
|
||
the PR boundary, mandatory review, protected `main` — every one of these was designed to let work
|
||
flow from someone whose every change you don't personally vouch for. That's the exact profile of an
|
||
agent. You don't need new tooling to put an agent to work; you need the tooling you just learned,
|
||
pointed at a new kind of contributor.
|
||
- **Volume goes up; the gate has to hold.** A human contributor opens a PR a day. An agent can open
|
||
five before lunch. The review gate (Module 10) and the protected-branch rules are what keep that
|
||
volume from landing unreviewed on `main`. The faster your contributors, the more the gate earns its
|
||
keep — same lesson as Module 1, one layer up.
|
||
- **Parallel agents are a solved problem, on purpose.** Two agents at once is just two contributors
|
||
needing isolation — worktrees (Module 7) and separate branches. You already have the answer; this
|
||
module is where you see *why* you were given it.
|
||
- **The auto-closing trail is memory for the next session.** Issue → PR → diff → merge is exactly the
|
||
durable, on-disk-and-on-host record a fresh agent reads to reconstruct "why does this exist?"
|
||
(Module 2's durable-memory reframe, now spanning the whole loop). Linking the PR to the issue isn't
|
||
bookkeeping; it's writing the project's memory in a form the next contributor — human or machine —
|
||
can follow.
|
||
|
||
You're not learning collaboration *and then* learning to work with agents. They're the same skill.
|
||
|
||
---
|
||
|
||
## Hands-on lab
|
||
|
||
**Lab language:** shell (git commands) plus your host's web UI for the issue, PR, review, and merge
|
||
steps. You'll implement the feature with your AI the way Module 4 taught — agent editing the files
|
||
directly, you reviewing the diff.
|
||
|
||
The goal is to run the **entire outer loop once**, on the `tasks-app`, and watch the issue close
|
||
itself on merge. One small feature, all seven stations.
|
||
|
||
**The feature:** add a `clear-done` command to the CLI that removes every completed task. It's a
|
||
deliberately small, two-file change (logic in `tasks.py`, wiring in `cli.py`) — small enough that the
|
||
loop, not the code, is what you're practicing.
|
||
|
||
**You'll need:**
|
||
|
||
- Your `tasks-app` repo from earlier modules, with a remote on your git host (Module 8) that supports
|
||
issues and PRs.
|
||
- Push access to that repo (it's yours, so you have it).
|
||
- Your editor-integrated AI tool (Module 4).
|
||
- Optionally, your host's CLI (`gh` for GitHub, `glab` for GitLab, `tea` for Gitea/Forgejo) — the web
|
||
UI works for everything here, so the CLI is convenience, not a requirement.
|
||
|
||
Starter artifacts are in this module's `lab/`: `issue.md` (the issue to file) and `pr-body.md` (the
|
||
PR description, including the load-bearing closing keyword).
|
||
|
||
### Part A — Set the guardrail (one-time)
|
||
|
||
Before the loop, make `main` enforce what you've been doing by hand. In your host's web UI, open the
|
||
repo's branch-protection settings and protect `main` with **"require a pull request before merging."**
|
||
|
||
```bash
|
||
# Confirm the rule bites — this push should now be REFUSED by the host:
|
||
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
|
||
```
|
||
|
||
If the push went through, protection isn't on — fix that before continuing. Feeling the server say
|
||
*no* is the point: "never commit to `main`" is now a rule, not a resolution.
|
||
|
||
### Part B — Issue → branch
|
||
|
||
1. **File the issue.** Create a new issue from `lab/issue.md` (title and body). Note its number — say
|
||
it's `#42`. This is the contract.
|
||
|
||
2. **Branch for it**, naming the branch after the issue:
|
||
|
||
```bash
|
||
git switch main && git pull # start from current main
|
||
git switch -c 42-clear-done-command # use YOUR issue number
|
||
```
|
||
|
||
### Part C — Implementation (with AI)
|
||
|
||
3. Point your editor-integrated AI at the repo and ask for the feature:
|
||
|
||
> "Add a `clear-done` command. In `tasks.py`, add a `TaskList` method that removes all completed
|
||
> tasks. In `cli.py`, wire up a `clear-done` command that calls it, saves, and prints how many
|
||
> were removed. Match the existing style."
|
||
|
||
4. **Review the diff before you trust it** — the Module 2 habit, the Module 10 skill:
|
||
|
||
```bash
|
||
git diff
|
||
```
|
||
|
||
Confirm it touched only `tasks.py` and `cli.py`, the logic lives in `tasks.py` (not crammed into
|
||
the CLI), and it does what you asked. Run it:
|
||
|
||
```bash
|
||
python cli.py add "keeper" ; python cli.py add "trash" ; python cli.py done 1
|
||
python cli.py clear-done # expect it to remove the completed one
|
||
python cli.py list # "keeper" remains, "trash" is gone
|
||
```
|
||
|
||
5. Commit and push the branch:
|
||
|
||
```bash
|
||
git add tasks.py cli.py
|
||
git commit -m "Add clear-done command (closes #42)"
|
||
git push -u origin 42-clear-done-command
|
||
```
|
||
|
||
### Part D — PR → review → merge → auto-close
|
||
|
||
6. **Open the PR** from your branch into `main`, using `lab/pr-body.md` as the description. Make sure
|
||
the body contains the closing line with **your** issue number:
|
||
|
||
```
|
||
Closes #42
|
||
```
|
||
|
||
7. **Review it.** Open the PR's "Files changed" tab and read the diff *as a reviewer*, not as the
|
||
author — the Module 10 move. For the full effect, pretend an agent wrote it (in a moment, one
|
||
will): is the logic where it belongs? Any edge case missed (empty list, nothing done yet)?
|
||
Approve it.
|
||
|
||
8. **Merge it.** Click merge (your protection rule required the PR and, if you added it, the
|
||
approval). Delete the branch when prompted.
|
||
|
||
9. **Watch the issue close itself.** Open issue `#42`. It should now be **closed**, with a link to
|
||
the PR that closed it. You didn't touch the issue — the merge did. That click is the whole loop
|
||
landing.
|
||
|
||
```bash
|
||
git switch main && git pull # bring the merged work down locally
|
||
git branch -d 42-clear-done-command # tidy up the local branch
|
||
```
|
||
|
||
### Part E — Now make the contributor an agent
|
||
|
||
Run the loop one more time, but this time **let an agent be the contributor for steps 2–6.** File a
|
||
second issue (e.g. "Add a `pending` command that lists only incomplete tasks" — the `TaskList.pending()`
|
||
method already exists, so this is wiring only). Then prompt your agent:
|
||
|
||
> "Take issue #43. Create a branch named `43-pending-command`, implement the feature, commit
|
||
> referencing the issue with a closing keyword, push the branch, and open a PR into `main` whose
|
||
> description closes #43."
|
||
|
||
Let the agent drive to the open-PR state. Then **you** are the human at the gate: review the diff,
|
||
and merge (or request changes) yourself. You've just watched the exact loop run with a non-human
|
||
contributor — and felt precisely where you, the human, stayed in it. If you want the parallel-agents
|
||
case, file two issues and run two agents in separate worktrees (Module 7), each on its own branch.
|
||
|
||
---
|
||
|
||
## Where it breaks
|
||
|
||
- **Auto-close only fires on merge to the *default* branch.** Closing keywords close the issue when
|
||
the PR lands on `main` (or whatever your default is). Merge into a non-default branch and the issue
|
||
stays open — by design. Keep the keyword in the *PR description* (or a commit message); a closing
|
||
keyword buried in a mid-thread comment 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`, needed when a fork's PR closes
|
||
an upstream issue) vary by host. When in doubt, mention-link and close the issue 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 judgment was the review (Module 10), and if review
|
||
was a rubber stamp, you just auto-closed an issue for broken work. 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: its token can push branches and, if
|
||
over-permissioned, merge them. Scope machine accounts to the least they need; this is the front edge
|
||
of a problem Unit 4 takes head-on.
|
||
- **Forks add real friction beyond the extra clone.** Keeping a fork in sync with a fast-moving
|
||
upstream is ongoing work, and PRs *from* forks are deliberately limited by hosts (for example, they
|
||
often can't access the upstream repo's CI secrets — relevant once you reach Module 14). For repos
|
||
you own, prefer branches; reach for forks only when you genuinely lack push access.
|
||
- **The loop diagram is the happy path.** Real PRs get change requests, need a rebase onto a moved
|
||
`main`, or hit a merge conflict (Module 6) when two contributors touched 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.
|
||
- **Squash-merge collapses authorship.** If your team squashes, the agent's (or your) individual
|
||
commits become one commit on `main`, and the per-commit trail lives only on the now-deleted branch /
|
||
closed PR. That's usually a fine trade for a clean history — just know the granular history moved
|
||
from `main` to the PR record.
|
||
|
||
---
|
||
|
||
## Check for understanding
|
||
|
||
**You're done when:**
|
||
|
||
- You ran the full loop on `tasks-app` at least once and watched an issue close itself on merge —
|
||
with `main` protected so the PR was mandatory, not optional.
|
||
- You can draw the seven-station loop (issue → branch → implementation → PR → review → merge → closed)
|
||
from memory and say which earlier module owns each station.
|
||
- You can state the branch-vs-fork rule in one sentence (push access → branch; no push access → fork)
|
||
and why an agent follows the same rule.
|
||
- You ran at least one trip around the loop with an **agent as the contributor** for the
|
||
implement-and-open-PR steps, and can point to the exact step where you, the human, stayed in the
|
||
loop (the merge).
|
||
- You can explain why the same tooling that coordinates human teammates is what makes accepting an
|
||
agent's work safe.
|
||
|
||
When the loop feels like one motion rather than six separate tools — and when "give the agent a
|
||
branch and review its PR" feels obvious rather than novel — you're ready for Module 12, where we make
|
||
the *recovery* half of this safety net its own discipline: reverting a bad PR after it's already
|
||
merged.
|