Skip to content

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

  • claude CLI 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).
  • codex CLI installed and signed in, if you plan to run a Codex agent.
  • gh CLI authenticated against the inboundx/inboundx repository (the agent prompt requires gh pr create at the end of every run).
  • gcloud CLI authenticated against the inboundx GCP project (used once to fetch LINEAR_API_KEY from Secret Manager).
  • Python venv with the repo's backend dependencies (./bootstrap.py from 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.

cd factory && just setup

That command:

  • creates ~/rose-factory/ if missing,
  • copies the example registry to ~/rose-factory/agents.yaml if 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-benoit
  • codex-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:

python -m factory.orchestrator --once --dry-run --agent benoit-claude

You should see [] if no labeled tickets are active, or a planned DispatchResult for each labeled ticket.

5. Run the orchestrator

python -m factory.orchestrator --run --agent benoit-claude --poll-interval 30

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:

  1. Transitions the issue to In Progress.
  2. Creates a git worktree at ~/rose-factory/workspaces/<agent>/<TICKET-ID> off origin/develop, on the issue's branchName.
  3. Spawns the engine (claude or codex) inside the worktree with a prompt built from the issue title, description, and recent human Linear comments.
  4. Expects the agent to commit, push the branch, and open a pull request targeting develop (the prompt instructs the agent to run gh pr create --base develop --fill).
  5. Transitions the issue to In Review on success.
  6. 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

  1. Create or pick a Linear ticket in the inboundx project.
  2. Move it to Todo (or In Progress / In Review / Rework).
  3. Add the agent's label (e.g. claude-benoit).
  4. Within --poll-interval seconds, the orchestrator dispatches it.
  5. To send follow-up instructions, post a Linear comment on the ticket. The orchestrator sees the bumped updatedAt on 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