Factory: local setup¶
This page walks through running the declarative factory orchestrator locally on your laptop so Linear tickets you label dispatch to your local Claude / Codex agents.
For background on the dual-plane (orchestrator / agent runner) design and the legacy Cloud Run path, see Factory.
Prerequisites¶
claudeCLI installed and signed in (claude— the Max subscription session must be valid; the orchestrator strips provider API keys before launching the engine so it always uses your subscription auth).codexCLI installed and signed in, if you plan to run a Codex agent.ghCLI authenticated against theinboundx/inboundxrepository (the agent prompt requiresgh pr createat the end of every run).gcloudCLI authenticated against theinboundxGCP project (used once to fetchLINEAR_API_KEYfrom Secret Manager).- Python venv with the repo's backend dependencies (
./bootstrap.pyfrom the repo root is the easiest path).
1. Bootstrap the per-machine registry¶
The agent registry is per-machine, not repo-tracked. Each laptop runs only
the agents it owns. The repo ships a canonical template at
factory/config/agents.example.yaml.
That command:
- creates
~/rose-factory/if missing, - copies the example registry to
~/rose-factory/agents.yamlif it does not already exist, - prints a reminder to trim the file to only the agents this laptop should run.
Open ~/rose-factory/agents.yaml and remove any agent entries owned by other
people. A typical local file declares just your two engines:
defaults:
project_slug: inboundx
active_states: ["Todo", "In Progress", "In Review", "Rework"]
terminal_states: ["Done", "Closed", "Cancelled", "Canceled", "Duplicate"]
workspace_base: ~/rose-factory/workspaces
transport: local
environment: none
agents:
- id: benoit-claude
owner: benoit
engine: claude
max_concurrent: 2
linear:
api_key_env: LINEAR_API_KEY
labels: ["claude-benoit"]
- id: benoit-codex
owner: benoit
engine: codex
max_concurrent: 2
linear:
api_key_env: LINEAR_API_KEY
labels: ["codex-benoit"]
Each agent declares one or more Linear labels that opt a ticket into that agent. Labels are the only routing signal — the orchestrator does not filter by assignee.
2. Create the Linear labels¶
In Linear, create one label per agent you declared. Names must match
linear.labels from the registry exactly. For the example above:
claude-benoitcodex-benoit
You can create labels from any ticket's label dropdown, or from Settings → Workspace → Labels.
3. Export LINEAR_API_KEY¶
The orchestrator reads LINEAR_API_KEY from the environment. Fetch it from
GCP Secret Manager:
export LINEAR_API_KEY=$(gcloud secrets versions access latest \
--secret=LINEAR_API_KEY --project=inboundx)
Add the same line to your shell rc (~/.zshrc) so future shells pick it up.
The key is your personal Linear key, so factory-authored Linear comments are attributed to you.
4. Smoke-test without running an engine¶
Confirm the registry, key, and Linear queries work without spending a Claude turn:
You should see [] if no labeled tickets are active, or a planned
DispatchResult for each labeled ticket.
5. Run the orchestrator¶
The orchestrator polls Linear every 30 seconds for tickets carrying the agent's
label and in an active_states state. For each new or updated ticket it:
- Transitions the issue to In Progress.
- Creates a git worktree at
~/rose-factory/workspaces/<agent>/<TICKET-ID>offorigin/develop, on the issue'sbranchName. - Spawns the engine (
claudeorcodex) inside the worktree with a prompt built from the issue title, description, and recent human Linear comments. - Expects the agent to commit, push the branch, and open a pull request
targeting
develop(the prompt instructs the agent to rungh pr create --base develop --fill). - Transitions the issue to In Review on success.
- Posts a single Linear summary comment with what was done and the PR URL.
State (last-processed updatedAt, factory-posted comment IDs) lives in
.context/factory-orchestrator-state.json. Stop the orchestrator with
Ctrl-C or kill <pid>.
6. Trigger and iterate on a ticket¶
- Create or pick a Linear ticket in the inboundx project.
- Move it to Todo (or
In Progress/In Review/Rework). - Add the agent's label (e.g.
claude-benoit). - Within
--poll-intervalseconds, the orchestrator dispatches it. - To send follow-up instructions, post a Linear comment on the ticket. The
orchestrator sees the bumped
updatedAton the next poll and re-runs the engine with your comment included in the prompt.
Multi-machine isolation¶
The same setup works on multiple laptops. Each machine keeps its own
~/rose-factory/agents.yaml with only the agents it should run. There is no
shared orchestrator process — each laptop polls Linear independently against
the labels its local registry declares.
When you eventually migrate to Linear's first-class Agents (OAuth + webhooks) — tracked in IX-2948 — the polling layer is replaced by a webhook receiver, but the rest of the orchestrator/agent_runner split stays the same.
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
Per-machine agent registry not found |
~/rose-factory/agents.yaml missing |
just setup from factory/ |
Missing Linear token env var |
LINEAR_API_KEY not exported |
step 3 |
Linear workflow state not found: ... |
An agent's state_on_start/state_on_success references a state name that does not exist in the team workflow |
Use the exact state name from your Linear workflow, or set the field to null |
| Orchestrator runs but never dispatches | Ticket missing the agent's label, or ticket state is not in active_states (e.g., Backlog) |
Add the label; move to Todo |
Error: When using --print, --output-format=stream-json requires --verbose |
Older claude CLI version |
Update Claude Code; the orchestrator already passes --verbose |
| Agent finishes but no PR appears | gh not authenticated in the worktree, or agent skipped the gh pr create step |
gh auth status; check the summary comment for the agent's PR step |