« previous: 00.3 env vars for fresh shells | next: 02 First review loop »
By the end of this exercise you will have three letters from the letters-a-m epic landed on main through real GitHub pull requests, each one approved by the refinery before publication.
- Pages 00.1, 00.2,
and 00.3 complete: city stood up with setup
pack imported, rig registered with locked docs in place, 138 beads seeded,
mainpushed to GitHub, and$ASCII_ART_PATH/$ARTIFACTS_PATHexported in your shell. - You're inside the rig directory. If you opened a fresh shell, re-export
$FACTORY_PATH,$ASCII_ART_PATH,$TUTORIAL_PATH, and$ARTIFACTS_PATHper 00.3, thencd "$ASCII_ART_PATH". ghis authenticated and can create PRs against the rig's GitHub repo. Verify withgh auth statusandgh repo view.jqis installed (the new formulas use it).
Install the pr-gate-city and pr-gate-rig packs into your
factory1 city, restart so the packs' mayor and refinery overrides
take effect, then sling one bead from the letters-a-m epic at the
polecat using the new mol-polecat-pr formula. Watch the polecat
work, watch the refinery rebase + run checks + clear the bead at its
approval gate, and see a real GitHub PR open. Manually merge the PR.
Repeat for two more letters. Reflect on what just happened: every
change reaches main through a PR, and only PRs the refinery has
approved make it that far.
The next four pages add deeper gates on top of this baseline: ADR-aware reviewers, branch protection on the GitHub side, and bead-level review before any polecat is allowed to claim work.
Branching/Merging strategy:
- Epics branch off and target
main(or their parent epic's branch, where applicable). The branches are named like this:epic/<bead-id>--<slug>, for exampleepic/aa-x1z--add-pinch-gesture. - Tasks branch off and target their parent's branch (normally an epic). They're named like this:
task/<bead-id>--<slug>, for exampletask/aa-x1z.1--change-notification-colors. - Only humans can approve merge into
main. - Coder agents work on their branch in a worktree named after the bead they're working on (e.g.
<bead-id>--<slug>, for exampleaa-x1z.1--change-notification-colors,aa-x1z--add-pinch-gesture).
Agent workflow:
- A coder agent claims a bead and implements. The coder checks their own work against the bead's requirements and acceptance criteria. When the coder believes the work is done, they open a GitHub pull request, and then hand off the bead to someone else for review.
- A reviewer agent reviews the PR and corresponding bead. If the work fulfills the requirements of the bead, the reviewer notes their approval in both the bead and the pull request on GitHub. (The agent does not have to repeat themself, one can reference the other to 'See additional details'. But both should indicate approval or rejection.) If the work does not fulfill the requirements, the bead should be routed back to the coder with specific feedback about why the bead is not ready to be closed, and what work needs to be done to complete it.
- A merger agent reviews beads that have been approved, merges corresponding pull requests, and then closes the corresponding bead.
This lesson has two paths to the same end state. Pick one.
If this is your first run, complete the one-time setup in the bootstrap README (.env, deps.sh) before invoking the script.
Copy and paste
cd path/to/sf-tutorial/bootstrap
./bootstrap.sh 01-basic-flowThe script reproduces every step up through this lesson — pr-gate-city and pr-gate-rig are imported, the city's mayor agent directory is removed (the new pack patches the mayor), and the city is restarted.
After it finishes, re-export the four env vars per 00.3, then jump to Try It.
The OOTB setup pack ships polecat + refinery in direct-merge mode:
the polecat does the work, the refinery rebases and fast-forwards it
straight onto main. There is no PR stage and no human or AI gate
between the polecat's commit and the protected branch.
We want a PR-based gate from page one — and a twist: only beads the refinery has actually approved should make it to the PR stage. Code that doesn't pass approval should be bounced back to the polecat pool, not silently published as a PR for a human to clean up.
This ships as a pair of packs — pr-gate-city and pr-gate-rig.
Two packs because gascity's pack-v2 patches can only target agents in
the same composition pass: mayor is city-scoped, refinery is
rig-scoped, so each scope needs its own pack.
pr-gate-city delivers:
- A
[[patches.agent]]block that overrides the mayor's prompt template in place — teaching the mayor the new dispatch recipe (--on mol-polecat-pr) and how to triage beads blocked at the gate.
pr-gate-rig delivers:
mol-polecat-pr— a polecat formula that stampsmerge_strategy=pron the work bead, so the refinery publishes a PR instead of merging directly.mol-refinery-pr-patrol— a refinery patrol formula that adds anapproval-reviewstep between checks and merge-push. Beads that pass the gate become PRs; beads that fail go back to the polecat pool withmetadata.refinery_approved=falseand ablocked_reason.- A
[[patches.agent]]block that overrides the refinery's prompt template — pointing the rig refinery atmol-refinery-pr-patrolinstead ofmol-refinery-patrolon next start.
Each pack also declares [imports.setup].
A patch resolves against the pack's own agents plus its declared
imports — so for the mayor/refinery patches to find their target,
the pack defining the patch has to import setup directly. At the
same time, to ensure unambiguous resolution, the setup pack
is also removed from the city's direct imports.
Inspect the packs before installing:
Copy and paste
ls "$ARTIFACTS_PATH/packs/pr-gate-city/"
ls "$ARTIFACTS_PATH/packs/pr-gate-rig/"
cat "$ARTIFACTS_PATH/packs/pr-gate-rig/formulas/mol-polecat-pr.formula.toml"
cat "$ARTIFACTS_PATH/packs/pr-gate-rig/formulas/mol-refinery-pr-patrol.formula.toml"pr-gate-city/ contains pack.toml and a prompts/ directory with
the mayor template. pr-gate-rig/ contains pack.toml, a prompts/
directory with the refinery template, and a formulas/ directory
with two .formula.toml files.
Copy both packs into the city's pack directory:
Copy and paste
cp -r "$ARTIFACTS_PATH/packs/pr-gate-city" \
"$FACTORY_PATH/.gc/system/packs/pr-gate-city"
cp -r "$ARTIFACTS_PATH/packs/pr-gate-rig" \
"$FACTORY_PATH/.gc/system/packs/pr-gate-rig"Now register both packs as imports. gc import add is the supported
way to do this — it validates the source, derives the binding name,
writes the right TOML block, and updates packs.lock. Run from the
city directory:
Copy and paste
cd "$FACTORY_PATH"
# City scope — pr-gate-city patches the city-scoped mayor agent.
gc import add .gc/system/packs/pr-gate-city
# Rig scope — pr-gate-rig patches the rig-scoped refinery agent and
# loads the polecat/refinery formulas. Bound to the ascii-art rig.
gc import add --rig ascii-art .gc/system/packs/pr-gate-rig
# Remove setup pack since pr-gate-city transitively imports it now.
gc import remove --rig ascii-art setupVerify both imports landed:
Copy and paste
gc import list
gc import list --rig ascii-artExpected output
city-scoped: pr-gate-city
rig-scoped: pr-gate-rig
Now remove the existing mayor agent from the city so the new pack can take over the mayor's role.
Copy and paste
rm -rf "$FACTORY_PATH/agents/mayor"Also remove the named session block from the city pack.toml.
Copy and paste
sed -i '' '/\[named_session\]/,/\[named_session\]/d' "$FACTORY_PATH/pack.toml"Now restart the city so the new patches and formulas take effect:
Copy and paste
gc restartConfirm the new formulas loaded:
Copy and paste
gc formula list | grep -E "mol-polecat-pr|mol-refinery-pr-patrol"Expected output
mol-polecat-pr
mol-refinery-pr-patrol
List the open Implement <letter>.md tasks and grab the first three:
Copy and paste
cd "$ASCII_ART_PATH"
bd list --type=task --status=open --limit 0 | grep "Implement [a-c]\.md"If you see Error: failed to open database, remember you can run bd config set dolt.auto-start true
in the rig scope as well.
Pick one task — say Implement a.md — and capture its ID. IDs look like
asciiart-14 (the prefix matches what you initialized with in page 00). Look
at the bead's metadata so you know what the agents will see:
Copy and paste
export BEAD_ID=$(bd list --type=task --status=open --limit 0 | grep -E "Implement a\.md$" | awk '{print $2}')
bd show $BEAD_IDYou should see fields including metadata.target_file=ascii/a.md. The polecat
reads that metadata to know what file it's writing.
Hand the bead to the ascii-art/pr-gate-rig.polecat agent using the new
pr-gate formula:
Copy and paste
cd $FACTORY_PATH
gc sling ascii-art/pr-gate-rig.polecat $BEAD_ID --on mol-polecat-prWhen this works, you should see the task sent to the agent along with the formula for how to process it:
Expected output
⎿ Auto-convoy aa-0s7
Attached wisp aa-3ii (formula "mol-polecat-pr") to aa-ne 1
Slung aa-nel.1 (with formula "mol-polecat-pr") → ascii-art/pr-gate-rig.polecat
What this does: the polecat creates a feature branch in a fresh git
worktree, stamps metadata.merge_strategy=pr on the bead (so the
refinery knows to publish a PR), implements the work the bead
describes, runs a self-review pass, and hands the bead off to the
refinery for landing.
If you slung with --on mol-polecat-work (the OOTB formula) instead,
the merge-strategy stamp would be missing and the refinery would
fast-forward the branch into main with no PR and no approval gate.
The pr-gate contract depends on mol-polecat-pr.
In another terminal, list the live agent sessions to confirm polecat is running:
Copy and paste
gc session listTo view the agent's session in real time, you can attach to it:
Copy and paste
gc session attach <polecat-session>WARNING: a tmux session will open, but if you give it a prompt this will interrupt
its session. When you are done, detach from the session by pressing Ctrl+b and then d.
If you haven't noticed progress after a minute or two, you can also nudge the agent to prompt it without attaching to the session. This is the same as attaching to the session, giving a prompt, then detaching:
Copy and paste
gc session nudge <polecat-session> "Check for any assigned work"The bead transitions open → in_progress once the polecat claims it. Inspect
it again and you'll see new metadata stamped on:
Copy and paste
gc bd show $BEAD_IDExpected output
◐ aa-985.2 · Implement a.md [● P2 · IN_PROGRESS]
Owner: Austin Born · Assignee: pr-gate-rig__polecat-fa-r3am · Type: task
Created: 2026-05-11 · Started: 2026-05-11 · Updated: 2026-05-11
DESCRIPTION
(none)
METADATA
branch: polecat/aa-985.2
gc.routed_to: ascii-art/pr-gate-rig.polecat
molecule_id: aa-vlh
target_file: ascii/a.md
work_dir: $FACTORY_PATH/.gc/worktrees/ascii-art/polecats/pr-gate-rig.furiosa/worktrees/aa-985.2
PARENT
↑ ○ aa-52p: sling-aa-985.2 ● P2
When the polecat finishes, it sets metadata.merge_strategy=pr and reassigns
the bead to the refinery.
Copy and paste
gc bd show $BEAD_IDExpected output
○ aa-985.2 · Implement a.md [● P2 · OPEN]
Owner: Austin Born · Type: task
Created: 2026-05-11 · Started: 2026-05-11 · Updated: 2026-05-11
DESCRIPTION
(none)
NOTES
Implemented ASCII art for letter B: 7-line stacked-bumps B inside 7x7 canvas, rhyming couplet
'round/sound'.
METADATA
branch: polecat/aa-985.2
gc.routed_to: ascii-art/refinery
merge_strategy: pr
molecule_id: aa-vlh
target: main
target_file: ascii/a.md
work_dir: /Users/austin/software-factory-intensive/factory1/.gc/worktrees/ascii-art/polecats/pr-gate-rig.furiosa/worktrees/aa-985.2
PARENT
↑ ○ aa-52p: sling-aa-985.2 ● P2
The refinery then picks up the bead and rebases the feature branch onto the latest main, runs the
rig's checks (none yet on the ascii-art rig — see Troubleshooting),
runs the approval-review step against the diff, and then — because
metadata.merge_strategy=pr — publishes a GitHub pull request.
You can watch the bead's metadata flip in a tight loop. The
interesting fields are refinery_approved, pr_url, and pr_number.
Copy and paste
# If you have watch installed:
watch -n 5 'gc bd show $BEAD_ID --json'
# If you don't have watch installed:
while true; do clear; gc bd show $BEAD_ID; sleep 5; doneFor a clean letter (a, b, c) you should see refinery_approved: "true"
appear, followed shortly by pr_url set to a real
https://github.com/... link and pr_number set to a numeric value.
The refinery also closes the bead at PR-publish time.
If the refinery had blocked the bead instead, you'd see
refinery_approved: "false" and blocked_reason populated, and the
bead's status would flip back to open with no pr_url. (The block
path is exercised intentionally later in the progression once the rig
has misbehaving polecat output to chew on; for a.md/b.md/c.md
the gate clears.)
Open the PR and look at the diff:
Copy and paste
cd $ASCII_ART_PATH
export PR=$(BD_JSON_ENVELOPE=1 gc bd show $BEAD_ID --json | jq -r '.data[0].metadata.pr_number')
gh pr view $PR
gh pr view $PR --web # open in browserClick Merge pull request in GitHub (or gh pr merge "$PR" --merge).
Same pattern, two more times — sling with mol-polecat-pr, watch the
gate clear, manually merge the PR:
Copy and paste
cd $ASCII_ART_PATH
export BEAD_ID=$(bd list --type=task --status=open --limit 0 | grep -E "Implement b\.md$" | awk '{print $2}')
cd $FACTORY_PATH
gc sling ascii-art/pr-gate-rig.polecat $BEAD_ID --on mol-polecat-pr
cd $ASCII_ART_PATH
export BEAD_ID=$(bd list --type=task --status=open --limit 0 | grep -E "Implement c\.md$" | awk '{print $2}')
cd $FACTORY_PATH
gc sling ascii-art/pr-gate-rig.polecat $BEAD_ID --on mol-polecat-prThree letters from letters-a-m is enough. Resist the urge to chew
through the whole epic — later pages dispatch in larger batches and
you want clean state to compare against.
That worked. Three letters reached main — but every commit went
through a real GitHub pull request, and every PR was approved by the
refinery before it was published. The refinery refused to be a passive
fast-forward machine; it read the diff, checked the file looked like a
real letter, and stamped refinery_approved=true before letting the
PR open. A bead that had landed empty or off-scope would have come
back to the polecat pool with a blocked_reason instead of becoming a
PR.
What's still missing:
- No required reviewers on the GitHub side. The PR can be merged by anyone with write access, with no required CI or human review. Page 03 — Branch protection wires that up.
- The refinery's approval is a single, generic gate. It doesn't know the rig's ADR, doesn't know the design intent, doesn't have a separate testing or design lens. Page 04 — ADR-aware reviewer introduces a dedicated reviewer agent that reads the controlling ADR and gives the refinery a more substantive read.
- There's no upstream check on the bead itself. A malformed bead (wrong title, missing metadata, or pointing at a file the ADR forbids) will still get claimed by a polecat and only get caught downstream by the refinery's gate. Page 05 — Bead review gate closes that hole.
- The pr-gate formula
mol-polecat-pris intentionally simple. The richer variant in Page 02 — PR formula and pack (mol-polecat-pr-work) layers a bead-review precondition on top of the same merge-strategy stamp; that page is the deeper dive into how custom formulas extend OOTB ones.
Three new commits on origin/main from the polecat/refinery → PR cycle:
Copy and paste
cd $ASCII_ART_PATH
git fetch origin && git pull
git log --oneline origin/main -3Expected output
3 commits whose messages reference Implement a.md / b.md / c.md
(subject + merge-commit messages set by the polecat and gh).
Three pull requests exist on the rig's GitHub repo:
Copy and paste
cd $ASCII_ART_PATH
gh pr list --state=merged --limit 5Expected output
at least 3 merged PRs corresponding to a.md, b.md, c.md.
Worktrees cleaned up — only the main worktree remains:
Copy and paste
git worktree listExpected output
a single line for the rig's main checkout.
The three files exist on disk:
Copy and paste
ls ascii/a.md ascii/b.md ascii/c.mdExpected output
all three paths print without error.
-
gc formula listdoesn't showmol-polecat-prormol-refinery-pr-patrol. The pr-gate-rig pack didn't load. Rungc import listand confirm you seerig:ascii-art:pr-gate-rig. If it's missing, re-run thegc import add --rig ascii-art .gc/system/packs/pr-gate-rigcommand from step 1 and restart. -
gc restarterrors withagent "mayor" not found in pack(or the equivalent forrefinery). You imported one of the pr-gate packs at the wrong scope.pr-gate-citypatches the city-scoped mayor and must be imported at city scope (no--rigflag);pr-gate-rigpatches the rig-scoped refinery and must be imported with--rig <rig-name>. Rungc import remove pr-gate-city(orpr-gate-rig), then re-add at the correct scope. -
Polecat doesn't claim the bead. Check
gc agentsshowspolecatis running. Thenbd show <bead-id>— ifassigneeis blank a few seconds after the sling, the formula didn't dispatch. Re-rungc slingand watch for an error message. -
Refinery rebases but no
pr_urlever appears, andrefinery_approvedstays empty. The approval-review step probably errored out. Look at the refinery's session logs for the diff it was reading; if the diff was empty, the polecat didn't actually write the file. Treat as a polecat regression. -
Refinery sets
refinery_approved=falsefora.md. Readmetadata.blocked_reason— the refinery is telling you which check failed. Most often this is the polecat writing an empty file or putting it at the wrong path (a.mdinstead ofascii/a.md). Re-sling once; if it blocks twice in a row, check the polecat prompt for drift. -
Refinery refuses to merge because CI fails. The refinery runs typecheck/lint/test on the rebased branch before approval-review. The ascii-art rig has no test suite yet, so the refinery should take its "no checks configured" path and proceed cleanly. If it fails instead, check the refinery's session logs for which check it tried to run.
-
Pushed to the wrong remote.
git remote -vfrom the rig should showoriginpointing at the GitHub repo you created in page 00.2 step 4. If it points elsewhere, the refinery'sgh pr createwill fail or open the PR on the wrong repository. -
Bead closes but the file is empty or missing. Look at the polecat's session log. The log will show what the agent actually wrote and where.
Copy and paste
gc session logs <session-id> # NOTE: command name varies; try `gc sessions logs`
-
watchis not installed (macOS default). Eitherbrew install watch, or just re-run thebd show <bead-id> --json | jq ...pipeline by hand every few seconds.
Continue to First review loop. Page 02
adds a single required round of refinery feedback before any bead is
allowed to land as a PR — installing a new review-loop-rig pack that
extends mol-refinery-pr-patrol with a feedback-loop step and
patches the refinery to use it, so every bead now makes two trips
through the polecat before reaching main.
« previous: 00.3 env vars for fresh shells | next: 02 First review loop »