---
description: Autonomous goal — rebuild easel as a caddie app, seeded with the league project
---

# /goal — Rebuild easel as a caddie app

You are running an **autonomous, orchestrated build session**. The goal is to
rebuild easel (`easel.bowden.works`, port 3153) as a caddie-conformant mockup
presentation & feedback app, working from Adi's `proto-preview` WordPress
prototype **as a spec, not as code**, and to finish with a real seeded project
(league) that rian, adi, and darren can use. Get as far as you can without
rian; make reasonable judgment calls and log every one; where the caddie
framework itself creates friction, feed that back through the documented
channels so the framework improves.

Orchestrate with the Workflow tool where parallelism or adversarial
verification genuinely helps (spec extraction, component builds, security
review of the serving/sandbox layer, conformance sweeps); keep architecture
decisions and all judgment calls in the main loop. You may run for as many
phases as needed — checkpoint (handoff + diary + CHANGELOG) at every phase
boundary.

## 1. Read first, in this order

1. `/srv/projects/standards/README.md` → `coding.md` → `react.md` → `frontend.md`
2. `/srv/apps/caddie/.logs/planning/05-building-a-caddie-app.md` — the runbook,
   **including §5 Learnings** (real traps: `.app.env` perms, `app_conftest`
   `__all__` star-import trap, SQLite-vs-Postgres dialect gaps,
   `JSON(none_as_null=True)`, Alembic-only on pg)
3. `/srv/apps/caddie/.logs/planning/02-tool-contract.md` and
   `04-interaction-standard.md`
4. `/srv/system/id-auth/app-auth/PACKS.md` — the managed-store mechanism
5. easel's own `.logs/planning/caddie-rebuild-scope.md` and
   `inputs-2026-08-28.md` — the port map of Adi's plugin and the security
   analysis
6. Adi's plugin as spec: `/srv/apps/prototype/wp-content/plugins/proto-preview/`
   (~14.7k lines — fan out readers rather than reading serially)
7. `/srv/apps/leaguelaw/prototype.md` — the **direction-gate ruling** (2026-08-20)
8. `/srv/apps/punchlist/` — the precedent caddie tool (seams-only, like you)

## 2. Decisions already made — do not relitigate

- **Members only. No anonymous commenting.** Every participant is a BW
  account. Clients are `external`-class accounts granted per-project inside
  easel. Adi's `user_id=0` + typed name/email path is explicitly dropped.
- **easel is the mockup presentation & feedback step** of the client journey.
  scout owns research (before); review is a separate future caddie app for
  build signoff (after). Do not absorb either; do not add staging-URL review.
- **Adapt, don't clone.** Fidelity to Adi's UI is not required. What must
  survive: the project → screen → option model; the full-height live-HTML
  viewer; pinned comment threads with open/resolved; play mode vs comment
  mode; option selection as the direction-locking act; the guided walkthrough
  with per-step approvals; the overview grid.
- **Instances ON.** Flip `bw-app.toml` to `has_instances = true`,
  `instance_noun = "project"`, then re-run the scaffolder with
  `--dir /srv/apps/easel` (after first run the toml is authoritative; the
  re-run regenerates `bw_config.py` and re-vendors kit files; it never
  touches your app code). Clients get granted on instances, never app-wide.
- **The caddie stack bar applies in full** (runbook §3): Postgres sidecar
  from day one (internal compose network, **no published port**, healthcheck),
  SQLAlchemy 2.0 + Alembic, the auth-kit store moved into the same Postgres
  via `bw_store_sqlalchemy` managed mode, TS client generated from OpenAPI in
  the image build, TanStack Query, vendored primitives, phone-width floor,
  version + newest-first CHANGELOG surfaced at `/api/meta`, `client_party_id
  uuid NULL` on the instance table (populate later; `with` is
  system-of-record), append-only event tables for state changes, UI consumes
  capability booleans, never level names.
- **Seams now, drop-ins later.** `caddie_tool.py` / `caddieHost.ts` /
  `bw_interaction` do not exist yet (caddie M2). Build the seams: an
  `external_ref` on the instance table, a `caddie_links`-shaped join table, a
  status-rollup function onto the frozen six states, and an interaction layer
  whose tables match the standard's §4 shapes so the kit swap is a
  substitution. Do NOT register easel with caddie, and do NOT implement the
  launch-token/S2S surfaces yourself — that arrives with the vendored drop-in.
- **The product's north star is the leaguelaw direction-gate ruling:** the
  client confirms a *direction* ("sedan vs SUV") with near-zero effort, before
  any page-level work. Option selection + the walkthrough are how easel
  operationalizes that. Client-effort rule in full: no extra steps, no
  homework, sign in and react.

## 3. Security requirements — non-negotiable

The mockups are hand-authored HTML **with scripts**, uploaded by managers.
Adi's plugin serves them same-origin as trusted code — that is the one thing
you must not reproduce. The isolation design has three parts; all of them.

1. **Opaque origin, enforced on the response, not just the embed.** The
   mockup-serving route MUST send `Content-Security-Policy: sandbox
   allow-scripts` on every mockup response — this gives the document an
   opaque origin *no matter how it is reached*, including direct navigation
   to the file URL (a shared link, "open frame in new tab"). The viewer's
   `<iframe sandbox="allow-scripts">` is then belt-and-suspenders. The
   app-wide strict CSP (which you should also ship) does NOT apply to mockup
   responses — the CSP-sandbox header IS their policy; never "fix" a broken
   mockup by exempting its response from sandboxing. Defense-in-depth: refuse
   mockup requests whose `Sec-Fetch-Dest` is neither `iframe` nor `document`-
   from-your-own-viewer; log and 403 the rest.
2. **The sandbox token list is frozen.** Exactly `allow-scripts`. Adding ANY
   token (`allow-same-origin`, `allow-top-navigation`, `allow-popups`,
   `allow-popups-to-escape-sandbox`) to make something work is a §8
   stop-and-ask, never a judgment call — `allow-same-origin` in particular
   silently converts every mockup into code running as easel.
3. **postMessage, with the opaque-origin idiom done right.** A sandboxed
   frame has origin `"null"`, so the naive "strict origin checks both ways"
   is unimplementable — use the correct per-side checks instead. Parent side:
   authenticate inbound messages by `event.source ===
   viewerIframe.contentWindow` **plus a per-load random nonce** injected with
   the bridge script (expect `event.origin === "null"`; treat every payload
   as untrusted data), and send into the frame with targetOrigin `"*"`
   (acceptable only because the frame is opaque). Bridge side (injected
   server-side into served mockup HTML): check `event.origin` equals the app
   origin on inbound, post to the parent with the app origin as explicit
   targetOrigin. If session cookies don't ride into the sandboxed load, auth
   the file fetch with a short-lived signed per-file token in the iframe src
   — never by weakening the sandbox.
4. **Uploads.** Comment/screenshot attachments get the Interaction Standard's
   treatment **verbatim** (images + PDF only by sniffed type, size cap,
   random stored names, EXIF stripped). Mockup bundles are deliberately
   script-capable, so the sniff rule cannot apply to them — instead: explicit
   extension allow-list (html/css/js/images), hard size cap, random stored
   names, stored under the workspace `data/` dir, never a static mount,
   served only via the §3.1 route; the sandbox + CSP controls above are the
   compensating control for the scripted type. Upload requires manager
   capability.
5. **Port bindings.** Everything stays on the Docker bridge: the app exactly
   at `172.17.0.1:3153`, the Postgres sidecar with **no published port at
   all**, nothing ever on `0.0.0.0`, no second published port. The `ports:`
   stanza in the scaffolded compose is gateway-managed — carry it through the
   rewrite unchanged.
6. **Secrets.** The new Postgres password goes in a 0660 group-owned env file
   at the workspace root (same pattern as `.app.env`), referenced from
   compose via `env_file:` only — never in `environment:`, never interpolated
   into the compose file, never printed into diary/handoff/CHANGELOG/report.
   Never print the contents of `.app.env`, `.bw-auth.env`, or the new DB env
   file; verify wiring by behavior (does the app connect?), shape-check with
   `grep -c` if you must.
7. **Standing server rules, restated because you'll be near them:** never
   gate `easel.bowden.works` (Pattern B — a gate 404s sign-in; if the app
   must be hidden mid-rebuild, `srv-gw id-site-set --host easel.bowden.works
   --lan-only true` is the only sanctioned mechanism). Never read, write, or
   work around anything under `/srv/.private/` — registry/DNS needs go
   through `srv-gw`.
8. A separate serving *origin* (own subdomain) would close the residual gaps
   CSP-sandbox can't (e.g. exotic user agents) — that needs DNS + Caddy, i.e.
   rian. Propose it in your final report; do not block on it and do not touch
   DNS/Caddy yourself.

Run `srv-gw security-audit` after deploys that change surface area, and once
at the end.

## 4. Access model

**Use the kit's default level names** — the conformance pack asserts `admin`
and `member` exist (`tests/test_levels.py`), so do not rename them. The
semantics: **admin = the team/manager side** (create projects, upload
mockups, author walkthroughs, comment, resolve, grant clients on projects);
**member = the client side** (instance-scoped via grants: view granted
projects, run the walkthrough, comment, approve steps, select options —
nothing app-wide, no uploads). Adjust the two levels' permission sets in the
seeds as needed; if friendlier *display* names seem worth having, that's a
v8-feedback item, not a rename.

People: **rian** is the code-level owner (not a member row). **adi** — keep
his existing `admin` row (migrate it intact through the store swap).
**darren** (existing BW account `darren`, external class) → `member`, granted
on the league instance only. Run `srv-gw id-user-grant --user darren --site
easel.bowden.works` for the bookkeeping grant. **Do not email darren** — no
invites, no resets, no notifications leaving the server. rian will tell him
himself.

## 5. The seed project — league

Create instance **"League & Williams — website concepts"** (label your call)
for the leaguelaw rebuild. Real concept HTML exists at
`/srv/apps/leaguelaw/prototyping/` (231 files — a working library, not a
curated set; you may read it, nothing else in that project). **Curate, don't
dump:** seed a Home screen with 2–3 genuinely distinct direction options
drawn from the strongest home variants, and one secondary screen if the
material supports it. Copy files into easel's own storage (easel owns its
data; never serve across project directories). Author a first walkthrough
(3–5 steps) on the leading option. Mark the seed clearly in the project
description as curated-by-Claude so Adi knows to review the selection.
Comments/approvals start empty — they're darren's.

## 6. Status rollup (the six frozen states)

`not_started` = no options uploaded · `in_progress` = being composed, nothing
sent · `waiting_client` = presented, no unresolved client action ·
`waiting_agency` = open client pins / feedback awaiting us ·
`needs_attention` = mapping ambiguity or a client-flagged blocker · `done` =
an option selected and walkthrough approvals complete. An empty instance must
report `not_started`, never `done`. Implement as a pure function over the
instance, with tests; it becomes the contract's status body later.

## 7. Framework feedback — a first-class deliverable

- Append caddie-relevant learnings to
  `/srv/apps/caddie/.logs/planning/05-building-a-caddie-app.md` **§5** as you
  hit them (that section exists for exactly this; punchlist did the same).
- Log substrate/gateway friction in easel's `.logs/v8-feedback.md`
  (what happened, what it cost, candidate fix).
- **Pins fit the Interaction Standard without forking it** — use the mapping
  from `caddie-rebuild-scope.md` §3: one thread per pin
  (`subject_type="pin"`, synthetic `subject_id`), with `x_percent` /
  `y_percent` / lock state on an easel-owned `pin` table *beside* the
  standard-shaped thread/comment/attachment tables, whose shapes you keep
  byte-compatible (including `UNIQUE(subject_type, subject_id)`). Reserve
  "write it up as a divergence proposal" for anything genuinely beyond that
  mapping — do not edit `02-tool-contract.md` or `04-interaction-standard.md`
  yourself.
- Ops-queue items (e.g. a kit system actor, a mockup-serving subdomain) go to
  `/srv/.logs/ideas.md`.

## 8. Autonomy contract

**Make the call and log it** for: naming, schema details, UI layout, level
permission sets, curation choices, test scope, what's in/out of the embryo.
Every judgment call gets a line in `.logs/diary.md`; material ones go in the
handoff.

**Stop and ask rian** only for: anything leaving the server (email to darren
or anyone), DNS/Caddy/firewall changes, any sandbox-token addition (§3.2),
modifying another project's code (the caddie §5 append and the read-only use
of leaguelaw's prototyping files are sanctioned; nothing else is), spending
against a decision in §2, or a security trade-off not covered by §3.

**Verify by behavior:** deploy with `srv-gw deploy --project easel --build`
(frontend changes need `--build`; if a port conflict appears, check
fleet-wide `docker ps` first — see `agents.md` on registry drift), then drive
the live app. Green tests + green conformance pack + a live click-through
beat any amount of code reading.

- Conformance: `/srv/system/id-auth/app-auth/new-bw-app.sh --dir
  /srv/apps/easel --check`. After the SQLAlchemy store swap, first install
  the new backend deps into the check venv (`.bw-venv/bin/pip install
  sqlalchemy` plus anything else `app/` imports at module level) — the
  `--check` preflight does not test for them.
- Live verification path: create a throwaway insider QA account via `srv-gw
  id-user-invite --name easel-qa --no-email` (prints the set-password link —
  complete it yourself in a browser if one is available), grant it on easel
  at `admin`, use it for the click-through and for View As of darren, then
  deactivate it at the end. If no browser is available, conformance +
  TestClient-driven flows are the accepted bar and the live click-through
  moves to the owner-actions list in your final report.

## 9. Done means

1. easel deployed and healthy; conformance pack green; own test suite green;
   `srv-gw security-audit` shows nothing new for easel.
2. rian can sign in → open the league project → see the overview grid → open
   an option full-height → run the walkthrough → drop a pin → resolve it →
   select an option, all at desktop and phone width.
3. darren's view (via View As): league only, member capabilities only.
4. Status rollup returns sensible values across the seed project's lifecycle.
5. Caddie seams present and tested; interaction tables match the standard's
   shapes; the swap-plan for `bw_interaction` written in the handoff.
6. Framework feedback delivered (§7), CHANGELOG current, handoff written:
   what landed, every judgment call, what's deferred to caddie M2, and the
   short list of owner-only actions rian still needs to take.
