Skip to content

Factory

The Rose Factory runs assigned Linear tickets through coding agents (Claude Code, Codex) in isolated git worktrees. It is a standalone, pipx-installable tool (rose-factory) with no dependency on the rest of the monorepo — see factory/README.md for the full command reference and Factory: local setup to run it.

It has two planes:

  • Orchestrator (factory/orchestrator/) — declares agents, owns Linear routing, scheduling, and the retry/reconciliation/status lifecycle. Runs as a local polling loop; engine runs execute locally per machine.
  • Relay (factory/relay/) — a small HTTP service (Cloud Run, or local) for the Linear-Agents (agent_session) OAuth + webhook path. The only hosted component.

The runner (factory/runner/) is the engine-neutral per-issue interface; engine adapters live under factory/engines/ (Claude, Codex, and a console smoke-test runner).

Architecture

flowchart TB subgraph Linear WH["Webhook / assignment / @-mention"] API["Linear API"] end subgraph "Relay (Cloud Run or local)" REL["/webhooks/linear → queue<br/>/events long-poll"] end subgraph "Local machine" ORCH["orchestrator loop<br/>(poll API or long-poll relay)"] RUN["runner → engine adapter"] WT["git worktree<br/>(from base clone)"] ENG["Claude / Codex CLI"] end subgraph Outputs PR["GitHub PR"] LC["Linear comment + status"] end WH --> REL --> ORCH API --> ORCH ORCH --> RUN --> WT --> ENG ENG --> PR & LC

Two tracker modes per agent:

  • api_key — the orchestrator polls the Linear API directly with the agent's token.
  • agent_session — Linear delivers webhooks to the relay; the worker long-polls the relay's /events instead of polling Linear. See the relay section of factory/README.md for local webhook setup (cloudflared tunnel).

Workspaces

Each issue runs in its own git worktree. A standalone install has no local checkout, so the factory keeps one base clone per repo per machine (~/rose-factory/repos/<slug>), fetches it each tick, and adds per-issue worktrees from it. The target repo + base ref + bootstrap command come from the agent's repo config in ~/rose-factory/agents.yaml. Passing --repo-root <path> uses an existing local checkout instead (in-monorepo dev).

Continuous orchestrator

The orchestrator polls Linear (or long-polls the relay) with the declared agent token, dispatches changed assigned issues to the configured runner, and records the last processed Linear updatedAt in ~/rose-factory/state.json.

rose-factory start --agent benoit-codex          # one agent
rose-factory start                                # all declared agents
rose-factory start --once --dry-run --agent X     # validate routing, no engine

When a reviewer comments on the ticket, Linear updates the issue; the next poll rebuilds the prompt with recent comments and reruns the runner in the same issue workspace. On start the orchestrator transitions the issue to In Progress; on finish it transitions to In Review (on success) and posts one human-facing summary comment. Factory-authored comments carry a hidden marker and are excluded from future prompts. A ticket left In Progress with no live owner is auto-recovered on a later tick (resume once, then escalate).

Claude and Codex runners use logged-in CLI/subscription auth, not provider API keys. Engine subprocess environments strip common LLM API-key variables (OPENAI_API_KEY, ANTHROPIC_API_KEY, …); Linear tokens are used only by the orchestrator/tracker layer.

Runtime environments

Runtime environments are separate from engines and transports. The first provider is none, which provisions no preview and runs the engine in a worktree. Later providers such as google_preview can add Supabase preview branches, Cloud Run tagged revisions, frontend bundles, and Cloudflare preview routes without changing orchestrator routing semantics.

Run as a service

rose-factory install-daemon wraps any command in the OS service manager (launchd on macOS, systemd --user on Linux) so it auto-starts and restarts on crash:

rose-factory install-daemon --env-file ~/rose-factory/daemon.env -- start
rose-factory install-daemon --name rose-relay --env-file ~/rose-factory/daemon.env -- relay --local-queue

Secrets

Resolved env-first, then GCP Secret Manager (relay only). Set the value as an env var named by the config key for a no-GCP run.

Secret Purpose
CLAUDE_AUTH_TOKEN Claude Code Max subscription auth
FACTORY_GITHUB_TOKEN GitHub clone/push/PR (private repos)
LINEAR_API_KEY Linear API for api_key agents
FACTORY_RELAY_WORKER_TOKEN Worker bearer token for agent_session agents
LANGFUSE_*, SENTRY_AUTH_TOKEN, SUPABASE_ACCESS_TOKEN Observability / MCP (optional)