# ADR-0002 — Distribution & packaging: repo-free, two-mode, auto-configured

- **Status:** Proposed (needs SRE + security sign-off; larger infra commitment than ADR-0001)
- **Date:** 2026-07-22
- **Deciders:** Forge/Fabric architecture, with SRE and Security
- **Scope decision captured:** targets are **macOS and Windows** workstations
- **Related:** ADR-0001 (RAG local/hybrid modes) — this ADR distributes what that ADR configures
- **Files in scope:** `tools/build_*.py`, `deploy/**`, `dashboard/backend.py` (serving + mode flags),
  `dashboard/rag_contract.py` (mode pair), `docs/architecture/frontend-distribution.md`

Forge must run in two modes — **Local** (one workstation, no server) and **Hybrid** (shared control
plane + workstation workers) — and in both, a team must get it running **without the source tree**,
from a distributed package, with **auto-configuration**. Today ~70% of the Hybrid path exists; Local
packaging and a repo-free backend do not. This ADR pins how that gap is closed.

---

## Context — what exists today (verified)

- **Control-plane / worker split is real.** `FORGE_CONTROL_PLANE=1` binds `0.0.0.0` and delegates
  native work to workers (`backend.py:183,263`; `deploy/README.md`). `/healthz` exists
  (`backend.py:12717`).
- **Repo-free packages exist for the *frontend* and *workers*:** `tools/build_frontend_package.py`
  (browser-only zip + SHA manifest, deterministic timestamps), plus native/diagnostic worker
  packagers and GoTL readiness gating (`dist/gotl/readiness.json`).
- **Frontend is compressed on the wire** — `_compress_response` gzips the 1.13 MB `index.html` to
  ~150 KB with cache headers (`backend.py:12522`).
- **ADR-0001 mode pair is wired** — `rag_contract.deployment_config` reads
  `FORGE_DEPLOYMENT_MODE` + `FORGE_RAG_STORE` (`rag_contract.py:30`).

## Context — the gaps this ADR closes

1. **No repo-free *backend* artifact.** The control plane is built *from the repo* (Dockerfile
   `COPY dashboard/ …`, "build from repo root"). No versioned/published image; no self-contained
   bundle. Standing up a control plane still needs source.
2. **No Local package at all.** Local mode = `python dashboard/backend.py` from the repo — which *is*
   sharing the codebase. No backend+frontend+runtime all-in-one.
3. **Setup is a manual checklist.** Copy `*.env.example`, hand-edit secrets, edit the plist, build,
   run. No bootstrap; worker token provisioning is out-of-band.
4. **Two un-unified mode axes.** `FORGE_CONTROL_PLANE` (server on/off) is separate from
   `FORGE_DEPLOYMENT_MODE`/`FORGE_RAG_STORE` (local/hybrid). No single deployment profile.
5. **macOS-only.** launchd + `.sh` + `plutil`; no Windows worker. Studio teams include Windows.
6. **Frontend never built.** 1.13 MB unminified monolith, no minify/split/fingerprint; HTML served
   `no-cache` (revalidated every load). Packaging just zips the raw file.
7. **Two unreconciled worker paths** (`install-macos.sh` is repo-based vs. the repo-free worker zip),
   and a **stale doc**: Dockerfile + `deploy/README.md` say "no pip deps" while `requirements.txt`
   has numpy/psycopg/pydantic/mcp/openpyxl/pypdf.

---

## Decision

### D1 — One deployment profile, not three flags

Introduce a single selector that fixes every downstream flag coherently. The overlapping axes
(`FORGE_CONTROL_PLANE`, `FORGE_DEPLOYMENT_MODE`, `FORGE_RAG_STORE`) become *derived*, never
hand-set at a site.

| Profile | Runs | RAG (ADR-0001) | Server flag | Bind |
|---|---|---|---|---|
| `local` | backend + frontend on one workstation | `local` / `local` | off | loopback |
| `control-plane` | headless shared server | `hybrid` / `postgres` | on | `0.0.0.0` |
| `worker` | workstation native executor, claims jobs | n/a (uses server) | n/a | — |

`FORGE_PROFILE={local|control-plane|worker}` is authoritative; the legacy flags are set from it and
validated for consistency at startup (a contradictory hand-override fails closed, it is not silently
honored). This is also the seam that keeps ADR-0001's mode pair correct by construction.

### D2 — The backend ships as a repo-free, versioned, per-OS artifact

A control plane and a Local install are stood up from a **self-contained bundle**, never the source
tree. The bundle carries a **pinned Python runtime + pinned wheels** (`requirements.txt` is the
contract) plus `dashboard/`, `perforce-mcp/`, `config/`, `workspaces/`, and the wiki — the same set
the Dockerfile copies, minus Git metadata, tests, and prompts that must not leave the server.
Because `numpy`/`psycopg`/`pydantic` are compiled, this is **two build targets (macOS, Windows)**,
not one. The container image remains a valid control-plane delivery vehicle; the bundle is what makes
Local and non-Docker Hybrid hosts possible.

### D3 — Local all-in-one is a first-class artifact

`local` profile ships as one package (backend + frontend + runtime) that starts with **one command
or a double-click**, binds loopback, uses the ADR-0001 `local`/`local` RAG store, and requires **no
server, no Docker, no repo**. It preserves the existing local resilience guarantees (atomic index
promotion, last-good-index retention) unchanged.

### D4 — `forge up` — auto-configuration is the default experience

A bootstrap entrypoint (per OS) replaces the manual checklist:

- **Detect** host facts: OS, Python/runtime, Unity project path, Perforce workspace root, git.
- **Generate** config from the chosen `FORGE_PROFILE` — no `.env` hand-editing for the common path.
- **Enroll** a worker via an authenticated device-enrollment flow against the control plane (issue +
  store a scoped token 0600), replacing out-of-band token hand-off.
- **Install + start** the service: launchd (macOS) or a Windows service / Scheduled Task, behind one
  UX. **Fail closed** with a specific reason if it cannot configure (missing runtime, unreachable
  control plane, enrollment denied) — never start half-configured.

### D5 — Cross-platform is macOS + Windows, native-tool bridges abstracted

The worker's native-tool bridges (Photoshop, Unity Editor, Perforce, git) sit behind an
OS-abstraction; installers and service wrappers are per-OS. `install-macos.sh` and a new
`install-windows.ps1` become **thin wrappers over the repo-free worker bundle** — the repo-based
install path is retired (closes gap 7).

### D6 — The frontend gets a build step (not a rewrite)

Add a build pass feeding the existing `build_frontend_package.py`: **minify, split** the monolith
into a small shell + lazily-loaded views, and **fingerprint** assets for long-lived caching (the
HTML shell may stay short-cache; heavy split chunks become immutable + `max-age` long). The dev
workflow keeps the single-file `index.html`; the build produces the distributed artifact. **This is
a build pass, not a framework migration** — do not rewrite into React/Vue here.

### D7 — Security & integrity boundary (carried forward and tightened)

- Packages are **signed** and carry a SHA-256 manifest (frontend packager already emits the digest;
  extend to backend + worker). `forge up` verifies signature+digest before running anything.
- **No secrets in any package.** Obfuscation is **not** a security boundary (per
  `frontend-distribution.md`); proprietary/sensitive behavior stays server-side.
- Worker enrollment is **authenticated**; tokens are scoped and revocable (aligns with ADR-0001 C-series governance).

### D8 — Versioning & updates

Artifacts are version-stamped (the packager already uses the short SHA). Ship a **manifest-based
update check** (installed version vs. published) that notifies on next start and pulls on confirm.
Full silent auto-update is **out of scope for v1** — notify-and-pull first.

---

## Consequences

**Positive** — a team runs Forge from a signed package in either mode, on mac or Windows, via one
command; no source leaves the repo; the mode model collapses to one profile; the ADR-0001 RAG modes
are set correctly by construction.

**Costs accepted** — the backend bundle is **two OS build targets** with a pinned runtime + wheels
(compiled deps); a Windows worker service wrapper is net-new; `forge up` must handle two service
backends; a frontend build introduces a build step the repo does not have today.

**Explicitly rejected** — building the backend from the repo at each site (D2); shipping secrets or
treating obfuscation as security (D7); rewriting the frontend into a framework (D6); silent
auto-update in v1 (D8); honoring contradictory hand-set mode flags (D1); autonomously running the
GoTL hybrid pilot (that stays human-gated, per ADR-0001).

---

## Rollout (incremental; each lands behind tests + a smoke start)

1. **`FORGE_PROFILE` unification** — derive legacy flags, validate consistency, fail closed on
   contradiction. Pure config; no packaging yet.
2. **Repo-free backend bundle (macOS first)** — pinned runtime + wheels; smoke-start a control plane
   and a `local` instance from the bundle with no repo present.
3. **Local all-in-one (macOS)** — one-command start, loopback, ADR-0001 `local` store.
4. **`forge up` bootstrap (macOS)** — detect → generate → enroll → install → fail-closed.
5. **Windows parity** — backend bundle, worker service wrapper, `forge up` Windows backend.
6. **Frontend build** — minify/split/fingerprint feeding the existing packager.
7. **Signing + update check**; retire the repo-based worker path; fix the stale "no pip deps" docs.

Ship macOS end-to-end (1–4, 6) before Windows (5) so the abstraction is proven on one OS first.

---

## Core principle

**A team gets Forge as a signed, self-configuring package — Local or Hybrid, macOS or Windows —
without ever touching the source tree. The control plane keeps the code, credentials, and governed
knowledge; the package carries only what a machine needs to run.**
