# scout — diary

> Session log, newest first: what changed and what was verified. State, not summaries.

## 2026-08-18 (later) — M4 full BW Auth suite (v0.4.0 → 0.4.1)

- Read the contracts first: VIEW-AS.md, bw_view_as.py API, invite/send_reset/
  user_access/profile_html docstrings, ACCOUNTS.md invite section. Vendored
  `bw_view_as.py` byte-identical; ruff ignores extended.
- Backend built by the orchestrator (view-as service/policy/router, level admin,
  matrix, invites, resets, forgot, delete-project, /api/me extensions,
  middleware write-block, migration 0005). OpenAPI verified in-container before
  the UI agent got the contract: 39 paths.
- Subagents: 2. UI agent built all panels AND deployed 0.4.0 itself (my prompt
  omitted the no-deploy rule this time — benign here, noted for next time); it
  flagged a residual gap (admin-target readonly view relies on backend 403 for
  admin tabs) and a validation nit. Tests agent wrote 31 tests, found TWO REAL
  BUGS and one catalog-drift, triaged its own 3 test bugs, never touched prod.
- Bugs fixed in 0.4.1: (1) assignability now resolves per-project
  (`assignable_on`) so the scoped-coordinator level actually works; (2) view-as
  rank derives from permissions, not level names, so custom levels can hold
  `scout.view_as` usefully; (3) account routes enforce the exact catalog
  permissions (add/change_level/delete); (4) invite can no longer sidestep the
  level-change guards on existing accounts (found in orchestrator self-review).
- **92/92 pytest** green in-container against real Postgres, all outbound BW
  calls stubbed through one funnel and recorded. **30/30 live checks** after
  0.4.1 plus the view-as edge script (admin-as-target, admins-have-no-view-as).
- No invite or reset email was fired at any real address during verification —
  forgot-password was exercised with nonexistent usernames only (constant
  response proven). Live email flows are rian's manual pass, by design.
- `notes/bw-auth-feedback.md` written as the milestone's deliverable: 11
  tensions, 5 gaps, works-list, bugs; T8–T11 added during the build as they
  were found.

## 2026-08-18 — M2 per-project levels (v0.3.0)

- Verified the two files the id-auth session added (`bw_auth.py`, `bw_accounts.py`)
  were byte-identical to `/srv/system/id-auth/app-auth/` before trusting them.
  `python3 main/app/bw_accounts.py` self-check: all passed, including its
  non-SQLite custom-store cases.
- Backed the database up to `backups/pre-0003-levels-*.sql` before migrating.
- Migrations `0003_per_project_levels` (app_levels; level + all_instances on
  app_accounts; level on project_members; role dropped) and
  `0004_staff_grants_not_demoting` (repairs grants that sat below their holder's
  app-wide level).
- Two builds failed before landing, both caught by `tsc` over the generated
  client: a `client_website` field the create dialog sent but the router dropped
  (fixed, v0.2.1), and an unused `levelLabel` binding. The generated-client gate
  earned its keep again.
- **Bug found by verifying data rather than trusting the migration:** `adi`
  (app-wide admin) resolved to `reviewer` on project 5 with `can_manage=False`,
  because 0003 wrote a default `reviewer` grant over their `all_instances`
  standing. Fixed in three places — data (0004), cause (`add_member` clamps),
  and a regression test.
- Subagents: 2 (frontend screens, test suite). The frontend agent correctly
  refused to edit `App.tsx` (not its file) and left a documented fallback; the
  orchestrator then removed that fallback by passing `me` properly, since a
  `level === 'admin'` comparison is exactly what agents.md forbids. The test
  agent reported a stale `ADMIN_REQUIRED` assertion in a file it did not own.
- Two of its tests failed on first run — both test bugs, both informative: one
  built its fixture through `add_member` (which now clamps), so it was split into
  two tests documenting both rules; the other expected an app-wide
  `effective_level` of `None` for an account that legitimately holds one.
- **61/61 pytest** green against real Postgres (`scout_test`).
- **14/14 live HTTP checks** against the deployed app and production data:
  anonymous 401; reviewer 200 on their project but 403 on its results; non-member
  404 (not 403); admin 200 on both; owner allowed everywhere despite holding no
  level row; deactivated account 401; project-visibility counts 0/1/3 correct.
- Demonstrated the point of the milestone on real data: `mchen` is a reviewer on
  League Law and a lead on Harbour Dental.

## 2026-08-12 (overnight M1 build — autonomous session)

Executed `.logs/planning/02-m1-direction-loop.md` in full. Deploys: three
(`0.1.1` Stage A backend → unbumped Stage B/C/D frontend build → `0.2.0`
final). One build failure total: the Stage A first build, caught by `tsc` —
`ProjectCreate` gained `client_website` and `Projects.tsx` didn't pass it
(openapi-typescript marks defaulted fields required). The generated-client
mechanism did its job.

**Verified, with numbers:**
- Migration `0002_direction_loop` ran on deploy (5 new tables + 1 column).
- 36/36 in-container HTTP checks against the LIVE server (minted session
  cookies, throwaway accounts, cleaned up after): import contract (zip-slip,
  bad schema_version, slug mismatch, not-a-zip all 422 with the right
  error_code; 3 options created as drafts), draft invisibility (member bundle
  empty, member draft screenshot 404, admin 200), outsider 404-not-403,
  publish → member bundle 3 options in an order stable across fetches, no
  source fields leaked while anonymized, autosave idempotence, note preserved
  across rating tap, aspect upsert/clear, final pick + completed_at,
  re-import updated 3 slugs with reviews + publish state intact, member
  results 403, results rollup counts correct, report markdown contained the
  note + closing note + source name, materials note/file round-trip, `.exe`
  upload rejected, member materials 403, briefing zip contained
  BRIEF.md/links.md/INSTRUCTIONS-FOR-CLAUDE.md/materials/.
- 42/42 pytest in-container against `scout_test` (real Postgres). Invocation:
  `docker cp main/tests scout-app:/app/tests` then
  `docker exec -w /app scout-app sh -c 'SCOUT_DB_URL="${SCOUT_DB_URL%/scout}/scout_test"
  SCOUT_DATA_DIR=/tmp/scout-test-data BW_CLIENT_ID="" BW_CLIENT_SECRET=""
  python -m pytest tests -q'` (pytest+httpx pip-installed ad hoc; wiped by
  every redeploy).
- Capture tool verified 14/14 then 2/2 real captures (5 options + League Law
  baseline + spares; sizes 1.7–5.4 MB, desktop 1440px, mobile 780px@2x).
- v0.2.0 live: `/api/meta` shows 0.2.0, `/healthz` 200, `HEAD /` 200 (was
  405), hashed bundle contains the review/admin/results surfaces.
- League Law seeded through the real endpoints (materials ×5, import → 5
  created), all options draft, darren a member. Rian's browser-created
  "League Laq" project was renamed in place, not duplicated.

**Not verified (recorded honestly):** no browser-level exercise of the new UI
— no Playwright run against the SPA, no human walkthrough yet. The review
game's correctness is attested by tsc + the API tests beneath it only.

**Subagent fleet:** 5 sonnet agents (review game 10 files, admin tabs 3,
results tab 2, capture tool 3, pytest suite 6). All integrated; one
orchestrator fix pre-build (narrowing in OptionsTab), zero post-build type
errors. Agents' setgid stumbles on `src/admin/` were self-corrected via
`sg`/`fix-permissions`.

**League Law option set** (research via WebSearch/WebFetch, curated against
Darren's stated criteria; 8 sites evaluated and rejected — see handoff):
align-family, reaves-lee, elcmd, burnham, podhurst.

## 2026-08-11

- Ran `/srv/projects/standards/new-workspace.sh scout --instance main`. Needed
  `srv-gw fix-permissions --project scout` first — the gateway-created `.logs/` was
  `755 srv-gateway:scout-dev`, so the scaffolder could not write into it.
- Replaced the nginx static scaffold with FastAPI + React + Postgres 17 (compose sidecar,
  internal network, no published port).
- Copied `bw_auth.py` from `/srv/system/id-auth/app-auth/` verbatim into `main/app/`.
- Created BW account `darren` (dwilliams@leaguelaw.com). No password set — owner-only,
  interactive.
- Registered the BW app client: `srv-gw app-client-register --name scout --redirect-uri
  https://scout.bowden.works/auth/callback --secret-out /srv/apps/scout/.bw-auth.env`.
  Registration auto-un-gated the host (expected for Pattern B); the host had been serving
  an empty directory listing, so nothing was exposed in that window.
- Generated `.app.env` (660 scout-dev) with a random DB password and session secret.
  Values never printed.
- First build failed: the SPA catch-all's `FileResponse | PlainTextResponse` return
  annotation made FastAPI try to build a response model. Fixed with `response_model=None`
  and a `Response` annotation. Second build deployed clean.
- Verified live at `https://scout.bowden.works`: `/api/meta` and `/api/me` public;
  `/api/projects` and `/api/accounts` 401; `/auth/login` → `app-authorize` with PKCE S256;
  `/auth/probe` adds `prompt=none`; SPA shell and hashed assets serve; deep links serve
  the shell. Alembic ran `0001_baseline` on boot and seeded rian + adi as admins.
- Exercised the M0 flow for real inside the container against the live DB and the live BW
  server: create project → `bw.lookup_identity("darren")` → provision → add member
  (twice, to prove idempotence) → visibility per role → re-login preserves an
  admin-set role. 16/16 assertions passed; the script deleted everything it created.
- `srv-gw security-audit` run after deploy: no CRITICAL, and no scout WARNING. Scout
  appears only as INFO (container running, `.env` 660, 0.0GB, conf recently modified).
