# RHA Wrapper — Execution Instructions (for Cursor)

Combines the **licensing finish-up** with the **latest UI / plugin / admin work**.
Execute top-to-bottom. Each task lists exact files, current state, target state, and an
acceptance check. Do NOT touch the frozen-IR / bounded-repair invariants in `src/core/`
unless a task explicitly says so (Part F does, carefully).

**Same-binary note:** `web/src/App.tsx` is served BOTH as the canonical hosted instance
(rha.software) and as the downloadable self-host "applette." Tasks that must differ
between them branch on a new `meta.canonical` flag (Task C1).

**Contact email everywhere is `info@rha.software`.**

---

## PART 0 — Execution protocol (read first) + current baseline

**Every task here is idempotent. Verify state before acting; never blindly recreate.**
- Before each task, check the on-disk state (grep / `ls` / acceptance checks are provided). If the
  target is already satisfied, SKIP and note "already present" — do not recreate, overwrite, or
  duplicate.
- Read every "create X" as "ensure X exists and matches spec" — if X exists, verify/extend it.
- Do not delete correct work just because it isn't mentioned here.
- After a run, report per task: **already-done / newly-done / skipped**, and flag any
  spec-vs-code drift you hit (append it here as a note so the next cycle starts from truth).

**Baseline after Cursor cycle 2 — reconciled against disk (typecheck clean, 57/57 tests):**
- ✅ DONE (verify present, do NOT redo): **A** (licensing web copy — CC-BY gone), **B** (plugin URL /
  andyfunke removed), **C** (canonical flag, `brand-inert` logo, external footer, `app.zip` strips
  landing/privacy), **D** (one-green badge, cloud/local labels, `#a78bfa` gone), **E**
  (`evictOldPatches(root, 1)`), **F** (`config.ts` + admin config endpoints/UI + knobs incl.
  `sev1IrFraction`), **G** (first-launch scrypt password: `admin-auth.json`, `/state`, `/setup`),
  **J** (glob `tests/**/*.ts`, `VitestRun.collected`, `collection-integrity.test.ts`, zero-executed
  guard), **K** (`approveGateFeatures` + `/api/jobs/:id/gate/approve` + accept/auto-requalify UI),
  **K3** (`autopilotDefault: true`), **K5** (build-gate reframe copy), **L** (`src/core/alerts.ts`
  `raiseSev1` wired at zero-features / zero-executed / IR-fraction sites).
- ✅ DONE — cycle 3 (verified on disk, typecheck clean): **P** (spec-collapse guard `isSpecCollapse` +
  enumerated floor + zero-features SEV1), **M** (hardcoded `claude-opus-4-8` removed from `/api/meta`;
  connected-only badge), **N** (Advanced Mode + `src/core/orchestrator.ts` enqueue/steal queue),
  **O1–O3** (`interrupted` state + `resume()` + `POST /api/jobs/:id/resume`), **Q** (options page +
  idle timeout + multi-role auth), **R** (Gemini/GLM/Kimi presets + wiki).
- ✅ DONE — cycle 4 (verified on disk, typecheck clean): **O4** (`recompute()` + `core/recompute.ts`),
  **S** (`getConfig(role)` per-role config, plugin role dropdown, notif grouping), **T** (`ExtStatus`
  'outdated' version skew reintroduced), **U** (star/pin flags + iterate-in-place), **V1** (`outputDir`
  + `syncDeployable`), **V2** (`showDirectoryPicker` grant+persist), **V3/V4** (OPSEC + incremental).
- ❌ PENDING:
  - **V5** — hosted byte-push wire: V2 grants+persists a folder handle but does NOT yet stream artifact
    bytes from a remote page through the extension into it. Without V5, hosted→folder sync is
    non-functional (only V1 self-host works). Needed for the skeptical-user "audit on the site" case.
- ℹ️ DESIGN NOTE (accepted, not a gap): the Orchestrator is a **single-loop cooperative enqueue/steal
  scheduler**, NOT true multi-process workers — deliberately, to avoid shared-workspace/ledger/git
  races. It still runs operator LLM calls concurrently (the slow part); only disk/git mutations are
  serialized. True process-level parallelism, if ever wanted, needs per-operator workspaces + a merge
  step (future batch).

  Leave the ✅ DONE parts untouched unless a grep gate shows a regression.

---

## PART A — Licensing finish-up

> **Already applied in the working tree — verify present, DO NOT recreate:**
> `LICENSE` (PolyForm Shield 1.0.0), `COMMERCIAL-LICENSE.md`, `CONTRIBUTING.md`,
> `package.json` (`"license": "SEE LICENSE IN LICENSE"`), README License section, and
> the contact email `info@rha.software` in all three. The tasks below are the REMAINING
> public-copy changes that still advertise CC BY 4.0.

Canonical license link: `https://polyformproject.org/licenses/shield/1.0.0` · text `PolyForm Shield 1.0.0`.

### A1 — Landing ethos section
`web/public/landing.html` (~lines 214–223). Replace the `<h2 id="ethos">…</h2>` block
through `The one rule: keep the attribution.` with:

```html
  <h2 class="sec" id="ethos">Why source-available — yours to build on, not to be strip-mined</h2>
  <p style="color:var(--dim); max-width:72ch;">
    RHA Wrapper is source-available under the
    <a href="https://polyformproject.org/licenses/shield/1.0.0" target="_blank" rel="noreferrer">PolyForm Shield License 1.0.0</a>.
    That means anyone may:
  </p>
  <p class="freedoms">
    Clone it, host it, fork it, skin it, ship it, break and fix it.<br>
    Prompt it, wrap it, stack it, split it, merge it, branch it, patch upgrade it.<br>
    <span class="andmost">Build and sell your own product on top of it.</span>
  </p>
  <p style="color:var(--dim); font-size:12.5px; max-width:72ch;">
    The one line you can't cross: you can't repackage RHA Wrapper <em>itself</em> as a
    competing product. Need to? <a href="/COMMERCIAL-LICENSE.md">That's a commercial license.</a>
  </p>
```
The Pravin Lal epigraph may remain.

### A2 — Landing final block + footer copy (`web/public/landing.html`)
- ~line 230 `finalbloc`: *"…cloned, hosted, copied, modified, and distributed freely by anyone."*
  → *"…cloned, hosted, modified, and distributed under the PolyForm Shield License by anyone —
  the one exception is repackaging it as a competing product."* Keep the "original instance at
  https://rha.software" + scam-warning sentences.
- ~line 252 footer: `© 2026 Andy Funke — free to use, even commercially, with attribution.`
  → `© 2026 Andy Funke — source-available under PolyForm Shield 1.0.0.`

### A3 — Footer license-link swaps (three files)
Replace the CC BY 4.0 anchor with the PolyForm link/text in each:
- `web/public/landing.html` ~248, `web/public/privacy.html` ~136, `web/src/App.tsx` ~631.

### A4 — Backlog footer text
`CURSOR_BACKLOG.md` ~82: `about · privacy · admin · CC BY 4.0` → `… · PolyForm Shield 1.0.0`.

### A5 — Grep gate (must pass)
```bash
grep -rniE "CC.?BY|creative ?commons|licenses/by/4" web src README.md CURSOR_BACKLOG.md | grep -v node_modules
# EXPECTED: no matches.
```

---

## PART B — Plugin

### B1 — Plugin URL: rha.software, not andyfunke.com
`rha.software` is already in the manifest; DELETE the bogus `andyfunke.com` patterns.
- `extension/manifest.json` `content_scripts[0].matches` (~22–23): remove
  `"https://rhawrapper.andyfunke.com/*"` and `"https://*.andyfunke.com/*"`.
- `extension/manifest.json` `externally_connectable.matches` (~43–44): remove the same two.
- `extension/popup/popup.html` ~169: `andyfunke.com/rha` → `<a href="https://rha.software" target="_blank">rha.software</a>`.

Acceptance: `grep -rn "andyfunke" extension/` → nothing.

### B2 — Plugin is version-FUNGIBLE (drop all version-mismatch support)

Decision: the extension carries no version-specific features, so we do NOT support
version-mismatch detection or update nagging. Detection collapses to **installed / not
installed**. Remove the mismatch/corrupted/update-available machinery from the UX.

`web/src/App.tsx`:
- Simplify `ExtStatus` (line ~422) to `'unknown' | 'present' | 'absent'`.
- In the detection effect (~439–489): any signal from the content script (postMessage
  `rha-ext-meta`, `window.__RHA_EXT__`, or the `documentElement.dataset.rhaExt` attribute)
  ⇒ `'present'`, regardless of version or hash. Do NOT fetch/compare `/api/extension/meta`
  versions for status. Report `'absent'` only after the poll window with no signal ever seen;
  never downgrade a seen extension back to `'absent'`.
- Delete the version/hash comparison, `cmpSemver` usage for status, and the `corrupted`/
  `mismatch` state handling.
- Sidebar banner (~570–583): show ONLY when `extStatus === 'absent'`, with a single message:
  `Provider Vault extension not detected — [Download] → then load it at chrome://extensions.`
  Remove the "newer available / corrupted" branches.
- Setup panel status (Task B3-Setup below) shows only Detected ✓ / Not detected ✗.

No manifest version bump is required for detection anymore (bump only on real changes).

### B3 — Setup ("Install the extension") status line
`web/src/App.tsx` `ExtensionGuide()` (~897). Pass `status: ExtStatus` from App state
(update call site ~616). At the top of the `<details>` body render exactly one chip:
- `present` → green (`var(--ok)`): `✓ Provider Vault extension detected.`
- `absent`  → red (`var(--bad)`): `✗ Not detected — follow the steps below.`
- `unknown` → dim: `Checking for the Provider Vault extension…`

---

## PART C — Downloadable applette: pure app module, external footer, inert logo

### C1 — `canonical` flag
- `src/server/index.ts` `/api/meta` (~74): add `canonical: process.env.RHA_CANONICAL === '1'`.
  Only the official rha.software deploy sets `RHA_CANONICAL=1`; downloads never do.
- `web/src/App.tsx` `Meta` type: add `canonical?: boolean`.

### C2 — Strip soft content from the bundle
`src/server/index.ts` `/api/app.zip` (~270): extend the `zip -x` excludes with
`-x 'web/public/landing.html'` and `-x 'web/public/privacy.html'` (and any landing-only
assets not used by the running app). Do NOT exclude the built app, `logo.svg`, or fonts the
app uses. If the app imports anything from those pages, move it to a shared asset first.

### C3 — Inert logo in the applette
`web/src/App.tsx` ~562. Branch on `meta?.canonical`:
- canonical → keep `<a className="brand" href="/landing.html" …>…</a>`.
- else → `<div className="brand brand-inert" title="RHA Wrapper">…same img + h1…</div>` (no
  href, no onClick). Add `.brand-inert { cursor: default; }` to `web/src/styles.css`.

### C4 — Footer links → real website in the applette
`web/src/App.tsx` `.footlinks` (~624–632). Branch on `meta?.canonical`:
- `about`: canonical `/landing.html` else `https://rha.software/` (`target="_blank" rel="noreferrer"`).
- `privacy`: canonical `/privacy.html` else `https://rha.software/privacy.html` (new tab).
- `admin`: ALWAYS local `/admin` (the user's own instance telemetry).
- license link: `PolyForm Shield 1.0.0` (Task A3), same both.

Acceptance: with `RHA_CANONICAL` unset the logo doesn't navigate and about/privacy point to
rha.software; with `RHA_CANONICAL=1` the logo → `/landing.html` and footer is local. Downloaded
`/api/app.zip` contains no `landing.html`.

---

## PART D — Model badge: one green, label carries the meaning

`web/src/styles.css` ~56. The badge is GREEN whether the model is local or cloud — no
different greens, no color difference by provider. The text label ("cloud · <model>" or
"local · <model>") is the only differentiator.

- `.localbadge`: change `color` and `border` from `#a78bfa` to the SAME green the live badge
  uses: `var(--ok)`. So both `.livebadge` and `.localbadge` are `var(--ok)`.
```css
.localbadge { color: var(--ok); font-size: 11px; border: 1px solid var(--ok); border-radius: 999px; padding: 2px 10px; align-self: flex-start; }
```
- Relabel the cloud badge from "live" to "cloud" for clarity. `web/src/App.tsx` ~591:
  `live · {meta.model}` → `cloud · {meta.model}` (keep class `livebadge`). Local stays
  `local · {meta.model}` (~592).

Acceptance: `grep -rn "#a78bfa" web/` → nothing. Both badges render the same green; only the
word (cloud/local) + model name differ.

---

## PART E — Patch retention: keep only the most recent snapshot

`src/server/update.ts`:
- `snapshotCurrent()` (~234): `evictOldPatches(root, 5)` → `evictOldPatches(root, 1)`.
- Update the doc comment (~218) "ring buffer of 5" → "keeps only the most recent snapshot".

Effect: exactly one prior version is retained and freely revertable; older snapshots are
evicted. The admin revert list (`listPatchVersions`) will show at most one entry.

Acceptance: after two consecutive updates, `patches/` holds exactly one version dir.

---

## PART F — App Admin: runtime configuration / feature toggles + model knobs

Add a password-gated "runtime configuration" surface so the instance owner can tune the
algorithm without editing env and restarting. Build it INTO the existing secret admin panel
(`src/server/admin.ts` `createAdminRouter` / `ADMIN_HTML`) — that panel is already
auth-gated; do not create a second unauthenticated settings page.

### F1 — Effective-config module (`src/core/config.ts` — ALREADY EXISTS; verify + extend)
> NOTE: `src/core/config.ts` now exists with `getConfig()` / `getConfigStatus()` /
> `swarmTemperatures()` and is wired into `llm.ts`, `rha.ts`, and `pipeline.ts`. Do NOT
> recreate it. Verify it persists `data/runtime-config.json`, covers every knob in the table
> below, and that F2 (admin endpoints) + F3 (admin UI card) are present — those may still be
> pending.

`getConfig()` returns the effective knobs with precedence
**runtime-config.json > env > built-in default**:
- Persist overrides in `data/runtime-config.json` (already gitignored via `data/`, excluded
  from `app.zip` and the fingerprint).
- Replace the module-level `const X = Number(process.env…)` reads in `llm.ts`, `rha.ts`,
  `bounded.ts`, and `pipeline.ts` with `getConfig().X` reads at call time (so a change takes
  effect on the next job without a restart). Keep the same env names as fallbacks.

Knobs to expose (all currently env-backed — see the note answering "do we have temperature"):
| Key | Env fallback | Type / range | Meaning |
|---|---|---|---|
| `model` | `RHA_MODEL` | string | generation model id |
| `effort` | `RHA_EFFORT` | low·medium·high·xhigh·max | depth vs latency |
| `concurrency` | `RHA_CONCURRENCY` | 1–16 | max parallel LLM calls |
| `swarmWidth` | `RHA_SWARM_WIDTH` | 1–8 | fresh-take candidates on stall |
| `maxPasses` | `RHA_BOUNDED_MAX_PASSES` | 1–100 | bounded-loop ceiling |
| `maxEpisodes` | `RHA_MAX_EPISODES` | 1–200 | RHA episode ceiling |
| `maxAttempts` | `RHA_MAX_ATTEMPTS` | 1–100 | attempts per node |
| `stallLimit` | `RHA_STALL_LIMIT` | 1–10 | stall → escalate |
| `progressStall` | `RHA_PROGRESS_STALL` | 1–20 | plateau → escalate |
| `gateRounds` | `RHA_GATE_ROUNDS` | 1–10 | input-gate reassess cap |
| `autopilotDefault` | — | bool | pre-check the autopilot box |

> **Temperature (advanced):** it IS available — `DrawOpts.temperature` flows into the
> Anthropic call (`llm.ts` ~198, clamped [0,1]) and today drives the anti-anchoring swarm's
> ladder `[0.5, 0.8, 1.0]` (`rha.ts` ~224). But in this pipeline temperature is an INTERNAL
> decorrelation device, not a global "creativity" dial — the encode/IR/build calls run without
> it ON PURPOSE (determinism is the product thesis). So expose it only as an advanced
> `swarmTempCeiling` (0.2–1.0) that scales the swarm ladder, NOT as a global generation
> temperature. Label it clearly as affecting repair-retry diversity, not normal generation.

### F2 — Admin endpoints (`src/server/admin.ts`, in `createAdminRouter`)
- `GET  /<slug>/config` (auth) → current effective config + which values are overridden.
- `POST /<slug>/config` (auth, JSON) → validate against the ranges above, write
  `data/runtime-config.json`, return the new effective config. Reject out-of-range values.
- `POST /<slug>/config/reset` (auth) → delete `runtime-config.json` (revert to env/defaults).

### F3 — Admin UI card (`ADMIN_HTML`)
Add a "runtime configuration" `<div class="sec">` + `.card` with a labeled input per knob
(number inputs with min/max, a `<select>` for effort, a checkbox for `autopilotDefault`,
`swarmTempCeiling` under an "advanced" `<details>`), a Save button hitting `POST config`, and
a "Reset to defaults" ghost button. Reuse existing tokens/classes. Show a green confirmation
on save.

Acceptance: change `effort` and `swarmWidth` in the panel, start a job, confirm the new
values are used (they appear in the debug I/O log / telemetry) with no server restart; Reset
restores env/defaults.

---

## PART G — Secret owner control plane (ALREADY EXISTS — just configure it)

**Do not build this — it is already implemented** in `src/server/admin.ts`
(`createAdminRouter`) and mounted at the secret slug in `src/server/index.ts` (~72). It already
does everything requested:
- **Telemetry** — `GET /<slug>/telemetry`.
- **Authorize patches** — "Approve fleet upgrade" (`/release/approve`); clones checking this
  origin only see an update once approved.
- **Control the endpoint the fleet connects to** — that endpoint is `ORIGIN_URL`
  (default `https://rha.software`); the panel's self-update card sets/checks it, and each
  shadow download polls `<ORIGIN_URL>/api/version` + `/api/update/latest`.
- **Extension distribution** — replace the served `.zip`.

It is protected by TWO gates: a secret URL **slug** (`ADMIN_SLUG`) and a **password**
(`ADMIN_PASSWORD`, timing-safe compared). `/admin` is a SEPARATE unauthenticated per-instance
telemetry page — leave it as-is.

### G1 — First-launch password: owner sets it once, it's saved (hashed)

> ⚠️ **No password in source or env.** This repo is PUBLIC and the owner panel can push
> patches to every shadow download — a leaked password is a whole-fleet supply-chain
> compromise. So there is NO hardcoded and NO default password. Instead: on first launch the
> panel makes the owner CREATE a password, which is hashed and saved to disk; every later
> launch requires it.

Rework the password gate in `src/server/admin.ts`:

1. **Persisted credential.** Store at `data/admin-auth.json` (already gitignored via `data/`,
   excluded from `app.zip` and the fingerprint):
   `{ "algo": "scrypt", "salt": "<hex>", "hash": "<hex>", "createdAt": "<iso>" }`.
   Hash with `crypto.scryptSync(password, salt, 64)` and a fresh 16-byte random salt; verify
   with `timingSafeEqual` on the derived key. Never store the plaintext.

2. **Password state** (in `resolveAdminConfig` or a small helper): the slug logic stays as-is
   (generated + printed if `ADMIN_SLUG` unset). For the password, compute one of:
   - `env` — `ADMIN_PASSWORD` is set (still honored as an override; if set, no first-run setup).
   - `saved` — `data/admin-auth.json` exists.
   - `unset` — neither → first-run setup required.

3. **Endpoints** (in `createAdminRouter`):
   - `GET  /<slug>/state` (unauth; slug already gates discovery) → `{ "configured": boolean }`
     where `configured = state !== 'unset'`.
   - `POST /<slug>/setup` (JSON `{ password }`) → ONLY when state is `unset`. Require length ≥ 8;
     hash + write `data/admin-auth.json`; issue a bearer token (same `issue()` as `/auth`).
     If already configured → `409 { error: 'already configured' }`.
   - `POST /<slug>/auth` → verify against the env password when state is `env`, otherwise against
     the saved scrypt hash. Unchanged token issuance on success.

4. **UI (`ADMIN_HTML`):** on load, `GET /<slug>/state`. If `configured === false`, show a
   "Create admin password" card (password + confirm, min 8, mismatch inline error) that calls
   `/setup`; on success go straight into the panel. If `configured === true`, show the existing
   "Unlock" login calling `/auth`. Reuse existing tokens/classes.

Acceptance: fresh instance (`rm -f data/admin-auth.json`, `ADMIN_PASSWORD` unset) → visiting the
slug shows "Create admin password"; after setting one, the panel opens, `data/admin-auth.json`
exists with only salt+hash (no plaintext), and a restart now shows "Unlock" and accepts that
password. Setting `ADMIN_PASSWORD` in env still overrides and skips first-run setup.

---

## PART K — Input gate: consolidate, per-feature control, immediate approval (no re-probe)

Context (how it works today, `src/core/pipeline.ts` `runEncodingAndIR` + `src/server/jobs.ts`):
the gate enumerates features, probes each up to 3× to derive a concrete assertion, and blocks
(`insufficient-input`) listing the features it couldn't. The operator has TWO levers:
**Override** = skip the gate for ALL ambiguous features at once (proceed to build); **Re-evaluate**
= re-enumerate + re-probe the unresolved ones stochastically (token burn, returns a different set).
There is NO per-feature approval and approval does not validate in place. Fix all three.

**K1 — One consolidated panel.** Render every ambiguous feature together (it is already one
`promptAssessment.deficiencies` list) with, per feature: the derived assertion OR the open
question, and a **checkbox "accept / auto-requalify."** State precisely what approving assigns
(feature ids + names). No sequential re-prompting round after round.

**K2 — Immediate approval, ZERO re-probe.** Add a job action `approveGateFeatures(id, names[])`
(`jobs.ts`): mark the named features `clear` directly in `job.promptAssessment.verdicts` (inject a
synthetic cleared verdict), recompute `sufficient`; when all are clear, proceed straight to
encode/build with **no `enumerateFeatures`, no probes, zero new LLM calls.** Keep `reassess`
(re-probe) as an explicit secondary "look again" button — NOT the approval path. Wire a new
route `POST /api/jobs/:id/gate/approve` (body `{ features: string[] }` or `{ all: true }`).

**K3 — Autopilot-leaning default.** Default the "Unsupervised E2E" toggle ON, driven by
`getConfig().autopilotDefault`. With autopilot on, gates AUTO-APPROVE after clearly demarcating
(and ledger-recording) what they cover, instead of hard-blocking. The HIL gates still exist and
can be re-enabled per job — "assume the user wants automatic, but keep the gates as demarcation."

**K4 — Precise demarcation.** Every gate/approval surface states exactly what it commits: which
feature ids + names (and at the build gate, which suites / cases / files). No bare "Approve?".

> ⚠️ Copy caveat: the INPUT gate is about *intent* (what a feature means), which downstream can
> NOT recover — auto-approving proceeds on the model's best-guess intent, so word it as
> "proceed on best-effort intent (spot-check recommended)." That is different from the BUILD
> gate (K5), where RHA genuinely heals. Do not use identical "approve" language for both.

## PART K5 — Build gate: frame approval as "proceed to healing," not "ship flawed"

At `awaiting-build-approval`, authentication may show failing tests, and today the button reads
like "ship this flawed thing?". Reframe so approval clearly means advancing to the next healing
round (`web/src/App.tsx` build-approval view + phase/step labels):
- Show: **"N features authenticated · M tests failing. Approving does not finalize — RHA runs a
  bounded healing pass on all failures next. Proceed to RHA."**
- The IR-gate and build-gate copy should preview the stages that follow ("→ then authenticate →
  then RHA heals failures") so approving never reads as shipping bad. Button label e.g.
  "Proceed to RHA healing" rather than "Approve build".

## PART L — SEV1 alerting for algorithm breakdown (halt + notify off-hours)

Some conditions are not normal escalations — they mean the pipeline ITSELF broke and must never
be papered over as "complete." Treat these as SEV1: halt the run, record with severity, and
notify ops regardless of business hours.

**L1 — SEV1 conditions** (raise at the point each is detected):
- a suite with **"has no test cases"** (`pipeline.ts` ~268 — no `it(` in the assembled suite).
- a built suite that **executed 0 of its declared cases** (the Part J collection guard).
- **enumeration produced 0 features**, or IR produced **0 cases** across all surviving features.
- (tunable via config) IR escalations ≥ a configured fraction of total features.

**L2 — `raiseSev1(code, context)` helper** (new, e.g. `src/server/alerts.ts`): append a row to
`data/sev1.jsonl` (job id, condition code, feature, ledger node, ISO timestamp); mark the job
`failed` with a SEV1 reason (NEVER `complete`/`verified`); emit a `sev1` SSE event the UI renders
in red; and if `process.env.RHA_ALERT_WEBHOOK` is set, POST a compact JSON alert to it
(Slack/PagerDuty-style) — this is the off-hours interrupt. A failed POST is logged, never
silently swallowed.

**L3 — Surface in the owner admin** (Part G panel): a SEV1 count + recent-list card, so ops sees
breakdowns without tailing logs.

Acceptance: force a "no test cases" suite → the run halts as `failed` with a SEV1 reason (not
"complete"), `data/sev1.jsonl` gains a row, and if `RHA_ALERT_WEBHOOK` is set it receives a POST.

---

## PART J — Verification integrity: never green-light zero tests (CRITICAL)

Context: the scaffolded runner glob was `tests/**/*.test.ts`, but `feature.testFile` is
model-supplied. A weak model that named suites `foo-test.ts` (ignoring the `.test.ts` naming
instruction in `prompts.ts` ~48) made vitest collect **0 files**, so the run reported
"complete ✓" over **0 executed tests** — a false green for a verification product.

**J1 — DONE (verify present):** `scaffoldWorkspace` (`src/core/sandbox.ts` ~46) now uses
`include: ['tests/**/*.ts']` so collection is independent of the model's file naming. Proven on
a real workspace: 0 files/0 tests → 4 files/27 tests after the change.

**J2 — Guard: a built suite that executes 0 tests is a FAILURE, not a pass.**
- `runVitest` (`sandbox.ts`): add `collected: number` (= `json.testResults.length`) to `VitestRun`.
- The IR manifest knows each frozen suite's declared case count. In build authentication
  (`pipeline.ts` ~1186) and the RHA final proof (`rha.ts` ~430), for every FROZEN, non-escalated
  feature: if executed tests (`passed + failed`) === 0 while declared cases > 0, emit a hard
  finding `[(runner)] tests/<file>: declares N cases but 0 executed (collection mismatch)` and
  treat authentication / final proof as FAILED. The job must never reach "complete / verified"
  while any built suite executed 0 of its declared cases.
- Optional hardening: normalize `feature.testFile` to the canonical `tests/<id>-<slug>.test.ts`
  at ingestion so the frozen artifact is consistent even when the model deviates.

**J3 — Close the mock blind spot.** The mock E2E never caught this because the fixture uses
compliant `.test.ts` names. Add a regression test: a workspace whose suite file is named
`tests/foo-bar-test.ts` with ≥1 case must (a) be collected and executed, and (b) if a suite
collects 0 tests, the run must report failure — asserting the J2 guard.

Acceptance: synthetic `tests/foo-bar-test.ts` with 3 cases runs 3 tests; a 0-collected suite
yields a failing run, never "complete ✓."

---

## PART M — Model badge & /api/meta: show the CONNECTED model only, never a default

Problem: `/api/meta` ([src/server/index.ts](src/server/index.ts) ~75) falls back to a hardcoded
`'claude-opus-4-8'` (default from `src/core/config.ts` ~57) whenever neither `MOCK_LLM` nor
`LOCAL_LLM` is set — so before any provider is activated the app shows a phantom "connected to
Claude 4.8" with NO verified connection. The local path VERIFIES (pings `/models`); the cloud
path just ASSERTS a string. Inconsistent and misleading. There must be **no default — blank until
a provider is actually connected and verified.**

A provider becomes real only via a successful `/api/provider` activation (index.ts ~150; the
extension's "Set Active"), which sets the env flags. Nothing is connected before that.

**M1 — Connection state, verified.** In `/api/provider`, after setting env, VERIFY before marking
connected:
- openai-compat / local: `GET <baseURL>/models` must return an id (already the local pattern).
- claude: a lightweight auth check (e.g. `GET https://api.anthropic.com/v1/models` with the key)
  must succeed.
On success store in memory `{ connected: true, type, model, verifiedAt }`; on failure return an
error to the extension and DO NOT mark connected (badge stays blank). The handler already clears
env at the top — clear this connection state there too, so a failed switch never leaves a stale
"connected".

**M2 — `/api/meta` model = the connected model, else `null`.** Remove the hardcoded fallback at
index.ts ~93. Branch:
- not connected → `model: null` (blank).
- mock → the mock label (a real state).
- connected local → the verified `id @ url` (only when verified; unreachable ⇒ null, not a label).
- connected claude → the activated `RHA_MODEL`; **no `|| 'claude-opus-4-8'`**.
Do NOT ping the provider on every meta poll (it polls ~2.5s) — read the cached verification from M1.

**M3 — Badge** (`web/src/App.tsx` ~591–592; composes with Part D). When `meta.model` is null/empty
and not mock: render NO badge, or a muted, non-green hint — `no model connected — open Provider
Vault → Set Active`. Never show the green connected badge without a connected model. The Part D
green applies only once connected.

**M4 — Cosmetic:** startup log (index.ts ~500) fallback `'claude-opus-4-8'` → `'(none configured)'`.
The admin `cfg_model` placeholder may remain as an example value.

> Open question (flag, don't guess): `config.ts` ~57 still defaults the GENERATION `model` to
> `claude-opus-4-8` for actual LLM calls. Per "no default," generation should refuse when no
> provider is connected rather than silently use that string. Decide: (a) keep the internal
> generation default, or (b) **hard-require a connected provider before a job can start
> (recommended)** — gate `createJob` on connection state and surface "connect a provider first."

Acceptance: fresh server, nothing activated → badge blank, `/api/meta` `model: null`, log says
"(none configured)". Activate Claude with a valid key → badge shows that model (green). Activate
with a bad key/URL → activation errors and the badge stays blank (no phantom default).

---

## PART N — Advanced Mode: Operator / Supervisor / Orchestrator agent schema

Supersedes the earlier two-agent draft. Opt-in power-user layer. **The single-agent config ships
standard and is the default; everything here is OFF until Advanced Mode is toggled** — with it off,
behavior is byte-for-byte today's single-agent flow. Do not regress the default path.

**N0 — Advanced Mode toggle + Agent Roster.** Add one top toggle **`Advanced Mode`** in the sidebar
alongside "Unsupervised E2E". OFF → nothing changes. ON → reveal an **Agent Roster** module beneath
the top controls: three role cards in descending hierarchy — **Operator(s)**, **Supervisor**,
**Orchestrator**. Each card carries: role label + color, live online status with local-vs-cloud +
IP + model-id demarcation (identical to the app badge; compose with Part M — a card shows online
only when that role is connected+verified), a **Wiki link**, and an activate toggle that
shows/expands its controls (deactivated = the whole box collapses/greys, so a precluded role is
obviously precluded).

**N1 — Role→provider schema (fungible).** Generalize provider config to a roles map:
`operator` (1–2, ≥1 required), `supervisor` (0–1), `orchestrator` (0–1). Each entry → a provider
`{ type: claude|openai-compat|mock, baseURL?, model, apiKey? }`. Persist to `data/providers.json`
(gitignored) + in-memory. `/api/provider` ([index.ts](src/server/index.ts) ~150) accepts
`{ role, index?, provider }`; a single provider with no role = `operator[0]` (back-compat). The LLM
layer resolves the client by the ROLE of the current call, not global env.

**N2 — Topology constraints (enforce in UI + server).** Operators cap at 2. **Orchestrator is
REQUIRED iff Operators = 2** (it exists solely to parallelize — same job, parallelized). Supervisor
is a *delegate* (IR + escalations), never a parallelizer; it's independently optional. So the ONLY
orchestrator-free stacks are `1 Op` and `1 Op + 1 Sup`. Valid stacks:

| Stack | Ops | Sup | Orch | Doctrine |
|---|---|---|---|---|
| Solo (default) | 1 | — | — | single agent, today |
| Failsafe | 1 | 1 | — | "just ships" — Op works, Sup is the safety net |
| Parallel pair | 2 | — | 1 | strict-partition parallel; peer-escalation |
| Full stack | 2 | 1 | 1 | 2 small Ops + Sup for structural/idiosyncratic superflux |

Reject any other combination in the UI (disable the Save) and server-side.

**N3 — Operator controls.** The existing top controls — max passes, temp/warmth (`swarmTempCeiling`),
effort, swarm width — ARE the Operator's knobs (per-operator when two exist). Operator does
`generateImplementation` (build) + `repair` + swarm draws (the tree traversal / bounded repair).

**N4 — Supervisor: COMBINABLE behavior toggles** (independent checkboxes, not radio). Philosophy:
*delegate maximally to the cheap Operator; the Supervisor is the ship-every-time safety net.* Toggles:
- **Resolves Test Suite IR then hands off** — Supervisor runs the front-end (`enumerateFeatures` /
  `probeFeature` / `encodeFeatures` / `generateTests` / `fixTestCases` / `fixStub`), freezes, hands
  build+repair to the Operator.
- **Resolves ALL Operator failures** — Supervisor steps in on any failed local recheck, not just stalls.
- **Resolves ONLY Operator escalations** — Supervisor engages only when the Operator stalls/ceilings.
- Guardrail: "all failures" ⊇ "only escalations" — if both are on, "all failures" wins; surface that,
  don't error. Any Supervisor code obeys the SAME `boundaryViolations()` write-boundary (more
  reasoning + full `substrate.lineage()` as context, NEVER more write scope — no regeneration
  backdoor). Supervisor also stalls → surface to human.

**N5 — Orchestrator: strict-partition parallelization via a non-blocking SHARED QUEUE (work-stealing),
ledger-reconciled.** Only active with 2 Operators. It partitions the feature tree into **disjoint
subtrees**, one per Operator, worked in parallel. **Escalation is enqueue-and-continue, never
blocking:** when an Operator stalls on a node, it **ENQUEUES** that node into a shared escalation
queue (orchestrator-managed) and immediately returns to **finish its own partition first** — it must
NOT stop and wait. When an Operator becomes free (own instructions complete) it **returns to drain the
escalation queue** (a re-attempt at a different temp/seed = decorrelation). So the escalating Operator
moves on assuming the other delegate — or itself, once free — will take the queued item later;
whichever Operator frees up first steals it. Every enqueue + resolution is recorded in the supergit
ledger; any upstream/blast-radius effects of a late convergence are captured by RHA's normal re-sweep
over the forward pointers. **I/O discipline: never serialize the two Operators — no one stops and
waits.** With a Supervisor also present (full stack), the Supervisor catches structural / idiosyncratic
issues and handles escalations neither Operator resolves.

**N6 — Per-role KPIs (thorough).** Tag every usage record (`llm.ts` `onUsage`) with `role`
(operator[i]/supervisor/orchestrator) + provider + model. Aggregate per role: calls,
input/output/cache tokens, duration, est. cost — per phase. Split the job's token-expenditure table
by role and add a per-role admin breakdown. Goal: the Supervisor's share reads as a small % — make
the delegation min/max measurable.

**N7 — UI hierarchy + colors.** Operator = **green** (`var(--ok)`); Supervisor = **tritium frost
blue** (`var(--frost)` #5fd7ee); Orchestrator = **hot-pink→cyan gradient** (interpret your "Hot Pink
:: Cyan" as a two-stop gradient, e.g. `linear-gradient(90deg,#ff3ea5,#34e0e0)` — confirm if you meant
split/dual-tone). Cards stack in that hierarchy top-to-bottom. Tag each action in the run trace /
episode table by the agent that performed it, colored to match. Every control + role is Wiki-linked,
including the inter-Operator algorithms (partition + non-blocking handoff).

**N8 — Supervisor activation notification.** When the user activates the Supervisor (and likewise
Orchestrator), a distinctly-colored notification box (frost-blue for Supervisor) appears confirming
it's live and stating what to do next (assign its provider, pick behavior toggles). Make activation
unmissable — do not let it silently flip on.

**N9 — Plugin multi-agent auth** (see also Part Q). The Provider Vault must configure up to 4
providers and assign each a role slot (operator[0], operator[1], supervisor, orchestrator), each with
its own encrypted key/endpoint, visibly demarcated by role color. `/api/provider` carries
`{ role, index?, provider }`. Single provider, no role → operator[0].

**N10 — Control Panel niceties.** The Control Panel page (host of Advanced Mode + the `config.ts`
knobs, reusing the SAME `getConfig()`/`setConfig()` store — do not fork it) also gets: a **Restore to
Defaults** button (calls `config/reset`, clears role assignments to Solo), and a **Wiki** link header.

Acceptance: Advanced Mode off → identical to today. `1 Op + 1 Sup` with "IR + escalations-only" →
Supervisor builds/freezes IR, Operator builds+repairs, stalls go to Supervisor, badges show
frost-blue-over-green. Selecting a 2nd Operator forces the Orchestrator on (Save disabled until set).
Two Operators run disjoint subtrees in parallel; a stalled node hands to the peer without blocking;
the token table splits by role.

## PART O — Checkpointed re-entry: resume + incremental subtree (stop losing 30–60 min runs)

The supergit ledger is already a complete checkpoint log (append-only, freeze hashes, per-repair
git commits) that is currently WRITE-ONLY. Read it back to make runs interruptable and continuable.
Resume and incremental recompute are one mechanism: "continue this job from ledger state at stage
X, scope Y."

**O1 — Interrupted ≠ failed.** Today mid-phase restart and any error mark the job `failed` and
forget it, though the workspace/ledger/git survive ([jobs.ts](src/server/jobs.ts) ~36–85, ~450).
Add an `interrupted` state (resumable) carrying the last completed stage. Reserve `failed` for
genuine dead-ends (e.g., SEV1). Never discard the workspace on interruption.

**O2 — Resume entry.** Add `resume(id)` + `POST /api/jobs/:id/resume`: reopen the substrate
(`Substrate.open`, the pattern gate re-entry already uses — do NOT truncate the ledger), read the
last completed stage from the ledger, and re-invoke the pipeline from there:
encode-done→IR; IR-frozen→build; build-done→RHA; mid-RHA→re-invoke `applyRha` (it re-sweeps from the
committed disk state and continues — healed nodes already pass, so no rework). `verifyFreeze` must
still pass on resume (freeze hashes persist in the manifest).

**O3 — Episode-level resume** (journaling already exists — recording per-episode ledger nodes + git
commits; this adds continuation). On mid-RHA resume, `applyRha` continues from the last committed
repair. Persist `attemptsByFeature` / episode counters into the ledger so stall/attempt budgets
resume rather than reset (a fresh budget is acceptable but note the choice). No re-healing of nodes
already green on disk.

**O4 — Incremental subtree recompute** (same re-entry, scoped — the only pending item; O1–O3 are done).
Turn resume into targeted recompute of one feature and its blast radius, reusing the O2 machinery
(`Substrate.open`, no ledger truncation; append new nodes for provenance).

- **Entry point:** on a `complete`/`interrupted` job, the user edits ONE feature's spec text in the UI
  and triggers **Recompute subtree** on that feature. API: `POST /api/jobs/:id/recompute`
  `{ featureId, spec }`. (No whole-prompt re-encode; the edit is scoped to that feature.)
- **Changed feature:** re-freeze ONLY that feature's suite (new freeze hash) and rebuild ONLY that
  feature against it. Everything else's frozen suite + code stays **byte-identical** (assert via the
  stored freeze hashes; untouched nodes are skipped, not regenerated).
- **Dependents (blast radius):** found via the forward-dependency pointers, they are **re-HEALED by
  RHA's normal parent-recheck / bounded repair — NOT rebuilt.** A dependent that cannot conform to the
  changed interface **escalates through the normal ladder** (operator → supervisor/queue → human),
  exactly like any other RHA failure. Do not silently rebuild dependents; healing + escalation is the
  contract.
- **Ledger:** record the recompute as new append-only nodes (a `decision` "incremental recompute of
  <featureId>", the re-freeze, the rebuild, and the ensuing repair episodes) so provenance is intact.

Acceptance: on a completed job, edit F3's spec → `POST /recompute {F3}` re-freezes only F3's suite,
rebuilds only F3, re-heals F3's dependents (and escalates any that can't conform), and every other
feature's suite + code hash is unchanged; no full re-run occurs.

Acceptance: kill the server mid-RHA on a real job → the job shows `interrupted`, not `failed`;
`POST /resume` re-enters `applyRha` and finishes without redoing healed nodes; a full re-run is never
required to recover from an operational error.

---

## PART P — Spec gate: stop whole-spec regeneration + guard feature collapse (CRITICAL / live crash)

Symptom: a prompt that enumerated **40 features** froze a spec with **1** (F3); the lone suite then
escalated → *"every feature's IR escalated"* → `failed`. Root cause: the spec-validation `converge`
loop ([pipeline.ts](src/core/pipeline.ts) ~480–492) "repairs" by **re-encoding the ENTIRE spec from
scratch** each pass — `spec = await llm.encodeFeatures(job.prompt, feedback)`. On a weak model each
re-encode returns a different, shrinking feature set; the loop silently accepts the last one even
when it collapsed 40→1. This violates the doctrine stated 20 lines below (~529): *"the spec is never
re-encoded — whole-artifact regeneration is the failure mode this architecture exists to prevent."*
`validateSpec` (~210) flags only STRUCTURAL/mechanical per-feature issues (non-`src/` files,
missing/duplicate stubs, empty `edgeCases`, dep/ownership) — none justify regenerating all features.
(Strong models hid this by re-encoding consistently; the fix must be model-robust.)

**P1 — Feature-collapse guard (stops the crash).** The gate must NEVER accept a spec with materially
fewer features than were enumerated at the input gate. Capture the enumerated/probed count (and the
pre-repair `spec.features.length`). If a re-encode returns a strict subset or a materially smaller
set, REJECT it: keep the prior spec and count the pass as no-progress (let `converge` stall →
escalate) rather than adopting the collapse. A spec gate that would emerge with far fewer features
than enumerated must FAIL loudly — and raise SEV1 (Part L) `spec-collapse` (`N→M features`), never
proceed.

**P2 — Pre-sanitize before validating (kills spurious re-encodes).** Before the FIRST `validateSpec`
and before every `measure`, run the deterministic normalizers that already exist: `sanitizeSpec`
(strip `tests/` paths from `files[]`) and per-feature `mergeStubsByPath` (currently only run AFTER
the gate at ~538). Then duplicate-stub and test-path-in-files issues are fixed mechanically and never
trigger a re-encode. Add safe deterministic fixes for the other mechanical classes (e.g. `testFile`
not under `tests/` → normalize; missing stub for an owned file → emit a minimal throwing stub).

**P3 — Replace whole-spec regeneration with bounded per-feature repair.** Per doctrine, the spec gate
must not call `encodeFeatures(job.prompt, …)` (regenerate all). For issues surviving P2, repair ONLY
the offending features — a targeted call returning just those features' corrected fields, merged back
into the existing spec, every other feature preserved byte-identical. Add `llm.repairFeatures(spec,
offendingIds, problems)` if no bounded method exists, mirroring the stub gate's per-file repair.

Acceptance: a 40-feature prompt on a weak model either freezes all 40 (issues fixed in place / per
feature) or FAILS with a clear spec-gate error — but NEVER silently proceeds with a collapsed subset.
`spec.json` feature count == enumerated count minus only explicitly-escalated, ledger-recorded ones.
Add `spec-collapse` to Part L1 SEV1 conditions.

**P4 — Deterministic dependency-id resolution (follow-on; live crash on weak models).** `preSanitizeSpec`
mechanically fixes paths/testFile/stubs but NOT deps. A model often gives feature 2 the id
`F2_application_shell` yet writes another feature's dep as bare `F2` — same feature, different string —
so `validateSpec` reports "depends on unknown F2", the per-feature repair can't fix it, and the gate
stalls→escalates→fails. Add to `preSanitizeSpec`: index every feature by exact id AND by leading
`/^F?\d+/` token; rewrite each dep to the resolved id when a token maps to exactly one feature (ignore
ambiguous tokens); drop self-deps; dedupe; leave only genuinely-unresolvable deps for `validateSpec`.
Unit test: `F40_x` with deps `["F2","F10"]` + features `F2_shell`/`F10_render` → deps become
`["F2_shell","F10_render"]`, zero "unknown" errors.

---

## PART Q — Provider Vault plugin: lock behavior, options, key links, URL reset

**Q1 — Fix the lock/logout bug.** The vault currently locks between actions taken within an object
(mid-flow) — it must NOT. Lock ONLY on **loss of view** (popup close / `document.visibilitychange`→
hidden / window `blur`) or on the idle timeout (Q2). Never lock between sequential actions on the same
object/flow while the user is actively working in view.

**Q2 — Idle-timeout option.** Configurable auto-lock: **5 / 10 / 15 min / never** (default 10). Timer
resets on activity; while the popup is in view and active it never prompts. Locks on either view-loss
(Q1) or idle expiry. Purpose: stop re-prompting the password while the user is actively iterating.

**Q3 — Options page.** Add a Chrome `options_ui` page hosting the idle-timeout selector, notification
prefs, and default key visibility.

**Q4 — API-key deep link.** In the provider dropdown, far-right, an external-link (↗ / "opens a page")
icon linking to that provider's key dashboard: Anthropic Console, OpenAI Platform, Google AI Studio,
Zhipu open.bigmodel.cn, Moonshot platform. Icon must obviously read as "external page."

**Q5 — Localhost URL reset.** Next to the RHA server URL field, a reset button restoring the default
(`http://localhost:4100`) if the user clears or breaks it.

**Q6 — Recommended extras:** a **Test Connection** button (verify → composes with Part M), key
**mask/reveal** toggle, per-provider **request timeout**, **auto-lock on browser sleep**, and
**enhanced notifications** (add the `notifications` permission; fire an OS notification on gate/complete
even when the app tab is closed). **Do NOT build VPN/Proxy/SSH into the extension** — `baseURL` already
accepts any endpoint, so a user routes through their own proxy/tunnel by pointing baseURL at it;
document that, don't reimplement networking.

**Q7 — Multi-role auth** (with Part N9): configure up to 4 providers, assign each a role slot
(operator[0], operator[1], supervisor, orchestrator), each visibly demarcated by role color.

## PART R — Providers (Gemini / GLM / Kimi) + Wiki scaffolding

**R1 — Three new presets via the EXISTING `openai-compat` client (no new client code).** Add to the
provider dropdown + server presets, each with its Q4 key-dashboard link:
- **Gemini** → `https://generativelanguage.googleapis.com/v1beta/openai/`
- **GLM (Zhipu)** → `https://open.bigmodel.cn/api/paas/v4/`
- **Kimi (Moonshot)** → `https://api.moonshot.cn/v1`
Verify each exposes OpenAI-shaped `/models` + `/chat/completions`; if one deviates (Gemini's endpoint
can be finicky), note the minimal adapter and keep it isolated to that preset.

**R2 — Wiki scaffolding (stubs now, prose later).** Create a Wiki (`/wiki` route or static pages) with
one page per feature/control, linked from the Control Panel, each role card, and each knob. Every Wiki
link must resolve to at least a stub. **Do NOT write the deep articles now** — full RHA / bounded-loop
IR / agent-algorithm docs are deferred until explicitly requested (the existing articles were vague
pre-build pitches; the bar is now much higher). Stubs unblock the links.

---

## PART S — Advanced Mode refinements: per-role config, plugin role dropdown, notification grouping

**S1 — Per-role independent config (supersedes N3's shared-controls shortcut).** Move `config.ts` from
one global `RuntimeConfig` to a PER-ROLE map: each active slot (operator[0], operator[1], supervisor,
orchestrator) has its OWN `{ maxPasses, swarmWidth, swarmTempCeiling, effort, maxEpisodes, maxAttempts,
stallLimit, gateRounds }`. `getConfig(role)` returns that role's effective config
(per-role `runtime-config.json` > env > default); the LLM call resolves knobs by the ROLE of the
current op. Rationale: a temperature/effort that *enables* a weak Operator can *degrade* a Supervisor —
knobs must never be shared. **UI:** in Advanced Mode each role card has its OWN on/off toggle AND its
own dials underneath — not one shared top panel.

**S2 — Plugin role dropdown.** In the Provider Vault, each configured provider carries a **role
dropdown** (Operator / Operator 2 / Supervisor / Orchestrator). The user assigns e.g. gemma→Operator,
claude→Supervisor; the extension pushes `{ role, index?, provider }` and the app reads the role→provider
map (N1) and routes accordingly. The role is the user's explicit choice, color-coded to match N7.

**S3 — Notifications grouping.** Move the "🔔 Browser notifications" checkbox inline with the other run
toggles (Unsupervised E2E, Advanced Mode) so the group reads as deliberate.

## PART T — Reintroduce extension version control (reverses Part B2)

Part B2 made the plugin version-fungible (present/absent only) because it then had no features. It now
has role dropdowns, an options page, and folder access — evolving paradigms — so **version skew matters
again; reintroduce mismatch detection** (this intentionally reverses B2):
- App compares the installed extension version (content.js already posts it) against the app's
  bundled/expected version; classify match / outdated.
- Outdated → a NON-blocking prompt: "A newer Provider Vault is available — Download → reload at
  chrome://extensions." A mere version gap still counts as **present/operational** — never "not
  detected."
- Bump the manifest version on each paradigm change; `/api/extension/meta` reports the bundled version.

## PART U — Continuous project: Pin/Star + iterate-in-place (add + edit)

The single flawless build is the base (Part P); this is the layer that turns a finished build into a
living project you keep shipping from. **The benefit persists ONLY while all work stays inside RHA** —
the supergit scaffolding carries it; editing the project through another interface breaks the chain.

**U1 — List actions.** Add **Star** (favorite/filter — a cosmetic, filterable bookmark) and **Pin**
(designate a "living project": keeps its scaffolding warm and unlocks the iterate-in-place UI) to each
run in the job list. Distinct roles: Star = bookmark; Pin = active project.

**U2 — Iterate-in-place UI** (the "different UI" a successful build unlocks). A `complete`/pinned job
opens a project workspace where you enter a NEW prompt into the SAME project. A new prompt may:
- **Add** features → append new nodes to the existing frozen tree, wire deps, build+heal ONLY the
  additions; existing frozen suites+code stay byte-identical (hash-verified).
- **Edit** existing features → the O4 subtree recompute.
Both reuse the O1–O3 / O4 re-entry machinery (`Substrate.open`, append-only ledger). The UI
distinguishes an "add feature" intent from an "edit feature" intent.

**U3 — Scaffolding-integrity guard.** Because the continuous benefit dies if the workspace is mutated
outside RHA, on re-entry verify the workspace against the ledger + freeze hashes (both already exist);
if external edits are detected, warn clearly ("scaffolding interrupted — external edits detected") and
offer reconcile-or-fork rather than silently corrupting the chain.

## PART V — Folder-sync deployment (both paths) + OPSEC demarcation

Ship the artifact to a user-chosen local folder with NO download/extract, keeping RHA↔deployment synced.
Because the hosted site and the local app are the SAME codebase, **both paths exist by construction**:

**V1 — Self-host path (base — build FIRST).** The RHA server writes the artifact tree directly to a
user-configured OUTPUT directory (the server already has fs access). On build success and on each
incremental recompute (U2/O4), sync the workspace's deployable files into it.

**V2 — Hosted path (layer).** The Provider Vault brokers the File System Access API: the user picks a
folder and grants WRITE access; the extension **persists the directory handle** so even a REMOTE
rha.software instance can sync the artifact into that folder. Same UI as V1.

**V3 — OPSEC demarcation (required, honest).** When granting a remote page folder access, show a clear
notice explaining WHY it is safe HERE: **write-only** to the **single folder you picked** (no read, no
other paths), **revocable anytime**, and the **blast radius is bounded to that folder** — point users at
a dedicated/empty deploy folder for near-zero exposure. State plainly that routing filesystem permission
through a remote website is normally an OPSEC consideration, and that this case is constrained by the
architecture (write-only, single-folder, revocable) and the browser's own sandbox. No dark patterns —
the grant is explicit and legible.

**V4 — Sync trigger.** On build success and each incremental recompute, write ONLY the changed
deployable files (incremental) into the synced folder, preserving tree structure. Do NOT sync
`.supergit`/scaffolding by default (deployable code only; offer a toggle if the user wants the ledger).

**V5 — Hosted byte-push wire (completes V2).** V2 today is grant+persist only (`showDirectoryPicker`
+ persisted handle); wire the actual transport so a REMOTE (hosted) page can sync into the folder
without V1's server path:
- The page fetches the changed deployable files (V4) from the API and sends them to the extension —
  hosted uses `chrome.runtime.sendMessage` via `externally_connectable` (rha.software is already
  listed) or `window.postMessage`→content→background; chunk large files.
- The extension context that holds the persisted directory handle (background/offscreen) writes each
  file: walk path segments with `getDirectoryHandle({create:true})`, then
  `getFileHandle(name,{create:true})` → `createWritable()` → write → close.
- **Path safety:** reject any path that escapes the granted folder (no `..`, no absolute); write-only;
  only within the one handle. **Re-verify permission** (`queryPermission`/`requestPermission`) before
  each sync — persisted handles need re-grant across sessions.
- Report per-file success back to the page for a "synced ✓" state. Incremental only (V4).
Acceptance: on rha.software (hosted), after granting a folder, a build (and each recompute) lands the
changed files in that folder with no download — path-escapes rejected, permission re-verified.

---

## PART H — Verification (run after all tasks)
```bash
npm run typecheck
npm test
npm run build:web
grep -rniE "CC.?BY|creative ?commons" web src README.md CURSOR_BACKLOG.md | grep -v node_modules   # none
grep -rn "andyfunke" extension/                                                                     # none
grep -rn "#a78bfa" web/                                                                             # none
```
Manual smoke:
- Extension installed (any version) → Setup shows green "detected"; no update/mismatch nag.
- No extension → red "not detected" + download banner.
- Local model → green badge "local · <model>"; cloud model → green badge "cloud · <model>".
- `RHA_CANONICAL` unset → logo inert, footer about/privacy → rha.software; `/api/app.zip` has no landing.html.
- After two updates, `patches/` holds exactly one snapshot.
- Fresh instance: owner panel at `/<ADMIN_SLUG>` shows "Create admin password" on first launch; after setting one, restart requires it; `data/admin-auth.json` holds only salt+hash. Runtime-config card changes take effect on the next job.

---

## PART I — Human decisions (NOT for Cursor)
1. **`info@rha.software`** — confirm it routes (used in LICENSE / COMMERCIAL-LICENSE.md / README).
2. **Repo visibility** — `andyfunke/rhawrapper` is PUBLIC; decide whether to flip private.
3. **Ethos wording (A1)** — proposed reframe; adjust tone if desired.
4. **Counsel / patent** — the license protects the code, not the algorithm.
```
