---
type: plan
milestone: M1 — the direction loop
role: This file is BOTH the M1 plan AND the prompt for an autonomous overnight
      build session. The orchestrator is Fable; it delegates to lower-tier
      subagents. Read it start to finish before writing any code.
---

# M1 — the direction loop (overnight build)

## Mission

Ship Scout v0.2.0: the full loop that turns a client's rambling redesign input into
a ranked, annotated design direction — deployed, verified, and **dogfooded on the
real League Law project** so rian wakes up to draft options ready for his review.

The loop, end to end:

```
1 INTAKE     admin dumps client materials into the project (notes, files, links)
2 EXPORT     one click → a briefing package rian feeds to a Claude session
3 RESEARCH   that Claude session (NOT Scout) finds candidate sites + screenshots
4 IMPORT     admin uploads the research package → options appear, in DRAFT
5 REVIEW     each client stakeholder plays a 5–15 min rating game on their phone
6 REPORT     admin reads the rollup; exports the "Scouting Report" (markdown)
```

Scout contains **zero AI calls** in M1. Claude does the thinking *outside* the app,
connected through two file contracts (the briefing export and the research import).
That keeps the app deterministic, the pipeline debuggable, and tonight buildable.
Do not add an LLM client, an API key, or a queue to Scout tonight.

## Orient first (in order, before any code)

1. `agents.md` — the constitution. Every rule in it binds tonight; three matter
   most: never gate this host, the column-scoped upsert, default-deny middleware.
2. `brief.md`, `.logs/planning/01-baseline.md` — what exists and why. M0's locked
   decisions D1–D5 still stand. This file continues the numbering.
3. `/srv/projects/standards/coding.md`, `react.md`, `frontend.md`.
4. `League Law's next phase - time for a new website.eml` (workspace root) — the
   real client thread. Parse it with Python's stdlib `email` module (it worked in
   the M0 session). Darren's final message IS the requirements source for the
   dogfood step.
5. `.logs/handoff.md` — current state. The app is deployed and healthy at v0.1.0.

## Locked decisions (D6–D16)

**D6 — Claude works outside Scout, through two contracts.** Export produces what a
Claude session needs; import accepts what that session produces. Scout never calls
a model. (Rationale: matches rian's "downloadable package" workflow, removes API
keys/cost/latency from the app, and makes every stage inspectable on disk.)

**D7 — Reviews are per-person, never shared.** Each member rates independently and
never sees another member's ratings. Disagreement between stakeholders (four people
are CC'd on the League Law thread) is *signal for rian*, not noise to average away
in front of the client. Admin sees per-person and rollup views.

**D8 — Options are anonymized by default.** Clients react to "Option C", a neutral
descriptor, and screenshots — not to firm names (big-name bias is real). The real
name/URL is always visible to admins; a per-option `reveal_source` flag (default
false) can show it to clients when rian chooses.

**D9 — The game is: gut rating → aspect reactions → optional note → final pick.**
One option on screen at a time. A 4-point forced-choice scale (no neutral
midpoint): 0 "Not for us" · 1 "Could work" · 2 "Like it" · 3 "Love it". Then
per-aspect thumbs (👍/👎/clear) on a fixed vocabulary. Then one optional note.
After all options: a final pick — "which one is closest to how *your* site should
feel?" — chosen from the ones they rated ≥2 (fall back to their single
highest-rated if none reach 2), plus one free-text "anything else we should know?"

**D10 — Aspect vocabulary is fixed in `app/constants.py`** (single authority,
served via `/api/meta` like the other vocabularies). Six aspects, keys → labels:
`colours` "Colours" · `typography` "Fonts & type" · `imagery` "Photos & imagery" ·
`layout` "Layout & spacing" · `personality` "Personality" · `navigation` "Menus &
structure". Votes are -1/0/+1 per (option, user, aspect).

**D11 — Option order is randomized per reviewer, stable across visits** (seed =
hash of username + project id). Kills order bias; resuming keeps the same order.
Admin screens use canonical (sort_order) order.

**D12 — Every interaction autosaves.** Each tap PATCHes immediately (upsert
semantics, idempotent). A partial 4-minute session is still usable data. There is
no "submit" cliff; "Finish" just marks their review complete (they can reopen —
no locking in M1).

**D13 — Materials and options live on disk at the workspace root** (`data/` —
runtime state stays out of `main/` per v8), bind-mounted `./data:/app/data`.
Postgres stores metadata only. **All files are served through authed endpoints
with membership checks — never via `/assets` or any public path.** Uploads:
25 MB/file cap, extension allowlist (png jpg jpeg webp pdf txt md eml docx),
stored under a UUID name; original filename kept as metadata only.

**D14 — Clients see exactly one thing: the review experience.** Materials, export,
import, results, and the report are admin-only. A member opening a project with no
published options sees a friendly "we're still preparing your options" state.

**D15 — Screenshot capture is host-side tooling, not an app feature.** A script in
`tools/` runs Playwright inside the official Docker image already present on this
host (`mcr.microsoft.com/playwright/python:v1.49.0-noble`) — zero host installs,
nothing added to the app image. Scout itself never crawls in M1.

**D16 — The deliverable of the whole loop is the Scouting Report** (on-brand name;
use it in the UI). Design it first, then make the data collection serve it.

## Build order (each stage ends DEPLOYED and healthy — never leave main broken)

Deploy incrementally with `srv-gw deploy --project scout --build` after each
stage. If morning arrives early, cut from the bottom, never the middle.

**Stage A — schema + options + import (the spine).**
- Alembic revision `0002_direction_loop`. New tables:
  - `materials` (id, project_id FK, kind: note|file|link, title, body text for
    notes, url for links, file_path/file_name/mime/size for files, added_by,
    created_at)
  - `options` (id, project_id FK, slug, display_label "Option A", descriptor short
    neutral phrase, source_name, source_url, why_selected text admin-facing,
    design_notes text, reveal_source bool default false, status draft|published,
    sort_order int, screenshot_desktop path, screenshot_mobile path, created_at)
  - `reviews` (project_id, option_id, username, rating int nullable 0–3, note
    text, updated_at; PK option_id+username)
  - `aspect_votes` (option_id, username, aspect, vote -1|1; PK option+user+aspect)
  - `final_picks` (project_id, username PK pair, option_id, closing_note,
    completed_at)
- Routers: options CRUD (admin) + publish toggle + reorder; research-package
  import endpoint (zip upload → validate manifest → create draft options + store
  screenshots). **Guard against zip-slip** (reject entry names with `..` or
  absolute paths); reject unknown schema_version; import is idempotent by option
  slug (re-upload updates drafts, never touches published options' collected
  reviews — republishing an updated screenshot on a published option is allowed,
  reviews stay).
- Authed file-serving route for screenshots (members) and materials (admins).
- Add `python-multipart` to requirements. Add the `./data:/app/data` volume to
  compose (only that line — port binding and env_file blocks are untouchable).

**The import contract** (authoritative; also embedded in the export package for
the research session to follow). Zip layout:
```
manifest.json
screenshots/<slug>-desktop.png
screenshots/<slug>-mobile.png
```
manifest.json:
```json
{
  "schema_version": 1,
  "project_slug": "<must match the Scout project>",
  "options": [
    {
      "slug": "kebab-case-stable-id",
      "display_label": "Option A",
      "descriptor": "Confident & structured",
      "source_name": "Firm Name LLP",
      "source_url": "https://…",
      "why_selected": "admin-facing: which client criteria this hits",
      "design_notes": "admin-facing: what to look at",
      "suggested_aspects": ["layout", "navigation"],
      "screenshots": {"desktop": "screenshots/x-desktop.png",
                       "mobile": "screenshots/x-mobile.png"}
    }
  ]
}
```

**Stage B — the review game (the heart; spend the design effort here).**
- Client-facing screens, mobile-first (the League Law partners will do this on
  phones), built from the existing token/component system — extend tokens, no
  inline styles, no new UI library:
  1. **Welcome** — project name, the framing ("Help us pin down the look and feel.
     ~10 minutes, no wrong answers."), progress if returning, one big Start.
  2. **Option cards**, one per screen: the screenshot in a scrollable framed
     viewport (desktop/mobile toggle; default mobile capture on narrow screens),
     descriptor, the 4-point gut scale as big tappable buttons, the six aspect
     rows with 👍/👎 toggles under a "What stands out? Skip anything you didn't
     notice." prompt, an optional note field, Next/Back. Progress dots.
  3. **Final pick** — the qualifying options as small cards; pick one; one
     closing question ("Anything else we should know?"); Finish.
  4. **Done** — thank-you, what happens next, their picks at a glance, "revisit
     your answers" link.
- Feel: smooth, quick, quietly satisfying — micro-transitions on selection,
  instant response to taps (optimistic updates via the query layer), progress
  always visible. Professional-playful, not confetti. These are law firm
  partners; the fun is *how little friction there is*, not stickers.
- Every tap persists via the autosave endpoints (D12). Kill the network and
  reload: state comes back.
- Admin preview mode: admins can walk the same flow read-only-into-their-own-row
  (admins' reviews are stored too — rian's own ratings are useful — but excluded
  from the client rollup by role, shown separately).

**Stage C — results + the Scouting Report.**
- Admin results tab per project: per-option rating distribution + mean, aspect
  net tallies (👍−👎 per aspect), all notes attributed, per-reviewer completion
  status, final picks. Call out the largest disagreement ("Darren loves C,
  Janice rated it Not-for-us") — consensus AND splits are the product.
- "Download Scouting Report" → markdown file assembled server-side:
  project brief header → per-option results (source revealed here — this is
  admin/designer-facing) → aspect story across options → every note verbatim →
  final picks → an empty "Direction (designer's synthesis)" section at the top
  for rian to fill, and an `INSTRUCTIONS-FOR-CLAUDE.md`-style footer explaining
  how to feed this report into the next design step.

**Stage D — intake + briefing export.**
- Materials tab (admin-only) on a project: paste a note, upload files, add links;
  list/delete. Standard field: `client_website` (add column to projects; also
  editable in project settings). The `.eml` at the workspace root is exactly the
  kind of file this must accept.
- "Download briefing package" → zip: `BRIEF.md` (project fields + all notes),
  `materials/` (the files), `links.md`, and `INSTRUCTIONS-FOR-CLAUDE.md` — a
  carefully written prompt that tells the receiving Claude session to (1) distill
  the client's stated direction with quotes, (2) research candidate sites against
  rian's three criteria — matches the client's stated vision; diversifies where
  the vision is ambiguous; avoids features outside project scope (no showing a
  resource-center-hero to a client who isn't buying one) — and (3) produce a
  valid research package per the import contract above (embed the full contract
  + the capture tool usage). This file is the pipeline's connective tissue —
  write it like it will be run verbatim by a fresh session with no context.

**Stage E — the capture tool.**
- `tools/capture/capture.py` + `tools/capture/run.sh`: reads a JSON list of
  {slug, url}, runs inside the Playwright image via `docker run --rm` with the
  output dir mounted, produces `<slug>-desktop.png` (1440×900 viewport,
  full-page) and `<slug>-mobile.png` (390×844, full-page), realistic UA, waits
  for network-idle + a settle delay, dismisses obvious cookie banners
  best-effort. Verify it on 2 real sites before relying on it.

**Stage F — dogfood: seed League Law for real.** (Requires A–E working.)
1. Parse the `.eml` → a clean `thread.md` (chronological, signatures trimmed).
   Move the original `.eml` into `notes/` for safekeeping.
2. Create project "League Law — 2026 redesign" (slug `league-law-redesign`,
   client_website `https://www.leaguelaw.com`, status active, created_by rian)
   via the service layer in-container (the M0 verify script pattern). Add
   `darren` as member (role user). Do NOT create BW accounts for the other
   stakeholders; list them in the project brief text instead.
3. Attach materials: the thread note, the distilled direction (quote Darren's
   own words — "league of teams", contact "front and centre", no "Rocky
   Mountains" heroes, practice-area zones, "modern, clean, sharp, organized, a
   little different"), and current-site captures of leaguelaw.com (baseline).
4. **Run the research yourself** (you have WebSearch/WebFetch): find 5–6 law-firm
   sites meeting Darren's stated direction with deliberate diversity on the axes
   he left open — colour temperature, density, personality (safe ↔ distinctive),
   how staff photography is used (new team photos arrive in October). Prefer
   contact-forward, practice-area-first layouts. Exclude: video/scenic heroes,
   scroll-animation showpieces, sites whose appeal depends on out-of-scope
   features (client portals, chatbots, resource-center-as-hero — the $5,900
   scope is rebuild + facelift). A couple of "best of the best" from larger
   markets + at least one BC/Pacific-Northwest firm. If a site won't capture
   cleanly, swap it — every option needs clean screenshots.
5. Capture, assemble the research package zip, import it through the real import
   endpoint (not by writing rows directly — this validates the contract), write
   descriptors and why_selected for each.
6. **Leave every option in DRAFT.** Client-facing content ships only after
   rian's eyes are on it. Do not publish, do not notify anyone.

**Stage G — hardening + handoff.**
- pytest suite (in-container against a `scout_test` database on the sidecar;
  install pytest+httpx via `docker exec` or a requirements-dev layer — tests
  must run against real Postgres because the upsert uses the PG dialect).
  Minimum coverage: the authz boundary (member vs non-member 404-not-403, admin
  routes reject users, file routes enforce membership), review autosave
  idempotence + rating/aspect validation, import validation (zip-slip, bad
  schema_version, republish-preserves-reviews), and a full flow test. To
  authenticate in tests, mint the session cookie with the app's own
  SessionMiddleware signer — do not add a test-only login route to prod code.
- Fix the known HEAD-on-`/` 405 (accept GET+HEAD on the SPA catch-all) — it's a
  one-liner and it's already listed in brief.md's known gaps.
- Bump `version.py` to 0.2.0 + CHANGELOG entry. Update brief.md (Status + Known
  gaps honestly — say what has automated coverage and what doesn't). Append
  handoff.md (newest-first: what landed, rian's morning checklist) and diary.md
  (what was verified, with counts). Add any substrate friction to v8-feedback.md.
- `chmod 664` new files / `775` new dirs (or `srv-gw fix-permissions`), then
  `srv-gw security-audit` and read the scout lines of the output.

## Orchestration (you are Fable; delegate, but own the spine)

- **Orchestrator owns:** schema + migration, API contracts, auth boundaries,
  compose changes, all deploys, integration, the dogfood research/curation, and
  every verification step. These never go to a subagent.
- **Delegate to `sonnet` subagents:** self-contained implementation chunks —
  individual React screens/components against a stated API contract, the capture
  tool, the pytest files, CSS/token work, the INSTRUCTIONS-FOR-CLAUDE template.
  Batch independent chunks in parallel. **Subagents get no conversation
  context:** each prompt must carry the relevant spec section verbatim, exact
  file paths they own, the component/token conventions (no inline styles, tokens
  only, existing components), and what "done" means. No two agents touch the
  same file. Subagents never deploy, never run srv-gw, never touch compose,
  `.env*`, or migrations.
- **Use `haiku`** for mechanical sweeps (permissions fixes, boilerplate lists).
- Review every subagent diff before integrating; a red `tsc` or ruff failure is
  an integration failure — fix before the next stage's deploy.

## Verification bar (prove it, don't assert it)

After each stage: build deploys clean, `/healthz` 200, `/api/meta` shows the
version, and the stage's feature exercised **through HTTP** with curl or the
in-container script pattern from M0 (see diary 2026-08-11). Final acceptance,
all through the deployed app:
1. Import a research package via the endpoint → draft options exist with
   screenshots served through the authed route (and 401/404 correctly for
   anonymous / non-member).
2. Simulated member session completes a review via the autosave endpoints;
   partial state survives; rollup reflects it; Scouting Report downloads with
   the data in it.
3. The League Law project is seeded, materials attached, 5–6 draft options with
   clean screenshots, darren a member, **nothing published**.
4. Full pytest suite green against real Postgres; `tsc` green (it runs in the
   image build); security-audit shows no new scout warnings.
Record all of it in diary.md with numbers, not adjectives.

## Guardrails (tonight-specific; agents.md still applies in full)

- Do not touch `/srv/apps/leaguelaw` (the WordPress project) beyond read-only
  reference, and never write outside `/srv/apps/scout` + your scratchpad.
- Do not email anyone, publish options, create/modify BW accounts, register
  clients, change DNS/Caddy/gateway, or run anything as another user.
- Compose: you may add the `data` volume mount and nothing else. Never touch
  `.env`, `.app.env`, `.bw-auth.env`, port bindings, or env_file lines.
- Screenshots of third-party public sites are internal design reference
  (standard mood-board practice) — fine. Don't hotlink or re-host them publicly;
  they live behind the authed route.
- No new Python/JS dependencies beyond: `python-multipart`, pytest/httpx (dev),
  and nothing new in the frontend. If you believe you need another dependency,
  you're off-spec — redesign within what's here.
- If a stage is unrecoverable, ship the stages before it, write the honest
  handoff, and stop. A green v0.2.0 missing Stage E beats a red everything.

## Morning handoff (what rian finds at 7am)

`handoff.md` topmost entry, containing: version deployed + what each stage's
status is (shipped/cut, with why) · the League Law project state and a direct
link · **rian's 10-minute checklist**: review the draft options and descriptors,
tweak/publish, then tell Darren it's ready (his reply drafts itself: "you asked
for examples you could rank — here they are: https://scout.bowden.works") · what
Stage 2 of the pipeline looks like when reviews come back (download the Scouting
Report) · anything cut and the cleanest path to finish it.
