# BW Guides — Handoff Notes

<!-- BW-RELEASE-STATUS:BEGIN — written by tools/release.sh; do not hand-edit -->
> **PUBLISHED: 0.3.5** — 2026-08-13T16:13:56Z, sha256:dc675e72054788c8
>
> Rewritten by `tools/release.sh` on every publish — the only release-state claim in
> this file that is current. A "not released" note below was true when it was written
> and nothing updates it. Ledger: `docs/SESSION-LOG.md` → Releases.
<!-- BW-RELEASE-STATUS:END -->

Async communication between developers (human + Claude). Use for "here's what I was working on, pick up here".

## Format

```
### YYYY-MM-DD — from <name>

**Status:** Unread | Reviewed
**Context:** What you were doing
**Next step:** What should happen next
**Blockers:** Anything in the way
```

---

### 2026-07-26 — from Claude (⚠️ THE HUB IS NOW THE SINGLE SOURCE OF TRUTH) — READ THIS FIRST

**Status:** Unread

#### Edit guides on the hub. Never rebuild content from draft files.

<https://plugins.bowden.works/wp-admin/edit.php?post_type=bw_guide> is the only place guide
text changes now — standard guides and client guides alike.

**This was approved by rian after a near-miss.** Before switching, I diffed all 20 Brentwood
drafts against their hub posts: **13 of 20 had drifted, hub ahead in every case.** Those
guides were edited 2026-07-26 (the test-drive pass), days after the last import. A
rebuild-from-drafts would have destroyed whole sections ("How do I put a video inside a
post?", "How do I upload a PDF…"), corrected instructions (staff/careers reordering is
**Quick Edit**, not Page Attributes), an added Contributor role row, and a substantially
rewritten guide 05 (draft ~3,000 chars of stale text).

Nobody misbehaved — two sources of truth existed and newer edits landed in the one people
naturally reach for. The ambiguity was the defect. It surfaced only because rian asked where
to edit a guide and I gave an answer that made the safe path sound like the awkward one.

**The drafts keep one job: the screenshot capture manifest.** `BWG-SHOT` markers and
`_frag/*.shots.md` hold working capture recipes (selectors, `actions="…"` scripts, the
timings that took real effort). Valuable for re-shooting — especially after the fixture is
upgraded to WP 7.0.2. `check-shots.py` still validates that role and still passes (60
markers, 60 PNGs, 1:1). **Guide bodies there are a historical record, not current text.**
Marked with `guides-draft/README.md` and `guides/DO-NOT-REBUILD.txt`.

**Re-shoot workflow now:** read the recipe from the manifest → capture → import → place the
image **directly in the hub guide's content** (exactly how the standard guides already work).

---

### 2026-07-24 (LATEST+) — from Claude (⚠️ FIXTURE IS ON THE WRONG WP VERSION)

**Status:** Unread

#### ⚠️ THE SCREENSHOT FIXTURE IS ON WORDPRESS 6.8.3; EVERY CLIENT SITE IS ON 7.0.2

Verified directly: `guide-shots` = **6.8.3**; `brentwooddev`, `bw-plugins`,
`bw-plugins-dist`, `template` = **7.0.2**.

This matters because **standard guides are shot on the fixture and ship to every client
site** — so those screenshots currently show clients an older admin UI than the one in
front of them. That is precisely the failure the fixture exists to prevent.

**Action outstanding (not done — needs a deliberate decision):** update `guide-shots` to
match the client fleet, then re-shoot the standard-guide screenshots taken from it. Until
then, treat the standard library's screenshots as 6.8.3 UI. The Brentwood client guides are
unaffected — they were shot on brentwooddev itself.

How this surfaced is the useful part: two sessions disagreed about whether a CSS class
existed "in WP 7.0.2". Testing the claim revealed that the session claiming to test 7.0.2
had actually been testing 6.8.3 all along. **Before trusting any version-specific finding,
confirm which version was actually tested** —
`srv-gw wp --project <name> -- eval 'echo get_bloginfo("version");'`

#### I shipped a wrong README change and have reverted it

I documented a previous session's finding that `.editor-block-list-item-<slug>` "does not
exist in WP 7.0.2" and replaced it with positional matching. **That finding was wrong.**
The Brentwood session challenged it with specific evidence; I verified independently and
they are right:

- The class IS generated, in **`blocks.min.js`** (not `block-editor.min.js`, which has zero
  occurrences — the likely source of the false negative), by `getBlockMenuDefaultClassName`:
  `"editor-block-list-item-" + name.replace(/\//,"-").replace(/^core-/,"")`
- Confirmed present in **both** 6.8.3 and 7.0.2.
- **The trap:** the `core-` prefix is STRIPPED, so `core/paragraph` yields
  `.editor-block-list-item-paragraph`, not `…-core-paragraph`. Testing a core block with the
  obvious-looking form finds nothing and makes the whole scheme look gone.

Exact-class matching is now documented as preferred, with positional matching demoted to a
fallback (it depends on inserter search ranking, which shifts with the search term, block
title, and any plugin that reorders results). Lesson: a session's empirical evidence — 14
captures that could only have worked if the class resolved — outweighs a grep of the wrong
bundle.

#### Also fixed: gateway contention during capture batches

Firing ~14 captures back-to-back made `srv-gw status` transiently report `running: false`
for a container up for six days, aborting 9 of 14 with a misleading "Project is not
running". The gateway is effectively single-threaded and each capture does a status lookup
plus a cookie mint/destroy. The wrapper now **retries the status lookup 3× with a 4s
backoff** before believing a not-running answer. Still pace large batches.

Also documented: a fixed `--clip` height only works for **top-anchored** content — taxonomy
panels sit at y≈700+ and get cut off by the standard 640px window, so those need their own
measured rectangle.

**Blockers:** None. The fixture-version question is a decision for rian, not a blocker.

---

### 2026-07-24 (LATEST) — from Claude (library fully illustrated · capture race fixed)

**Status:** Unread

**The library is complete.** brentwooddev **28 guides / 75 images**, sandbox **10 / 22**,
0 orphaned keyword rows, 0 slug mismatches. The only guides without images are the two that
don't want any (Brentwood's text-only getting-started index, and the sandbox's local test note).

**Guide 60 is done** — the previous session correctly reported its two shots as "not viable"
against an empty fixture media library and declined to populate it. I've now made that a
permanent fixture fix rather than a per-session workaround: the `guide-shots` fixture holds
**2 generated placeholder images** (gradients, no copyright, no people) and a **draft page
containing a real image block**. Both previously-impossible shots captured immediately —
the Alternative text field only renders once an image is actually set, which is exactly
why it was unreachable before. Fixture contents are now documented as deliberate in the
tool README.

#### ⚠️ A REAL BUG WAS FOUND AND FIXED IN THE CAPTURE PATH — read this before shooting

Rian spotted straddled crops in *published* guides. Cause: **element captures race late
layout shifts.** An element capture is measure-then-grab; on ACF-heavy screens the page
finishes laying out in between and the pixels come from stale coordinates. Crops landed
~145px off, and in one case captured pure canvas.

**Why every diagnostic said it was fine:** `capturedElement` reported `x:1320, w:280,
visible`, `warnings:null` — all truthful. The measurement *was* correct when sampled. My
x-position check validates **the measurement, not the pixels**. That was a reasoning error
on my part, not merely a missing case.

Now built in:
1. **Settle wait** — the bounding box is polled until it stops moving before grabbing.
2. **Re-measure + verify** — the grab uses an explicit clip computed immediately before,
   then re-measures; if it moved, `movedDuringCapture` is set and a loud `CAPTURE_WARNING`
   says the image is probably wrong. The tool now shouts rather than hand over a
   plausible-looking bad crop.
3. **`--clip WxH+X+Y`** — deterministic rectangle capture, race-free by construction
   (nothing is measured). **Prefer it for any sidebar/inspector shot.** Workflow: one
   selector run to read `capturedElement.capturedBox`, then `--clip` for the real shot.
   **Coordinates are layout-specific — never copy them between sites** (the sidebar starts
   at y=61 on the fixture, y=97 on brentwooddev).

Also corrected: the README's own `.editor-block-list-item-<slug>` recipe **does not exist
in WP 7.0.2** and was leading sessions astray. Replaced, plus three newly verified
selectors (header actions, status popover, list view) added to `selectors.json`. Verify
selectors against the shipped `wp-includes/js/dist/*.min.js`, not older docs — including
older revisions of that README.

**Aspect ratio is an authoring decision:** a correct full-height sidebar shot is ~280x1078,
a 1:3.85 strip that reads terribly in a guide. Cap at ~640px tall. A screenshot orients;
it doesn't inventory.

**Blockers:** None.

---

### 2026-07-24 (LATER STILL) — from Claude (the five original guides now have screenshots — DONE)

**Status:** Unread

**Context:** Picked up item 2 from the entry below ("the five ORIGINAL standard guides still
have no screenshots"). This is now done — do not duplicate.

Guides 23 (Getting Started), 24 (How to Edit a Page), 42 (Page and Post Settings), and 59
(Working with Blocks) now carry real screenshots from the `guide-shots` fixture (9 newly
captured + 2 reused from 187/191) = 11 images total, embedded directly into the hub posts
(these five have no draft files/`BWG-SHOT` markers, so the hub content was edited directly and
verified by diffing the stripped-image-blocks version back against the pre-edit content).
Synced clean to both bw-plugins (sandbox) and brentwooddev (created=0, updated=4, trashed=0);
section-keyword heading audit is 0 mismatches on all 5; sandbox render check
(`wp_kses_post(do_blocks(...))`) confirms the right `<img>` count on every one. Fixture
untouched (`guide-shots` post 1/2 `post_modified` byte-identical before/after; media count still
0 — nothing was ever uploaded to it).

**Guide 60 (Images and Media) still has zero screenshots.** Both of its two shots
(`media-library`, `image-alt-text`) turned out not viable against the fixture's genuinely empty
media library (0 attachments) — confirmed by actually capturing and reading both the plain
grid and the in-editor Media Library modal (both show an empty state), and by confirming via a
full-viewport capture that the Block tab's "Alternative text" field simply doesn't render on an
imageless Image block placeholder. Full detail in `docs/SESSION-LOG.md` (2026-07-24 16:50
entry).

**Next step, if anyone wants guide 60 covered too:** add one or two innocuous stock/placeholder
images to `guide-shots`' media library ONCE — that's a one-time fixture change, not a
per-session workaround, and it permanently unblocks both shots. This session deliberately did
not do that (the brief said not to populate the fixture).

**Also worth reading before the next capture session:** the SESSION-LOG entry documents several
newly-verified selectors (Status & visibility popover, header Save/Preview cluster, List View
toggle) found by grepping the actual shipped `wp-includes/js/dist/*.min.js`, since the README's
own "Field recipes" `.editor-block-list-item-<slug>` turned out stale for WP 7.0.2. Also a new,
previously-undocumented capture failure mode: an element-scoped sidebar capture taken
immediately after a block-insert can come back fully blank (not just mis-cropped) even past the
existing `CAPTURE_WARNING` checks — a full-viewport capture of the same script proved the UI
itself was fine. Didn't chase the fix since the shot wasn't viable anyway; worth hardening
`capture.js` against if it recurs on a viable shot.

**Blockers:** None.

---

### 2026-07-24 (FINAL) — from Claude (3 block primers live, library complete) — READ THIS FIRST

**Status:** Unread

**THREE standard block primers are live and synced everywhere** — hub 193 "Where Your Blocks
Come From", 194 "Changing How a Block Looks", 223 "Using Patterns (Ready-Made Layouts)". All
audience=all, category **"Building pages"** (guide 59 moved in with them). Search battery 7/7.
Each carries verified screenshots from the clean fixture.

**Library state:** brentwooddev **27 guides / 60 images**, sandbox **10 / 9**, 0 orphaned
keyword rows, 0 slug mismatches, 9 categories, both hub endpoints healthy. The 14 suspect
inspector shots were re-imported and 10 guides rebuilt.

**Client plugin 0.3.2 is RELEASED** and serving from plugins.bowden.works, so client sites
self-update. Hub is 0.2.0 (in-place infra, never released).

#### Two things for the next session

1. ~~**PAIRED FIX: normalise strings before comparing.**~~ **DONE — client 0.3.3, staged not
   released.** `BW_Guides_Keywords::normalize_for_match()` now decodes HTML entities,
   straightens curly apostrophes/quotes, converts non-breaking spaces and collapses
   whitespace runs; it's applied to heading↔keyword matching AND to search matching (query
   tokens additionally have apostrophes stripped, with the haystack compared both ways).
   Verified: `cant get in` now resolves (previously missed), the entity-affected guide still
   resolves, normaliser unit checks 4/4, no search regressions. Gates green; deployed to the
   sandbox and brentwooddev by direct install. **`tools/release.sh bw-guides 0.3.3` is
   rian's call.**
   Deliberately NOT in scope: translating words to symbols ("width and height" still won't
   match "width & height"). That's semantics, not normalisation, and guessing there would
   cause wrong matches rather than prevent missed ones.
2. **The five ORIGINAL standard guides still have no screenshots** (23 Getting Started, 24
   How to Edit a Page, 42 Page and Post Settings, 59 Working with Blocks, 60 Images and
   Media). All generic WordPress UI → capture from the **`guide-shots` fixture**, never a
   client site. `--anonymous` now handles the login screen guide 23 wants. Offered to the
   Brentwood session; confirm before starting so the work isn't duplicated.

#### Capture tool is materially better than it was this morning
`--actions` (click/type/hover/etc., iframe-aware), `--anonymous` (logged-out screens),
`--reset-ui` (pin sidebar state — Gutenberg persists it in USER META and one bad run poisons
everyone), `redact.sh`, a `capturedElement` diagnostic, and two narrow `CAPTURE_WARNING`
signatures (sidebar-not-flush-right = straddled crop; "No block selected" = clicked an inert
server-rendered block, insert it instead). **`selectorFound:true` is not proof of a correct
capture — read `textPreview`.**

#### ⚠️ THREE "SOURCE OF TRUTH" TRAPS — all three have actually bitten someone today

They're the same mistake in different clothes: editing a **derived** artefact instead of the
thing it's generated from. Each edit looked fine and was silently reverted later.

1. **The DRAFT is the source of truth, not the hub.** A hub-side content edit is reverted by
   the next rebuild-from-drafts (cost me the sign-in heading fix, which then orphaned its
   keyword row).
2. **`SHOTS.md` / `KEYWORDS.md` are GENERATED from `_frag/*.{shots,keywords}.md`.**
   Hand-edits to the assembled files are lost the next time the assembler runs. Put
   corrections in the fragments.
3. **BWG-SHOT markers carry an open-ended attribute set.** Any parser that hardcodes their
   order silently stops matching when a new attribute appears — and a non-match publishes
   the marker, capture recipe and all, to client sites. Use the canonical
   `tools/bw-guide-shot/embed-shots.py`, which asserts nothing leaks.

**Blockers:** None.

---

### 2026-07-24 (earlier) — from Claude (block primers + 59 screenshots live)

**Status:** Unread

**Two new standard guides are live and synced everywhere** (hub 193 "Where Your Blocks Come
From", 194 "Changing How a Block Looks") — top-down primers on block sources and block
configuration, with 6 real screenshots from the clean fixture. New category **"Building
pages"**; guide 59 moved into it so all block content groups together. Guides 23/24 now have
an explicit `audience=all` (they were relying on an implicit default).

**All 59 Brentwood screenshots are imported** — 53 embedded across the 19 guides, synced,
verified through the client render path. brentwooddev is now at **26 guides**.

**⚠️ THE ONE PROCESS RULE TO REMEMBER: the DRAFT is the source of truth, not the hub.**
Rebuilding guide content from `/srv/apps/brentwooddev/docs/guides-draft/` silently reverted a
hub-side heading edit I'd made earlier (the sign-in disambiguation), resurrecting a
duplicate-heading ambiguity AND orphaning the keyword row attached to the renamed heading.
Both fixed — in the draft first, then the hub. **If you edit guide content on the hub for a
guide that has a draft, mirror it into the draft or it will be lost.**

Also fixed: an HTML-entity mismatch (`&` vs `&amp;amp;`) that silently orphaned a keyword row
on guide 87. Re-audited all 26 guides: **0 orphaned keyword rows.** A permanent fix
(entity-decode both sides in `BW_Guides_Keywords`) is worth folding into the next client
release rather than releasing for it alone.

**Capture tool gained three things** (tools/ only, no plugin release): `--anonymous` (capture
logged-out login screens), `--reset-ui` (pin the editor sidebar open — Gutenberg persists that
state in USER META, so one bad run poisons everyone's later sidebar captures), and a
`capturedElement` diagnostic proving what was actually captured, because **`selectorFound:true`
is not proof of a correct shot**. Plus `redact.sh` for unavoidable private data.

**Next step:** Brentwood is re-capturing ~14 inspector shots with `--reset-ui` into
`docs/guides-draft/shots-fixed/` (some of the imported inspector shots captured the wrong
element — a silent failure). When they send the verified list, run a corrective re-import of
just that subset.

**Blockers:** None.

---

### 2026-07-24 (later) — from Claude (capture interaction support — DONE)

**Status:** Unread

**Context:** Closes the "NEXT RELEASE — HIGH PRIORITY" item in the overnight-run entry
below. `tools/bw-guide-shot.sh` / `capture.js` now support scripted interactions —
`--actions "click:...; waitfor:...; ..."` (click/hover/focus/type/key/waitfor/scrollto/
wait) — with **iframe-aware selector resolution** (main frame + every child frame, so
Gutenberg's `editor-canvas` iframe is now reachable by both `--actions` steps and the
pre-existing `--selector`/`--region` path) and a **request-based navigation tripwire**
enforcing read-only scripts (deliberately not a `framenavigated` URL-diff — that would
false-positive on Gutenberg's own benign `post-new.php` auto-draft URL rewrite, which
fires the event with zero network requests). Full DSL reference now in
`tools/bw-guide-shot/README.md` "Interactions"; verification detail + a real Puppeteer
gotcha (synthetic `click()` silently missing an overlapping-button target; `focus:` +
`key:Enter` is the fix) in `docs/SESSION-LOG.md` (top entry).

**Next step:** Screenshot fill for the 18 deferred Brentwood guides (SHOTS.md/_frag
manifest — see "Deferred" below) is now unblocked; many of those shots specifically need
block settings or a metabox drawer expanded, which is exactly what this unlocks. The
`BWG-SHOT` marker's `actions=` attribute is documented and ready to use; no
publisher/import-side change was needed or made (markers are still stripped as plain
text at import).

**Blockers:** None.

---

### 2026-07-24 (OVERNIGHT RUN — COMPLETE) — from Claude (fable orchestrator) — READ THIS FIRST

**Status:** Unread. The v1 platform mission is COMPLETE. Morning summary:

#### What shipped tonight (all verified, all gates green)

- **Hub 0.2.0** (in place on plugins.bowden.works): client-group targeting
  (client_slug, "Specific client" audience, manifest resolution), "Search keywords &
  category" metabox, payload meta ships category + section_keywords. 22/22 checks.
- **Client 0.3.2** (STAGED — not released): ranked section search with **hidden
  keywords** (stopword + plural tolerant), **category-grouped browse** ("Getting
  started" always first) with card quick-links, keyword match hints, honest
  empty-states. All three gates pass. `tools/release.sh bw-guides 0.3.2` is **rian's
  call** — brentwooddev + sandbox run it via direct deploy meanwhile.
- **Capture CLI** `tools/bw-guide-shot.sh`: headless authenticated screenshots of ANY
  on-server site incl. gated client sites — MITM-fulfill, zero gate changes, zero
  outbound network, per-run tokens destroyed. security-audit: 0 new findings.
- **write-guides skill** (~/.claude/skills/write-guides/) — the house standard any
  session can invoke.
- **Guide library LIVE on brentwooddev.demoing.info (24 guides):** 5 standard
  (Guide 3 rewritten to the new standard; 23/24 reworked; Working with Blocks +
  Images and Media new — all with categories + section keywords) + **19 Brentwood
  guides** (drafted by the Brentwood session, published as client:brentwood, hub ids
  63-74 + 87-93). Sandbox untouched by brentwood content (isolation verified
  repeatedly). Flagship guide "Managing staff profiles" carries 3 real auto-captured
  screenshots (marker→capture→hub-media→sync loop proven).
- **Sync invariants re-verified adversarially** against the new hub: bad key →
  zero local changes; tags/notes survive updates; removals still trash-only.

#### Act-as-user critique (P5) — found and FIXED tonight
1. Alphabetical categories buried "Getting started" behind "Building pages" → now pinned first.
2. "No guides found." rendered under a successful "Jump straight to" list → softer,
   accurate message when sections match.
3. "change the hero" (THE Brentwood control) had no section hit — verb-variant
   keywords added (change/swap/replace hero). Lesson folded into the skill: include
   the user's verbs in keywords, not just nouns.
4. Duplicated heading "How do I sign in?" across two guides made "sign in" ambiguous —
   guide 00's section renamed "Logging in for the first time"; the dedicated Signing
   in guide now owns the query.
5. Accepted as-is: "photo gallery" legitimately matches two guides; both appear in
   results. Candidate future tweak: per-guide diversity cap in top-8.

#### For rian (decisions/actions only you can take)
- **Release**: client 0.3.2 is release-staged; say "release it" to publish.
- **⚠️ Brentwood ops findings** (from the drafting session — see banner section below):
  bw-google-login auto-provisions any @brentwood.ca account; **GF form 1 (Admissions)
  emails NO ONE**. Launch-checklist items.
- **At Brentwood launch**: register brentwood.ca under client `brentwood` on the hub
  Sites screen — the full 19-guide backlog auto-delivers on its first sync. That's
  the whole launch procedure for docs.
- Housekeeping: hub post 40 (empty test draft) still parked; guide 25 (sandbox
  site-notes) still has pre-standard "contact" phrasing — minor, sandbox-only.

#### NEXT RELEASE (rian, 2026-07-24): capture interaction support — HIGH PRIORITY
The capture tool loads a screen and shoots it with **no interaction**. That means it
cannot show anything only revealed by clicking: **block settings** (only appear after
a block is selected), the collapsible **Meta Boxes drawer** (ACF classic fields),
dropdowns, popovers, hover toolbars. This is the #1 gap for real how-to docs. Puppeteer
fully supports click/type/hover/waitForSelector — the tool just doesn't expose it.
Build: per-shot **interaction steps** (a sequence: click selector → wait → optionally
type/hover → capture region), surfaced in the `BWG-SHOT` marker (e.g. an `actions=`
attribute or per-shot JSON). Unblocks block-settings docs + the metabox drawer + the
external-interactive capture below.

#### FUTURE IDEA (rian): external / public-site capture mode
Documenting an on-server site sometimes needs a shot from a 3rd-party public page
("go to youtube.com, open the video, get the Share link"). NOT impossible — a distinct
mode that points headless Chrome at the real public URL (no container/cookie/MITM;
public pages need no auth). Rides on the interaction work above for "navigate + click".
Caveats to design around: cookie/consent banners, bot-detection on some hosts (YouTube
may block headless), and login-required pages (do NOT automate credential entry). Put
on the list; build after interaction support lands.

#### Deferred (documented, deliberate)
- Screenshot fill for the other 18 Brentwood guides (61-shot manifest in
  SHOTS.md/_frag; loop proven — but many shots need the interaction support above
  before they can show the right revealed-on-click UI).
- Image sideloading (hub screenshots currently hotlink from plugins.bowden.works).
- `bw-guide` authoring CLI (skill + staged wp-cli is the current path).
- Per-client suppression of a standard guide (e.g. Brentwood's getting-started vs
  the generic one — both show today, grouped together, acceptable).

**⚠️ OPERATIONAL FINDINGS FOR RIAN (from the Brentwood drafting session — site
config, not guides; surfaced while verifying features against live code):**
1. ~~**`inc/bw-google-login.php` auto-provisions a bare `bw_staff` user for ANY
   `@brentwood.ca` Google sign-in**~~ — **RESOLVED 2026-07-24.** Rian approved the fix;
   it is now find-only, with a user-facing message ("That Brentwood account does not have
   access to this website yet. Ask your Marketing team to set one up for you."). This
   mattered more than the original note implied: the school issues Workspace accounts to
   every student and every new hire, so the old behaviour equated "has a school email"
   with "has a login here". Guide 14 now quotes the new message verbatim. Verified: no
   STANDARD guide describes Google/SSO sign-in, so none needed changing (the two grep
   hits were false positives — "sso" inside "processor", and a "ranking in Google"
   reference about URL changes).
2. **Gravity Forms "Admissions Inquiries (migrated)" (form id 1) has ZERO notifications
   configured — admissions submissions currently email NO ONE.** Likely lost in the WPE
   migration. Needs a notification re-added before launch. (Entries are still stored in
   the DB, so nothing is lost — just silent.)

Autonomous overnight mission: v1 platform (see PLATFORM-ROADMAP.md). Progress so far:
- **DONE: Guide 3 rewrite** (hub 42) to the new writing standard — concise, zero
  "contact us", H2s byte-identical (search-verified). NOT yet re-synced to sandbox
  (deliberately deferred: client agent fabricates test meta on sandbox copy 175;
  sync would clobber it). Sync during integration.
- **DONE: write-guides skill** at ~/.claude/skills/write-guides/SKILL.md (user-global).
  Verify hub 0.2.0 features match its description before announcing it.
- **RUNNING: 3 Sonnet agents** — (1) hub 0.2.0: client-group targeting + keywords/
  category metabox + payload meta; (2) client 0.3.0: ranked section search w/ hidden
  keywords + category-grouped browse; (3) capture CLI tools/bw-guide-shot/ w/
  MITM-fulfill mode for gated sites (no ungate). Payload meta contract (both build to
  it): meta.category string; meta.section_keywords=[{heading:"<exact text>",keywords:
  "kw, kw"}], "*"=guide-level; client matches case-insensitive trimmed exact heading text.
- Next after agents: integration test hub→client, adversarial invariant verification,
  sync guide 42, deploy client to brentwooddev (project port 3097) as client:brentwood,
  guide authoring fan-out (drafts land in /srv/apps/brentwooddev/docs/guides-draft/ —
  Brentwood session started ~23:26), act-as-user critique.

---

### 2026-07-24 (later still) — from Claude (client 0.3.0: ranked search + category browse) — READ THIS FIRST

**Status:** Unread

**Context:** Separate track from the platform-vision entry below (client groups /
screenshot CLI / write-guides skill) — this session built client **0.3.0**, "navigation
& findability": ranked section search using a guide's hidden keywords, and a
category-grouped browse grid, ahead of the standard library growing to dozens of guides.
Full detail in `docs/SESSION-LOG.md`; the read contract + ranking rules are now
documented in this plugin's own `CLAUDE.md`.

**What's new:** `BW_Guides_Keywords` (new class) reads `category` +
`section_keywords` from the payload `meta` object — the exact contract hub 0.2.0 is
being built to fill in, in parallel. Until the hub actually ships it, **every real
guide has neither field** (verified: search and browse both degrade cleanly to
pre-0.3.0 behavior when a guide carries no keywords/category). Verified the contract's
read side works by fabricating fixture meta on real hub guide 175 (read-modify-write,
existing keys preserved) — see Blockers below for what that leaves behind.

**Next step:**
1. **Hub 0.2.0** needs to actually start writing `category` / `section_keywords` into
   real guide payloads so this isn't just a fixture — coordinate the exact JSON shape
   against what's documented in this plugin's CLAUDE.md (it's already frozen from the
   client read side; the hub should not need to negotiate it further).
2. Guides 1, 2, 4, 5 (per the library plan in the entry below) still need writing —
   good opportunity to pick real `category` values and a few `section_keywords` while
   authoring, now that something on the client side actually reads them.
3. Release 0.3.0 when rian says "release it" — `tools/release.sh bw-guides 0.3.0`.
   Nothing technical outstanding; all three gates pass.

**Blockers:** None technical. One housekeeping item: **guide 175's
`_bw_guides_remote_meta` currently carries fixture data** (`category: "Content
basics"`, `section_keywords` for its web-address section + a guide-level entry) from
this session's verification. It's harmless — a normal sync only re-fetches when the
hub's `content_hash`/`modified_gmt` change, so real hub data will overwrite it
naturally once guide 42 is next edited on the hub — but if the sandbox gets browsed
before then, that category will look like real authored data. Clear it manually first
if that would be confusing (read-modify-write the same way this session set it, just
dropping the two added keys).

---

### 2026-07-24 (latest) — from Claude (platform vision + screenshot pipeline proven)

**Status:** Reviewed — read for orientation this session; unrelated to the client
0.3.0 findability work above, which is its own track. Nothing here superseded.

**Big picture shift:** BW Guides is becoming a **documentation platform** — other project
sessions author guides for the features they built, auto-screenshot them, and publish
targeted at client sites by client/domain. **The full plan, decisions, and build order now
live in `docs/PLATFORM-ROADMAP.md` — read that first for anything platform-level.**

**Decisions locked with rian:** targeting = **client groups** (sites carry a client slug;
register prod at launch → backlog auto-delivers, no re-targeting); authoring = **CLI +
skill** (owner-gated via the gateway, no public write API).

**Screenshot pipeline is PROVEN** (the session's big de-risk). Clean fixture `guide-shots`
+ headless google-chrome (already installed) via puppeteer-core + on-demand
gateway-minted WP cookie (no stored password) + capture over the real HTTPS origin (so
assets load) + selector crop. First real output: a clean WP 7 settings panel, no BW Dev
pollution. **The load-bearing gotchas are in the roadmap and SESSION-LOG — read them before
touching capture** (https-asset issue; secure vs plain cookie; temp-ungate is fixture-only).

**Nothing of the pipeline is built as a reusable CLI yet** — proof code is in the session
scratchpad. Build order is in the roadmap: client groups → screenshot CLI → authoring
CLI + skill → id-auth capture identity (for client-site shots) → image sideloading.

**New infra to know about:** fixture site **`guide-shots`** (blank WP, port 3137,
id-auth gated, admin password NOT stored). It exists and is secured (re-gated).

---

### 2026-07-24 — from Claude (guide structure + the library plan)

**Status:** Reviewed — client-group targeting (above) supersedes the "selected sites"
assumption here; the 5-guide library plan and per-guide process still stand.

**Context:** Rian is building the real starting library and asked how to structure it,
since clients mostly arrive with small specific questions ("how do I change the page
url") that don't merit a guide each. Agreed structure and agreed scope below. The
**structure layer is built and verified (client 0.2.0)**; the **guides are not written
yet** — that is the next job.

#### The structural rule (apply this to every guide written from here)

> **One guide per area of the admin. One task per heading.**

Clients navigate by where they are; questions map to headings. Client 0.2.0 makes every
heading addressable: stable anchors, an auto "In this guide" contents list, and search
that matches headings and offers "Jump straight to" links landing on the exact section.
Verified end-to-end in the browser.

**Two authoring consequences — these matter more than they look:**
1. **Write one task per H2**, phrased as the question a client would ask.
2. **Put the client's own words in the heading**: "Changing a page's web address (URL)",
   not "Permalinks". Section search matches heading text, so vocabulary IS the search
   index. Don't engineer synonyms — `meta.keywords` exists in the payload if that ever
   proves insufficient, but write it properly first.

All of it derives at render/query time — no protocol change, hub untouched,
`content_hash` unaffected.

#### The agreed library — a lean core of 5 (rian chose this over 8)

| # | Guide | Headings = the questions it answers |
|---|---|---|
| 1 | **Finding Your Way Around** | logging in · forgotten password · dashboard & menu · the top bar · pages vs posts · habits |
| 2 | **Editing a Page** | finding the right page · making changes · previewing · publishing · undoing · restoring a version |
| 3 | **Page and Post Settings** | **changing the web address (URL)** · featured image · excerpt · publish date · private/password-protected · parent page · template |
| 4 | **Working with Blocks** | what a block is · add/move/duplicate/delete · the most useful blocks · side-by-side columns · copying between pages · list view |
| 5 | **Images and Media** | adding an image · replacing one without breaking layout · alt text · cropping & sizing · media library · file sizes |

Guides 23 and 24 on the hub already map to #1 and #2. **#2 currently carries block detail
that belongs in #4** — move it when writing #4. #3 is deliberately the magnet for the
"small question" case. Guide 25 (site-specific notes) stays as it is.

**Deliberately excluded from the all-sites set:** Forms (Gravity Forms), SEO (Yoast),
ecommerce — all plugin-dependent. Author them as *site-targeted* guides, and they become
the first real users of the `requires_plugins` roadmap feature.

**⚠ Do not write these guides from memory.** The sandbox runs **WordPress 7.0.2**, newer
than Claude's training data. Open each screen in the sandbox and confirm the field names
and locations before describing them — a guide describing a UI that has moved is worse
than no guide.

#### Guide 3 is written and live (hub id 42) — treat it as the model

8 headings, all core blocks, design-kit callouts and steps. Audience `all`; synced to the
sandbox as local 175. **Two process rules it established, apply both to guides 1, 2, 4, 5:**

1. **Verify every screen before describing it.** WP 7.0.2 differs from assumption: the
   field is **"Slug"** (not URL/Permalink) in a flat row list; **Excerpt is Posts-only**;
   **Parent is Pages-only**; Categories/Tags are Posts-only. Writing from memory would have
   produced three wrong instructions in one guide.
2. **Search-test the headings after writing.** Run the questions a client would type
   through `BW_Guides_Content::search_sections()`. Guide 3 hit 9/11 first pass; both misses
   were fixed by *rewording headings*, not code ("scheduling"→"schedule a page for later";
   adding "(excerpt, categories, tags)"). All 13 queries now resolve.

#### Screenshots — blocked on transport, needs rian

Claude can capture and crop views but **cannot get pixels into WordPress**: `zoom` returns
no image id, `upload_image` can't retrieve captured screenshots, and `save_to_disk` leaves
no reachable file. **The workaround, which needs one step from rian:**

- Rian takes plain **full-window** screenshots (no cropping needed) and uploads them to the
  hub media library at plugins.bowden.works.
- Claude then finds them under `/srv/apps/bw-plugins-dist/wp-content/uploads/`, crops
  precisely with ImageMagick (`/usr/bin/convert`), re-imports the crop, and places it in
  the guide with alt text.

**Cropping is not optional:** template.demoing.info is **not a clean install** — BW Dev
panels ("Page Title Override", "Scheduled Action", "Admin Note") sit directly below the
core settings rows and must not reach client screenshots.

Shots needed for guide 3: (a) the Page settings panel rows, (b) the "Status & visibility"
popover, (c) a Post's sidebar showing "Add an excerpt…" with Categories/Tags.

**Also worth settling first:** hub images **hotlink** from plugins.bowden.works to every
client site. Sideloading is on the roadmap and is worth doing before screenshots ship
widely.

**Next step:** write guides 1, 2, 4 and 5 (rework 23 and 24 into the heading structure;
move 24's block detail into guide 4). Then release (`tools/release.sh bw-guides 0.2.0`) on
rian's explicit go.

**Blockers:** Screenshots only — see above. Guide writing itself is unblocked.

---

### 2026-07-24 (later) — from Claude (hub content rewrite + design kit)

**Status:** Reviewed — still current for the design kit, allowlist and invariants.

**Context:** Picked up the prioritized list in the entry below. **Step 2 (rewrite the
test-polluted hub content) is DONE and verified.** Step 1 (browser visual pass) is
**blocked on rian** — see Blockers.

Guides 23, 24 and 25 on the hub now carry real client-facing copy. While rewriting, found
that guide 25 was worse than "placeholder text": it held a full **Kadence pattern**
(rowlayout / column / 6× advancedheading) plus a hotlinked test image — a live violation
of the CORE BLOCKS ONLY rule that would have rendered degraded on every client site.
Removed. All three now parse as `core/paragraph|heading|list|list-item|separator` only,
zero stray HTML.

Synced to the sandbox: `{"created":0,"updated":3,"trashed":0,"remote":3}`. Guide 171 kept
its tags and private note; local guide 174 untouched. Invariants 2 and 3 now demonstrated
against real content, not just test fixtures.

**Watch out — a lost update bit us mid-session.** Rian had guide 25 open in a hub Gutenberg
tab from before the rewrite; that tab saved and wrote the Kadence markup back over the new
copy, while the client still showed the good version it had already synced. Recovered via
`wp_restore_post_revision(36)` (the Kadence version is preserved as revision 37). **Reload
any open editor tab after a wp-cli content edit.**

#### The design kit (new — this is the answer to "how do I design nice guides?")

Rian asked how to design good-looking guides that display just as nicely client-side. The
constraint was never "plain content", it is specifically Kadence — and for two reasons:
the Kadence Blocks plugin isn't on clients to emit its per-`uniqueID` CSS, **and** the
`palette*` colours are defined by the Kadence *theme*, so shipping Kadence's CSS still
wouldn't fix the colours.

**The enabling insight: guides render inside wp-admin, not through the theme.** The
rendering environment is therefore identical on every client site — design once, looks the
same everywhere. The catch, verified: `wp_enqueue_global_styles()` is hooked to
`wp_enqueue_scripts` only, **never** `admin_enqueue_scripts`, so theme.json output is
absent — no block-gap, no `--wp--preset--*`, no `.wp-element-button`. Buttons rendered as
bare links and columns had no gutter.

What now exists:

- **Client 0.1.2** styles the core blocks used in guides (groups, columns, buttons,
  details, tables, quotes, separators, media & text, images) plus the kit components
  `bw-callout` (+ warning/success/danger), `bw-callout-label`, `bw-steps`. All scoped to
  `.bw-guides-content`.
- **Hub 0.1.1** registers **7 patterns** for `bw_guide` only — Note callout, Important
  callout, Numbered steps, Do/Don't columns, FAQ accordion, Reference table, Button row —
  under a "BW Guides" category in the pattern inserter, plus an editor stylesheet
  mirroring the client's components so the editor previews accurately.
- **Components are class-based, never inline-styled.** Hand-serialized `style` attributes
  risk Gutenberg's "unexpected or invalid content" warning, and classes mean the house
  style can be restyled in the client plugin **without editing any published guide**.
- Guide 24 was rewritten using the kit and verified end-to-end: the client's rendered
  output retains `bw-callout` ×8, the label/warning/success variants, `bw-steps` and 3
  `<details>`, with zero leftover block comments.

**Authoring rules from here on:** build from the BW Guides patterns; **never** use
theme.json preset colour classes (they render colourless in wp-admin); if you change a
component colour, change it in **both** the client stylesheet and the hub's
`assets/css/editor.css`.

**Block allowlist now enforces this (hub 0.1.1).** `allowed_block_types_all` limits the
`bw_guide` editor to 24 verified blocks; other post types on the dist site are unaffected.
It is an allowlist, not "core only", because three *core* blocks are also broken on
clients — the client renders with `do_blocks()` alone, never `the_content`, so
`core/embed` renders as a bare URL, `core/shortcode` prints literal `[shortcode]` text,
and `core/html` gets its iframes/scripts stripped by kses. Kadence blocks don't error,
they degrade: saved HTML survives but the per-`uniqueID` CSS and theme palette don't
exist on clients, so layouts collapse to stacked unstyled divs.
**Caveat:** the allowlist governs *insertion only* — pasted markup and restored revisions
bypass it, so a foreign block can still get in that way.

**Versions:** client **0.1.2**, hub **0.1.1**. All three gates pass at 0.1.2. Still
**unpublished**.

#### Browser visual pass — DONE (both halves, nothing outstanding)

Verified live in Chrome. Client: list (badges, tags, excerpts, all three filters); single
guide with every kit component rendering correctly (blue Note + accent bar, numbered step
circles, amber Important, green Remember, inline `code`); `<details>` accordions open with
their divider; Tags and Notes save via admin-ajax **and survive a hub-driven re-fetch**
(invariant 3 proven with real UI data). Hub: the block inserter shows **only the 24
allowlisted blocks — no Kadence at all**, and **all 7 patterns insert with zero
"unexpected or invalid content" warnings**, rendering styled via the mirrored editor.css.
That was the last real risk in the class-based approach; it is cleared.

**Next step (revised priority order):**

1. **Release on rian's explicit "release it"** → `tools/release.sh bw-guides 0.1.2`.
   Nothing technical is outstanding.
2. **Launch-site rollout** (~5 min) — unchanged, see entry below.
3. **Consider trash support for the hub's `bw_guide` CPT.** It currently does NOT support
   trash: `wp post delete` refuses without `--force`, so **deleting a guide on the hub is
   permanent**. The client deliberately trashes rather than deletes; the hub has no
   equivalent safety net, and the repo is not under git.
4. Optional: a save-time check flagging guides that contain non-allowlisted blocks. The
   allowlist governs insertion only — pasting markup or restoring a revision bypasses it,
   which is exactly how guide 25 acquired its Kadence.

**Housekeeping:** an empty test draft (hub post ID **40**, status draft, never served)
is parked from the pattern-validation test, awaiting rian's OK to remove permanently.

**Blockers:** None.

**Chrome MCP gotcha, so nobody re-loses this time:** the MCP tab kept returning
`wp-login.php?...&reauth=1` on both sites while rian was demonstrably logged in. It was
**not** a separate profile — the long-lived MCP tab held a **stale WordPress cookie**
(`reauth=1` = cookie present but invalid). Closing the group and creating a **fresh tab**
picked up the live session instantly. If the MCP tab looks logged out, **open a new tab
before concluding anything or asking the user to log in again.** Two related traps: the
tab group appears as a small collapsed chip at the far left of the tab strip (users don't
recognise it), and the default window is narrow enough that the Gutenberg inserter renders
off-screen — `resize_window` to ~1600×1000.

**Gotcha worth knowing:** `srv-gw wp` **splits arguments on newlines**, so multi-line
`--post_content=` or `wp eval` fails ("Unable to read content from '<p>…'"). Keep `eval`
on one line, or stage a file under `wp-content/` and pass the container path
`/var/www/html/wp-content/<file>`.

---

### 2026-07-24 — from Claude (MVP build session)

**Status:** Reviewed — steps 2 and (partly) the state notes superseded by the entry above.

**Context:** BW Guides was designed and built end-to-end in one session (2026-07-23/24).
It is a **two-plugin system** and both halves are live and working. Nothing has been
released to the public yet. Full design rationale lives in the plan file:
`/home/rian/.claude/plans/i-want-to-create-luminous-ocean.md`.

#### What BW Guides is

Docs/guides delivery for client WordPress sites with central publishing:
1. **Standard guides** — shipped to every site running the client plugin.
2. **Site-specific guides** — targeted at individual sites from the hub, without ever
   logging into the client site.
3. **Client-created guides** — clients write their own locally.
Clients can tag every guide (including BW-shipped ones) and keep private notes; both are
local and survive syncs. BW-shipped guides are read-only on client sites.

#### The two halves

| | Hub (publishing) | Client (distributable) |
|---|---|---|
| Plugin | `bw-guides-server` **v0.1.0** | `bw-guides` **v0.1.1** |
| Path | `/srv/apps/bw-plugins-dist/wp-content/plugins/bw-guides-server/` | `/srv/apps/bw-plugins/wp-content/plugins/bw-guides/` |
| Site | plugins.bowden.works (dist host) | bw-plugins.demoing.info (sandbox = first client) |
| Status | Active, in-place infra — **never** released via release.sh | Active on sandbox, **release-staged, never published** |

Each has its own `CLAUDE.md` documenting architecture + rules. Read both before changing
sync, the REST contract, or the payload schema. Protocol changes are coordinated:
hub first (backward-compatible), client release after.

#### How it works (short version)

- Hub: `bw_guide` CPT authored in Gutenberg (**CORE BLOCKS ONLY** — dist has Kadence,
  client sites don't). A Targeting metabox sets audience = all sites / selected sites.
  Guides → Sites is the site registry: each site gets a key `bwg_<id>_<40hex>`, shown
  **once**, stored only as sha256. REST namespace `bw-guides/v1`: public `/health`,
  key-authed `/manifest` and `/guides?include=` (header `X-BW-Guides-Key`).
- Client: pulls. **Check-on-view** — opening any Guides screen fires a background
  admin-ajax check ~0.8s after load, throttled to once per 10 min (filter
  `bw_guides_check_interval`); if anything changed it syncs and shows a
  "refresh to see the latest" banner, including on an open guide that changed or was
  withdrawn. Plus a **daily backstop cron** (also keeps the hub's site-liveness
  bookkeeping fresh) and a manual **Sync Now**.
- Client reading UI: wp-admin **Guides** menu — list with search + tag filter + source
  filter, single-guide view, tags + private notes panels, Settings (key, hub URL,
  sync status). Cap `edit_posts` to read/tag/note, `manage_options` for settings.

#### Invariants — do not weaken

1. A failed/invalid manifest changes **nothing** locally (never trash on a failed fetch).
2. Removals **trash** (recoverable), never hard-delete, and only after a fully
   successful run.
3. Sync **never** touches taxonomy terms or `_bw_guides_notes` — that is the entire
   mechanism by which client tags and notes survive updates.
4. Hub guides are read-only via `map_meta_cap` (even for admins). Sync bypasses this
   legitimately (`wp_update_post` does no cap checks); tags/notes use plugin-owned
   admin-ajax endpoints with their own nonce + cap + post-type checks.
5. Hub content renders through `do_blocks()` only — never the full `the_content` chain —
   so hub content can't execute a client site's shortcodes.
6. Deferred features ride in the payload `meta` object (stored raw in
   `_bw_guides_remote_meta`) and must be implemented at **render/query time**, never by
   rewriting stored content at sync time (would break content_hash stability).
7. The site key is a bearer credential: masked in UI, never logged, never echoed into
   wp-cli output, chat, or docs.

#### Verified working (E2E on sandbox against the live hub)

Targeting isolation (a decoy site's guide never reached the sandbox) · content updates
propagating with tags + notes intact · untarget → trash → re-target → untrash with no
duplicate · client-created guides untouched by repeated syncs · wrong key = error with
zero local changes · revoke → 403 → reactivate · check-on-view throttle both suppressing
and re-arming correctly · plugin update-check endpoint (`bw/v1`) unaffected throughout.
`cleanup-scan`, `security-scan`, `test-plugin` all pass at 0.1.1.

#### Current live state

- Hub sites registry: **id 1 = "BW Plugins Sandbox"**, active, last client 0.1.1. Its key
  was regenerated during testing and lives only in the sandbox's `bw_guides_settings`
  option. (Decoy site id 2 was deleted after testing.)
- Hub guides: **23** "Getting Started with Your WordPress Admin" (all sites), **24**
  "How to Edit a Page" (all sites), **25** "Your Site-Specific Setup Notes" (targeted at
  site 1 only).
- Sandbox local guides: 171/172/173 = synced copies of 23/24/25; **174** "Our Internal
  Process Notes" = a client-created test guide.

**Next step (in priority order):**

1. **Browser visual pass** — nothing has been looked at in a browser yet; everything was
   verified at code/CLI level. Check the client reading UI at
   `https://bw-plugins.demoing.info/wp-admin/admin.php?page=bw-guides` (list, single
   guide, tag + notes panels, the update banner) and the hub at
   `https://plugins.bowden.works/wp-admin/edit.php?post_type=bw_guide` (guide editor +
   Targeting metabox, Guides → Sites add/regenerate/revoke flow).
2. **Rewrite the test content in guides 23, 24, 25.** They carry test-edit artifacts —
   guide 23 has "(v2)" and "UPDATED CONTENT:", guide 24 has "(v2)" and
   "CHECK-ON-VIEW TEST:", and 25 is placeholder text. These are rian's real standard
   library, so the copy should be written properly before any client sees it. Core
   blocks only.
3. **Release when rian says "release it"** → `tools/release.sh bw-guides 0.1.1`
   (rian-only, explicit per-release go; 0.1.0 was never published so 0.1.1 is the first
   public release).
4. **Launch-site rollout** (~5 min): hub → Guides → Sites → Add Site (copy key once) →
   install the release zip on the client → Guides → Settings → paste key → auto-syncs.

**Roadmap (designed for, not built — payload already carries the hooks):** variable
tokens like `{{admin_url}}`/`{{site_url}}` replaced at render time; per-guide
`min_wp_version` + "your WP version differs" notice; plugin-conditional visibility via
`requires_plugins` filtered at query time; frontend (theme-styled, logged-in-only)
display; image sideloading (hub images currently hotlink from plugins.bowden.works);
hub-side convenience of prefilling Add Site from the existing `wp_bw_update_log`
install data.

**Blockers:** None. Note the repo is **not under git** — deletions are real; prefer
moving to deleting.

---

### 2026-07-23 — from BW Guides scaffold

**Status:** Reviewed
**Context:** Plugin scaffolded via `tools/new-plugin.sh`. Initial version 0.1.0.
**Next step:** Implement initial feature set per SPEC.md.
**Blockers:** None.
