# Factory presets — seat/model arrangement and account, selectable by name

audience: AI coding agents first. This is the request spec for a factory run. Contract-level:
seams and behavior are pinned; bodies are yours to write by reading the repo.

**Goal:** `factory --preset <name> <adw> "request"` picks a named seat/model arrangement,
and optionally the pi account the run authenticates as. Both are recorded and visible.

**Repo root:** `/home/user/Projects/overdeck/.worktrees/factory-live`. Every path below is
relative to it. All factory code lives under `modules/harness/factory/`.

## Why this exists

The roster in `modules/harness/factory/sssf.config.yaml` is a single fixed arrangement.
Changing which model sits in which seat means editing the shipped config or hand-writing a
whole overlay file per run. The owner needs named arrangements, chosen per run, plus the
ability to pin the account for a run. This is a daily-driver control, not a nicety.

## Part 1 — named presets

### Where presets live

`modules/harness/factory/presets/<name>.yaml`. One file per preset. Each file is a
**config overlay** in exactly the shape `sssf.config.yaml` already uses — nothing new to
learn, and `_deep_overlay` already merges it.

Ship two real, usable presets (not placeholders):

- `presets/fast.yaml` — cheap/quick arrangement: every seat on `openai-codex/gpt-5.6-luna`,
  `thinking: low` for scout/reviewer, `medium` for planner/builder.
- `presets/deep.yaml` — quality arrangement: planner `openai-codex/gpt-5.6-sol` /
  `thinking: high`, builder `openai-codex/gpt-5.6-terra` / `thinking: high`, reviewer
  `openai-codex/gpt-5.6-sol` / `thinking: medium`, scout unchanged.

**Critical merge fact you must respect:** `_deep_overlay` merges mappings recursively but
**replaces lists wholesale**. `agents:` is a list. A preset that names `agents:` therefore
REPLACES the entire roster and would drop every agent it does not re-list, along with their
`prompt_engineering` paths and `tools`. Do NOT make preset authors re-list the roster.

Introduce a preset-only key that merges per agent by name:

```
seats:                       # top-level key in a preset file; merged by agent name
  planner:  {model: openai-codex/gpt-5.6-sol, thinking: high}
  builder:  {model: openai-codex/gpt-5.6-terra, thinking: high}
account: multideal           # optional; see Part 2
```

`seats` keys are agent names as they appear in `agents[].name`. Values may set any of
`model`, `thinking`. A `seats` key naming an agent that does not exist in the roster is a
hard error listing the known agent names — never a silent no-op.

### Resolution seam

`modules/harness/factory/adw_modules/agents.py`:

```
load_config(path: str = ..., preset: str | None = None) -> SSSFConfig
```

Order, unchanged in spirit from today: central `sssf.config.yaml` → requested config
overlay (when different) → **preset overlay last**, so a preset wins over a project config.
Apply `seats` by looking up each named agent in the merged `agents` list and overlaying its
mapping; apply any other top-level keys in the preset through the existing `_deep_overlay`.

The existing stderr provenance line MUST grow the preset:
`factory config layers: <central> -> <requested> -> preset:<name> (<preset path>)`.

**Fail closed on an unknown preset**, modelled exactly on `resolve()`'s existing behavior:
`SystemExit(f"preset {name!r} is not defined — available: [...]")`, listing the stems of
`presets/*.yaml`. It must fail before any run state, session row, or agent process exists.

### CLI seam

`/home/user/.local/bin/factory` is a copy of `modules/harness/factory/bin/factory` — edit
the repo copy. Add `--preset NAME`, alongside the existing `--repo` / `--config`, for the
ADW form and for every control subcommand that already accepts `--config`. Extend the
`usage()` heredoc: the wrapper's usage text is the only discovery surface a user has.
Add `factory presets` — lists available preset names, one per line, with the seat
arrangement each pins. Pass the preset through to the Python entrypoints.

Every `modules/harness/factory/adw_*.py` currently exposes only `--config` (with a stale
default of `"adws/adw_sssf_config/sssf.config.yaml"`). Add `--preset` to each and thread it
into its `load_config` call. Do not otherwise touch the stale default in this change.

## Part 2 — account selection

### The mechanism (measured, do not redesign it)

pi resolves `auth.json`, `models.json`, sessions and settings from **one** directory:
`process.env.PI_CODING_AGENT_DIR`, tilde-expanded, else `~/.pi/agent`. Verified by A/B
test: an agent dir containing an account's `auth.json` authenticates as that account; a dir
without one fails with `No API key found for openai-codex.`

`PI_AUTH_PATH` and `PI_AUTH_HOME`, referenced in `adw_modules/agent_pi.py`, **do not exist
in pi**. They are factory-local inventions used only to render a label. Setting them changes
nothing about which account pi uses.

### What to build

A preset (or `--account <slug>`) naming an account materializes a **composed pi agent dir**
and exports it as `PI_CODING_AGENT_DIR`:

- Location: `/home/user/.local/state/overdeck/factory/pi-accounts/<slug>/`. Idempotent —
  rebuild/refresh on each run, never accumulate stale entries.
- Contents: a symlink for **every entry of `~/.pi/agent` except `auth.json`** — enumerate
  the directory at materialization time, never a hardcoded name list, or a future pi file is
  silently missing. Plus `auth.json` → `/home/user/.local/state/overdeck/systray/runtime/accounts/<slug>/PI_HOME/auth.json`.
- Rationale you must preserve: each account's `PI_HOME` holds **only** `auth.json`. Pointing
  `PI_CODING_AGENT_DIR` straight at it would lose `models.json`, and with it the whole
  `gpt-5.6-*` model family. The composition is what keeps models resolvable.
- **NEVER mutate `~/.pi/agent/auth.json`.** That symlink is owned by the systray, which
  switches the machine-wide account. A factory run must not have a global side effect.
- Unknown account slug (no such `accounts/<slug>/PI_HOME/auth.json`) → fail closed before
  the run starts, listing the slugs that do exist under
  `/home/user/.local/state/overdeck/systray/runtime/accounts/`.

Injection point: `operator_env()` in `adw_modules/utils.py:16` — every child process
(`subprocess.run` and `subprocess.Popen` in `agent_pi.py`) already receives `env=operator_env()`.
Thread the resolved account through to it; do not set the variable in the parent process's
own environment.

### Fix the account the factory reports

`authenticated_account()` in `agent_pi.py` currently reads its answer from the fictional
`PI_AUTH_PATH`/`PI_AUTH_HOME` while pi authenticates from `PI_CODING_AGENT_DIR`. Once
`PI_CODING_AGENT_DIR` is set per run, the reported account and the real one can diverge, and
the /factory Account column would confidently show the wrong account. That is worse than no
column.

Derive the reported account from **the same resolution pi performs**: `PI_CODING_AGENT_DIR`
if set, else `~/.pi/agent`; realpath its `auth.json`; take the `accounts/<slug>` segment.
One function, used by every consumer. Remove the `PI_AUTH_PATH`/`PI_AUTH_HOME` reads —
nothing may keep depending on a variable pi does not read.

## Part 3 — visibility

The owner's bar: "if I can't see it, it's not done."

- `adw_modules/tracer.py`: add `preset` (TEXT) to the `sessions` table. Additive columns go
  in `MIGRATIONS` — **NEVER edit a shipped `CREATE TABLE`**; the `IF NOT EXISTS` block never
  revisits an existing table. Record the preset name (or NULL) when the session row is
  created. `agent_attempts.account` already exists and must now carry the truthful value
  from the corrected resolution above.
- `collector/src/adapters/factory.ts`: carry `preset` through into the `factory-runs` panel
  payload the collector serves at `/api/collector/state`.
- `apps/web/src/components/**` (the `FactoryRunCard` path): show the preset on the run,
  next to the existing Model / Account / Where signals. UI rules are MANDATORY — read
  `.claude/skills/od-ui-dev/SKILL.md` first. Compose existing exports; tokens only, both
  themes; **never create a new primitive**. If this needs a primitive that does not exist,
  stop and say so instead of inventing one.

## Acceptance — every one of these must actually be run and its real output reported

1. `python3 -m pytest modules/harness/factory/tests/ -q` — green. Repo CLAUDE.md makes this
   mandatory for any change to factory phase/retry/gate/permission/commit behavior.
2. New tests under `modules/harness/factory/tests/` covering: preset overlay applied to the
   right seats; `seats` naming an unknown agent errors; unknown preset name exits with the
   available list; the roster is NOT truncated by a preset (every agent from the base config
   still present, with its `prompt_engineering` paths intact); account overlay dir contains a
   symlink for every non-`auth.json` entry of the source agent dir; unknown account slug
   fails closed; `~/.pi/agent/auth.json` is unchanged after materialization.
3. `bin/factory presets` lists `fast` and `deep`.
4. `bin/factory --preset nope prompt "x"` exits non-zero, prints the available presets, and
   creates no session row.
5. `bin/factory --preset fast prompt "reply with the single word ok"` completes; the stderr
   provenance line names the preset; the run's `sessions.preset` column reads `fast`.
6. `pnpm --filter web typecheck` and `pnpm --filter web build` — green, if you touched
   `apps/web`. `bun test` in `collector/` — green, if you touched the collector.

## Hard prohibitions

- NEVER weaken or bypass a gate to make this run pass.
- NEVER mutate `~/.pi/agent/auth.json` or anything under
  `/home/user/.local/state/overdeck/systray/runtime/accounts/`.
- NEVER hardcode the list of files inside `~/.pi/agent`.
- NEVER touch `modules/harness/presets/` — that is a dead v2 registry, unrelated to this.
- Work only inside the repo root named above.
