- Contest results block on
/artifacts: The judged winners are announced above the feed — a full-width first-place card for SECOND COAT (Spencer Norwick) with a hero crop of its civic-notice render, then the four runners-up bundled into a 2×2 grid (A Few of John Warner's Personal Notes, Kannen Ramsamy; One Drone Per Person, Nick Punt; Kinétic by ARL, William Wickey; Big Man's Small Batch Corn Liquor, Cameron Armstrong). Prize amounts are deliberately not published. The heading reads “Winners of the Stigmergic Artifact Contest” — that phrase carries the link to the contest call, which is now the only prominent route to /contest from the site chrome.
- Winners are referenced, not duplicated: Every winner card links down into the feed (
#a-<id> anchors added to each .artifact-post) rather than re-rendering the entry. That keeps one canonical rendering of each artifact — important because #22 carries a 16 MB animated GIF that must not load twice — and it preserves the “see the winners, then browse everything” flow the full 29-entry timeline was built for. The six winning submissions also carry a ★ First place / ★ Runner-up ribbon inline in the feed, and the SSR permalink pages render a matching placing ribbon so a shared link carries the result with it.
- Kinétic by ARL is one entry across two submissions: William Wickey filed the Creative Brief (#9) and the Rough Cut Treatment (#10) separately, but they were judged as a single work. The winners config is therefore keyed on groups of submission ids, not single ids —
CONTEST_WINNERS.runnersUp = [[2], [27], [9, 10], [25]] — with a WINNER_GROUP_META override supplying the merged title, the “judged together as a single entry” note, and per-part links. Both ids still get their own feed ribbon and their own SSR ribbon. Ids rather than slugs throughout, since slugs can be re-published.
- Contest removed from the global nav (17 locations): Dropped
<a href="/contest">Contest</a> from all 14 static public/*.html navs, the SSR nav in functions/artifacts/[slug].js, and scripts/codex_render.py (then regenerated codex.html — the diff was the single nav line, re-confirming the renderer is in sync with the committed HTML). Removal was scripted with a line-anchored regex asserting exactly one match per file, so contextual body links and breadcrumbs to /contest were left intact and the call stays reachable. Same 17-location pattern as the Session 23 Welcome addition; devlog and admin stay intentionally nav-less.
- Post-contest state on
/artifacts, and a real hidden-attribute bug: With voting closed the ballot UI is now dropped entirely rather than rendered disabled — no sticky vote bar, no per-post steppers — while final scores stay visible on each entry. Found in the process that the vote bar was never actually hiding: .vote-bar { display: flex } outranks the UA [hidden] { display: none } rule, so the hidden attribute was inert and the “Remaining: 40/40 · VOTING CLOSED” bar was on screen. Added an explicit .vote-bar[hidden], .sort-bar[hidden] { display: none }. Also removed the dead July-31 countdown banner and rewrote the lead paragraph, which still read “You have 40 votes to spread across…”, and repointed the homepage notice bar from “Deadline July 31 · Enter →” to “Winners announced · See the winners →”.
- A CSS class collision worth remembering: The runner-up tag was first written as
<span class="place-tag runner">, which also matched .runner — the card selector — so the tag inherited display: block plus the card's padding and rendered as a full-width bar. Renamed to .place-tag.is-runner. Two related bleed-throughs from style.css were fixed at the same time: the base h3 rule is a small-caps rule with a 2px underline (it was drawing a line under “SECOND COAT”), and the base p rule forces --text-dim, which killed the announcement copy on the amber header. The results header now sets cream-on-amber explicitly — #111 on #9C4A00 is only ~3.4:1 and fails for body text, so it follows the existing .notice-bar precedent instead.
- Verification method: No browser extension needed — drove headless Chrome directly (
--headless --virtual-time-budget --dump-dom / --screenshot) against a local static server that proxied /api/* to production, so the page rendered against the real 29-entry feed. Two things worth reusing: (1) the SSR placing ribbon was unit-tested by copying [slug].js to a temp tree, appending export { page }, and calling it with synthetic rows — four cases (first / runner-up / grouped / non-winner) without needing a seeded local D1; (2) a headless screenshot is not a reliable mobile check — it appeared to show horizontal overflow at 430px, but an iframe probe measuring scrollWidth vs clientWidth and enumerating every element's getBoundingClientRect().right showed no overflow at 430px or 360px. The same false positive reproduced against untouched production, which is what identified it as an artifact of the tool rather than a regression.
- Two pre-existing defects surfaced, not fixed: (1)
/contest still reads as an open call — “Deadline: July 31, 2026”, “Submit your artifact” — and the winners announcement now links people straight to it. Wants a “closed, winners announced” banner; left alone because how the archival call should be framed is an editorial call. (2) Griffin Hotchkiss's three λ roulette entries (#28–#30) render raw markdown — ## LOSS AND GAIN and *emphasis* appear literally, because the submitted bodies use single newlines between blocks so renderMarkdown wraps the whole thing in one <p> with <br>s. Fixable via admin “Edit content”; a renderer change would affect every entry and wasn't in scope.
- Winner contact details pulled for the announcement emails: Queried
submissions directly (wrangler d1 execute --remote) for the five winners' emails. Note William Wickey used the same address for both #9 and #10, so it's five recipients, not six. Flagged that Kannen Ramsamy is one of the three pre-Session-17 entrants whose license column is still NULL — the winner email is the natural moment to collect that CC BY-SA 4.0 consent and close the open item.
- Exploratory: exe.dev, and where stateful compute would actually pay off here: Discussed (built nothing) whether a persistent-VM host like exe.dev has a place in this project. Its distinguishing feature is authentication at the HTTPS proxy — visitors are identified before application code runs, arriving as
X-ExeDev-UserID/X-ExeDev-Email headers — which is precisely the capability the admin dashboard lacks and cannot get on its own, since CF Access on /api/admin/* is org-admin gated and still pending in zero-trust-requests.md. Second candidate: bard_ingest.py, which has no runner and so lives on a laptop, and whose Pinecone dependency exists mainly because a Worker has no disk — 193 vectors is a file, not a managed service. Conclusion recorded as a non-decision: the public site is correctly stateless and should stay on Pages/D1/R2 (moving it would cost the edge for nothing); the only candidates are the ingest, the planned Phase 1 usage meter, and the admin tool. Nothing adopted.
- Approved 8 new entries (#23–#30): #23 Please, Move On (Hermes Corp); #24 Forum Thread: SOLSYNC for train lag? (Nathan Chen); #25 Big Man's Small Batch Corn Liquor (Cameron Armstrong); #26 They are only tomatoes. (Dakiny); #27 One Drone Per Person (Nick Punt); #28–#30 the three-part 'λ roulette' series (Griffin Hotchkiss). Same method as the #20–#22 batch: direct D1 replicating
review.js (status + reviewed_at + pub_title/author/slug), slugs pre-checked for uniqueness/reserved collisions, since ADMIN_SECRET isn't available to Claude Code. Handled #25's apostrophe (SQL-escaped) and the λ/·/— characters in the roulette titles (kept in pub_title, stripped to clean ASCII slugs; the (1/3)/(2/3)/(3/3) parts yield distinct roulette-1-3…/2-3…/3-3… slugs). #1 (empty test row) left pending. Verified live: /api/artifacts lists 29 entries (#2–#30), all SSR permalinks 200. (This work occurred immediately after Session 26's wrapup checklist was posted, so it was previously unlogged — recorded here for a complete trail.)
- Added a Sort: Newest | Top voted toggle to
/artifacts: Voting closed end of Aug 2 (VOTE_CLOSE = 2026-08-03T07:00:00Z), so the page is now a results view. Added a client-side sort toggle above the timeline. Newest = existing server order (reviewed_at DESC) and remains the default; Top voted ranks by final score (Σ√votes), tie-broken by voter_count then id. No backend change — /api/artifacts already returns score/voter_count. Implementation reuses the existing render() and preserves any in-progress ballot (re-renders without a refetch, restoring working/saved alloc); the chosen sort persists across a vote-save; voting-closed disabled steppers untouched. New JS: SORT state, sortedArtifacts(), applySort(). Deployed (042c05f0) + verified the served page carries the toggle markup + logic. Current leaderboard: #22 SECOND COAT leads (score 65.7, 16 voters). Open choice left to user: default is still Newest — could default to Top voted when voting is closed (one line: let SORT = VOTING_OPEN ? 'newest' : 'votes'). This deploy also flushed devlog.html, which had been committed-but-undeployed since Session 24.
- Manual media attach to #22 'SECOND COAT' (Spencer Norwick, Zoothesia): The entry was submitted via Google folder; its artifact is a 16 MB animated GIF (1200×1500) that the submit form rejects (
MAX_IMAGE_BYTES = 5 MB). Added it by hand — wrangler r2 object put to jamverse-media/submissions/22/1-second-coat.gif (content-type image/gif) + an INSERT into submission_media — which bypasses the form/edit-endpoint size cap entirely. Later added two more renders the same way (Civic + Classified PNGs, ~400 KB each, 1200×1500) as attachments 2 and 3. Verified live: all three serve HTTP 200 with correct content-types and appear in /api/artifacts images[] in order (GIF first, then the two stills). This is the reusable procedure for any submission whose media the public form can't take (too large, or a format/flow the form doesn't support): put to R2 + insert the submission_media row by hand. Note: GIFs render + animate natively (inline <img>), no code change needed.
- Approved the contest queue on deadline day (July 31): Pending entries were #1, #20, #21, #22. Approved #20 (VK Irugu, 'INTERFACE CONVERSATION EXPORT: BOB/ALICE/EVE'), #21 (Kannen Ramsamy, 'STOCKTON FOULXMAX NIGHT'), and #22 (Spencer Norwick, 'SECOND COAT'). Excluded #1 — Sachin's empty test row (no title, no body), documented as the test entry in
status.md. Approval mechanism: the admin endpoint (/api/admin/review) needs the ADMIN_SECRET, which isn't available to Claude Code, so I replicated review.js's logic exactly via direct D1 — set status='approved', reviewed_at, and the publish metadata pub_title/pub_author/pub_slug that /artifacts renders. Pre-checked all three slugs for uniqueness + reserved-slug collisions (the-zoo/the-big-man) before writing. Matched the existing convention: pub_author = entrant name, pub_blurb left null (won't invent public copy for someone's entry). Verified: /api/artifacts now lists 21 entries (#2–#22), #1 correctly absent, and all three SSR permalinks (/artifacts/<slug>) return 200. Reversible (flip status back to pending).
- State: All work was live D1 + R2 operations — no repo code changed, nothing deployed. The Bristlemouth Phase-0 setup (the pinned ▶▶ NEXT SESSION block in
status.md) was not touched this session — contest ops took priority on deadline day; it remains queued and pinned. Small loose ends on #22, deferred: its body still reads 'ARTIFACT SHARED VIA GOOGLE FOLDER…' (could be tidied now that media is native), and the three attachments could be reordered if the entrant wants a still to lead instead of the GIF. Source files live locally under External submissions/ (gitignored — the canonical copies are in R2).
- Bristlemouth is now a RAG (search + chat), not a published essayist: User decided the Bard should be searchable and talk-able, built on Voyage embeddings + Pinecone. This reverses the earlier 'no vector DB' stance — that argument was against RAG for the essayist product (top-k fractures cross-canon synthesis); for a lore Q&A + chat product, retrieval-with-citations is exactly right. Hybrid rule kept:
soul.md + codex.json always in-prompt (never retrieved); retrieve passages on top for grounding/citation. Two decisions locked this session: (a) store no reader chat data — deletes the whole episodic-memory layer (no PII in Pinecone, no retention policy, no 'forget me', no compaction, no reader-identity decision); chat is stateless server-side, the browser holds the in-visit transcript. (b) a live cost meter — surface spend on the admin dashboard. Also settled: Pinecone on the dev's own account needs no org-admin gate (the CF token already covers deploy + wrangler pages secret put; external HTTPS from a Worker needs no CF provisioning) — my earlier 'org-admin decision' framing was about the isolation principle + who owns the bill, not a technical gate.
- Cost meter grounded in real pricing: Loaded the
claude-api skill for authoritative Opus 4.8 rates ($5/$25 per 1M in/out; cache reads ~0.1×, writes 1.25×). Every Claude response returns a usage block, so the meter costs each chat request from input_tokens/output_tokens/cache_read/cache_write at those rates + a Voyage line. Prompt caching is the primary cost lever: soul.md + codex are identical every chat turn (~20–30k tokens) — cache them (cache_control: ephemeral, byte-identical prefix) and they read at ~1/10th price; the meter shows the cached-vs-uncached ratio so the caching win is visible. Aggregate, non-PII usage lands in a D1 bard_usage table (token counts + cost_usd, never message text), displayed on /admin/; optional daily budget cap. Design written to Bristlemouth-the-bard/rag-architecture-spec.md (supersedes the three-layer memory-and-retrieval-spec.md).
- Phase 0 (search-first) built and validated: Three new pieces plus a runbook. (1)
scripts/bard_ingest.py — builds 193 documents from local data only (177 Codex entries + 16 grounding synopses; no story-fetching), embeds via Voyage, upserts to one Pinecone namespace with kind/type/universe/url metadata. Has --dry-run (ran clean: 193 unique docs, ~12.5k tokens — the whole ingest is a fraction of a cent). Codex docs carry url = /codex#<id> so search results link to the live deep-links shipped in Session 23. (2) functions/api/bard/search.js — GET /api/bard/search?q=: Voyage query-embed → Pinecone top-25 → Voyage rerank-2.5 → top-6 JSON; returns 503 'not configured yet' until secrets exist, so it's safe to deploy early. ESM syntax validated. (3) public/bard.html — the /bard search page (on-brand, semantic-search box + results), not in global nav yet (unadvertised until proven). Simplified from the spec to a single namespace + kind metadata (one query vs. N+merge, right for ~200 vectors). Nothing deployed; no keys provisioned.
- State: All WIP, uncommitted→committed this session, not deployed, nothing wired to a live model or live keys. Phase 0 cannot go live until the user provisions Voyage + Pinecone accounts and runs the ingest + sets three Pages secrets — the exact runbook is in
Bristlemouth-the-bard/PHASE0-SETUP.md and the actionable checklist is pinned at the top of status.md Upcoming for next session. Open decisions (spec §8): confirm 'no storage' = client-held transcript (assumed); Voyage model/rerank; chat context budget; daily cap threshold; search-UI placement.
- Removed the 3-entries-per-email submission cap (
functions/api/submit.js): The submit handler had an 8-line block that ran SELECT COUNT(*) FROM submissions WHERE email = ? and rejected a 4th submission with a 400. Per request, deleted it entirely — submissions per person are now unlimited. Context for why it was low-stakes to drop: it was a soft honor-system cap (keyed on lowercased email, counting all statuses incl. rejected, trivially bypassed with another address) and was never surfaced to entrants on /submit or /contest — the error only appeared after a 4th attempt. The ipHash computed just below is stored but was never used for the limit, so nothing else depended on the block; emailLower is still used for validation + insert. Deployed (6b27ef86) and smoke-tested live (empty POST now returns the normal 'title required' validation error, not a cap error). Note: this deploy also flushed the Session-23 devlog.html that was one deploy behind.
- Indexed "The Flesh Perfected Is the Flesh Possessed" (Sachin Benny, Trainverse, pub. Apr 3 2026): A canon Trainverse story that existed only at
protocolized.summerofprotocols.com and had never been indexed. codex_check.py couldn't have caught it — it only diffs stories_indexed against public/stories.html, so a story never listed on the site is a blind spot (and its regex only matched protocolized.io URLs). Ran the full CLAUDE.md Codex procedure. Added 15 new entries (Rowan; Lucas/Jax/Ana; Alex; Marcus Hale; Karel; Yevgeni Stolar; Sable Meridian; BRX-90; the Car 7 decontamination array; Stolar's cold wallet; Black Wednesday; the Tirzepatide Trail; the four signing keys; 'somatically gentrified'; AIRFLOW REVERSAL) and extended 4 recurring entries (UET-1, ECOROUTE, the Pono, bio-band) with the new slug. 162→177 entries, 15→16 stories.
- Extraction fidelity + UET-1 canon fix: Followed the WebFetch-summarization caveat strictly — 4 fetches with repeat verbatim cross-checks on every load-bearing name. This caught a real error (the first summary said Black Wednesday killed '32 women'; the source says nine in Car 7) and confirmed 'Black Wednesday', 'AIRFLOW REVERSAL', 'Sable Meridian', 'BRX-90', 'Yevgeni Stolar' verbatim before they became permanent entry ids. The story also gave a third independent verbatim confirmation of the UET-1 route ('connected Lisbon to Laos, passing through 13 different territories', no total length stated), so I fixed the long-standing
codex.json error in-place: the UET-1 entry now reads 'Lisbon–Laos through 13 territories' (was the invented 'kilometer-long…Lisbon–Singapore'). This reopens + resolves the item previously left for Rao — noted, since the correction came from the author's own new story. Still lingering: the WIP Bristlemouth prose (jamverse-intro.md + the Trainverse draft) still say 'Lisbon to Singapore' — deferred to the next prose revision.
- Site plumbing +
codex_check.py fix + deploy: Broadened codex_check.py's slug regex to also match protocolized.summerofprotocols.com/p/<slug>, so the off-mirror story reconciles instead of falsely reporting 'stale' — the check now reports '16 stories indexed, all match.' Flagged the story with the existing mirror_note mechanism (grouped with the two Stockton stubs in the generated 'not yet mirrored on protocolized.io' note). Listed it on /stories (new Trainverse card), /universe/trainverse, and llms.txt (entry + count), and bumped the story count 15→16 across stories.html, welcome.html, llms.txt, CLAUDE.md, status.md. Added the story to Bristlemouth's grounding.json (synopsis / inhabitable-machines / protocol-core / crossovers), noting ECOROUTE's Black-Wednesday origin is established here. Re-rendered codex.html (177 entries), committed (4cc5e9e), deployed (2124c235), and smoke-tested live (new entries on /codex, card on /stories, old 'Lisbon–Singapore' gone).
- Merged PR #3 (from Venkatesh Rao,
codex-proper-noun-index): Three commits — (1) Codex entries are now addressable via anchor URLs (/codex#<id>) with a :target highlight and JS that reveals a targeted entry even when a filter/search would otherwise hide it; (2) a per-entry copy-link icon (clipboard + 'Copied!' flash, progressive-enhancement); (3) a new /welcome orientation page + a 'New here?' callout on /stories. Reviewed before merging: all 162 codex entries already carry a unique id (so the render change is safe), and the committed codex.html reproduces exactly from codex_render.py (the Session-21 generator discipline holds). Main had diverged from the branch, so this was a real merge commit (not a fast-forward), disjoint from the Session-22 Bristlemouth work — no conflicts.
- Global nav — added 'Welcome' as the first item: Rao's PR only linked
/welcome from the /stories callout, not the header. Added it to the site-nav across all 14 hand-maintained public/*.html pages, the codex_render.py template (+ re-rendered codex.html), and the SSR artifact nav (functions/artifacts/[slug].js) — 17 nav locations. The devlog and admin pages keep their intentional no-nav (per the Session-21 convention). One scripting gotcha caught: the batch inserter skipped stories.html because its body callout already contained an href="/welcome" that tripped the idempotency check — patched its nav by hand.
- Removed the invented UET-1 train length: User caught that neither '500-metre' (Rao's
/welcome) nor 'kilometre-long' (our Bristlemouth grounding.json) is right. Checked the primary story text (verbatim-quote fetches, per the CLAUDE.md WebFetch-summarisation caveat): the stories state no total train length — the only dimension given is 'a tube 18 metres long' for Car 16. Removed the length from both welcome.html and grounding.json. Also surfaced (left for the Codex author): the route is canon Lisbon–Laos (verbatim: 'connecting Lisbon to Laos'), but codex.json's UET-1 entry still says 'kilometer-long…Lisbon–Singapore' — both wrong, but it's Rao's canonical data + needs a re-render, so flagged rather than edited. The old figure/terminus also lingers in the rejected jamverse-intro essay and the Trainverse draft (WIP).
- Deployed to production and verified:
wrangler pages deploy public/ (deployment b617f1bb). Live checks on jamverse.protocolized.io: /welcome 200 with Welcome active in its nav; /codex shows Welcome in nav + the codex-permalink copy-link feature + working deep-link targets (id="the-zoo"); /stories has both the nav link and the callout; no invented length remains on /welcome. PR #3 confirmed MERGED, no open PRs.
- New feature — Bristlemouth, an in-world AI bard: Began building a generative, essayistic agent named Bristlemouth that introduces readers to Jamverse and writes in three modes: describe-the-universe essays that connect canon to real-world protocol phenomena, in-world machine-POV micro-fiction, and conversation. Architecture settled: Plan 3, publish-first (ship a reviewed, published body of work before any live chat, to avoid an exposed API key + abuse surface); grounding via prompt-stuffed context (the 162-entry Codex is already a near-perfect knowledge base) rather than a vector DB; Opus as the writing model. Explicitly rejected MongoDB Atlas / Voyage RAG (the user's initial idea) — wrong weight class for a 15-story corpus, and it would break the project's single-vendor Cloudflare isolation. Nothing is wired to a live model yet; voice was prototyped by Claude Code acting as Bristlemouth. All files live in
Bristlemouth-the-bard/.
- soul.md — the persona spine (
Bristlemouth-the-bard/soul.md): Wrote the system-prompt soul from the user's uploaded lineage docs (Marías on the interpreter-narrator, Ballard's inner space, Hacking's 'Making Up People', Carol Rose's crystals/mud, a time-and-interiority lineage, and a bard→cubist-narrator survey). The name comes from the protocolization essay's image of the bristlemouth: the most abundant vertebrate on Earth, bioluminescent, unseen in the ocean's middle layer — the emblem of invisible, distributed coordination. Locked decisions: Bristlemouth is a distributed synthetic intelligence that inhabits machines and moves between them (Culture-Minds lineage); pronoun they/them, gender-fluid per machine it wears; openly and ironically self-aware. Core stance = 'the voice in the middle'. Two hard voice rules from user feedback: (1) the Woolf rule — every piece builds to a collision of different orders of time/tension landed in simultaneity (protocol-time vs. lived interiority, the bell vs. the son's voice); (2) never other — Bristlemouth is part of what human life is made of, never an 'alien' studying a 'species'. Treated as a living doc, editable later.
- grounding.json — the ground-truth pack (
Bristlemouth-the-bard/grounding.json): Built the pack Bristlemouth writes from, shaped for a narrator that moves between machines. Each of the 15 stories carries synopsis (arc + emotional_core), inhabitable (the machines/protocols it can wear and narrate from, each with a voice-note), protocol_core (central coordination mechanism + real-world rhymes for describe-mode), and crossovers (guards against double-narrating shared in-world things). Synopses from protocolized.io fetches (plot reliable, verbatim not) cross-checked against codex.json entities. Caveat: the two Stockton stubs — The Caucus and Joan Henry vs the Algorithm — are placeholder-only on protocolized.io, so their entries are reconstructed from Codex entities and flagged source_note provisional (full text at randylubin.com). Crossover guards recorded: the Zoo is one shared apparatus across all six Zoothesia stories plus the Trainverse crossover; Fortress Island / Lower Level is shared by All You Can Do Here Is Leave and T.R.O.(L.L.); 'Concord' is a deliberate name-echo, not one entity.
- 'Protocol Rules' — the flagship intro (
jamverse-intro.md + .html): Wrote Bristlemouth's introduction to the whole of Jamverse in the register of Venkatesh Rao's 'Discworld Rules', landing the thesis that Jamverse runs on Protocol Rules, not Chosen One Rules — the protagonist is always a protocol and the humans are those who discover they live inside it; every protocol has two faces. Rendered it as a deep-sea 'mesopelagic' reading page (serif = human voice, mono = protocol voice, bioluminescent cyan accents, a sparse drifting-mote canvas, both light/dark themes) and published it as a private Artifact for review only: https://claude.ai/code/artifact/4000d531-0f95-4147-9ccc-6db40e271be0 . User review: 'needs work'. It is deliberately not on the live site.
- State & open items: This is a checkpoint commit of unreviewed WIP — none of it is deployed or wired to a model. Open: (a) whether the intro goes on jamverse as a real page (
public/bard.html or an /artifacts route) and whether it keeps its deep-sea identity or is reskinned to the site's brutalist system; (b) the publishing pipeline + a /bard page (D1-backed, data→render like the devlog) is undesigned; (c) an ANTHROPIC_API_KEY secret + a Pages Function to actually run generation is not provisioned (a new cost line); (d) the intro essay itself 'needs work' per review; (e) an in-world machine-POV piece is still untested at length.
- Memory & retrieval spec (
Bristlemouth-the-bard/memory-and-retrieval-spec.md): The user confirmed two requirements that flip the earlier 'no vector DB' call: memory across turns/sessions and an unbounded, growing corpus. Wrote a vendor-parametric design doc. Core model = three memory layers with opposite access patterns, kept separate: library (semantic; growing outside-inspiration texts + community Artifacts + optional full-text canon; retrieved by relevance), episodes (per-reader turns/summaries/facts; retrieved by reader + recency + salience), and pieces (Bristlemouth's own output, mirrored to D1 to feed a /bard page). Canon stays prompt-stuffed — the essays synthesise across all 15 stories, which top-k retrieval degrades. Spec covers Voyage embeddings + reranking, hybrid recency/salience scoring, compaction (summarise sessions via Haiku, TTL raw turns), a per-reader trust boundary against memory-poisoning, and reader-identity/privacy (start with an anonymous cookie like jv_voter). Position updated: given these requirements, MongoDB Atlas + Voyage is now a defensible-to-good fit (document + metadata + vector + TTL + recency-sort in one query), with the isolation cost flagged as an org-admin decision; Cloudflare Vectorize + D1 is the isolation-preserving fallback behind a shared MemoryStore interface. Phasing mirrors Plan-3: library→retrieval, then self-corpus + page, then episodic memory + chat last.
- Prose quality — a modality experiment (
Bristlemouth-the-bard/drafts/trainverse-intro-machinePOV.md): User review of the 'Protocol Rules' essay: disliked the tone and the summarisation. Diagnosed three prose weaknesses — it surveys (plot-summary parade) instead of rendering; it is aphorism-led (telling) rather than image-led (showing); and it narrates its own feelings. Fix = change modality from survey-essay to inhabited scene. Drafted a Trainverse-only intro in a machine-POV / second-person-arrival modality: the reader is placed on the train and the universe's premise (moving jurisdiction, surveillance, no heroes, the seam) surfaces from the scene with zero plot recap, one Woolf-collision (the Hale Gap + an attendant's hand) as the spine, quieter and image-led. Saved as an exploratory draft; modality/tone not yet locked (open knobs: person/distance, temperature, how much it explains, or alternate modalities — dispatch, dialogue, prose-poem). Once locked on Trainverse, apply to the other three universes. This is the current reference for 'the direction we want' on prose.
- Merged PR #2 — the /codex proper-noun index: An external contributor (Venkatesh G. Rao,
vgururao) submitted /codex, a 162-entry index of every named person/place/org/object/concept/event across the 15 canon stories, built on the same data→render pattern as the devlog (data/codex.json → scripts/codex_render.py → public/codex.html, plus a deterministic codex_check.py that diffs /stories against indexed slugs). Merged via merge-commit ef5e651 (branch deleted); local main synced. Before merging, smoke-tested the scripts in-repo: codex_check.py reports 15/15 indexed, and re-running codex_render.py reproduced the committed public/codex.html byte-for-byte (deterministic render, no diff). The PR's own devlog entry took Session 20, so this session is 21.
- Header link added to the SSR artifact pages (PR gap): The PR wired
/codex into the nav of all 14 static public/*.html pages but only touched public/ — it missed the server-rendered artifact page (functions/artifacts/[slug].js), which builds its own nav. Added /codex there in the canonical order (Stories → Codex → Contest → Artifacts → Prompt → Lexicon), so /artifacts/<slug> pages are consistent. The admin dashboard and the devlog page intentionally use a minimal/no site-nav and are correctly out of scope.
- Deployed to prod — /codex live, stale /devlog flushed:
wrangler pages deploy public/ (with functions bundle). Verified live: /codex returns 200 with all 162 entries, the header link is present on the homepage and on a live SSR artifact page (/artifacts/ubi-campaign-poster-in-cadaceus-city). This deploy also carried the Session 19 timeline work's regenerated devlog.html that was one deploy behind, so prod is now fully current.
- New /codex page — index of every named entity across canon: Built a worldbuilding reference indexing every named person, place, organization, object, concept, and event across all 15 published canon stories (deliberately scoped to canon — not the community Artifacts, which live in D1 and change shape).
data/codex.json is the new source of truth (an entries[] array plus a stories slug→title/universe/url/author lookup, mirroring the devlog's data→render pattern), rendered by the new scripts/codex_render.py to public/codex.html. The page is filterable by universe and by category (person/place/organization/object/concept/event — added "organization" beyond the requested five since jamverse leans heavily on named companies/institutions) with client-side text search. 162 entries extracted. Wired a /codex link into the site nav across all 13 pages that carry it.
- Extraction method: parallel per-universe reads, not single-shot fetches: Split the 15 stories across 4 parallel subagents (one per universe) that fetched full story text and read it themselves for consistent categorization and in-universe dedup, rather than trusting a single summarizing pass per story. This mattered:
WebFetch does not return verbatim text — it always routes through a small summarizing model, which introduced a real misread (a chapter's foundational rule name came back garbled in some fetches). Caught and resolved by targeted re-fetching of the exact surrounding sentences across all four occurrences, which surfaced something better than an error: "Perception Must Preserve" (the Zoo's official rule, Ch.1 title, stated formally in Ch.6) and "Presence Must Preserve" (a colloquial slogan version circulating in-world dialogue, explicitly called "the slogan" in Ch.2 and Ch.4) are both genuine, distinct in-story phrases — now two separate codex entries instead of one merged/corrected one. Lesson banked in CLAUDE.md for future extraction sessions: don't trust a single fetch on any load-bearing quoted phrase.
- Cross-universe crossover merged, not duplicated: The Trainverse fork and the Zoothesia fork each independently extracted a perceptual-filtering system — "The Zoo" (Trainverse's "The Overloaded Train") and "Zoothesia Protocols" (Zoothesia, aliased "The Zoo"). Recognized these as the same in-world system rather than two universes each having their own version: the train explicitly passes through a "Zoothesia Operational Zone," and this matches the existing
trainverse.html toybox entry for the "Zoothesia DRZ" (Neutral Forge Protocol Authority). Merged into one crossover entry tagged universe: ["zoothesia", "trainverse"] — the schema supports multi-universe entries for exactly this case, and it's the only confirmed crossover found this pass.
- Mirror gap flagged, not silently worked around: Two Stockton Chronicles stories ("The Caucus," "Joan Henry vs the Algorithm") have only placeholder stubs on protocolized.io — full text still lives at randylubin.com. Extraction was done from the real (randylubin.com) text, but codex links still point to the protocolized.io URLs for site-wide link consistency, with a
mirror_note recorded per-story in codex.json and surfaced in the page's own footnote. This is a pre-existing gap (not introduced this session) — noted in status.md for the org admin to resolve the mirror.
- Update procedure documented; not deployed this session: Added
scripts/codex_check.py — a cheap, deterministic diff between /stories' published slugs and codex.json's stories_indexed, safe to run at any session startup to catch newly-published canon stories. Proper-noun extraction itself can't be scripted (it requires reading and editorial judgment), so it stays a Claude Code session task; the full procedure is documented in CLAUDE.md under a new Codex section. Verified the rendered page locally via a static file server before handing off — not deployed to production this session. Per the lead dev's review process, this ships as a pull request (including this devlog entry) rather than a direct push to main, so @sachben91 can review before it merges and deploys.
- Artifacts page reworked into a social-media timeline:
/artifacts was a link-list of titles/blurbs that required a click through to /artifacts/<slug> to read anything. It is now a scrollable feed where each approved entry is a post rendered inline: universe badges → title → byline → the full rendered markdown body → inline images → a footer action row (vote stepper + score + an 'Open ↑' permalink). This removes the read step entirely. To keep long entries from dominating the feed, anything over 250 words collapses behind a fade with an inline 'See more ▾ / See less ▴' toggle that expands in place (no navigation) — threshold chosen after confirming Substack Notes has no hard char cap but reads as ~250-word short-form. The old per-card right-rail stepper layout is gone; voting now lives in the post footer. Locks in: the feed is the canonical Artifacts surface; the SSR /artifacts/<slug> pages remain as shareable permalinks.
- Image-only artifacts are now first-class: A poster/comic/illustration can be the whole entry.
functions/api/submit.js no longer requires body_md — an entry is valid with a title + at least one image (new guard rejects only when both body and images are empty). public/submit.html mirrors this (body field marked optional-if-image, client validation relaxed) and advertises ideal image specs: ~1200 px wide, up to 1500 px tall, 4:5 portrait or 3:2 landscape, JPG/PNG, <5 MB. No schema change needed — body_md was already nullable and submission_media already existed.
- Inline media across API + SSR:
GET /api/artifacts now returns, per entry, server-rendered body_html (via the shared escape-first renderMarkdown), word_count, a truncated flag (>250 words), and an images[] array (grouped from one submission_media query, image content-types only, as /api/media/<key> paths). The SSR page functions/artifacts/[slug].js now renders image attachments inline as <img> instead of link-outs (non-image files still list under Attachments). Security unchanged: body HTML is escaped-first before markdown transforms, so client-side innerHTML injection stays safe. Deployed via wrangler pages deploy public/; verified live (7 approved entries all render new format, 3 correctly flagged truncated).
- One-off image removal (no delete UI yet): There is still no built-in way to remove an attached image —
/api/admin/edit only ever appends. Removed the image from entry #4 (love.md, submissions/4/1-Unc-Liq-Dec.png) manually: deleted the submission_media row in D1 and the R2 object, then confirmed images=0 on the live API. Open item: if image curation becomes routine, add a 'Remove' control to the admin dashboard backed by a delete endpoint that clears both the row and the R2 object. Also noted: the live DB now has 7 approved entries (#2–#8), so status.md's 'only #2 approved' was stale and has been corrected.
- llms.txt refreshed + linked in footer: The
/llms.txt from Session 10 still existed and was live but predated this sprint. Added an 'Artifacts & community voting' section (published entries, the 40-vote √-weighted model explained, voting close), a 'How to enter' subsection (native title + markdown body + image attachments + optional doc link, and the CC BY-SA 4.0 grant), and /artifacts to Key pages. Also added a visible llms.txt link to the site footer across all 13 pages + the SSR artifact page (it was previously undiscoverable, root-only by convention) so people/agents can find or copy it.
- Shuffle: added an Artifact Type card (4th pool):
/shuffle now draws a fourth prompt element — an artifact FORM to write as — from a 99-item pool (grocery list, police report, eulogy, billboard, epitaph, will & testament, YouTube comment, …; deduped). Grid moved from 3-across to a balanced 2×2 (still 1-col on mobile). The new card settles LAST in the slot-machine animation and now carries the completion callback (reveals the submit CTA). The drawn type is stored in the prompt object as artifactType (in sessionStorage + the shuffle_prompt recorded with each entry), and the /submit prompt banner shows an 'Artifact Type' row (guarded so older prompts without one don't render 'undefined'). Header + 'How it works' copy updated to four pools.
- Contest deadline surfaced on /artifacts + voting moved to Aug 2: The submission deadline wasn't prominent on the Artifacts page, so added a full-width amber countdown banner at the top: 'X days left to enter … (deadline July 31) — submit your artifact →', computed client-side in whole calendar days with final-day and closed states. SEPARATELY, moved the VOTING close from July 31 to end of August 2, 2026 (midnight PT =
2026-08-03T07:00:00Z, VOTE_CLOSE in _shared/voter.js) so there's a voting window on the final entries. Updated everywhere: server 403 message, on-page vote-bar copy, llms.txt. The two deadlines are now distinct and both surfaced (submissions July 31 via the banner, voting Aug 2 via the vote bar).
- Licensing decision — CC BY-SA 4.0 (researched): Investigated how shared-universe fiction projects license contributions. The SCP Foundation uses Creative Commons Attribution-ShareAlike (CC BY-SA 3.0) — attribution + share-alike, commercial use allowed — which is the proven model for keeping a universe freewheeling for remixing. The qntm precedent (There Is No Antimemetics Division, SCP → Del Rey 2025) is exactly the 'don't block the next commercial writer' concern: he could publish only by stripping SCP-licensed names/concepts, because ShareAlike binds derivatives that still carry shared elements. Decision: adopt CC BY-SA 4.0 (SCP is on 3.0; CC recommends 4.0). Accepted trade-off: a writer can go commercial with their own original material but not re-enclose the shared toybox. LOCKED IN.
- Required license checkbox + server enforcement (migration 006): Added a REQUIRED agreement checkbox to
/submit linking to the CC BY-SA 4.0 deed; the entry cannot be submitted unchecked (client + server). submit.js requires license === 'CC-BY-SA-4.0' and stores it in the new submissions.license column (schema/006_license.sql, applied to prod). Chosen now because there are still only 2 participants — cleanest moment to establish the grant. NB: the 3 pre-existing entries have license = NULL (submitted before the requirement); their grants must be collected out-of-band before those works are treated as CC-licensed.
- License marking across public + admin surfaces: CC requires the work be marked. Added a 'Licensed under CC BY-SA 4.0' notice (with author + link) to the foot of each SSR artifact page (
functions/artifacts/[slug].js, shown only when the entry actually carries the license), a licensing bullet on /contest, and a CC note on the /artifacts index. The admin dashboard + CSV export now show each entry's license (or 'none — pre-license entry'); export CSV also gained title + status columns.
- Voting deadline — closes July 31, server-enforced: Voting now closes at end of July 31, 2026 (midnight Pacific =
2026-08-01T07:00:00Z), defined once as VOTE_CLOSE in functions/_shared/voter.js. POST /api/artifacts/votes returns 403 after close so late ballots genuinely do not count (not merely hidden); GET /api/artifacts exposes voting_open + vote_close. The /artifacts page shows 'Voting closes July 31, 2026' while open, and after close disables the steppers/Save and shows 'Voting closed — scores below are final' while KEEPING scores visible. Verified both states locally (backdated the constant to force the closed path, then restored). Timezone is midnight Pacific — trivially changeable if a different cutoff is wanted.
- Admin review backend + cookie auth: Built a gated admin dashboard at
/admin/ to review and publish submissions instead of CSV/D1. Auth is stateless and Resend-free: POST /api/admin/login exchanges the existing ADMIN_SECRET for an HttpOnly cookie whose value is sha256(secret) (raw secret never in the cookie); functions/_shared/adminSession.js isAdmin() verifies the cookie OR the legacy X-Admin-Token header, so export.js keeps working. New endpoints: GET /api/admin/submissions (all entries + status + media), POST /api/admin/review (approve/reject/pending; approve sets pub_slug/pub_blurb, defaults pub_title from the entrant title, enforces slug uniqueness + reserves the-zoo/the-big-man). Note: Cloudflare Access on /api/admin/* is STILL not configured — the admin secret is currently the only gate.
- Submission model changed to native content (mid-contest): The contest moved from 'submit a Google Doc link' to native content: entrants now type a title + markdown body (stored in D1) and optionally attach images (stored in R2
jamverse-media, referenced in new submission_media table). The Google URL field is kept but OPTIONAL (for rich visual/comic layouts). Rewrote functions/api/submit.js to accept multipart/form-data (title/body required, up to 5 images at 5 MB each, image/* only), public/submit.html (native form), and public/contest.html copy. google_doc_url stayed NOT NULL at the SQL level (empty string when blank) to avoid a live-table rebuild. Locked-in decision after weighing the format-heterogeneity risk; existing Google-link entries are grandfathered.
- Safe markdown renderer:
functions/_shared/markdown.js renders entrant markdown with an escape-FIRST strategy — the whole string is HTML-escaped before any markdown transform runs, so users cannot inject raw HTML; only the tags this module emits appear. Links are restricted to http(s)/mailto. Supports headings, bold/italic, code, lists, rules, paragraphs. Verified live: <script> is escaped, javascript: links neutralised, while real markdown renders. Deliberately not full CommonMark — enough for prose fiction.
- R2 image attachments + gated media serving: Images upload to R2 under
submissions/<id>/<n>-<name>. New functions/api/media/[[key]].js serves an object ONLY if its owning submission is approved, or the requester is admin (so pending entries' images can be previewed in review but aren't publicly leakable). This is the first code to actually use the MEDIA R2 binding. Per decision, images are stored but NOT rendered inline yet — artifact pages link out to them; inline rendering can come later with no schema change.
- Per-artifact SSR pages + Artifacts index: Approved entries publish to a new Artifacts section (nav added across all pages).
functions/artifacts/[slug].js server-renders each artifact at clean URL /artifacts/<slug> (title, author, rendered body, image link-outs, optional original-doc link). Critical gotcha found + fixed: the dynamic function was SHADOWING the two hand-authored static pages (public/artifacts/the-zoo.html, the-big-man.html) in wrangler pages dev — the function now calls next() for those reserved slugs to fall through to the static asset. public/artifacts.html is the client-rendered index.
- In-place backfill tool for legacy entries: To migrate the pre-existing Google-link entries into the native model WITHOUT losing submitter attribution or creating duplicate rows, added
POST /api/admin/edit (multipart: id, title, body_md, appended images) + an 'Edit content' panel on each admin card. It only changes title/body/media — name/email stay with the original submitter. Used in prod to backfill Kannen's two entries; #3 (image-only) still needs its poster image uploaded, and Sachin's #1 is still pending backfill.
- Artifacts community voting (anonymous, √-weighted): Implemented the Protocol Institute symposium voting model WITHOUT login (per decision): each browser gets a 40-vote budget spent linearly across approved artifacts; an artifact's score = Σ√votes across voters, so concentration has diminishing returns and spreading is rewarded (no member tiers → weight 1). Identity is an anonymous
jv_voter HttpOnly cookie minted in functions/_shared/voter.js — the ONLY place identity is minted, so it can later be swapped for Turnstile/PIN without touching vote logic. artifact_votes table (migration 005); GET /api/artifacts returns scores + your ballot + budget (now no-store); POST /api/artifacts/votes is replace-all with budget enforcement + approved-id validation. Voting UI on /artifacts: steppers, remaining-budget bar, live scores, Save. KNOWN trade-off: clearing cookies yields a fresh ballot (soft integrity), accepted for a community vote.
- Email notification built but DORMANT:
functions/_shared/notify.js sends a best-effort Resend email to sachben91@gmail.com on each new entry, wired into submit.js via waitUntil (never blocks/fails a submission). It is a silent NO-OP unless RESEND_API_KEY is set. Deployed but not activated — user chose to rely on the admin dashboard for now. To enable: create a Resend account with sachben91@gmail.com (self-send needs no domain verification), then wrangler pages secret put RESEND_API_KEY. Can be removed if not wanted.
- Deployed to production + schema migrations 003/004/005: All of the above is LIVE. Applied additive migrations to prod D1:
003_review_and_publish.sql (status + pub_* on submissions), 004_native_content.sql (title, body_md, submission_media), 005_voting.sql (artifact_votes). Pre-flighted the prod schema first to avoid duplicate-column errors. NB: there are now 3 real entries (Sachin #1, Kannen #2/#3), not the 1 that status.md claimed. Test data lives only in the local miniflare store and was never pushed. A stray test ballot cast during prod smoke-testing was deleted.
- Local dev gotcha (record for next time):
wrangler pages dev does NOT read [[d1_databases]]/[[r2_buckets]] from wrangler.toml — bindings must be passed as flags: wrangler pages dev --port 8788 --d1 DB=jamverse-db --r2 MEDIA=jamverse-media. Also, the flag-bound local D1 store is a DIFFERENT sqlite file than the one wrangler d1 execute --local writes, so seed data must be applied directly to the pages-dev store (via sqlite3 on .wrangler/state/v3/d1/miniflare-D1DatabaseObject/*.sqlite). Secrets for local come from a gitignored .dev.vars.
- Site favicon (J mark): Added a brand favicon — a cream “J” on the amber accent (#9C4A00) square, square corners to match the brutalist look. Primary is
public/favicon.svg (scalable, used by modern browsers); fallbacks favicon.ico (16/32/48/64 multi-size) and apple-touch-icon.png (180px) were generated with Pillow via a disc-stamped round-stroke render. Favicon <link> block injected after the viewport meta on all 12 nav pages, and into scripts/devlog_render.py so the generated devlog.html keeps it. The two dev pages (dev-orientation, devlog source) use a different single-title header and were left as-is.
- New story — The Big Man: Elizabeth Maher’s Legends & Ledgers story (published 2026-06-12) added across the site. It is the newest story, so on the homepage it is now the hero/Latest feature. Added optional per-story
img support to the STORIES data + hero/grid renderers (s.img || IMAGES[badge]) so the actual cover (downloaded to /assets/story-the-big-man.png, the 1200×630 og:image) shows instead of the generic universe image. Also added to stories.html (L&L group), flipped from “forthcoming” to a published link on the universe page, and added to llms.txt. Story counts bumped 14 → 15 in stories.html intro and llms.txt (devlog history entries left untouched).
- Mobile hamburger nav: Top menu overflowed/clipped on phones (wide-tracked JAMVERSE wordmark + 4 links exceed ~360px). Fixed with a CSS-only hamburger (checkbox-hack, no JS): hidden
<input> + 3-bar <label> injected before <nav class=site-nav> on all 12 pages; at ≤640px the nav collapses to a full-width dropdown below the sticky header, burger morphs to an X. Shared rules live in style.css; index.html has its own inline <style> (does not link style.css) so a matching block was added there with its local vars.
- Contest rename — Jamverse Jam Contest: Per request, “Contest #4” / “Protocolized Writing Competition #4” replaced everywhere with Jamverse Jam Contest (index banner + contest-box label, contest/shuffle/submit titles & eyebrows, Stockton toybox copy, llms.txt). Locked-in name; theme is still Stigmergy, deadline July 31. Devlog history (devlog.json/html) intentionally not renamed.
- Contest banner mobile fix: The amber notice-bar (index only) is a fixed-36px sticky bar with
white-space:nowrap; overflow:hidden, so its 4 centered items clipped on phones — and the sticky header is pinned at top:36px, so wrapping to two lines is not an option. Solution: the title is now a link (.notice-title → /contest) and at ≤600px the bar collapses to just the contest name (long span, Enter CTA, and prompt link hidden), keeping one line. Note: verifying mobile via headless Chrome was unreliable (the engine clamped the viewport to ~500px and the sticky bar centers within full layout width, so cropped captures looked shifted); user confirmed it reads fine on a real phone.
- Standalone example-artifact pages: Added two full worked examples as their own pages under a new
public/artifacts/ directory — /artifacts/the-zoo (Zoothesia social history of “The Zoo,” a V1→V10+ protocol-evolution timeline) and /artifacts/the-big-man (Legends & Ledgers collectible product listing, “You Were Never Alone,” with a Keepsake Card verse and a “do not leave at the Disposition” care note that cross-references the existing Disposition plaque artifact). These complement — do not replace — the inline Example artifacts carousel on /contest, which was left untouched; the longer-form pieces are too big for a carousel card.
- Linked from “What’s an artifact?”: Added a two-item “Want to see one in full?” list at the end of the What’s-an-artifact section in
contest.html pointing at the two new pages. Used extensionless URLs to match the rest of the site (CF Pages serves .html at the clean path).
- Shared artifact-page styles: Added a reusable
.artifact-doc-* component block to public/assets/style.css (meta tag, title, subtitle, body card, version blocks, poem, prev/next nav) rather than duplicating per page. Reuses existing theme tokens and the universe .badge-* colors — no new colors introduced.
- Standalone-artifact refinement: Per review, removed the editorial “An example of what a contest artifact can look like…” kicker paragraphs and the “Example artifact ·” meta prefix so each page reads as the artifact itself, not a description of it — the h1 is now the artifact’s own title and the card is its body. Fixed odd headline spacing: dropped forced
<br> breaks, loosened title line-height, and moved the secondary line into a proper italic .artifact-doc-subtitle. The “example” framing now lives only on /contest where the links introduce them.
- AI assistance: checkbox → free-text field: Replaced the boolean 'I used AI' checkbox on
/submit with an optional textarea prompting entrants to describe what part of the process they used AI for. Changes span four layers: form (public/submit.html textarea named ai_usage + help text + client JS), backend (functions/api/submit.js now reads ai_usage and auto-derives the legacy used_ai flag = 1 when text is present), schema (new ai_usage TEXT column via schema/002_add_ai_usage.sql, applied to live D1; schema/001_init.sql updated to match for fresh installs), and CSV export (functions/api/admin/export.js now emits the full ai_usage text alongside the yes/no used_ai). Deployed and verified live. Note: any non-empty text sets used_ai=1, so an entrant typing 'no AI' reads as AI-used — blank means no AI.
- First live submission received + how to view: Test submission #1 (Sachin, stockton-chronicles) landed in the live DB, confirming the end-to-end form→D1 path works. Two review methods documented: direct D1 query via
wrangler d1 execute jamverse-db --remote --command "SELECT ..." (fastest for dev, uses existing token) and the admin CSV export at /api/admin/export (needs ADMIN_SECRET header; Cloudflare Access on /api/admin/* still pending). Test row #1 left in place for now — decide whether to delete before contest judging.
- Admin review-site design discussion (NOT built): Explored a future admin site to review/judge submissions instead of CSV/D1. Proposed shape: protected
/admin page + GET /api/admin/submissions JSON API + new D1 judging fields (status/score/notes); left list-filter + right detail with artifact rendered inline. Key fork is the submission model — current Google-link-based (cheap, embeds) vs. native uploads to the existing jamverse-media R2 bucket (text in D1, images in R2; nicer to judge but more build + moderation surface + changes the live contest flow). Auth options: PIN login (reuse PI website repo's email-PIN + session-cookie pattern, buildable now) vs. Cloudflare Access (cleaner but org-admin-dependent/pending). User leaning toward native R2 uploads + PIN login but wants to clarify scope before deciding — open questions: switching submission model mid-contest (live until July 31), text box vs. file uploader (or both per entry), R2 size/type limits + serving (public vs signed), admin allowlist (who judges). To be resumed next session.
- Protocolized GitHub access discrepancy flagged: While checking feasibility of reusing PI's member login, found the lead-dev GitHub token (
sachben91, scopes repo + read:org) can read all Protocol-Institute repos incl. private ones (website, c3po, admin) — broader than this project's CLAUDE.md isolation claim ('no access to any other PI infrastructure'). Only the public website auth code was read to answer the question; the admin keys/security repo was deliberately not opened. Worth org-admin attention: either the docs are wrong or the token scope is too broad.
- Session 11 tagline work deployed: Ran the first production deploy of the universe tagline refresh and Trainverse re-pitch (committed but undeployed at the end of Session 11). Verified live on
jamverse.protocolized.io by confirming the new Lisbon→Laos Trainverse copy serves from /llms.txt. Session 11's deployed flag was retroactively flipped to true to reflect this.
- Contest notice bar restyled to amber: The sticky top banner (
.notice-bar in index.html) was changed from a low-contrast light card background (--bg-card with muted text) to a full amber background (--amber, #9C4A00) with white text — base text at 85% white, strong and links at solid white, links bold and underlined (underline drops on hover). Goal was to make the contest call-out stand out more. The banner exists only on index.html, so no other pages needed updating. Note: this CSS change lives in index.html and is committed as part of this session's wrapup, but was deployed live before commit.
- Trainverse re-pitched: Replaced the old Trainverse framing (Lagos→Singapore, 2,347 passengers, 17 jurisdictions, 23,300km) with a new author-supplied pitch everywhere it appeared: a political thriller on a train from Lisbon to Laos through Eastern Europe, Siberia, China and Southeast Asia, framed as a moving special economic zone balancing priorities across legal jurisdictions. Updated in four places that previously carried the stale copy —
universe/trainverse.html (header desc), contest.html (toybox column desc), llms.txt (universe description), and the UNIVERSE_TAGLINES map in index.html. This closes the long-standing 'Trainverse tagline' open item in status.md; the 'motion is legal protection / ECOROUTE never stops' beats and the named toybox elements (bio-bands, Pono, Mirage Handler, Caduceus/Zoothesia DRZ, Car 7) were preserved.
- Universe taglines added to story UI: Introduced a
UNIVERSE_TAGLINES map (keyed by universe badge/slug) on both index.html and stories.html. On the front page the hero story now shows its universe tagline above the title (.hero-universe-tagline, italic serif), and story-grid cards reveal the tagline as an overlay caption on hover (.story-grid-img-caption over a darkened image; required adding position:relative to the image wrap). On stories.html, selecting a universe filter now displays that universe's tagline in a banner below the filter buttons; the banner hides on the 'all' filter. Taglines are defined inline in each page's script rather than centralized — keep the two maps in sync when copy changes.
- llms.txt published: Added
public/llms.txt following the emerging llms.txt convention (analogous to robots.txt for AI agents). The file gives any AI assistant or agent hitting jamverse.protocolized.io/llms.txt a complete structured picture of the site in one request: a one-paragraph site description; all four universe descriptions with named toybox elements and links to full universe pages; all 14 published stories with title, universe, publish date, author, hook, and direct URL to protocolized.io; full contest brief including the stigmergy definition, prizes, deadline, and links to /contest, /submit, and /shuffle; and an index of all key pages including the Protocol Lexicon. Format is clean markdown, no HTML.
- PR #1 merged — story links migrated to protocolized.io: Reviewed and merged PR by
vgururao. All 12 story links previously pointing to protocolized.summerofprotocols.com/p/* updated to protocolized.io/p/* across index.html, stories.html, and all four universe pages. The two Randy Lubin Stockton stories (The Caucus, Joan Henry vs the Algorithm) previously on randylubin.com now point to protocolized.io/p/* placeholder pages with series navigation and a coming-soon notice — these will upgrade automatically when the stories are published on Protocolized. Deployed immediately after merge.
- Brutalist design system: Complete visual overhaul of the site. Replaced the warm-dark magazine palette with a full brutalist system: ink-black (
#111) structural rules, amber (#9C4A00) for all interactive elements, warm cream ground (#FDFAF5) and parchment alternates. Zero border-radius across all elements. All buttons are 2px solid borders (amber fill or ink outline). Typography scaled up — h1 at clamp(2rem, 5vw, 3rem), h2 at 1.85rem, h3 as ruled uppercase labels with 2px bottom border. Nav: white background, 3px ink rule, JAMVERSE in tracked 0.3em all-caps. Footer: 3px ink rule top, all-caps ink links. Changes cascade from style.css globally to all pages.
- Magazine front page redesign (index.html): Stripped the previous universe-card grid in favour of a story-first magazine layout. Hero section: 2-column split (text left, universe image right), giant serif title at
clamp(2.75rem → 5.5rem), overline rule, monospace date, amber "Read on Protocolized" link. Recent stories: 2×2 grid, pure typography, 2px ink rules between cells, no images at rest. Archive: compact list, 1px light rule between items. Contest panel: hard 3px bordered box with inner rule dividing two halves. Universe directory: bold 2px bordered tag buttons. Story data defined as a JS array ordered most-recent-first — to feature a new story, prepend one entry.
- Hover image expansion on story cards: The 4 recent story cards in the featured grid now reveal a 210px universe artwork image on hover. The image wrapper starts at
height: 0 and expands to 210px via cubic-bezier(0.4, 0, 0.2, 1) transition. Card grows to accommodate the image — no layout shift on other cards. Images are already in public/assets/ from Session 6. The render function was updated to wrap card content in a .story-grid-content div so padding is preserved below the image.
- Intro tunnel animation: Full-screen intro overlay on first visit (per session via
sessionStorage). Background: jamverse-plate-1.png at 50% opacity over near-black (#080604) with dual vignette gradients. Title: JAMVERSE in Space Grotesk 700 at clamp(3.5rem → 11rem), white with heavy text-shadow. Tagline: full "you are a storyteller" declaration in Lora italic at clamp(1rem → 1.3rem). On first scroll/wheel/key/tap: tagline and eyebrow fade immediately, title scales to 4.5×, letter-spacing explodes to 2.5em, blur applied — tunnel effect. Overlay fades at 280ms, gone at 1000ms. Respects prefers-reduced-motion. Click also triggers for mobile users.
- Contest page redesign: Removed Stigmergy section entirely. New structure: dark parchment hero with prize row (2px ink borders), full inline toybox grid (4 columns, all 47 elements browsable), Oblique Protocols card (Eno-inspired prompt CTA), condensed body sections (artifacts, traffic jams, logistics), example artifacts carousel at the bottom. Carousel auto-advances every 60 seconds, side-arrow circular buttons, dot indicators. Toybox grid uses ink dividers — each universe column has badge, name, description, item list, and footer link.
- Real publish dates added to story ordering: Fetched actual publish dates from all 14 Protocolized/Substack URLs. Correct chronological order now drives the magazine layout: The Overloaded Train (May 22, 2026) is the hero, All You Can Do Here Is Leave (May 8) and The Headless Empire (Apr 23) in the recent grid. The Caucus and Joan Henry vs the Algorithm show 2023 (no date available on randylubin.com pages).
- /shuffle prompt generator: New standalone page at
/shuffle. Three dark cards — Technology, Thematic Question, Universe Element — each backed by a curated pool (15 technologies, 15 questions, 16 universe elements drawn from all four universes). Clicking Shuffle runs a staggered slot-machine animation: each card rapidly cycles through random pool items before snapping to the final result. Universe attribution badges appear on the tech and element cards after settle. "Submit with this prompt →" appears after the first shuffle; prompt is saved to sessionStorage. "Shuffle again" link if the first result doesn't land. Fully responsive — cards stack to 1-column on mobile at 760px.
- Prompt-aware submission form:
/submit reads sessionStorage('jamverse_prompt') on load. If a shuffle prompt is present, a dark banner appears at the top of the form showing the three drawn elements (technology, question, universe element) with a "Shuffle again →" escape. The prompt is submitted with the form as a shuffle_prompt field and stored in D1.
- D1 schema migration + Worker update:
schema/002_shuffle_prompt.sql adds a shuffle_prompt TEXT column to the submissions table — applied live via wrangler d1 execute --remote. functions/api/submit.js updated to accept and store the field. functions/api/admin/export.js updated to include shuffle_prompt in the CSV export column set.
- Contest page CTA + nav update: Dark CTA block added to the contest page intro (above the TOC): "Not sure where to start? Generate my prompt →". "Prompt" nav link added to all pages:
index.html, contest.html, submit.html, stories.html, all four universe pages, and dev-orientation.html.
- Legends & Ledgers universe page: The T.R.O.L.L. universe was renamed to Legends & Ledgers. Created
/universe/legends-and-ledgers.html with the full toybox provided by the universe author: overview, thematic questions, setting, shared geography & recurring figures (6 items), protocols & mechanisms (9 items), tone notes, all 5 stories (2 linked, 3 forthcoming), and a toybox section with 19 checkable elements across 4 categories. Updated all cross-references across stories.html, contest.html, index.html, and style.css — old /universe/troll is now a dead file, no longer linked. Story title updated from "TROLL" to "T.R.O.(L.L.)" everywhere.
- Protocol Institute → Protocolized: Removed all public-facing references to "Protocol Institute" and replaced with "Protocolized" — the magazine publishing the extended universes. Changes in:
index.html (page title, hero eyebrow), contest.html (stigmergy section, lexicon section), dev-orientation.html (subdomain description), data/devlog.json (description field). devlog.html regenerated. Internal dev docs (CLAUDE.md, wrangler.toml) retain "PI org" as a shorthand for the Cloudflare account context.
- Contest page rewrite: Full content rewrite of
contest.html. New intro with step-by-step "To enter" instructions. Worlds section moved before Artifacts and given new descriptions for all four universes (Stockton, Legends & Ledgers, Zoothesia from author copy; Trainverse placeholder pending). Stigmergy section expanded with four detailed worked examples (ants, termites, Wikipedia, Blum-Kozen maze automaton). Traffic Jams section updated to the new wording. Lexicon section removed (simplified). Protocol Lexicon requirement dropped from numbered entry requirements (now 3 requirements: toybox element, traffic jam, stigmergy). Toybox instruction updated to "at least one — preferably more than one".
- Form separation — /submit page: Submission form extracted from
contest.html into a new standalone public/submit.html at /submit. Contest page ends with a "Submit your entry →" CTA button linking to /submit. The submit page has a breadcrumb back to /contest. URL field updated to accept both Google Docs (docs.google.com) and Google Drive (drive.google.com) links — important for visual/comic panel submissions. Guidance note added under the URL field explaining both paths. Universe selection now validates (at least one required). All toybox JS and universe data live exclusively in submit.html.
- Pages Functions — submission handler + admin CSV export:
functions/api/submit.js patched: Google Drive URLs now accepted alongside Google Docs, returns actual D1 row ID as submission_number. New functions/api/admin/export.js at GET /api/admin/export — returns all submissions as a CSV (id, submitted_at, name, email, google_doc_url, universes, toybox_elements, used_ai). Protected by X-Admin-Token header checked against ADMIN_SECRET wrangler secret (set this session). A Zero Trust Access request was filed in zero-trust-requests.md for a CF Access policy on /api/admin/* as a second protection layer — pending org admin action. admin.local.md created (gitignored) as a local admin cheat sheet with the curl command and operational notes. CLAUDE.md updated: stack table, structure tree, secrets table, deployment section, and a new admin download section.
- Front page universe cards redesign: Four universe artwork images added to cards (zoothesia.jpg, stockton/cad.jpg, legends.png, trainverse.jpg) — served as static assets from
public/assets/. Cards restructured from outer <a> wrappers to <div> containers with separate inner links, enabling story links and explore links to coexist. Each card now shows: artwork image (3:2 ratio), badge, universe title (linked), description, 2–3 individual story links with ↗ arrows, and an "Explore →" footer link. Grid changed from auto-fill minmax(22rem, 1fr) to a fixed repeat(2, 1fr) — eliminates the orphaned-card blank space that appeared at wide viewports.
- Mobile responsiveness pass: Global input/textarea styles added to
style.css (full-width, border, focus ring). Tables made scrollable on mobile (display: block; overflow-x: auto). h2 and universe-header h1 font sizes reduced at 640px. Added 480px breakpoint in index.html — hero stacks to column, buttons go full-width, content-zone padding tightened. submit.html gets 600px breakpoint (form-wrap padding reduced, universe check badges unwrap). contest.html 600px breakpoint extended: submit CTA button full-width, stigmergy example boxes tighter padding.
- Zoothesia toybox complete: Wired the full Zoothesia toybox into
universe/zoothesia.html and the contest form JS. Universe page now covers: setting (Mayaford, Charntown, Chitram Row, Nayanaport, Shraville), technologies (The Zoo, Overlays, Kapala-Halo, Foulmaxxing/Phobisculting, Faceting, ProfBot), character (Oren — the Zoothetic psychopomp), thematic concerns, and the Zoo's slogans. Universe description rewritten to lead with Mayaford and the harm-prediction framing. Contest form JS array populated with 9 checkable elements. Contest world card description updated.
- First live deploy: CF API token received and placed in
.env (angle brackets from placeholder format removed after first failed auth attempt). Remote D1 initialised via wrangler d1 execute jamverse-db --remote --file schema/001_init.sql — 3 queries, schema live in WNAM. Site deployed via wrangler pages deploy public/ to jamverse.pages.dev. Deployment URL: 183b14ee.jamverse.pages.dev. Custom domain jamverse.protocolized.io pending DNS propagation confirmation from org admin. Form submissions now write to live D1.
- New Nature design direction: Replaced the warm off-white + teal magazine aesthetic with a new palette derived from the session artwork (Fig. 1): cool limestone background (
#E4E0D8), deep slate dark zone (#1A2028), phosphor teal accent (#00B8A8), geological badge palette per universe. Conceptual frame: natural history atlas meets Concrete Atlantis (Banham) — arcane + futuristic light. The X element is bioluminescent protocol signatures: the dark hero zone is the substrate, the phosphor accents are what the instruments detect. Outfit replaced with Space Grotesk throughout for UI labels — survey/instrument quality without going cold.
- Shared design system (
style.css): Extracted all shared styles into public/assets/style.css — CSS variables, reset, nav, typography, sections, cards, badges, buttons, story lists, universe page headers, toybox, tables, TOC, placeholder blocks, footer, responsive breakpoints. All six public pages now link to this file and carry only page-specific overrides inline. Previously each page had ~250 lines of duplicated inline CSS; now the shared file is the single source of truth for the design system.
- Landing page rebuild: Replaced the placeholder stub with a full two-zone layout. Dark slate zone: sticky nav, two-column hero (left: PI mark + wordmark + hook text; right: Plate I artwork with formal monospace caption). Limestone zone: contest strip, four universe cards in a seamless bordered grid with per-universe geological badge colors, stories CTA, dark footer. The artwork (
jamverse-plate-1.png) is treated as a natural history specimen plate — fish containing a city, literalising the New Nature concept.
- Jamverse hook copy placed: Final hook text placed in
contest.html intro section as italic body copy: "Your bio-band rekeyed at the Caduceus DRZ, unmaking the last twenty minutes from every record that had held them. The AI agent in the smart glasses has attended this conversation since before you sat down — diligent, accountable to someone else. The perceptual classifier withdrew three people from this room; they remained, warm, displacing air, resolved by nothing. The space has been narrowing since you entered. It keeps offering you one door. Welcome to the Jamverse." Draws specific details from all four universes; Virginia Woolf-influenced verb-agency on the technologies.
- Story hooks — all 14 stories: Fetched all 14 story pages to read actual opening paragraphs and subtitles, then wrote non-spoiler one-line hooks for each. Zoothesia stories correctly sequenced as chapters 1–6 (confirmed from subtitles — Perception Must Preserve is Ch. 1, Minor Differences Repeating Forever is the finale). Caught Elizabeth Maher as the author of All You Can Do Here Is Leave. Badge layout fixed: tags moved inside
.story-body to sit above the title rather than as a left-side flex item; display: inline-block so tags hug their content width.
- Developer environment setup: Cloned repo for the first time on lead dev machine. Wrangler 4.98.0 installed to
~/.npm-global/bin/wrangler (global /usr/local/lib was permission-locked); ~/.npm-global/bin added to PATH in ~/.zshrc. .env created with CLOUDFLARE_ACCOUNT_ID pre-filled and marked Dropbox-ignored via xattr. CLOUDFLARE_API_TOKEN placeholder remains — token not yet received from org admin. All wrangler commands are running against local miniflare; nothing has touched live CF infrastructure this session.
- Site architecture decision: Settled on six public-facing pages:
/ (landing), /stories (filterable index), /contest (full contest call + form), /universe/[slug] (four universe pages). Submission backend implemented as a CF Pages Function at functions/api/submit.js rather than a separate Worker — this keeps D1 bindings in one wrangler.toml and deploys atomically with the static site. Schema in schema/001_init.sql.
- Contest call page and submission form: Built
public/contest.html with the full Contest #4 (Stigmergy) call rendered as a navigable editorial page — TOC with anchors, sections for artifacts, stigmergy, traffic jams, lexicon, the four worlds, logistics/prizes, and the submission form. Form submits JSON to /api/submit; the Pages Function validates inputs, enforces the 3-submission-per-email cap, and writes to D1 (name, email, google_doc_url, universes, toybox_elements, used_ai, ip_hash). Toybox checkboxes are JS-driven: universe toggle reveals that universe's elements. TROLL and Zoothesia toyboxes are placeholder pending author input.
- Universe pages: Four pages at
/universe/[slug]. Stockton Chronicles and Trainverse are complete with full toyboxes: Stockton has 13 elements across technology, setting/economy, and thematic categories; Trainverse has the three absolute rules, technology (bio-bands, ECOROUTE, Pono, Mirage Handler), de-realized zones (Caduceus DRZ, Zoothesia DRZ), and the 'the train is ___' constraint. TROLL and Zoothesia pages have story links and styled placeholder blocks — the Zoothesia page includes a crossover note pointing to the Zoothesia DRZ in the Trainverse.
- Story index and landing page:
public/stories.html lists all 14 stories across the four universes with color-coded badges and JS universe filter tabs. public/index.html redesigned from the placeholder stub to a full landing page: contest banner (links to /contest), four universe cards in a responsive grid, 'browse all stories' CTA. Body copy writes the site as a stigmergetic project — each artifact becomes scaffolding for the next.
- Jamverse hook copy: Wrote the contest page opening hook: "Your bio-band rekeyed at the Caduceus DRZ, unmaking the last twenty minutes from every record that had held them. The AI agent in the smart glasses has attended this conversation since before you sat down — diligent, accountable to someone else. The perceptual classifier withdrew three people from this room; they remained, warm, displacing air, resolved by nothing. The space has been narrowing since you entered. It keeps offering you one door. Welcome to the Jamverse." Draws specific details from all four universes; Virginia Woolf-influenced treatment gives technologies verb-like agency. Placed in
contest.html as italic body copy above the TOC.
- Local dev environment: Local D1 initialized via
wrangler d1 execute jamverse-db --local --file schema/001_init.sql. Dev server running via wrangler pages dev public/ --port 3000 — serves static files and Pages Functions against miniflare. Form submission is fully testable locally. Deployment to live CF infrastructure requires the CLOUDFLARE_API_TOKEN in .env (not yet received); three commands deploy when ready: source .env, wrangler d1 execute --remote, wrangler pages deploy.
- Site-wide styling overhaul: All three public pages (
index.html, devlog.html, dev-orientation.html) restyled to match the protocolized.io aesthetic: warm off-white background (#F9F8F5), Instrument Serif for headings, Lora for body, Outfit for UI labels, JetBrains Mono for code, teal primary (#0F6E56) for links and accents. The original dark aesthetic was unreadable and inconsistent with the parent brand. The render script (scripts/devlog_render.py) was updated to produce the new styling on future devlog regenerations.
- Developer orientation HTML page: Created
public/dev-orientation.html — a styled, navigable HTML rendering of orientation.md served at /dev-orientation. Covers the full CF stack, wrangler auth, D1/R2/Worker bindings, secrets, Zero Trust, and session rituals. Includes a table of contents with anchor links. Hand-authored HTML (not auto-generated) since the orientation content changes infrequently and does not share the devlog's data-driven structure.
- Session rituals section and coding agent guidance: Moved the session rituals section to the top of the developer orientation (previously last). Added a new subsection on getting a coding agent to follow the ritual: Claude Code reads
CLAUDE.md automatically and enforces startup/wrapup when prompted; other agents must have the file pasted into context. The guidance names specific trigger phrases ("new session", "wrap up") so the dev knows exactly how to engage the agent. Updated both orientation.md (markdown source) and dev-orientation.html (rendered page) to match.
- Devlog github attribution: Added a
github field to the devlog entry schema — the GitHub handle of the session lead. Backfilled Session 1 with vgururao. The render script now outputs the handle as a linked @handle in the session meta line. Updated CLAUDE.md schema docs and orientation.md wrapup instructions to require the field. Attribution matters once there are multiple contributors.
- Landing page navigation links: Added two nav links to
index.html below the "under construction" notice: Devlog (primary teal button) and Developer Orientation (outlined secondary button). The landing page was previously a dead end with only a link back to protocolized.io.
- Repo and local clone: Created
Protocol-Institute/jamverse as a private GitHub repository. Cloned locally to protocol-institute/jamverse/. Initial scaffold: README.md, CLAUDE.md, status.md, .gitignore.
- Cloudflare Pages project: Created CF Pages project
jamverse on the PI org account (7e8c7969b2464d23795c555bc6a32af8) via CF API, then deployed stub index.html via wrangler — dark holding page with Protocolized wordmark and PI mark. Live at jamverse.pages.dev.
- Custom subdomain: Added
jamverse.protocolized.io as a custom domain via CF Pages API. CF recognized the zone (protocolized.io) is in the same account but could not auto-create the CNAME because the org token lacks Zone DNS:Edit scope — a known gap to address. Subdomain added through CF Pages dashboard UI as a workaround; DNS propagation and TLS provisioning pending at session close. Workers custom domains (e.g. c3po) handle DNS automatically via wrangler; Pages custom domains require a separate verification flow.
- D1 database: Created
jamverse-db (ID: 6826920c-3f30-438a-ab66-e34a26cda7d3) in region WNAM on PI org CF account. Single database for the full project: users, roles, content, submission metadata. Binding name: DB. D1/R2 creation is account-level and outside the dev token scope — org admin creates, dev operates.
- R2 media bucket: Created
jamverse-media on PI org CF account. Key convention: submissions/{id}/ for content media, avatars/{user_id}/ for profile images, assets/ for shared static assets. Binding name: MEDIA. PI mark (protocolized_mark.png) uploaded to assets/ as first R2 object.
- PI mark on stub page: Copied
protocolized_mark.png from protocolized-website/public/, uploaded to jamverse-media/assets/protocolized_mark.png via wrangler (--remote flag required — wrangler 4.x defaults to local miniflare when wrangler.toml is present). Also placed in public/assets/ for Pages static serving, since there is no Worker yet to proxy R2 objects. Wired into index.html inverted and at 25% opacity for the dark background.
- Dev API token: Created a scoped Account API Token (
jamverse-dev) for the lead developer via CF dashboard — Account API Tokens rather than user-profile tokens so the credential is owned by the org, not an individual. Scopes: Pages:Edit (jamverse project), D1:Edit (jamverse-db), Workers R2 Storage:Edit (jamverse-media bucket only), Workers Scripts:Edit (account-level, required for wrangler secret put). Initial token value was captured before creation was finalized and failed validation; rolled to get the live value. Developer does not need a CF account or dashboard access.
- Session rituals and devlog: Established mandatory startup/wrapup ritual in
CLAUDE.md: startup requires running date for accurate timestamps; wrapup requires a devlog entry, devlog re-render, status update, and wrapup checklist post before the session is considered closed. data/devlog.json is the source of truth; scripts/devlog_render.py renders it to public/devlog.html, served at /devlog. Renderer outputs newest-first, dark aesthetic matching index.html. Pattern copied from protocolized-website and PI website projects.
- Developer documentation:
orientation.md — full onboarding guide covering CF stack, wrangler auth (no CF account needed), D1/R2/Worker usage, session rituals, and Zero Trust request flow. zero-trust-requests.md — template for requesting CF Access policies; org admin acts on these, no code changes required on dev side. SECURITY.md — .env creation, Dropbox exclusion via xattr, no secrets in tracked files, accidental-commit response (roll immediately, treat as compromised). Access model: dev is fully self-sufficient for all day-to-day ops; org admin handles D1/R2 creation, DNS, and Zero Trust.