# SSSF v3 Adoption — one factory, owned by overdeck, used by every project

Audience: AI coding agents first.

## Goal

A factory that finishes runs, with full observability, a way to kill a stuck run,
and systray + deck visibility across ALL projects.

Take SSSF's **code** as-is. Reject SSSF's **installation topology**.

Source: `modules/harness/reference/sssf/` (95 files, complete).
Read `cookbooks/install.md` and `references/observability.md` before editing anything.

## Decisions already made — do not re-litigate

- **The core lives ONCE, in overdeck**, at `modules/harness/factory/`. `adw_modules/`,
  the role prompts, the tracer, and the schema are copied there verbatim and owned by
  overdeck. No project ever gets its own copy.
- **Reject `scripts/install.py`.** It stamps `adws/`, `justfile`, and all of
  `adw_modules/` into every target repo root. That is the spreading we are refusing.
  Read it to learn what it wires, then do not run it.
- **A project holds only what is specific to it** — its config, its own ADWs, its own
  prompt overrides. Nothing else. Target shape in a project: a single `.factory/` dir.
- **One global trace database** at `~/.local/state/overdeck/factory/sssf.db`, not one
  per repo. Global visibility is the whole point; per-repo dbs cannot serve systray.
- **Harness v2 is not modified, not deleted, not migrated.** No task here touches
  `modules/harness/v2`. Forward-only; nothing targets historical runs.
- **The UI is the existing overdeck deck.** SSSF's `apps/visualizer` is REFERENCE ONLY
  — read it for its queries and polling contract, never ship it.

## Wave 1 — establish the core (read-only investigation first)

### Task t1: Report the model-provider seam (READ-ONLY, no edits)
Read `references/config.md`, `templates/adws/adw_modules/agent_pi.py`,
`templates/adws/adw_modules/agents.py`, and `templates/sssf.config.yaml`.

Answer exactly these, quoting file+line for each. Never infer:
1. How does pi resolve a model id? Is `~/.pi/agent/models.json` the only registry?
2. Does pi support any provider besides OpenRouter? Name each and the key that selects it.
3. Minimum `models.json` entry (required fields) for one model?
4. Can a model point at an OpenAI-compatible base URL + key? Which keys?
5. Is the coding agent swappable — could `cdx` back an agent instead of pi
   (see `agent_cc.py` as the second-adapter precedent)?

**Files:** `modules/harness/factory/docs/model-seam-report.md`

**Acceptance:** every answer cites a real file and line; no answer is inferred.

**Verify:** `test -s modules/harness/factory/docs/model-seam-report.md`

### Task t2: Report the repo-coupling seam (READ-ONLY, no edits)
Establish exactly what makes SSSF assume "cwd is the repo". Read `scripts/install.py`,
`adw_modules/runner.py`, `adw_modules/session.py`, `adw_modules/git_helper.py`,
`adw_modules/tracer.py`, `adw_modules/utils.py`.

Answer, quoting file+line:
1. Every path SSSF resolves relative to cwd, and what each is for.
2. How `observability.db` is resolved from config — can it be an absolute path?
3. How config is located and loaded; can its path be injected?
4. Where prompts are read from; can a project override one file without copying all?
5. What in `git_helper` assumes cwd is the repo root?

**Files:** `modules/harness/factory/docs/coupling-report.md`

**Acceptance:** a complete list of cwd assumptions, each with file+line; explicitly
states for each whether it is already configurable or needs a change.

**Verify:** `test -s modules/harness/factory/docs/coupling-report.md`

## Wave 2 — vendor the core into overdeck

### Task t3: Copy the engine verbatim
Copy `templates/adws/adw_modules/`, `templates/adws/adw_*.py`, and
`templates/prompt_engineering/` into `modules/harness/factory/`. Byte-for-byte —
no edits, no reformatting, no renames. Record provenance (source path + upstream
commit) in `modules/harness/factory/PROVENANCE.md`.

**Files:** `modules/harness/factory/`

**Acceptance:** every copied file is byte-identical to its source; `PROVENANCE.md`
names the upstream commit.

**Verify:** `diff -r modules/harness/reference/sssf/templates/adws/adw_modules modules/harness/factory/adw_modules`

### Task t4: Repo-root and config injection
Per the t2 report, make the factory take the target repo and the config path as
inputs instead of reading cwd. Smallest possible diff against the vendored code;
every change traceable to a numbered finding in the t2 report.

**Files:** `modules/harness/factory/adw_modules/`, `modules/harness/factory/docs/coupling-report.md`

**Acceptance:** the factory runs against a target repo that is NOT the cwd; the
trace db resolves to an absolute global path; no cwd assumption from t2 remains
unaddressed.

**Verify:** `cd modules/harness/factory && uv run python -m pytest tests -q`

### Task t5: Global trace db with a repo column
`sessions` gains the target repo it ran against, so one db serves every project.
Follow `references/observability.md` exactly for the 7 tables and the WAL pragmas
(`journal_mode=WAL`, `synchronous=NORMAL`, `busy_timeout=5000`) on EVERY connection.

**Files:** `modules/harness/factory/adw_modules/tracer.py`, migration + tests

**Acceptance:** two runs against two different repos land in one db, each row
carrying its own repo; existing rows without the column still read.

**Verify:** `cd modules/harness/factory && uv run python -m pytest tests/test_tracer.py -q`

### Task t6: Global entrypoint
One CLI on PATH that runs the factory against whatever repo the engineer is in,
following overdeck's existing dev-tool pattern (tool under `modules/`, symlink from
home — never a copy). It resolves the target repo, loads that project's `.factory/`
config if present, and falls back to overdeck defaults.

**Files:** `modules/harness/factory/bin/`, module manifest

**Acceptance:** invoked from an arbitrary project dir, it resolves that repo as the
target and writes to the global db; invoked with no project config, it uses defaults.

**Verify:** the entrypoint runs from a temp git repo and exits 0

## Wave 3 — make one agent actually run

### Task t7: Configure models
Per the t1 report, configure the models the report proved reachable, and the factory
config to name them.

**Acceptance:** the model the config names as default resolves; a one-shot prompt ADW
completes and lands a `sessions` row with `status='success'`.

**Verify:** `sqlite3 ~/.local/state/overdeck/factory/sssf.db "select count(*) from sessions where status='success';"`

### Task t8: Prove a stuck run can be killed
The `processes` table is the only thing that can answer "what is this run running and
how do I stop it" — a hung agent emits nothing. Children stop before the parent, and
the recorded `command` is verified against the pid before signalling.

**Acceptance:** a killed run lands `sessions.status='fail'` with its process rows
closed — never left reading `running`.

**Verify:** `sqlite3 ~/.local/state/overdeck/factory/sssf.db "select count(*) from processes where ended_at is null;"`

## Wave 4 — surface it where the engineer already looks

### Task t9: Collector adapter over the global db
Read-only adapter over `sssf.db` projecting sessions/phases/events onto the deck's
normalized panel schema. Live view polls the rowid cursor per
`references/observability.md`. No push, no socket.

**Files:** `collector/src/adapters/factory.ts`, `collector/src/adapters/factory.test.ts`

**Acceptance:** returns a completed run with phases, events, and per-phase spend
against a fixture db; a still-running run projects partial state without error;
runs from different repos are distinguishable.

**Verify:** `cd collector && bun test src/adapters/factory.test.ts`

### Task t10: Deck panels
Wire the adapter into the deck's existing panels. Reuse existing components — never
inline markup (/plans componentization mandate).

**Acceptance:** the deck renders a live run's phases and per-phase spend; no panel
shows an empty state for a run that has events.

**Verify:** `pnpm --filter web test`

### Task t11: Systray integration
The tray shows factory state across ALL projects from the one global db: whether a
run is live, which repo it targets, and a way to reach it. Follow the existing
systray module's patterns.

**Files:** `modules/systray/`

**Acceptance:** with a run live in any project, the tray reflects it; with none, it
shows idle. Never shows a stale `running` for a dead process.

**Verify:** `python3 -m pytest modules/systray/tests -q`

## Wave 1 findings — resolved, do not re-investigate

**No OpenRouter key is needed.** `pi --list-models` resolves against `openai-codex`,
the same subscription path `cdx` uses. Reachable models, exactly four:
`gpt-5.3-codex-spark`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`.

**5.6 / Luna / Sol / Terra ARE reachable through pi — solved, do not rebuild.**
pi's built-in catalog is static and predates 5.6, but `models.json` is a supported
merge point (`core/model-registry.js:264` loads it; `mergeCustomModels` appends by
provider+id). Registering the three families against the built-in `openai-codex`
backend — the same one `cdx` uses (`~/.codex/config.toml:2` runs `gpt-5.6-terra`) —
makes them resolve with ZERO code edits, to pi or to vendored SSSF. Proven live:
`pi -p --provider openai-codex --model gpt-5.6-luna` returned `stopReason: stop`.

Registry is overdeck-owned at `modules/workstation/pi/agent/models.json`, symlinked
from `~/.pi/agent/models.json`. **A `cdx`-backed adapter is therefore unnecessary** —
do not build one. 5.6 pricing is unpublished locally, so the entries carry GPT-5.5
rates as a labeled proxy: token counts are measured, dollar figures are estimates.

**Repo-coupling is mostly already configurable** — do NOT rewrite what already works:
- `observability.db` is passed through to Tracer unrewritten, so an absolute global
  path already works (`tracer.py:102-109`). No code change.
- Config path already injects via `--config` (`adw_prompt.py:20-24`). No code change.
- System and user prompt paths are independent config fields read directly, so a
  project overrides ONE prompt file without copying the tree (`agents.py:84-92`).
- `git_helper` inherits launch cwd and falls back to `Path.cwd()`
  (`git_helper.py:9-13`) — launching the subprocess with `cwd=<target repo>` is the
  entire fix. No code change.

Consequence: t4 is not a rewrite. The entrypoint (t6) supplies cwd, `--config`, and
an absolute db path; vendored code should need close to zero edits. Any edit to
vendored code MUST cite a numbered finding in `coupling-report.md` that proves
configuration alone cannot do it.
