---
title: "Standing Up Your First Factory"
date: "2026-08-04"
description: "One empty folder, one afternoon, and the modest goal of watching a factory of coding agents open a pull request on its own. What Gas City looks like when you actually run it, and where it fights you."
tags: ["engineering", "ai"]
series: "Software Factories"
part: 2
language: "en"
draft: false
---

At the end of the last piece I said I would start from an empty directory, stand a factory up, hand it a small project of my own, and watch it open a pull request on its own. So that is what this is. One afternoon, one empty folder, and the modest goal of watching the loop close on its own at least once.

Some vocabulary is needed. A city is the factory that runs on your machine. A rig is a project you register with it. Agents do the work, drawn from pools, following formulas, and every piece of work is a bead in a shared database. That was Part 1. If none of those words mean anything, go read it first, because here we take those as given.

Also, this is a taste, not the manual. Gas City ships a real tutorial that goes far deeper than an afternoon, and I will point you at it at the end. The project I picked is deliberately small and a little dull, because I am not testing the code, I am testing the loop, and a boring function is the cleanest way to see whether the loop holds. If it works on something boring, it works.

## What you actually need first

Gas City CLI is `gc`. Before `gc` will do anything useful, a few things have to be on the machine.

A working CLI coding agent, first. Gas City does not bring its own intelligence. It drives the agents you already run, so you need at least one of them installed and logged in. I used Claude Code. Codex works too. That is also the part that costs money, which I will come back to.

Then Gas City itself, which on a Mac is one line:

```
brew install gascity
gc version
```

That brings beads CLI `bd`, the issue tracker from Part 1, along with it. Underneath, the stack is Go, Dolt, and tmux. You do not touch them directly, and `gc` will tell you if any are missing.

The GitHub CLI, `gh`, is optional right until the end, when we want a real pull request. Install it and log in before you get there and you will not have to stop halfway.

And the budget. A factory runs real agents in a loop, writing, checking, and re-checking each other's work, and none of that is free. Start your concurrency low for that reason. The first time I ran one of these, I set it going, walked away for a few hours, and came back to find I had hit my daily usage limit with the job still unfinished.


## Standing up the city

With that in place, you turn the empty folder into a factory with one command:

```
gc init --provider claude text-toolkit-factory
```

```
[1/8] Creating runtime scaffold
[2/8] Installing hooks (Claude Code)
[3/8] Writing default prompts
[4/8] Writing pack.toml
[5/8] Writing city configuration
[6/8] Checking provider readiness
[7/8] Registering city with supervisor
[8/8] Waiting for supervisor to start city
  Adopting sessions...
```

Those eight steps write a `city.toml`, a `pack.toml`, and a `.gc/` directory of runtime state, register the city with a machine-wide supervisor, and start it, with Claude Code as the provider. The folder is a running city now, Dolt and the agents' tmux sessions and all. If you ever stop it, `gc start` brings it back.

When something feels stuck, and it will, there is a health check:

```
gc doctor
```

It looks for tmux, git, `bd`, and dolt, checks the city's structure and the rig, and tells you exactly what is wrong. It runs about fifty checks, so here is a healthy run with the middle trimmed out:

```
  ✓ city-structure: city.toml present
  ✓ city-config: city.toml loaded (0 agents, 1 rigs); effective city name "text-toolkit-factory"
  ...
  ✓ tmux-binary: found /opt/homebrew/bin/tmux
  ✓ git-binary: found /usr/bin/git
  ✓ dolt-server: reachable on 127.0.0.1:13851
  ✓ beads-store: store accessible
  ...
  ✓ rig:text-toolkit:path: path "text-toolkit" exists
  ✓ rig:text-toolkit:git: git repository
  ✓ rig:text-toolkit:beads: store accessible
  ⚠ v2-agent-format: legacy [[agent]] tables found in pack.toml
      hint: run "gc doctor --fix" to rewrite safe mechanical cases, then rerun "gc doctor"

52 passed, 1 warnings
```

That last line is the one you read first. Green summary, work continues; a red `failed` count, and it is usually the Dolt server not running, which `gc start` fixes. The single warning here is a harmless nag about an old config format, not something that stops the factory. Run this now, and again whenever the factory goes quiet for no reason.

At any point you can attach to a running session and watch a worker think:

```
gc session attach <session>
```

If you made it here, congratulations! Notice that out of the box, the city is empty: a factory with no project and no workers. We will cover that next.

## Handing it a project

Alright! Let's do this! For the purposes of this tutorial, I built a tiny library I am calling text-toolkit: about thirty small string functions, the kind every codebase grows sooner or later. `slugify`. `titlecase`. `word-wrap`. `pad`. `count-words`. Nothing clever. Each one is pure, no side effects and no dependencies, input in and output out, and each gets its own file and its own test.

That last constraint lives in a short document that sits with the project, called an architecture decision record, or ADR. It is worth separating from the beads in your head. A bead is the spec for a single function. The ADR is the standing rule for the whole project, one document that is true of every function in it: here, one function per file, pure, no dependencies, always a test.

Today the ADR is not enforced. Nothing in the basic loop reads a finished change back against it, which is why you also write the same rule into each function's acceptance criteria, so the worker sees it either way. The ADR starts doing real work in Part 3, when a reviewer checks each change against it and bounces whatever breaks it. For now it is the house rule, written down and waiting for something to enforce it.

I picked this shape on purpose as a base scenario. Thirty functions that do not depend on each other is thirty independent jobs, which is what a fleet wants: work it can pick up in parallel without tripping over itself. It is also real code. These are functions I have written by hand at some point before and been mildly annoyed to write again.

Registering it takes one command:

```
gc rig add ./text-toolkit --prefix tt
```

Now you have a rig: a beads database inside the project, wired into the city, and the bead prefix `tt`, so every piece of work here is named `tt-` something.

A rig on its own has no workers. They come from a pack: a bundle of configuration, the agent definitions and the formulas they follow, in a folder you import into the city. Gas City ships a few builtin, you can write your own, and you can pull someone else's the way you pull a library. Bringing the workers in is a second command:

```
gc import add --rig text-toolkit .gc/system/packs/gastown
```

That is the gastown pack, the reference town from Part 1, and it ships more than we use: a mayor that coordinates the work, a witness that flags stuck jobs, a couple of watchdogs on patrol, and the two that matter here, a polecat that writes the code and a refinery that reviews and merges it. The roles are just configuration: none are built into Gas City, they come with the pack, and you could swap it for another.


Then you point the project at a fresh GitHub repository, because the payoff at the end is a real pull request, and a pull request needs somewhere to land.

Now the work. Each function is a bead, and the bead is the spec. The agent builds what the bead says, not what you meant, so you write each one the way you would write a ticket for someone who is going to take it literally:

```
bd create "count-words(text)" \
  -d "Count the whitespace-separated words in the input. Trim first; empty or whitespace-only input is zero." \
  --acceptance "pure function, its own file, one test covering the empty string, whitespace only, and runs of spaces"

bd create "slugify(text)" \
  -d "Lowercase the input, strip punctuation, collapse runs of whitespace to a single hyphen, and trim hyphens off both ends. ASCII output." \
  --acceptance "pure function, its own file, one test covering the empty string, punctuation, and repeated spaces"

bd create "titlecase(text)" \
  -d "Capitalize the first letter of each word and lowercase the rest. Leave existing spacing alone." \
  --acceptance "pure function, its own file, one test"

bd create "word-wrap(text, width)" \
  -d "Wrap text to the given column width on word boundaries. Never split a word. Return the wrapped string." \
  --acceptance "pure function, its own file, one test for a word longer than width"

bd create "pad(text, width)" \
  -d "Right-pad the string with spaces to the given width. If it is already at or past the width, return it unchanged." \
  --acceptance "pure function, its own file, one test covering a string below the width, one at the width, and the empty string"
```

That is all five, one bead each. You could type them out like this or seed the batch in one go.

A vague bead gets you a vague guess. If you never say what `slugify` does with a trailing space, the worker decides for you, and it may decide differently than you would have. So most of the real work moves upstream, into writing beads a literal-minded worker cannot misread. Gas City has machinery for keeping beads honest, a gate that turns back underspecified ones before an agent burns a run on them, but that is a Part 3 topic. The rule of thumb until then: if you would not hand the ticket to a contractor you have never met, do not hand it to the fleet.

I watched this play out on my own run. My ADR said pure, one file, tested, but it never named a language. The worker on `count-words` picked JavaScript, wrote it cleanly, and then left a note on the bead: it had no way to know whether the functions being built beside it would choose the same stack, so it flagged that a human should settle the convention before anything merged. It did exactly what I told it. That is the good version of an underspecified bead. The bad version is the same gap with nobody flagging it.

Then ask the tracker what is available:

```
bd ready
```

```
○ tt-1rk ● P2 count-words(text)
○ tt-4d4 ● P2 titlecase(text)
○ tt-adq ● P2 pad(text, width)
○ tt-fo7 ● P2 word-wrap(text, width)
○ tt-i9h ● P2 slugify(text)

Ready: 5 issues with no active blockers
```

The ids are hashes, not counters (`tt-1rk`, not `tt-1`), so agents creating work at the same moment never collide. And that is the list of open, unblocked work: every function, waiting, claimed by nobody. The whole project as data, before a single agent has touched it.

## The loop, once

The city is running, the rig is in, and five beads are sitting in the ready queue. None of them has been touched yet.

One decision first, because it changes the ending. Left alone, the refinery uses its default strategy, `direct`: it merges straight to `main`, fast-forward, push, done. Fine for a toy, alarming for anything real, so I want a pull request instead. You get one by writing a single piece of metadata onto the bead, a merge strategy of `mr`, short for merge request, which the refinery reads at the end and obeys:

```
bd update tt-1rk --metadata '{"merge_strategy":"mr"}'
```

For now you tag each bead you want as a PR by hand. Part 3 automates that, and puts a review in front of it.

Now sling it. One bead, on purpose, so the handoff is legible. With thirty going at once it looks like weather; with one you can follow it:

```
gc sling text-toolkit/gastown.polecat tt-1rk
```

```
Auto-convoy tt-7bm
Slung tt-1rk → text-toolkit/gastown.polecat
```

That routes the bead to the polecat pool, and the loop starts turning on its own. A polecat claims the bead atomically, opens its own worktree and a branch, writes the function and its test, runs the test, reviews its own diff, pushes the branch, records what it did on the bead, hands it to the refinery, and exits. Done means gone. The worker does not linger waiting for praise.

The refinery picks the bead up, rebases the branch onto `main`, runs whatever checks the rig has wired up, and because I asked for `mr`, opens a pull request instead of landing the change directly. It records the PR on the bead, closes the bead, and leaves the branch and the PR standing for the merge.

I slung `count-words`, waited, and a pull request appeared on GitHub that I had not opened: number 1, `count-words(text) (tt-1rk)`, open, its body a single line, `Automated pull request published by Gastown Refinery`. Inside it, a clean diff: a pure `count-words.js` and a real test file covering the empty string, whitespace, and repeated spaces. Nobody on my side wrote a character of it.

![The first pull request showing up in the repo on its own: number 1, count-words, open. I had not opened it.](/images/gas-city-first-factory/pr-list-1-open.png)

The account on the PR is mine, because the `gh` login is mine. The body is not. Open it up and it reads one line:

![Inside the first pull request. The body is a single line, "Automated pull request published by Gastown Refinery," and it sits ready to merge.](/images/gas-city-first-factory/pr-1-count-words-conversation.png)

I had spent a year steering one agent in a chat window, approving every step. I had never once watched one open its own pull request against my repo while I was off doing something else.

Be clear on what did and did not happen, though. The pull request is open, not merged. In this mode the fleet writes the change and opens the PR, and the merge is still your click, which on a real repo is exactly where you want the human standing. Making it merge without you is a Part 3 gate, not a fresh-install default.

Then I let the rest go. I tagged the other four functions the same way and slung them, and the polecats worked in parallel, each in its own worktree on its own function. A little later the repository carried five pull requests, one per function, none of which I had opened:

```
5  word-wrap(text, width) (tt-fo7)  OPEN
4  pad(text, width) (tt-adq)        OPEN
3  slugify(text) (tt-i9h)           OPEN
2  titlecase(text) (tt-4d4)         OPEN
1  count-words(text) (tt-1rk)       OPEN
```

Five functions written, each with its own test, each on its own branch, each waiting in its own PR, while I did other things.

![Five pull requests open at once, one per function, none of which I opened.](/images/gas-city-first-factory/pr-list-5-open.png)

The timestamps in that shot tell the truth: I did not get all five in one clean sitting. I hit my usage limit partway through and picked the run back up over the next couple of days.

And the code inside them was real. Here is the one for `word-wrap`, the function and its test, exactly as a polecat wrote and pushed it:

![The word-wrap pull request: the function and its test, written by a polecat.](/images/gas-city-first-factory/pr-5-word-wrap-diff.png)

While it ran, I did the thing Part 1 kept promising: I checked, without watching. The mayor is a session you can just talk to, so I sent it one line asking how the work was going, and it went and looked. It came back with a real status: slugify's tests looked solid, the refinery had cleared its queue and gone idle, and, unprompted, one of the watchdog agents had been stuck for eight minutes on a bug it could not fix. A live, addressable account of a fleet I was not babysitting, out of a single message. It also told me it had "merged" titlecase when the record said that PR was only open, a small reminder that an agent's summary is a convenience and the bead is the truth.

## What just happened, in last time's words

Everything that just ran was Part 1 vocabulary doing its job: a bead, a formula, agents from a pool, and a record on the bead.

The unit of work was a bead. Slinging it did one thing I glossed over: it took a formula, the written-down method for how a job gets done, and stamped a live copy of it onto the bead. That running instance of a formula has its own name, a molecule. The formula is the recipe. The molecule is the meal being cooked.

The polecat and the refinery were not special software. They were configured agents drawn from pools, exactly as Part 1 promised. You could swap either one for a differently shaped worker and the loop would not notice.

And the handoff, the whole back and forth, lived on the bead, not in a chat window. You can go read it:

```
bd show tt-1rk --long
```

```
✓ tt-1rk · count-words(text)   [● P2 · CLOSED]
Owner: Bryan McEire · Assignee: text-toolkit/gastown.refinery
Close reason: Pull request ready: https://github.com/mceire/gc-text-toolkit-demo/pull/1

METADATA
  branch: tt-1rk-count-words
  merge_result: pull_request
  merge_strategy: mr
  pr_url: https://github.com/mceire/gc-text-toolkit-demo/pull/1
  target: main
```

It is all there: who worked the bead, the branch they pushed, the pull request that came out, and (in the notes, trimmed here) the flag the worker raised about the language nobody had specified. That record is the light-factory idea from Part 1 in its plainest form. The database that coordinates the fleet is the same database that remembers what the fleet did. Coordination and accountability turn out to be the same object.

## Where it fought me

My real run did not go that smoothly.

The worst snag had nothing to do with Gas City. My Claude Code was set to a model that bills separate usage credits, so every polecat launched, hit a "switch models?" dialog, and froze there waiting for a keypress nobody was giving it. Three workers, all stuck before they wrote a line. I switched the default to an on-plan model, restarted them, and they came back to life. Frontier tooling is a stack of these: the thing that stops you is rarely the clever part, it is a dialog box in the dark.

The fleet also needed more minding than "autonomous" suggests. The refinery had a habit of finishing one pull request, checking for work, finding none for a moment, and going idle, so a branch a polecat had just handed off would sit untouched until I nudged the refinery to look again. And when it did process a branch, it ran no tests, because this rig had none configured. It rebased and opened the PR on trust. Fine for a toy. Also a preview of why Part 3 exists.

Then the cost, which you feel fast, because these agents think hard. The refinery alone spent something like thirty thousand tokens and six minutes on a single function. Getting all five pull requests open drained the usage allotment on the model I was driving, and I ran out before I was finished: the same wall from Part 1, where you hit your limit before the machine hits the end of the work. Keep your concurrency low and your expectations honest.

And the ground moves. Gas City ships new releases most weeks, so a flag here may have drifted by the time you read this. When something does not match, trust the docs over me.

None of it changed the result. Five pull requests I did not write, opened on their own against a project I handed the fleet that morning, are worth a rough afternoon. Safe is a different question.

## Go do it, and what I still do not trust

If you take one thing from this, make it a small one you can do this week. Install `gc`, stand up a city, add a rig with three throwaway functions, and sling one bead. That is the whole loop in miniature, and it will teach you more in an afternoon than another article will.

When you want the depth, the Gas City docs and the official tutorial go well past what I showed. This was the shape, not the manual. Start at `docs.gascity.com` and follow it to the Beads docs.

Everything here trusted the agents. The polecat wrote the code and its own test, the refinery rebased and opened the pull request without running a check of its own, and the only thing between that PR and your `main` branch is you, reading a diff. On a library of string functions, fine. On a real codebase, not yet: nothing checked the change against the architecture, and when the fleet flagged that it had picked a language nobody specified, nothing was set up to act on the warning.

So the next piece is about earning that trust back: the gates you put in front of the loop and the reviews you put inside it. A human who still owns the main branch. A reviewer that weighs a change against the architecture, not just the tests. More than one vendor's model getting a vote before anything merges. That is the distance between a demo that opens its own pull requests and a factory you would let near production. That is Part 3.
