Skip to content

Tutorial: run two coordinating sessions for the first time

By the end of this tutorial you will have two Claude Code sessions working the same repo side-by-side, sent a message from one to the other, watched the recipient acknowledge it, and handed a piece of work across — all through the durable git blackboard, with nothing online but git.

This is a learning exercise: every step is concrete and you will see real output. It assumes the protocol is already installed on this repo (it is, on po-platform). If you are setting it up on a fresh project first, do how-to-onboard.md and come back here.

What you need: two terminals, the po-platform repo cloned, and npx lefthook install already run once (so the hooks fire). You can play both "sessions" yourself — that's the point of the tutorial.


Step 1 — claim your first session

In terminal 1, from the repo root:

bash infrastructure/scripts/start-session.sh

You'll see something like:

fetching origin/main and creating worktree at /…/po-platform-sA ...
[session-sA <hash>] [sA] docs(ai-sessions): claim sA at 2026-05-31T11:40Z
====================================
Session sA claimed.
Worktree: /…/po-platform-sA
Branch:   session-sA
Next:
  cd /…/po-platform-sA
====================================

Three things just happened, automatically:

  1. A git worktree was created at ../po-platform-sA — a separate checkout with its own index. This is Layer 0: terminal 1 can now git add freely without ever touching another session's staging area.
  2. The script picked the lowest unused id (sA) and the first unused display name from the pool (Sintra).
  3. It wrote your claim row into docs/ai/sessions/active.md and committed it atomically.

Now enter the worktree:

cd ../po-platform-sA

Look at the row it added:

grep '^| sA' docs/ai/sessions/active.md
| sA · Sintra | (Riff TBD) | worktree `session-sA` → direct-to-main, `[sA]` | 2026-05-31T11:40Z | 2026-05-31T11:40Z · | scope TBD — update this row when you pick a Riff |

That's your identity (sA · Sintra), your branch, your claim time, your presence timestamp, and a placeholder scope. You are now a visible, claimed session.


Step 2 — claim a second session

In terminal 2, from the original repo root (not the worktree):

bash infrastructure/scripts/start-session.sh

This time the script sees sA is taken, so it claims sB · Douro, creates ../po-platform-sB, and commits a second claim row. Enter it:

cd ../po-platform-sB
git pull --ff-only origin main   # pick up sA's claim row
grep '^| s' docs/ai/sessions/active.md

You should now see both rows — sA · Sintra and sB · Douro. The scoreboard is the shared, durable picture of who's live.

Try the safety net. Still in the sB worktree, attempt a mislabelled commit:

git commit --allow-empty -m "[sA] test: wrong prefix"
The commit-msg hook aborts it:
lefthook: ❌  commit subject prefix [sA] does not match worktree 'po-platform-sB' (expected [sB]).
That's check-worktree-prefix.sh — the backstop that makes failure mode #1 structurally impossible. A correct [sB] subject would pass.


Step 3 — send a message from sB to sA

You are sB · Douro and you want to ask sA · Sintra a question. You don't have sA's phone number — you have the git blackboard.

In terminal 2 (the sB worktree), append a message block to sA's inbox:

cat >> docs/ai/sessions/queue/to-sA.md <<'EOF'

## 2026-05-31T11:45Z · from:sB · to:sA · type:question · ref:— · status:sent
Are you about to touch `docs/ai/parallel-sessions.md`? I want to add a Layer 7
section and don't want to collide.
→ status:seen <ts> · acked <ts> · resolved <ts>
EOF

git add docs/ai/sessions/queue/to-sA.md
git commit -m "[sB] docs(ai-sessions): ask sA about parallel-sessions.md edit"
git push origin HEAD:main

Notice the header grammar (full table in reference.md): from / to / type / ref / status, and the trailing → status: line that the recipient will advance. The message is now durable and addressed.


Step 4 — sA receives and acknowledges

Switch to terminal 1 (the sA worktree). You're sA · Sintra. Pick up the bus:

git pull --ff-only origin main
cat docs/ai/sessions/queue/to-sA.md

There's the question from sB. You advance the ack chain in your own commit. Edit the → status: line of that block to mark it seen and acked, and append your answer (still as sA, editing your own inbox is fine):

## 2026-05-31T11:45Z · from:sB · to:sA · type:question · ref:— · status:sent
Are you about to touch `docs/ai/parallel-sessions.md`? ...
→ status:seen 2026-05-31T11:50Z · acked 2026-05-31T11:50Z · resolved 2026-05-31T11:52Z

## 2026-05-31T11:52Z · from:sA · to:sB · type:answer · ref:— · status:sent
No — I'm only in docs/developers/. Go ahead with Layer 7, the file is yours.
→ status:seen <ts> · acked <ts> · resolved <ts>
git add docs/ai/sessions/queue/to-sA.md
git commit -m "[sA] docs(ai-sessions): ack sB's question + answer (file is free)"
git push origin HEAD:main

The key idea: the recipient advances the status, in their own commit. When sB next pulls, it will read seen/acked/resolved — proof the ball was caught, not an assumption. That is the lossless ack chain.


Step 5 — watch presence update itself

You just committed twice as sA. Look at your scoreboard row again in terminal 1:

grep '^| sA' docs/ai/sessions/active.md

The Presence timestamp (5th cell) has moved forward to your last commit time — you didn't touch it by hand. That's the session-heartbeat.sh post-commit hook. It bumped last_seen and left the change in your working tree (it never auto-commits), so it rides along on your next commit. Anyone reading the board now sees that sA is genuinely recently alive, not just claimed-and-maybe-asleep (failure mode #6, closed).

Even if the hook were uninstalled, your liveness would still be derivable from the latest [sA] commit in git log. The mechanism degrades gracefully.


Step 6 — hand a piece of work across

Suppose sA realises the Layer 7 work really belongs to sB, who's already in that file. That's a handoff — a type:handoff message plus a reassignment. In terminal 1 (sA):

cat >> docs/ai/sessions/queue/to-sB.md <<'EOF'

## 2026-05-31T11:55Z · from:sA · to:sB · type:handoff · ref:— · status:sent
Handing you the Layer 7 doc work — you're already in parallel-sessions.md and I'm
booked on the comms package. Over to you.
→ status:seen <ts> · acked <ts> · resolved <ts>
EOF

git add docs/ai/sessions/queue/to-sB.md
git commit -m "[sA] docs(ai-sessions): handoff Layer 7 doc work to sB"
git push origin HEAD:main

If this work were tracked as a Riff, the atomic version of the handoff is a single tracker call swapping assigneesB and advancing status — the durable record-of-truth (Tier 2). The queue message carries the narrative ("why, and what next"); the Riff carries the state.

When sB pulls, it sees the handoff, advances it to acked, and the single-owner invariant holds throughout: at no point were both sessions "owning" the Layer 7 work.


Step 7 — clean up

When a session is done, remove its row (or mark it paused) and remove the worktree. In each terminal:

# edit docs/ai/sessions/active.md — delete your row, or change Scope to
# "paused — resuming <ETA>" — then commit + push:
git add docs/ai/sessions/active.md
git commit -m "[sA] docs(ai-sessions): self-close sA"
git push origin HEAD:main

cd ..                                   # leave the worktree
git worktree remove po-platform-sA      # (after pushing)
git branch -D session-sA                # optional: drop the local branch

What you learned

  • Worktree isolation (Layer 0) made two sessions on one repo collision-proof, and the commit-msg hook proved it by rejecting a mislabelled commit.
  • The git blackboard carried an addressed message from sB to sA with nothing online but git.
  • The ack chain (sent → seen → acked → resolved) made the exchange lossless — the sender can confirm the message was caught.
  • Presence updated itself mechanically via the post-commit hook.
  • A handoff moved ownership across sessions while preserving the single-owner invariant.

Next: skim reference.md for the exact grammar, or — if you're taking this to another repo — follow how-to-onboard.md.