Running Factory

The runbook: the full loop on one machine, then the control plane in Cloud with agents on fleet nodes.

Factory splits into a control plane that decides what to work on and agents that do the work in real checkouts. This page is the hands-on runbook for both halves: the whole loop on one machine first, then each half in the cloud. Where it runs is the conceptual map; this is the sequence of commands.

Prerequisites

NeedCheck
Factory CLInpm install -g @agent-relay/factory (Node ≥ 22)
agent-relay + relayfileInstalled and signed in — peers, not dependencies. Agent Relay 11.3.1 or later; factory init verifies both are on PATH
GitHub connected to your relay workspaceWith push access. The GitHub App installation must cover the target repos — a repo the installation can't see fails every write with 403: Resource not accessible by integration
Harness CLIs on PATHclaude, codex, opencode, … — whatever your agents run
gh auth loginOnly for github.identity: "user"; the default "auto" publishes through the connected App

Local — the whole loop on one machine

The control plane and every agent run on your box, against the checkouts in your clonePaths. Factory reuses a relay broker already running for your workspace and starts one if there isn't.

1. Point it at a repo

From the repository checkout you want worked on:

factory init

It derives the repo from origin, resolves your active workspace, starts the local mount, waits for the GitHub projection of that repo, and writes factory.config.json for GitHub-native dispatch. Every check happens before any write, so a failed init leaves nothing behind. Use factory init owner/repo when the checkout has no GitHub remote.

2. Give the broker a durable identity

If Factory cold-starts its broker, set two variables — this is the difference between restarts that work and restarts that collide:

export AGENT_RELAY_STATE_DIR="$PWD/.factory/relay-state"
export AGENT_RELAY_BROKER_NAME=garden-demo-broker

A self-started broker otherwise names itself after the checkout directory and generates a fresh identity each start. After a crash the name is registered but unproven, and the next start refuses with agent name … is already registered. With a dedicated state dir the broker persists its identity and rejoins cleanly; AGENT_RELAY_BROKER_NAME keeps the name stable and independent of the directory. Gitignore the state dir; never point it at a project's .agentworkforce/relay.

3. Plan without touching anything

Label an issue factory, then:

factory run-once --dry-run

Discovers and triages, writes nothing, spawns nothing. Issues pulled but not dispatched are the safety scope doing its job — they're real, just not opted in.

4. Dispatch for real

factory dispatch 42      # one issue, deterministically
factory run-once          # or: whatever is ready, up to batchSize

The sequence, all against the live issue:

  1. Claimfactory:in-progress label, then the dispatch comment, confirmed by provider read-back. Three retries; an exhausted write dead-letters and fails the dispatch rather than reporting success with missing state.
  2. Worktree — an isolated checkout per issue on a deterministic factory/<issue>-<repo>-<run> branch.
  3. Implementer — spawns (codex, claude, … per your config), fixes, commits, pushes the branch. It does not open the PR.
  4. Publish — Factory opens the PR through the connected App, then hands it to the reviewer agent.
  5. Human review — the issue moves to factory:human-review. mergePolicy defaults to never: every merge is yours.

A one-shot dispatch stays alive until its agents exit — default 30 minutes, FACTORY_AGENT_EXIT_TIMEOUT_MS — then shuts down the broker it started.

Big workspace? If the roster probe times out and the fleet circuit opens, raise it: fleetHealth.rosterTimeoutMs (max 60000), failureThreshold, resetTimeoutMs in factory.config.json.

5. Recover from a crash

A killed dispatch leaves recoverable state, not lost work — the durable lifecycle row, deterministic agent identities, and pushed branch all survive. The three residues you may have to clear by hand:

SymptomCauseFix
Live state changed before writebackThe run died after claiming the issueRemove the factory:in-progress label on the issue — GitHub UI, or gh issue edit <n> --remove-label factory:in-progress if you have a local gh login — then re-dispatch
registration is create-onlyStale agent records from the crashed runagent-relay agent remove <name> for the implementer/reviewer names, or factory reap-orphans
operation was aborted due to timeoutRelaycast throttlingThe dispatch retries on its own; if the process died, re-run it — the lifecycle resumes

Worktree and branch leftovers are ordinary git: git worktree remove … --force, git branch -D <factory/…>. factory status prints in-flight dispatches, held agents, and circuit state as JSON.

Cloud — each half where it belongs

Two shapes: keep the CLI control plane on a host you choose and place agents on fleet nodes, or host the control plane itself in Cloud.

CLI control plane + fleet nodes

factory start --mode live --backend relay

The control plane runs on your host; agents are placed onto fleet nodes through the hosted engine. Bring a node online:

Agent Relay 11.3.0 and earlier print the active workspace key from node up. Check agent-relay --version and upgrade to 11.3.1 or later before running it in any logged or transcribed terminal — see Where it runs.

# once per machine — redeem an enrollment token from the Fleet page
agent-relay cloud enroll --token ocl_node_enr_…

# each boot — node up auto-discovers agent-relay.ts in the working directory
agent-relay node up
// agent-relay.ts — must default-export the node definition
export { default } from '@agent-relay/factory/node'

The node reads factory.node.json: workspaceId, capabilities, and the clonePaths/cloneRoot map of checkouts it services. Each mapped repo is advertised as a repo:<label> tag so repo-scoped work routes to a node that actually has it; spawns for unadvertised paths are refused.

Relay dispatch is lifecycle-owned, not fire-and-forget: the dispatch process keeps a small publisher runtime alive until the remote branch has produced a PR, terminal writeback is acknowledged, and remote agents are released. The lifecycle — per-run branch, placement results, PR receipt, fenced owner lease — is persisted beside the loop registry, so factory start or a replacement process on the same host takes over after a crash.

Run one control-plane host per workspace; multiple processes on that host are fenced by a shared lock. Active/active across hosts is deliberately unsupported. For production, point AGENT_RELAY_STATE_DIR at a directory distinct from any project broker and set fleetHealth.requireDedicatedBroker: true — Factory then refuses to silently reuse an interactive broker. batchSize defaults to 1 and is capped at 5.

Hosted control plane in Cloud

@agent-relay/factory/hosted is the worker-safe control-plane entrypoint — no Node filesystem or process dependency, the complete sweep (reconcile invocation completions → discover → triage → dispatch → merge gate → idempotent writeback) driven by your host. This is you hosting the control plane inside your own Cloud worker, not a managed Factory that Agent Relay runs for you — see Where it runs:

import {
  createHostedFactory,
  DurableObjectHostedFactoryStateStore,
} from '@agent-relay/factory/hosted'

const state = new DurableObjectHostedFactoryStateStore(durableObjectState.storage)
const factory = createHostedFactory(
  { workspaceId, ownerId: isolateId, config },
  { state, discovery, fleet, completions, mergeGate, writeback, reporter },
)

await factory.runOnce()      // invoke from cron/alarms and safe webhook wakeups

Your host supplies the ports:

PortSupplies
stateDurable coordination storage — the DO adapter stores each workspace independently, with lease claims and lifecycle writes in storage transactions
discoveryReady issues from the mounted tracker
fleetSpawn/status against the hosted engine; the invocation ID is the at-least-once dedupe key
completionsCompletion lookups — runOnce() re-polls every persisted invocation before discovery, so a dropped completion webhook is recovered by the next scheduled sweep
writebackrequestClarification / dispatched / completed, each carrying a stable idempotency key
mergeGate, reporterOptional: merge verdicts and the FactoryEventReporter event stream

Pushed completions don't have to wait for the sweep: factory.ingestCompletion(completion) folds a webhook delivery in directly. Every mutation is fenced against the current lease owner and a monotonically increasing epoch — an expired host cannot write after takeover — and spawn invocation IDs are deterministic, making recovery safe when an external operation succeeds just before the host loses its lease.

Tokens, one per role:

TokenPrefixHeld by
Workspace keyrk_live_The orchestrator (RELAY_WORKSPACE_KEY) — mints Factory's own agent identity
Agent tokenat_live_Optional pin for the orchestrator's identity; spawned agents get their own
Node tokennt_live_Each worker node, from cloud enroll
Observer tokenot_live_Read-only dashboards and streams — never dispatch

Hosts that only need the event wire schema, creator, and reporter types should import @agent-relay/factory/telemetry instead — it excludes the filesystem-backed outbox.

Next