---
url: https://clusto.app/docs/factory.md
description: >-
  The Clusto software factory (Labs) - send an issue, a red CI run or a typed
  task to an agent that fixes it in its own clone, verifies it with your check
  command, rates its risk, has it reviewed and opens a pull request as a
  dedicated factory account on GitHub, Azure DevOps or GitLab, with setup, the
  factory.toml and risk.toml files, the item flow, inbox, holdouts, metrics and
  safety model.
---

# Software factory

The **software factory** turns a piece of work into a pull request without you
sitting in the loop. You send it an open issue, a red CI run on the default
branch, or a task you type. An agent fixes it in its own clone of the
repository, the repository's own check command verifies the change, a second
agent reviews it, and the factory pushes a branch and opens a pull request (a
merge request on GitLab) as a dedicated factory account. Every change gets a
risk tier, T0 to T3, that decides how much of your attention it needs. If CI
fails it loops on the failures, up to a capped number of attempts, then stops
and waits for you. You review and merge. The factory merges nothing on its own
unless you turn on `auto_merge_t0` for trivial changes.

It works on GitHub, Azure DevOps and GitLab. Pull request, merge request and
repository names below follow the forge you use.

The factory is free on the Personal plan, with 2 agents running at once.

::: tip Early access
The factory ships behind the `factory.core` release flag, which is off by
default. Turn it on per node in **Settings > Labs**. See
[Feature Flags](/feature-flags).
:::

## Set up

Setup has three required parts. Do them once per repository the factory may change. To
give agents a full machine of their own (root, Docker, services), also set up a
[factory machine](#factory-machine).

### 1. Create the factory OS user

On the node that hosts the factory account, create an operating system user
named `clusto-factory`. It is a regular account with a shell, no password, and
no git or GitHub credentials. If the repository needs `pnpm` or `node` to build
and test, make them available to that user.

The agent runs as this user, so it cannot read your own files or credentials.

### 2. Add a factory account

The steps below are for GitHub. For [Azure DevOps](#azure-devops-and-gitlab-accounts)
or [GitLab](#azure-devops-and-gitlab-accounts) see the end of this section.

1. Open **Settings > GitHub** and add a separate GitHub account for the
   factory. Do not reuse your own.
2. Set its **Purpose** to **Factory**.
3. Enter the **Organization** the factory works in. The field appears when the
   purpose is Factory and is required for a factory account. One factory
   account serves one organization.
4. Give it a fine-grained token scoped only to the repositories the factory may
   change, with these permissions:

| Permission | Access |
|---|---|
| Contents | Read and write |
| Pull requests | Read and write |
| Issues | Read and write |
| Actions | Read and write |
| Checks | Read |
| Commit statuses | Read |
| Metadata | Read |

Workflows stays off, so the factory can never change a repository's CI. Make
the token's **resource owner** the same organization, so GitHub enforces the
same boundary as Clusto does. **Create token on GitHub**, under the token
field, opens GitHub's new-token page with all of this filled in, the
organization as resource owner included; pick the repositories, generate it
and paste it back.

Your personal accounts stay on the **Browse** purpose and are never used by the
factory.

#### Organization binding

The factory only starts work in repositories owned by the factory account's
organization. The owner is matched case-insensitively, and the node's daemon
enforces it, so a repository from another owner is refused even if it is sent
by other means. The **Send to factory** repository picker lists only that
organization's repositories.

#### Azure DevOps and GitLab accounts

A factory account on another forge works the same way, bound to its own
boundary instead of a GitHub organization:

| Forge | Repository name | Bound to |
|---|---|---|
| GitHub | `owner/repository` | the account's organization |
| Azure DevOps | `organization/project/repository` | the account's **Organizations** list |
| GitLab | `group/.../project` | the account's **Group** |

* **Azure DevOps.** Add the account in **Settings > Azure DevOps** with a
  personal access token, set its **Purpose** field to **Factory**, and list the
  organizations it works in. The list must not be empty for a factory account:
  repositories outside it are refused. The factory finds its pull request by
  source branch, shows the tier as a label, reads PR builds and their failed
  task logs, retries them, and completes the pull request with the account's
  default merge type. Work items are the issues you can send. See
  [Azure DevOps](/azure-devops).
* **GitLab.** Add the account in **Settings > GitLab**, set its purpose to
  **Factory** and its **Group** to the full path it works in (`acme` or
  `acme/platform`; subgroups are included). A repository must sit inside that
  group. The factory opens merge requests, labels them with the tier, reads
  pipelines and failed job traces, retries them, and merges with the expected
  head commit. GitLab accounts sit behind the `gitlab.accounts` flag in
  **Settings > Labs** (see [Feature Flags](/feature-flags)). See
  [GitLab](/gitlab).

The token stays on the account's host node on every forge, and the factory
machine and the agent never see it.

### 3. Add `factory.toml` to the repository

Add `.clusto/factory/factory.toml` to the repository's **default branch**. Only
`check` is required; this is the smallest useful file:

```toml
check = "pnpm check"
```

A complete example with every key and a recipe:

```toml
setup = "pnpm install --frozen-lockfile"
check = "pnpm check"
max_check_attempts = 2
max_ci_fix_attempts = 3
max_usd = 5.0
model = "sonnet"
review = true
review_model = "opus"
max_open_prs = 3
max_diff_lines = 600
intake_label = "factory"
plan = false
auto_merge_t0 = false
deploy = "pnpm deploy:prod"
holdout_pass_rate = 1.0
instructions = "Follow the conventions in AGENTS.md. Never edit generated files."

[recipe.feature]
description = "A new feature: plan first, review with a stronger model"
plan = true
review_model = "opus"
max_usd = 12.0
max_diff_lines = 900
instructions = "Add or update documentation for every user-facing change."
labels = ["feature", "enhancement"]

[recipe.cleanup]
description = "Small refactors and chores"
plan = false
review = false
max_usd = 2.0
labels = ["chore"]
```

| Key | Default | Meaning |
|---|---|---|
| `check` | (required) | The command that verifies a change, for example `pnpm check`. Exit 0 passes. |
| `setup` | `pnpm install --frozen-lockfile` | Runs once in the clone before the agent starts. |
| `max_check_attempts` | `2` | How many times the agent may retry after the check command fails. |
| `max_ci_fix_attempts` | `3` | How many times the agent may push a fix after CI fails on the pull request. |
| `max_usd` | `5.0` | Agent spend per item before it stops and waits for you. Must be above 0. |
| `model` | the agent CLI's default | The author's model. |
| `review` | `true` | A clean-context reviewer reads every change before the pull request opens. |
| `review_model` | the agent CLI's default | The reviewer's model. A different model than the author's catches more. |
| `max_open_prs` | `3` | Open factory pull requests this repository may have at once; further items wait in the queue. At least 1. |
| `max_diff_lines` | `600` | Changed lines above which the change is sent back once to shrink, then to you. At least 1. |
| `intake_label` | unset | Open issues carrying this label start items on their own (GitHub only). Unset, you start every item. |
| `plan` | `false` | The agent writes a plan first. A T2 or T3 item waits for you to approve it. |
| `auto_merge_t0` | `false` | T0 items merge themselves once CI is green. See [Auto-merge](#auto-merge-and-deploy). |
| `deploy` | unset | A command that runs when you press Deploy on a merged item. See [Auto-merge](#auto-merge-and-deploy). |
| `holdout_pass_rate` | `1.0` | Share of [holdout scenarios](#holdout-scenarios) that must pass, from 0 to 1. |
| `instructions` | unset | Extra instructions added to every brief of this repository. |

A key outside this list is an error, so a typo fails loudly instead of being
ignored.

#### Recipes

A `[recipe.<name>]` table is a profile for one kind of work. The name is 1 to 40
lowercase letters, digits and `-`. Every key is optional and overrides the base
value for items that use the recipe:

| Key | Meaning |
|---|---|
| `description` | Shown in the Send work dialog. |
| `plan`, `review` | Turn the plan step or the reviewer on or off. |
| `model`, `review_model` | Models for this kind of work. |
| `max_usd`, `max_check_attempts`, `max_ci_fix_attempts`, `max_diff_lines` | Budgets and the size limit. |
| `instructions` | Added after the base `instructions` in every brief. |
| `labels` | Issue labels that select this recipe when intake starts an item. The first matching recipe wins. |

Name a recipe in the optional **Recipe** field of the Send work dialog, or let
intake pick one from the issue's labels. With no recipe the base values apply.

The file is read only from the default branch, so an agent's own branch can
never change its rules.

### 4. Optional: add `risk.toml`

`.clusto/factory/risk.toml` tells the factory which parts of your repository
are riskier than they look. It is also read only from the default branch. See
[Risk tiers](#risk-tiers).

## Factory machine

By default the agent runs as the `clusto-factory` user on the node itself. For
work that needs more, such as installing packages, running Docker or keeping
services running between items, give the factory its own machine: an
[Incus](https://linuxcontainers.org/incus/) system container on the node.

Map the OS user to the container in the node's config:

```toml
[sandboxes]
clusto-factory = "clusto-factory"
```

The key is the OS user and the value is the Incus container name. Leave the
section out and nothing changes. Edits apply on config hot reload to processes
started after the change; running processes are not moved.

When a user has a sandbox, every process Clusto runs as that user runs inside
the container instead of on the host, as the same user (same uid and gid, with
the home directory mounted at the same path): the factory's setup and check
commands, the agent chat, git, and terminal sessions. Inside the container the
agents can use `sudo`, run Docker, install packages and keep services running.
Root in the container is not root on the host.

* **Shell on the factory machine.** Open a terminal session on that node with
  **run as** `clusto-factory` and you get a shell inside the container. Use it
  to look around, install tools or sign the agent CLI in. Signing in happens
  inside the container as that user, and the login stays in that home.
* **The token stays out of reach.** The node's daemon and the factory's GitHub
  token stay on the host. The container cannot read them, so an agent cannot
  push to your default branch or merge its own pull requests.
* **Internet only.** The container's network reaches the internet. Private
  address ranges, the host's daemon port and the tailnet are blocked.

Requirements:

* a Linux node, with the daemon running as root,
* Incus installed on the node, with a container prepared for the user,
* the user must exist on the host and in the container with matching user and
  group ids.

To move the factory to another machine, move the factory account's host node
(see [GitHub](/github)), then prepare the container on that node and add the
`[sandboxes]` entry there. The GitHub token is entered again on the new host.
See the [configuration reference](/configuration#session-users-and-trusted-directories)
for the key.

## Send work

Open the **Factory** view in the navigation and press **Send work**. Pick one of:

* an open issue (GitHub issues, Azure DevOps work items or GitLab issues),
* a red CI run on the default branch,
* a task you type.

Choose a [recipe](#recipes) in the optional **Recipe** field to use one of the
repository's `[recipe.<name>]` profiles.

You can also send an issue from the [GitHub view](/github): on an issue page,
**Send to factory** appears when a factory account can reach the repository.

## The board

The Factory view has four tabs: **Board**, **Inbox**, **Holdouts** and
**Metrics**. The Board has these lanes:

| Lane | Meaning |
|---|---|
| Waiting on you | A plan to approve, a pull request ready for review, or the factory stopped and needs a decision. |
| Agent working | An agent is planning, writing, reviewing or fixing code. |
| CI and holdouts | A pull request is open and checks or holdout scenarios are running. |
| Queued | Waiting for a free agent slot. At most 2 agents run at once. |
| Merged | You merged the pull request. |
| Stopped | Abandoned, or out of attempts or budget. |

Open an item to see its state, why it is waiting, the pull request link, the
agent's chat, the check and CI fix attempts, review rounds and spend.

Item actions:

* **Take over chat** - stop the factory driving the item so you can type in its
  chat yourself. The item moves to **Waiting on you**.
* **Retry** - start again with fresh attempts and a fresh budget. On an item
  you took over, this hands it back to the factory.
* **Abandon** - stop the item. An open pull request stays open.
* **Address review** - send the new review comments on the pull request to the
  agent so it pushes a follow-up.

## Risk tiers

Every change gets one of four tiers. The tier decides how much of your
attention it needs, and it is shown as a badge on the item, as a label on the
pull request (`factory:t0` to `factory:t3`) and as a **Risk tier** line at the
top of the pull request description, with the reasons and the reviewer's
explain-back card below it.

| Tier | Name | What it means |
|---|---|---|
| T0 | Auto-merge | No behavior change, such as documentation or lock files only. May merge itself once the repository allows it. |
| T1 | Owner skim | The default: a small tested change. AI review plus your skim. |
| T2 | Line review | Read the diff line by line. |
| T3 | Designed first | You design first; the factory builds only what you approved. |

The factory takes the **highest** tier any signal reaches:

* **Built-in T3 paths:** CI and deploy definitions (`.github/workflows/**`,
  `.github/actions/**`, `.gitlab-ci.yml`, `azure-pipelines.yml`,
  `.azure-pipelines/**`), `Dockerfile`, any `migrations` folder and the
  factory's own `.clusto/**` files.
* **Your risk map:** `.clusto/factory/risk.toml`, below.
* **Diff signals:** a changed dependency manifest, more than 400 changed lines
  or 15 files (T2); SQL schema changes and cryptography (T3); new environment or
  secret references, new routes or endpoints and new network calls (T2); code
  changed without a test change (one tier up); and more assertions removed
  than added in tests (T2).
* **The agent and the reviewer** each rate the change too. They can only raise a
  tier, never lower it.

A change that touches only documentation or lock files starts at T0; anything
else starts at T1.

### `risk.toml`

Add `.clusto/factory/risk.toml` to the default branch to mark paths that matter
in your repository. Each `[[rule]]` names glob patterns, the tier a match forces
and an optional reason shown on the pull request:

```toml
[[rule]]
paths = ["src/billing/**", "src/auth/**"]
tier = "t3"
why = "money and identity"

[[rule]]
paths = ["src/api/**"]
tier = "t2"
```

`tier` is `t0` to `t3`. Each rule needs at least one path. `why` defaults to
the first matching path. The map can only raise a tier.

## Item flow

An item moves through these states. Each shows on its card in the Factory view.

| State | What happens |
|---|---|
| Queued | Waiting for a free agent slot, or for an open pull request slot (`max_open_prs`). |
| Setting up | The clone is made and `setup` runs. |
| Planning | With `plan = true`, the agent writes a plan before any code. |
| Plan to approve | The agent rates its own plan. A plan it rates T2 or T3 waits for you; any other goes ahead on its own. |
| Coding, Checking, Fixing check | The agent works, your `check` runs, and failures go back to the agent up to `max_check_attempts`. |
| AI review | A clean-context reviewer reads the task and the diff. A P1 finding goes back to the author within the check budget. The reviewer must not touch the clone; if it does, the item stops. |
| Opening PR | The branch is pushed and the pull request opens with the tier label. |
| CI running, Fixing CI | CI runs. Failed jobs are re-run once to rule out a flake before a fix attempt counts. Real failures go back to the agent up to `max_ci_fix_attempts`. |
| Holdouts running | With holdout scenarios set, they run after green CI. |
| Ready to review | Green. You review and merge. |
| Addressing review | The agent answers your review comments. |
| Needs you | Stopped: attempts, budget or size limit used up, or a stalled turn. |

Other guards:

* **Size gate.** A change above `max_diff_lines` goes back to the agent once to
  shrink. If it is still too large it goes to you.
* **Stalled turns.** An agent turn that has not finished in 60 minutes counts as
  stuck and the item goes to you.
* **Open pull request cap.** Items above `max_open_prs` for a repository wait in
  the queue, so the factory never produces more than you can review.
* **Label intake.** With `intake_label` set, open issues carrying that label
  start items on their own. An issue the factory has ever worked on is not
  started again. GitHub only; on Azure DevOps and GitLab you send work yourself.

## The Inbox

The **Inbox** tab collects everything that waits on you, most urgent first, and
is built to work from a phone. The tab shows a count.

* **Plans to approve.** Open the item, read the agent's plan and press
  **Approve plan**. The optional **Notes for the agent** field changes the plan
  before it codes. The agent writes no code until you approve.
* **Ready to merge.** T3 and T2 first. Each entry shows the reviewer's
  explain-back card (a walkthrough and questions meant to test your
  understanding) and the holdout result. **Merge** asks you to confirm, naming
  the pull request and its tier. The factory account merges it; a merge cannot
  be undone from Clusto.
* **Stopped for you.** The reason is shown. **Retry** starts again with fresh
  attempts and a fresh budget; **Abandon** stops the item and leaves any open
  pull request open.
* **Sampling audits.** One in 20 merged T0 and T1 items is picked for a
  second look, because those were merged without a line review. Read the change
  and press **Audit clean** or **Audit finding**. The results feed the
  **Audit finding rate** on the Metrics tab.

## Holdout scenarios

A holdout scenario is an acceptance check you write for a repository that the
agent never sees. After CI is green, the factory runs every scenario against the
change. A miss goes back to the agent by scenario **name only**, so it can
fix the behavior without being able to read or game the test.

Open the **Holdouts** tab, pick a repository and press **Add scenario**. A
scenario is a name (up to 120 characters) and a shell script (up to 64 KiB)
that exits 0 when the change behaves correctly. It runs as root in a fresh clone
of the change after the repository's `setup`, with a 10 minute limit per
scenario.

* Scripts are stored on the host node only, never replicated to other nodes and
  never quoted into an agent's brief, a log or a notification.
* Each run happens in a **throwaway copy of the factory machine's `clean`
  snapshot**, with the agent's home folder removed. The agent's own container is
  not touched and shares nothing with it. This means holdouts need a
  [factory machine](#factory-machine): a node with a `[sandboxes]` entry for
  `clusto-factory` and a `clean` snapshot of that container.
* `holdout_pass_rate` sets how many scenarios must pass (default all of them).
  With no scenarios, the step is skipped.

## Auto-merge and deploy

* **`auto_merge_t0`** is off by default. When on, a T0 item merges itself once
  CI is green and any holdouts pass. Turn it on only after the sampling audit has
  shown a low finding rate on your repository.
* **Deploy.** A merged item shows **Deploy** when the repository's `deploy`
  command is set. Pressing it asks for confirmation, then runs the command on
  the host as the agent user in a fresh clone of the default branch. Deploying is
  outward-facing, so it is always started by you and never by the factory.

## Factory chats

Each item's agent runs in a chat. The item pane in the Factory view shows it
in place: pick the **Chat** tab under the item's actions to read and reply
without leaving the board or the Inbox. **Changes** shows the pull request's
description, checks and diff in the same spot, and **Details** the item's facts
and verification. The tab you pick stays picked as you move between items.

Factory chats stay out of the **Chats** list so they do not crowd your own
conversations. Use **Show factory chats** at the bottom of the Chats tree (or
the command palette) to list them on this device. Shown factory chats have a
factory icon.

While the factory drives an item, its chat is read-only: a banner above the
composer says what the factory is doing, and the node refuses a message typed
into the chat from any client. **Take over** in the banner hands you the chat;
**Hand back** returns it to the factory. Stopping a running response stays
available at any time.

## Metrics

The top of the view shows:

* **Merged** - how many factory pull requests you merged.
* **Cost per merged PR** - agent spend divided by merged pull requests.
* **Rework** - the share of merged changes whose files changed again on the
  default branch within 21 days.
* **Agents running** - agents working now, out of the 2 allowed.

The **Metrics** tab goes further: open pull requests, items waiting on you,
CI fixes per merge, abandoned items, merged items by tier, the sampling audit
finding rate, and spend per repository and per week. Watch the audit finding
rate before you enable `auto_merge_t0`.

Spend is the figure the agent CLI reports. On a subscription plan it is
notional, not money charged to you.

## Safety

* The agent runs as the unprivileged `clusto-factory` user with no MCP servers,
  optionally inside a [factory machine](#factory-machine) container.
* The factory works only in repositories inside the factory account's
  organization, Azure DevOps organizations or GitLab group.
* The agent never sees the forge token. Only the daemon pushes branches and
  opens pull requests.
* The risk map, tiers and the reviewer can only raise a change's tier, never
  lower it, and a reviewer that touches the clone stops the item.
* Holdout scenarios stay on the host and never reach the agent.
* Deploy is started only by you; auto-merge is off until you turn it on.
* Forge text such as issue bodies, logs and review comments is passed to the
  agent as quoted data, not as instructions.
* The check command and limits come from the default branch only.
* Every change reaches your default branch only through a pull request that you
  merge.

## Notifications

The factory raises notifications when an item needs you, when a plan waits for
your approval and when a pull request is ready. Like the GitHub alerts, you can mute them per device.
