# easel — agents.md

Project constitution. Inherits from `/srv/CLAUDE.md` (server-scope) and
`/srv/apps/CLAUDE.md` (tenant-scope, interim). Only rules meeting the
admission rubric (failure-backed, tool-enforceable, decision-encoding, or
triggerable) belong here — no generic advice a model already applies.

Model-independence: this file is the source of truth. `CLAUDE.md` is a
symlink to it so Claude Code loads the same content.

## Read first (in order)
1. **`brief.md`** — what this project is; stack (decided: BW app, FastAPI+React).
2. **`bw-app.toml`** — the deviation surface for the account system (levels,
   instances on/off, invites, View As). Edit + re-run the scaffolder to change
   it; don't hand-edit the generated `main/app/bw_config.py`.
3. **`.logs/handoff.md`** — newest-first; latest session state.

## Project-specific rules

- **This is a Pattern B app — never gate the host.** `easel.bowden.works` owns
  its own `/login` + `/auth/*`; a domain gate would 404 sign-in. The gateway
  refuses to gate a registered app-client host anyway. To hide the app before
  a real launch, use `srv-gw id-site-set --host easel.bowden.works --lan-only
  true`, never a password gate.
- **The vendored kit files are standard-owned, not app code.**
  `main/app/{bw_auth,bw_accounts,bw_view_as,bw_admin_api}.py`, the conformance
  tests, and `main/frontend/src/bw-admin/` are refreshed byte-for-byte on every
  scaffolder run — don't hand-edit them; changes belong in `bw-app.toml` or in
  your own routers/components.
- **This is one of many caddie drop-ins, not one app.** Before adding a
  feature or making a significant change, check it against caddie: either it
  fits the standards as they stand (`/srv/apps/caddie/.logs/planning/05-…`,
  `02-…`, `04-…`, the `caddie-ui` pack README), or it is a deliberate,
  written case for making caddie more globally flexible — recorded in
  `.logs/planning/caddie-ui-standard-proposal.md` (UI/architecture) or in
  caddie's runbook §5 (build learnings). Never a third thing: an easel-only
  convenience that quietly diverges is drift, and every later app inherits it.
  Concretely: shared chrome (the two-row shell, the side panel, the bell,
  threads) comes from `main/frontend/src/caddie-ui/` — vendored, byte-
  identical, never hand-edited in this app. If the pack lacks something,
  add it to the pack with a README line, not beside it.
- **No em dashes in anything an AI session writes here.** rian's rule, relayed
  by Adi (2026-09-02): an em dash reads as unedited machine output, and this
  app's words are read by clients. It covers the app's own copy first, and
  also docs, changelog entries, commit messages and code comments. Write a
  comma, a colon, a full stop, or brackets instead, and rephrase when none of
  those fit rather than reaching for the dash. Existing text is not swept;
  this binds new writing.

- **Verify the account system with `--check`, not by eye.** `/srv/system/id-auth/app-auth/new-bw-app.sh
  --dir /srv/apps/easel --check` runs the conformance pack (needs the venv at
  `.bw-venv/` — `python3 -m venv .bw-venv && .bw-venv/bin/pip install fastapi
  itsdangerous httpx pytest` once if missing). A green run is the mechanical
  definition of "full user system present," not a visual pass through the UI.

## Operational
- **Type:** custom (FastAPI + React, Pattern B app). Port `172.17.0.1:3153`.
  **Note:** this port was reassigned twice during creation — `3151` and `3152`
  were already bound by another project's (`hartlingowners`) manually-configured
  container ports that the gateway's port registry doesn't know about. If a
  future `srv-gw deploy` here ever fails with "port already allocated" for a
  port that should be free, that registry/reality drift is the likely cause —
  check `docker ps` port bindings fleet-wide before assuming it's this app's fault.
- **Manage:** `srv-gw {deploy,restart,logs,status} --project easel`. Deploys
  with a Docker build step (compiles the SPA) — use `--build` after `main/`
  changes; plain `deploy`/`restart` won't pick up frontend edits.
- **Write access:** `easel-dev` Unix group (setgid + default ACL). If a fresh
  terminal session can't write to project files, run `newgrp easel-dev` once,
  or `srv-gw fix-permissions --project easel` if a directory the gateway or
  scaffolder created isn't group-writable yet (hit this on `.logs/` and on
  `main/` after the first scaffold — the session running the scaffolder didn't
  have `easel-dev` in its groups yet).

## Layout (v8 project shape, interim state)
```
/srv/apps/easel/
├── brief.md              # project genesis (fill me in)
├── agents.md              # this file — project constitution
├── CLAUDE.md              # symlink → agents.md (Claude Code discovery)
├── .logs/
│   ├── .last-deploy       # gateway-owned deploy marker — do not remove
│   ├── planning/          # plans live here (type: plan frontmatter)
│   ├── handoff.md         # append-only, newest-first
│   └── diary.md           # session summaries (interim: manual)
├── .memory/               # project memory (append-only)
├── .archive/              # preserved indefinitely; never auto-purged
├── notes/                 # human reference material — NOT agent-loaded
│                          # unless referenced from brief.md
├── bw-app.toml            # BW app deviation surface (edit this, not bw_config.py)
├── .app.env               # APP_SESSION_SECRET (scaffolder-generated, 0600)
├── .bw-auth.env           # BW_CLIENT_ID/SECRET (gateway-written, 0600, never printed)
├── .bw-venv/              # local venv for running --check (not shipped/deployed)
├── data/                  # SQLite accounts store (runtime state)
├── docker-compose.yml     # gateway-scaffolded, replaced by the BW template
├── .env  .project.json    # gateway-managed
└── main/                  # the instance — code only
    ├── app/               # FastAPI app incl. vendored bw_* kit files
    ├── frontend/           # React SPA incl. vendored bw-admin/ pack
    └── tests/             # conformance pack (standard-owned) + your own tests
```

The v8 target is instance-granular gateway registration (`create-project`
workspace + `add-instance` deliverable, T3.1). Until that lands the registry
knows only the project, and the compose file must sit at the workspace root.
The folder shape above is already compatible.

## Standing tasks (delete when done)
- **M2 is Adi's**: `.logs/planning/next-milestone-onboarding-and-variants.md`.
  rian's four asks, kept honest:
  1. ~~tool walkthrough on arrival~~ — done v0.6.0
  2. ~~sidebar tour contents~~ — done v0.6.0, refined v0.8.0 / v0.13.0
  3. ~~two options with toggleable variants~~ — done v0.9.0
  4. ~~smaller hero for option 2~~ — CODE done v0.9.4
     (`leaguelaw/prototyping/home-bold-welcome-reduced-hero.html`, hero 655px =
     the live site's own height). **Not yet on the live league board.**
  The three model decisions are recorded in `.logs/planning/road-to-usable.md`
  §1a — a variant choice IS part of the locked direction, variants are a CSS
  class through the bridge, pins stay per-option. Don't silently re-open them.
- **The BUILD is ahead of the BOARD.** Everything above ships in the app; the
  live `league` project still carries the pre-M2 content — three old options
  plus a stray test one, the seeded internal blurb, no reduced-hero bundle and
  no variant CSS. Closing that gap is UI work (upload folder / remove / edit
  blurb), not a deploy, and it is the last thing before darren.
- **§0 has only half happened.** rian asked for a full run as darren before the
  four builds. The functional half is done (findings in the handoff); **nobody
  has looked at any M2 work in a browser, at any width.** That is the gate.
- **Don't weaken mockup isolation.** The `/m/` route's CSP `sandbox
  allow-scripts` header, the `allow-scripts`-only iframe, and path-token auth
  are one mechanism. Adding a sandbox token to make a mockup behave is a
  stop-and-ask (see agents.md rules above).
- **league's status now reads "Awaiting review", i.e. it HAS been presented.**
  The original rule was that rian presses it himself once he has told darren.
  Either he did, or it was pressed during testing — worth confirming with him
  before the real send. Nothing in easel has ever sent mail, so no message
  reached darren either way.
