Run Factory locally
August 19, 2026 ยท View on GitHub
This guide starts one control plane and one worker on macOS or Linux.
Requirements
- Go 1.25.13 or newer on the 1.25 release line, or Go 1.26.6 or newer
- Git
curljust- at least one authenticated Pi, Codex, or Claude Code CLI
- an authenticated
ghCLI for centrally managed GitHub repositories
Node.js is not required for normal startup.
Platform support
Factory supports macOS and Linux. The server, worker, and local launcher are tested on both Apple Silicon and Intel macOS workers. There are no intentional macOS feature gaps.
Windows is not supported. The launcher and worker lifecycle depend on Unix process signals, executable permissions, and shell tools.
Configure the control plane
Most control-plane configuration is stored in SQLite and managed through the
browser. The optional ~/.factory/config.toml contains only bootstrap settings
needed before SQLite opens:
listen = "127.0.0.1:7337"
database = "server/factory.sqlite3"
Relative database paths resolve from the config file directory. Unknown fields, symlinks, and files larger than 1 MiB are rejected. Command-line flags override the file. Copy the example only when changing these defaults.
The control-plane database contains prompts, results, and repository metadata.
Factory creates the database, its marker, and SQLite WAL and shared-memory files
with owner-only permissions (0600). When a valid existing database is opened,
Factory corrects broader permissions before use and rejects non-regular files or
symlinks in these locations. The containing database directory must be a real
directory owned by the server's effective user and must not be writable by group
or other users. Factory validates the configured path before creating missing
directories, then separately validates its resolved target. Path components and
symlinks must be owned by the effective user or root; group or world-writable
ancestors require sticky-bit protection. This permits protected system symlinks
such as macOS /var without trusting links controlled by another user. Existing
0755 database directories are accepted; use chmod go-w PATH before startup
when an explicit database directory is group or world writable.
Configure a worker
Build the binaries and copy the example:
just build
mkdir -p ~/.factory
cp examples/worker.toml ~/.factory/worker.toml
Edit ~/.factory/worker.toml:
server = "http://127.0.0.1:7337"
name = "local-codex"
runtime = "codex"
runtimes = ["pi", "codex", "claude-code"]
# Optional. Defaults to 10 and accepts values from 1 through 100.
max_concurrent = 10
One Worker is a pool for its configured coding agent runtimes. Each slot runs an independent Pi, Codex, or Claude Code session with its own worktree and process group. Preparing an attempt also consumes a slot. Choose a lower value when local CPU, memory, or provider limits require it.
The optional legacy runtime field is the default for older clients that do
not select a runtime. Existing single-runtime files continue to work without a
runtimes field. New configurations can list several runtimes and may omit
runtime, in which case the first listed runtime becomes the primary one.
Factory reports each one as ready, missing, unauthenticated, or unhealthy, and
the Worker stays available while Git and at least one configured runtime are
ready.
Factory migrates existing SQLite databases to the expanded worker capacity range when the control plane starts.
With this ~/.factory/worker.toml filename, Factory defaults durable worker
state to ~/.factory/workers/worker. The config filename, rather than name,
selects the state directory.
No worker repository list is required. Factory detects local GitHub access with
gh auth status and clones centrally managed repositories on demand. Optional
legacy repository paths remain available for manual UI delegation. Relative
paths resolve from the worker TOML directory, and each path must be a real,
non-bare Git checkout with an origin remote. Factory starts legacy work from
the origin default branch without changing the checkout. To use another base,
configure it under that repository:
base_branch = "release/2026.07"
To run only one coding agent, keep the existing single-runtime form, such as
runtime = "claude-code". Set data_directory only when an explicit relative
or absolute override is needed; never share one data directory between Worker
identities.
Start
The launcher builds the Go binaries, starts one control-plane process, waits for health, starts the worker, and waits for that worker to register. The server runs every provider and schedule Automation evaluation loop:
just run
Open http://127.0.0.1:7337.
Stop both processes with Ctrl+C.
Add a GitHub repository to the central fleet once:
curl --fail --silent --show-error \
-H 'Content-Type: application/json' \
-d '{"remote_identity":"github.com/OWNER/REPOSITORY"}' \
http://127.0.0.1:7337/api/v1/repositories
List the current fleet with GET /api/v1/repositories. Set
{"enabled":false} with PUT /api/v1/repositories/REPOSITORY_ID/enabled to
stop new routed work without interrupting an execution whose worker assignment
is already frozen. Posting a repository first discovered from a legacy worker
promotes it into the enabled central fleet. Reposting a centrally managed
repository does not override an explicit disable.
To start with a different Worker config:
just run ~/.factory/claude-worker.toml
To run more than one Worker, start the control plane once and then start each additional Worker directly:
~/.factory/bin/factory-worker \
-config ~/.factory/claude-worker.toml
Delegate a task
The manual delegation screen lists both optional legacy checkouts advertised by
the selected worker and every repository in the central catalog. An enabled
central repository is selectable when that worker is online, healthy, reports
the required GitHub access, and can acquire managed repositories. Factory
reserves it for that worker when the task is created. Repositories that are
disabled or unavailable remain visible with the reason. Add a
[repositories.<key>] entry only when delegation must use an existing local
checkout.
In the UI:
- Open Workers and confirm the worker is online and healthy.
- Select Delegate task.
- Enter a title and description.
- Select the worker and repository.
- Submit.
The Runs view shows the Session state. Session detail shows attempts, lifecycle events, results, and errors.
The same operation is available through the API:
curl --fail --silent --show-error \
-H 'Content-Type: application/json' \
-d '{
"request_key": "manual-example-1",
"title": "Review the README",
"description": "Review the README for errors, fix them, test the change, and commit it.",
"worker_id": "WORKER_ID",
"repository_id": "REPOSITORY_ID"
}' \
http://127.0.0.1:7337/api/v1/tasks
Worker and repository IDs are available from:
curl --fail --silent --show-error \
http://127.0.0.1:7337/api/v1/workers
Data and overrides
Factory stores state below ~/.factory by default. Common overrides:
FACTORY_DATA_HOME
FACTORY_SERVER_CONFIG
FACTORY_WORKER_CONFIG
FACTORY_BUILD_DIR
FACTORY_LISTEN
FACTORY_SKIP_BUILD
FACTORY_WORKER_READY_SECONDS
Examples:
FACTORY_LISTEN=127.0.0.1:7444 just run
FACTORY_DATA_HOME=/srv/factory \
just run /srv/factory/worker.toml
The server remains loopback-only.
Back up and restore the control plane
Use the server's backup mode instead of copying a live SQLite file. Backup mode
uses SQLite's online snapshot operation, includes every committed transaction
from the WAL, validates the result, writes files with mode 0600, and exits. It
does not interrupt a running control plane, create a missing source, or apply
migrations to the live database.
install -d -m 700 /secure/factory-backups/2026-08-05
~/.factory/bin/factory-server \
-backup /secure/factory-backups/2026-08-05/factory.sqlite3
The command uses the database selected by FACTORY_DATA_HOME,
FACTORY_SERVER_CONFIG, or -database, just like normal startup. A successful
backup contains these two required files:
factory.sqlite3
factory.sqlite3.v2-control-plane
The live factory.sqlite3-wal and factory.sqlite3-shm files are temporary and
must not be copied or restored. The backup command folds committed WAL data into
the standalone snapshot. Restore rejects a backup with either sidecar present.
Both commands refuse writable destination directories and never overwrite an
existing database, marker, WAL, or shared-memory path.
Configuration is not stored in the database snapshot. Copy the active server
config.toml and every worker TOML file into the protected backup directory.
Also retain any external secrets or service configuration used to start the
process. Keep their existing restrictive permissions. Worker identity and
retained-worktree state remain under each worker's configured data directory;
back them up separately when the recovery plan must preserve those local paths.
Restore runbook
Restore requires downtime and a fresh destination:
-
Stop the control plane and all workers. Keep the old Factory data home unchanged so rollback remains possible.
-
Install the backed-up configuration into a new mode-
0700Factory data home. Update absolute paths only when the recovery host differs. -
Restore with the same Factory version that made the backup, or a newer compatible version:
install -d -m 700 /srv/factory-restored FACTORY_DATA_HOME=/srv/factory-restored \ ~/.factory/bin/factory-server \ -restore /secure/factory-backups/2026-08-05/factory.sqlite3 -
Start the control plane with
FACTORY_DATA_HOME=/srv/factory-restored. Restore applies supported migrations in a private staging directory and compares the complete resulting schema with the binary before publishing the destination. It rejects a backup made by a newer schema, a corrupt database, a forged or incomplete migration ledger, a missing or invalid marker, or any existing destination. -
Check
curl --fail http://127.0.0.1:7337/healthz, then inspect retained task attempts and events, Workflows, Automations, and managed repositories in the UI. Start workers and confirm they register as healthy before enabling new work. -
Keep the old data home and backup until the restored system has completed a representative task. To roll back, stop the restored processes and restart the unchanged old data home with its original binary and configuration.
The restore command runs SQLite quick and foreign-key integrity checks before publishing the destination. The health check confirms that normal startup then opens the restored database. Never restore over an existing home and never combine a main database copied by hand with WAL or shared-memory files from a different point in time.
Migrate a legacy factory-poller
Migration is offline and disabled-first. Never run it while any legacy poller process can write the ledger.
- Stop every
factory-pollerprocess and keep it stopped. - Back up the legacy
poller.tomland ledger. - Start the current Factory control plane and open Automations โ Migrate legacy poller.
- Select the legacy paths when they are not the defaults, confirm the poller is stopped, and choose Preview locked snapshot.
- Review every resolved source path, managed-repository identity and ID,
proposed Workflow and Automation title, observation count, and error.
Unsupported command queues
remain recoverable from the later archive but are not imported. If the
ledger contains observations for a queue removed or renamed in
poller.toml, Factory shows that ledger-only queue and blocks Import. Restore the matching queue entry, stop the poller, and run Preview again so no observation identity is silently omitted. - Choose Import disabled Automations. Factory verifies the exact Preview snapshot while holding an exclusive legacy-ledger lock. Existing submitted task identities and deleted-task tombstones are retained. Imported Automations cannot be enabled yet.
- For every legacy pending observation, choose Resume to replay its exact durable task request or Skip to record that it must not dispatch.
- Choose Finalize and archive. Factory locks and verifies the same snapshot, then archives consistent copies of the config and ledger with a hash manifest. It does not modify or delete either source file.
- Review and test each typed Automation, then enable it. The control plane now owns evaluation and deduplication.
Preview, Import, and Finalize fail closed if the source paths, config bytes, ledger bytes, inode, schema or rows, snapshot digest, or lock availability changes. Correct the cause and run a new Preview. An archive write failure leaves the migration imported and safe to retry. Closing the browser or restarting Factory rediscovers the active imported migration and preserves pending Resume or Skip decisions and an already completed archive.
UI development
Only contributors changing the UI need Node.js. Use Node.js 22.22.2 or newer on the 22 release line, Node.js 24.15.0 or newer, or Node.js 26 or newer:
just ui-install
cd web && npm run dev
Before committing UI changes:
just ui-check
just ui-build 0
The operator build embeds the committed web/dist and never invokes npm.
Troubleshooting
127.0.0.1 refused to connect
- confirm
just runis still running; - inspect the terminal for server or worker startup errors;
- check
curl http://127.0.0.1:7337/healthz; - check that another process is not using port 7337.
Worker never becomes healthy
- confirm the selected runtime command is on
PATH; - authenticate Codex or Claude Code as the same OS user;
- confirm every repository path and its
origin; - ensure each worker has a unique data directory;
- inspect the worker JSON logs.
GitHub Automation reports gh_missing or gh_unauthenticated
- install
ghon the control-plane host; - run
gh auth loginas the same OS user that startsfactory-server; - verify
gh auth status --hostname github.com; - restart the server or test the Automation again.
Legacy poller migration cannot acquire its lock
- confirm every old poller process is stopped;
- confirm no SQLite inspection tool has a transaction open on the legacy ledger;
- do not copy or edit the source files during Preview, Import, or Finalize;
- retry the action after releasing the writer.
Legacy poller snapshot changed
- leave imported Automations disabled;
- inspect what changed in the config or ledger;
- return to the migration dialog and run a new Preview against the stable source. Factory does not partially import or archive a changed snapshot.
A worktree is retained
Factory keeps worktrees when they are dirty or may contain unpublished work. Open the assigned Worker to see retained paths and cleanup commands. Use the attempt ID from the Session detail or retained worktree card to preview cleanup:
~/.factory/bin/factory-worker cleanup ATTEMPT_ID \
--config ~/.factory/worker.toml
Add --confirm to remove the worktree. The local branch is preserved, but
uncommitted changes shown in the preview are lost.