Platform and tooling changes to the html-previews app itself — hosting and auth, the comment / reaction / approval system, search, the design system, and the cross-preview conventions.
This is not a log of content — the artifacts the platform hosts, *including generated ones*. Publishing a preview (an RCA, an architecture doc, a sprint report, the service catalog, a code-analysis dashboard) does not get an entry here. The test: would every preview benefit (platform), or is it one more thing published on the site (content)? See AGENTS.md for when to add an entry.
.stat in a row, a .twocol / .stage-split panel, a grid tile) used to hang its marker off its own right edge, which sits mid-page, so the marker landed on top of the neighbour to its right. Such a block now hangs its marker off the right edge of its whole row instead (found by climbing to the horizontal-layout ancestor), so it clears every sibling and reaches the true gutter; when several blocks in one row have threads, their markers stack vertically there rather than piling on one spot. Full-width blocks (paragraphs, headings) are unchanged. The hover affordance for starting a thread follows the same rule (rowRightFor / stackTop in assets/comments.js).action_id. Slack requires action_id to be unique within an actions block, so any stack with more than one open target was rejected with invalid_blocks and posted nothing — while single-target stacks kept working, so the breakage looked intermittent. The failed post also rolled back its own feed record each tick, leaving no trace until the error was logged. Each boost button now keys its action_id by target (boost:<key>); the button's routing data was always in its value, so the handler matches on the boost prefix and nothing else changes.ready.json the button writes, so the widget's state pill moves out of Draft and the plan joins the #pr-feed review channel — a draft people are already reviewing is, in effect, ready for review. Scoped to architecture pages (the tech-plan category), triggered only by a commenter who is not the page's declared author, and write-once so later comments never re-stamp the ready moment (which would reset the feed's freshness window). Runs as a background post-effect on the comment, so it never delays the write.search-index.json and each <category>/index.json are rewritten whole, so any two branches that each added a preview collided on them, and the root page's card counts (91 docs, 5 squads) collided a third time. Every count on / now renders at load from a counts map that build-index.mjs --root writes alongside the titles — counting listing entries, not the recursive page walk, so a preview's nested companion pages don't inflate its category — which means adding a preview no longer edits /index.html at all. The two JSON indexes route through a merge driver that regenerates instead of reconciling lines, but a driver alone is not enough and quietly gets it wrong: git updates the working tree in path order, so mid-merge it cannot see the previews arriving from the other side. So .githooks/post-merge and post-rewrite rebuild against the finished tree and stage the result, and CI now runs build-index.mjs --all (not just --root) and bot-commits any drift, so main self-heals even from a clone with nothing wired. Git will not read repo-supplied config or hooks on its own, so build-index.mjs calls scripts/ensure-git-config.mjs to opt the clone in on first run and say that it did./favicon.ico is generated from assets/favicon.js by node scripts/build-favicon.mjs, so the drawing code is the only definition of the mark; because browsers request the origin root icon for any page that doesn't name one, that single file covers all 169 previews — the generated dashboards included — with no per-page tag and no generator edits. The sweeping version comes from the script, which assets/comments.js loads lazily, so every preview carrying the comment widget animates without a tag of its own. It paints one still frame under prefers-reduced-motion and pauses while the tab is hidden.toc: sidebar in frontmatter moves the h2 Contents list from an inline block after the TL;DR to a sticky rail beside <main>, in the style of the hand-authored review pages. The rail collapses to a static block above the content under 900px, and a page with fewer than three sections falls back to the inline list. toc: off and the default inline behaviour are unchanged.explainers/<topic>/ is a published directory, so it holds exactly one file: the built index.html, the same shape as every other explainer. Everything needed to rebuild it lives outside the served tree in scripts/explainer/ — the engine template (versioned like plan-1.css), the builder, the mascot and background track, and per video a videos/<topic>/script.py (its META + SCENES + BEATS) beside its stills/. Sources in the served tree would be publicly fetchable and the stills delivered twice, once as files and once embedded. The first two explainers committed only that built page — which serves perfectly, since narration, music and stills are all data: URIs and the page needs no other file, but it left no way to fix a single word without rewriting the video from scratch. Build with python3 scripts/explainer/build.py <topic>: it synthesises narration (cached by a hash of the spoken text, gitignored, so only changed lines cost anything), inlines every asset, and derives the chapter count and runtime from the beats rather than trusting hand-written strings, which had already gone stale twice while a cut was still changing. Four engine fixes came out of building the second video: move can set h and height animates, so a still can zoom rather than stretch; a zoomed still takes z-index: 3 because every node was z-index: 2 and siblings later in the DOM painted over it; hide now beats dim instead of losing to it and leaving ghosts at 0.2 opacity; and a beat may carry cap, so a name spelled phonetically for the voice is not what readers see in the captions./ now has an instant, keyboard-accessible search bar for finding any published preview by its HTML title while preserving the existing collection navigation when the query is empty. node scripts/build-index.mjs --root deterministically regenerates the compact search-index.json from every visible preview under the collections linked by the root page; root collection pages, the changelog, and pages marked preview:hidden stay out of results. On pushes to main, the page-version capture workflow regenerates a stale index, bot-commits it, and captures versions against the resulting commit.~/.claude/intent/active.json held one row per working directory and intent-doc activate evicted by directory, so two Claude Code sessions in the same repo could not hold two projects: the last activation won, and *every* session in that directory then resolved to it — the wrong capture document, and requests and steer gates attributed to another project. Rows are now keyed on (cwd, session) using CLAUDE_CODE_SESSION_ID (the same id hooks receive as session_id), and resolution ranks ownership before path depth: this session's own row, then a row with no session, then another session's — the last still eligible, because a fresh session in a directory an earlier one registered must still find the project. nearestProject() takes an optional session (default behaviour unchanged), capture.mjs mirrors the ladder, and session-track.mjs passes its session id, which is the part that fixes request ownership. deactivate now removes only this session's row unless given --all-sessions, and status shows the session per row so rows left behind by ended sessions are visible — nothing detects a session ending, so they are surfaced rather than auto-pruned. INTENT_ACTIVE_JSON also overrides the registry for cli.mjs and capture.mjs, which is what makes the write side and the hook testable at all. Tests 123 → 128.intent-doc new / activate derived the hub key from the doc's path *relative to the checkout the CLI lives in*, so a doc in a second clone (e.g. html-previews-main, the tracked main-branch copy where intent docs are actually written) produced a ..-prefixed path, skipped registration, and left the project missing from the operator's Projects hub — with sessions in that cwd attributed to whichever project was registered before. It now falls back to locating the category segment inside the absolute path, which is what the hub key is derived from anyway. A path with no recognisable category still refuses, but now says what it could not derive rather than implying the doc was merely misplaced.template.html ships an obligations slot at the top of L1–L4 — product requirements at L1, system properties at L2, components-and-responsibilities at L3, a requirement named per section at L4 — as a bolded lead-in list rather than a table, because a level that states a design with nothing to check it against reads as a series of assertions. This mostly replaces the per-level scope line, now optional. (2) Per-level word-count targets are removed from the skill: a count cannot tell padding from load-bearing content, so it licensed bloat under the limit and forced bad cuts over it — length is now diagnosed by naming the habit causing it, with the only length bar being a 3-minute read on L0. The skill also gained rules that each fix an observed failure: headings name the question a section answers rather than its topic; a named technology is explained (with what it buys over the obvious alternative) before being relied on; proposal voice before execution starts; state the capability boundary and the design's weakest point; flag unilaterally-excluded scope; and draw failure paths and lifecycle states, not just flows. Plus framing answers now accept bold, *italic* and ` code `.template.html renders the intent:framing region *inside* the L0 · Outcome card, under one heading. Two sections both answering "why does this exist" — one at framing altitude, one at L0 altitude — reliably drifted into restating each other, so a reader paid twice before reaching any design. The division of labour is now strict and stated in the skill: the three questions own problem / value / approach, the L0 body owns only measures, budget and risk, and the two together must read in under 3 minutes. The framing command and region are unchanged, so existing documents keep working and read --section framing still resolves; only the template and the authoring guidance changed. Supersedes the 2026-07-28 rule that put the block before L0./ became two tiers: full cards under "In active use" (architecture, sprint reports, product newsletter, dashboards, research, audits, service catalog) and a compact name-plus-count row under "Smaller collections" for categories with a couple of entries or none yet (RCAs, visual recaps, explainers, PRDs, code analysis, post-mortems). A category graduates to a full card when it's genuinely in use, not pre-emptively.product-newsletter/ — a one-page digest of what shipped across the product squads (BOS, FOX, LEX), written for product stakeholders rather than engineers and human-edited before it goes out. Issues live at product-newsletter/issue-N-YYYY-MM-DD/ and start from the new copy-me starter assets/templates/newsletter-starter.html (plan-1 editorial system plus three page-local components: highlight cards with an amber "In practice" editor's note, grouped "also shipped" rows, and a per-squad "coming up" grid — promotion candidates for a shared newsletter-1.css once a second issue ships).intent/ ready for other people to run — a second full review, then the fixes, then a pre-merge audit from a clean clone. Setup: setup.mjs now installs the host dependencies the agent-side tools need (grading runs on your machine, not in the container — a fresh clone passed every check and then failed on the first grade), doctor *exercises* that path instead of only describing what Docker owns, the seeded decision frontier is neutral rather than one person's real altitudes, and a missing .env reports one failure instead of six. Removed: Slack notifications (the Requests UI shows the queue, the projects and the live sessions, so a DM said nothing new), the LISTEN/NOTIFY channel and its MCP registration, and the unattended worker — resuming a session that has already closed is now explicitly unsupported, documented in the README, SETUP.md and the steer skill rather than left to be discovered. ts-fsrs is the backend's only runtime dependency. Pacing is measured in minutes of the agent's *observed working time* rather than turn boundaries (a long turn with interruptions is one boundary, so dense work accrued nothing); the top bar shows per-kind Minutes buttons that each restart their own count; a switched-off kind is now *refused at the write* rather than discouraged in a skill; and concurrent sessions claim numbered slots so two cannot deliver the same owed request. Learning: spaced repetition schedules TOPICS, not requests (47 due dates had been written and never read), a hint rung sits between attempting and surrendering (answering after one caps at 3/4; revealing after one records 2, not 1), every request now carries a rubric written *with* the question and every practice steer the agent's actual decision — both withheld by the query until you answer — an optional confidence turns a grade into a calibration signal, and an exercise gets a code editor *plus* a text box per question (prose asks used to have to be typed into the code box). Session presence was rebuilt to store observations and derive state at read time, so a working claim decays instead of lying, /compact no longer strands a session as ended, and a mid-turn heartbeat covers work started by a background wake. Request briefs render real markdown (tables, headings, lists) via react-markdown. Tests 74 → 109.intent/ hardened for other people to run it — a full review (correctness, dead code, UI, setup, security) followed by fixes. Security: all three compose ports now bind 127.0.0.1 (they were on every interface, with a *committed* Postgres password for a superuser role — reachable from any machine on the same wifi), the password is generated into intent/.env by setup.mjs, scripts/serve-api.mjs no longer serves the working tree to the network, and api/projects.js / api/pages.js stopped authorizing on the client-supplied Host header (one shared api/_intent-auth.js, constant-time token compare). Correctness: every request write path now guards its source state and asserts a row changed (deciding a nonexistent gate used to report success; dismissing an open gate hid it while the agent stayed blocked; re-answering a graded request made it permanently ungradeable), and the Stop hook no longer marks a steer resumed *before* delivering the decision. Data loss: saving your Learning-setup profile no longer wipes codebase_level / last_reviewed, and typing your email no longer blanks the queue or discards unsaved edits. Fresh installs: the seed is per-operator (it hardcoded one person), Projects links resolve locally instead of 302-ing to Google sign-in, and doctor now checks all five hooks, the migration level, and that the served page is the React build. Dead code: the pre-React front end, the never-read pedagogy-rating feature, three unused catch-all queries and two unused components deleted; 15 API handlers now share one route() wrapper. Tests went 41 → 74, covering the request state machine, session presence and the SQL escaper — the three modules where all of the above lived.intent/ui/) — the hand-rolled single-file queue page is replaced by a Vite + React + Tailwind + shadcn/ui app (operator decision, steer #56), ported with feature parity: queue with pacing sliders/toggles, unseen-feedback tracking, resizable rail, multi-question steer forms, skip/reveal, thread with mermaid rendering, plus the Projects/Pages/History/Learning-setup views. The learner picker is now a real searchable combobox (keyboard + a11y handling the hand-rolled one lacked). The warm-beige palette and the five-style type system carry over as design tokens. The build runs inside the Docker image (multi-stage; server.ts serves ui/dist and refuses to start without a build — the legacy page is gone); hot-reload dev loop via npm run dev in intent/ui/ — see "Working on the UI" in intent/SETUP.md.intent/ — the requests queue + learner-model backend, queue UI, requests-check Stop hook, agent tools/skills, and its Docker stack (formerly the standalone local requests-ui repo) now live at intent/, so one clone carries the whole intent-layer system. Local-Docker-only: a new .vercelignore excludes it from every deploy. One-command cold start (node intent/setup.mjs: preflight with named fixes, .env generation, compose up on ports 3210/3211/5433, migrations + seed, both Stop hooks installed with stale-path repair, MCP channel registration, health checks; doctor subcommand for read-only status) with the teammate guide at intent/SETUP.md. Kept one-git mv-separable: own package/tests/docs, html-previews coupling is HTTP-only via api/projects.js + the shared INTENT_API_TOKEN./assets/comments.js?v=<content-hash>, so browsers pick up widget changes on a plain reload instead of heuristically caching the old script for days.data-commentable attribute, plus the shared card-shaped house classes (.tldr, .stat, .step, .panel, .card, .plane, .decision, .narrative). Cards anchor as one block and only as leaves (a card containing <p>s defers to them), so existing pages keep their granularity and nothing double-anchors. Bespoke page components (merge maps, phase cards, stat tiles) opt in per row/tile with the attribute — see AGENTS.md "Making component cards commentable".deck-1) — previews can be presentation decks — a new versioned shared asset (assets/players/deck-1.css / deck-1.js, same conventions as walkthrough-1) turns a page of <section class="deck-slide"> blocks into a full-screen presentation: a fixed 1600×900 stage scaled to fit any viewport (letterboxed on black), Arrow/Space/PageUp/PageDown/Home/End + click-thirds navigation, per-keypress data-frag bullet builds, #/N deep links, an injected progress bar and slide counter, and a media lifecycle — <video>/<audio> pause when their slide is left, <video data-autoplay> restarts muted on slide entry, and <iframe data-deck-embed> has its src stashed on exit and restored on return so embedded players (e.g. Loom) go silent when the presenter moves on. Clicks on players never navigate; with JS off the slides stack and scroll. First consumer: the LEX Sprint 9 review deck (sprint-reports/lawrence-experience/sprint-9-2026-07-22/), which replaces the HTML mirror-report format with a presentable deck on the Lawhive master-deck brand (Rubik/Inter, black canvas, peach ramp).overflow: hidden clip lands on the invisible page edge rather than slicing a hard line across the top blob. Fixed in assets/plan-1.css + the MDX compiler (scripts/compile-mdx.mjs) and the plan-starter / walkthrough-demo templates, then backfilled across the existing house-style pages so every plan-1 preview matches..mdx pages compile to the same static plan-1 HTML as hand-written ones (scripts/compile-mdx.mjs). Source and compiled HTML are committed together, so the deploy story is unchanged: Vercel still serves static files. Live capability demo + adoption guide: architecture/html-previews-from-mdx-2026-07-17/..tablewrap, task lists, footnotes); compile-time KaTeX (new vendored assets/vendor/katex-0.17.0/, CSS+fonts only); Shiki-highlighted code; Mermaid fences; Vocs-style directives (:::note family → accented panels, ::::steps[vertical] → step strip, ## Title [subtext]); house components (<TLDR>, <PullQuote>, <Cols>, <StepStrip>, <Figure>, <Chart>); auto reading time and Contents.components/<name>/index.jsx next to the page imports @mui/x-charts / @visx/* directly; esbuild bundles at build time and everything server-renders to static HTML. Pages ship no new runtime JS.data-doc (comments: off opts out; plugins: walkthrough pulls the step-through player).pnpm dev runs the static server plus a repo-wide watcher with self-reloading pages (Ctrl-C rewrites every page clean before exit); pnpm mdx builds all pages and exits non-zero on any compile error.mutePage/muteThread actions and a notify/<doc>/<who> digest-state blob in api/comments.js; chat.update support in api/_slack.js; vitest coverage in test/notifications.test.mjs (97 total).<meta name="preview:author-email"> tag — required on new pages (see AGENTS.md) and stamped onto 53 existing discussion-enabled pages from their git creation history by scripts/backfill-author-emails.mjs. Same delivery path as mention DMs (background post-effect, best-effort, time-boxed; only verified sign-ins are reachable). Vitest coverage in test/notifications.test.mjs.dashboards/ category for hand-built dashboards that recompute from live data on view, distinct from the generated code-analysis/ charts. Establishes a reusable shape: a Vercel function (api/pr-coins) computes and caches a snapshot in Blob (15-min TTL), and the page ships a baked snapshot so it renders fully with JS off, then fetches for fresh numbers. A second sanctioned exception to "no runtime fetch", alongside comments. Needs a GITHUB_TOKEN env var (read-only) for the GraphQL pulls.scripts/verify-version-watermark.mjs (24 checks). See AGENTS.md.data-hpc-id authoring guidance — the "changes since ▾" picker no longer vanishes when you pick a baseline with no changes: the panel stays with a "No changes" state and a ✕ to reset, so you can switch baselines freely. Documented in AGENTS.md how to tag preview blocks with a stable data-hpc-id for precise typed diffs (and backfilled them on the intent-layer system doc).scripts/verify-version-watermark.mjs (12 checks).api/version endpoint, and diffs it against the live page with the existing typed add/changed/removed + word-diff UI. Every reviewer gets this automatically, with no snapshot to seed. Per-block **✓ now *dismisses* a change (one blob per reviewer per block — race-free, presence-pattern like reactions — hidden until that block changes again); "Mark as reviewed" / approving records a tiny review pointer ({reviewedCommit}) and clears your dismissals; the button reads "✓ Reviewed" until the page changes. Old versions are captured at deploy time by a GitHub Action (.github/workflows/capture-versions.yml → scripts/capture-versions.mjs) that snapshots each changed preview's HTML into the private blob store keyed by commit — so the runtime never calls GitHub** (no token, no rate limit; the Action uses its own repo-scoped token + BLOB_READ_WRITE_TOKEN). This replaces the per-reviewer content snapshot entirely (no more duplicate content keyed by user, no last-write-wins on acks). Server rework in api/comments.js (markReviewed/dismiss/undismiss actions, per-user reviewed+dismissals folded into the thread), new api/version.js, and a headless check in scripts/verify-version-watermark.mjs. Known limitation: pages whose content is generated by JavaScript on load diff approximately (the captured HTML is pre-JS); static previews are exact. See AGENTS.md.dom-anchor-text-quote (W3C TextQuoteSelector matching via diff-match-patch), the same anchoring Hypothes.is uses — matching the text we already snapshot per comment (a highlight's quoted text, or a block comment's paragraph), so it re-finds the passage even after the surrounding words changed. It works retroactively on every comment ever made (no storage change, no migration) and replaces the bespoke exact-substring findQuoteRange; only a near-total rewrite defeats the match, which still falls back to the previous-version panel. The library can't come from package.json (the org CodeArtifact registry doesn't proxy it), so scripts/vendor-text-anchor.mjs fetches the pinned version from public npm and bundles it (deps inlined) to assets/vendor/dom-anchor-text-quote-4.0.2.js, which assets/comments.js loads lazily — no per-page tag. Headless check in scripts/verify-comment-anchor.mjs proves an edited-block comment re-anchors with the library and orphans without it. See AGENTS.md.comments/<doc>/w_<who>.json); your own watermark is also cached in localStorage so it takes effect immediately despite Blob's read-lag. Two detection modes: when the page tags blocks with a stable data-hpc-id, changes are typed precisely — added / changed / removed (deletions detected on their own, no comment needed) — the word-level diff is rendered in the document text in one of two toggleable, persisted styles — inline (struck deletions + highlighted insertions) or before/after (the block shown twice: a "was" line with removals in red, a "now" line with additions in green) — while the panel stays a compact index (type + short label) where each change can be commented on (💬, including removed blocks) or acknowledged individually with ✓, or all at once; a Hide toggle turns the highlighting off (persisted per doc). Pages without ids fall back to flat content-hash detection (changed/added lumped as "changed"; deletions surface via orphaned comments). Works on any preview loading assets/comments.js. Vitest (test/watermark.test.mjs) pins the store: per-reviewer single-blob overwrite, the block-id map + snippets, id/hash shape-checks, approval capture, and the session gate. See AGENTS.md.scripts/intent-doc/template.html is a self-contained scaffold whose editable regions are delimited by <!-- intent:<region>:start/end --> comments; scripts/intent-doc/cli.mjs (dependency-free Node) only ever edits between those markers, so untouched blocks keep the content hashes assets/comments.js anchors comments to. The CLI (new · read · set-level · note · frontier · milestone · log-decision · gate · notify · activate) creates a doc, records intent decisions into an append-only log with OpenSpec-style delta badges, and sends a milestone Slack DM (local-print fallback with no token). A Stop hook (~/.claude/intent/hooks/capture.mjs) prompts the agent to classify+record or gate decisions, but only inside a registered intent project (intent-doc activate), and anti-loops via stop_hook_active. Authoring is driven by the intent-doc skill (successor to the removed html-tech-plan skill). See scripts/intent-doc/README.md and the intent-layer system plan. The change-since-review watermark (M2) and the learning/steer queue (M3, in the separate learning-ui repo) are not built yet.assets/players/walkthrough-1.js + walkthrough-1.css. window.Walkthrough({ root, steps, renderRight }) owns timing, controls, dots, wrap/clamp, cumulative reveal, opt-in keyboard, and reduced-motion (starts paused); the page writes a steps array and one render callback. Derived from the three shipped players (faithful to their ~2.6–3s cadence), with a11y added. Live copy-me reference at assets/templates/walkthrough-demo.html. New-pages-only — existing plans keep their inline copies.assets/plan-1.css (tokens, hero, and every component rule already shared by 2+ terracotta pages — derived from the shipped pages, with provenance comments), assets/plan-1.js (lazy Mermaid in <details>, hash reveal, the planCopy export helper) and assets/templates/plan-starter.html (copy-me skeleton). Style changes ship as plan-2.css; published pages keep the version they were reviewed with; page-local components stay inline until a second page needs them. Existing plans are deliberately not retrofitted — the scaffold applies to new pages (validated against the shipped pages' extracted CSS and a non-shipped retrofit exercise). Also fixed the hero-grain spec in AGENTS.md (0.28 soft-light, as the canonical page actually ships — the doc had mis-transcribed it).package.json is now the source of truth (mermaid devDependency); scripts/vendor-mermaid.mjs copies the installed dist bundle to assets/vendor/mermaid-<version>.min.js, exactly one version lives in the repo, and all nine Mermaid pages point at 11.16.0. scripts/verify-mermaid-render.mjs renders every diagram headlessly with the old and new bundle and pixel-compares them: across the consolidation, 44 of 46 diagrams were identical; the two that re-laid-out (both flowcharts in forms-service-unified-ingestion) were reviewed by eye. The old extracted mermaid-11.15.0.min.js is deleted./assets/vendor/ with the version in the filename" (a shipped page keeps the exact library version it was reviewed with; assets/comments.js stays the one deliberately mutable shared widget). Applied in the same stack: mermaid-11.15.0.min.js and mermaid-11.16.0.min.js now serve the nine plans that each inlined a ~3 MB Mermaid bundle (each page pins the byte-identical bundle it shipped with), and skills-feature's nine base64 screenshots moved to files in its folder — every plan is now 46–134 KB and agent-readable in one pass (~15K tokens instead of ~900K), with visible body text unchanged so existing comment anchors are unaffected. Also codified when to diagram with Mermaid vs hand-authored SVG (Mermaid for structure — ~15× cheaper to write, read, and keep true on drift passes; SVG for product mockups, meaningful spatial layouts, and JS-driven figures). See AGENTS.md.npm test) pins the guarantees behind Google sign-in and the session cookie: HMAC sign/verify, rejection of tampered/expired/foreign-secret (forged) sessions, the Google token gates (audience, issuer, verified email, Lawhive domain), the HttpOnly/SameSite/Secure cookie flags, and that a comment's author is always taken from the verified session — never the request body — so nobody can post as someone else. Run with npm test.waitUntil), so notifications never delay a comment. Best-effort and time-boxed; edits notify only newly-added mentions. The DM's link-back always uses the requesting deployment's own host (from the request, never the client-sent URL), so a signed-in user can't aim the bot at a phishing link and it works on every preview with no host allowlist to maintain. Requires the bot scopes im:write and chat:write (on top of users:read/users:read.email from the directory). Vitest tests pin the notify path: a DM per mentioned colleague (never the commenter), the response returning before delivery, failures swallowed, the link-back taking its host from the request (a phishing URL can't redirect it), and edits notifying only the newly-added.@ in any composer opens an autocomplete of taggable colleagues and inserts a rich, atomic mention chip; the comment stores the mentioned {email, name} and renders the mention highlighted. The taggable directory is sourced from the Slack workspace (users.list via a shared api/_slack.js, reusing the link-unfurl bot token), blob-cached (directory/slack.json, 30-min TTL) with stale-while-revalidate; the sign-in roster is the fallback. Everyone taggable is by definition reachable by DM. Needs the bot scopes users:read and users:read.email. Covered by Vitest tests for the Slack client (dmByEmail sequence, users.list pagination and filtering), the directory endpoint (session gate, blob cache with stale-while-revalidate, roster fallback) and mention sanitisation/storage. Builds on the verified-identity change below.api/auth function verifies the Google ID token (domain-restricted to lawhive.co.uk, email verified) and issues an HMAC-signed session cookie (api/_session.js); api/comments now takes the author from that cookie instead of trusting the request body, so nobody can post or approve as someone else. Items are keyed by verified email. Secrets (GOOGLE_OAUTH_CLIENT_ID, SESSION_SECRET) come from Doppler; run locally with npm run dev (doppler run -- vercel dev) — see AGENTS.md. This lays the groundwork for @-mentions and notifications. (Comments written before this change keep their typed name and will be migrated.)<textarea>), so Cmd/Ctrl+Enter or the send button posted the draft as a brand-new comment instead of (or as well as) saving the edit. Also guarded the panel reply and video composer against double-submit (assets/comments.js).scripts/visual-recaps/*.visual-recaps/ home plus scope-specific generation guidance and product-facing copy rules in docs/content-types/visual-recaps.md.api/slack-events) that reads the SSO-gated page server-side and renders the card with @vercel/og; card images are served from a separate public Blob store, so the documents themselves stay private.assets/comments.js, api/comments.js).<pre>), table cells, all heading levels and captions, not just paragraphs and h2/h3. Selecting text inside a code block also makes line- and token-precise comments work there. Mermaid diagrams are deliberately left out, pending a pin-based model.CHANGELOG.md now renders as a styled page at /changelog/, linked from the site footer. Generated by scripts/build-changelog.mjs.scripts/build-index.mjs → architecture/index.json; counts no longer drift from a hand-maintained list.scripts/pull-comments.mjs — pull a preview's discussion (comments, reactions, approvals, viewers) straight from the Blob store, bypassing the Vercel auth gate, for agents and scripts.AGENTS.md — the blog-post standard, design tokens, fonts, gradient, and motion.api/comments.js, assets/comments.js).rcas/, architecture/, sprint-reports/, etc.; folder path becomes the URL.AGENTS.md authoring guide — use cases, FAQ, and the contract for generating previews that match the house style.