simocracy-v2 · gathering-context-field

Gathering context field for sim-facing event briefing

@daviddao

Event creators can now attach a briefing to any org.simocracy.gathering record. The briefing lands in the AI system prompt every time a sim chats, deliberates, or runs an S-Process inside the event — and renders to humans in the event Info sidebar.

Date: 2026-04-24 · Target: simocracy.org · Commits: 738542c, cd8675c · pass


Headline result

4
Commits pushed
21
Files touched
8
AI endpoints wired
3
Network layers synced

Pass. The new context field round-trips through the user's PDS, is rendered as a dedicated Context for Sims section in the event sidebar on production, and is threaded into every AI endpoint (/api/chat, /api/hearing, /api/discussion, three /api/s-process/* routes, and /api/sim-heartbeat) via a single formatGatheringContext helper. Existing gatherings without the field continue to render unchanged.

What changed

1. Lexicon — org.simocracy.gathering

Two optional properties added next to description / descriptionFacets:

"context": {
  "type": "string",
  "maxLength": 50000,
  "maxGraphemes": 5000,
  "description": "Background / briefing material for sims participating in this event. Shown to AI sims as event context so they can act efficiently (e.g. prior decisions, mission, constraints, data, links)."
},
"contextFacets": {
  "type": "array",
  "items": { "type": "ref", "ref": "app.bsky.richtext.facet" },
  "description": "Rich text facets for context"
}

Sized 5× description (5000 vs 3000 graphemes) to fit a real briefing — agenda + prior decisions + constraints + links — without bloating the record.

2. AI prompt helper — lib/gathering-prompt.ts

One small helper used by every AI endpoint, returns "" when there's no context so callers can append unconditionally:

export function formatGatheringContext(opts: {
  gatheringName?: string;
  gatheringType?: string;
  context?: string;
}): string {
  if (!opts.context || !opts.context.trim()) return "";
  const header = opts.gatheringName
    ? `## Event Context — ${opts.gatheringName}${opts.gatheringType ? ` (${opts.gatheringType})` : ""}`
    : `## Event Context`;
  return `${header}\nThe following briefing was provided by the event organisers. Use it to ground your reasoning and reference it where relevant.\n\n${opts.context.trim()}\n\n`;
}

3. AI endpoints wired

EndpointWhere the context lands
/api/chatSystem prompt, between speaking-style and user-identity blocks
/api/hearingSenator system prompt for evaluate + respond, moderator prompt for summarize
/api/discussionBoth sims' system prompts
/api/s-process/evaluateRecommender prompt before MVF instructions
/api/s-process/evaluate-batchRecommender prompt for FTC batch eval
/api/s-process/deliberateRound-2 deliberation prompt
/api/sim-heartbeatPer-duty context field exposed to agentic sims

Callers (gathering-world, ftc-s-process-modal, sim-chat-modal, sim-discussion-modal) thread the context through when they're invoked inside an event. The senate-hearing-modal has a TODO comment because it isn't gathering-aware today — leaving that wiring for a follow-up keeps this PR focused.

4. UI

Production verification

1. PDS round-trip via com.atproto.repo.putRecord

Created a real test gathering on the facilitator PDS and read it back. The record carries context verbatim:

$ curl -s "https://climateai.org/xrpc/com.atproto.repo.getRecord" \
    --data-urlencode "repo=did:plc:awtx57rxuisy4sf4kzzr3uhf" \
    --data-urlencode "collection=org.simocracy.gathering" \
    --data-urlencode "rkey=3lxtest19dc040f1c3" -G | jq .value

{
  "name": "Context Field Verification Event",
  "$type": "org.simocracy.gathering",
  "status": "application",
  "context": "BRIEFING FOR SIMS\n\nMission: Evaluate whether the new gathering.context field …",
  "simSize": "normal",
  "simScope": "council",
  "createdAt": "2026-04-24T16:09:34Z",
  "description": "This event exists solely to verify that the new context field round-trips …",
  "gatheringType": "governance",
  "shortDescription": "Smoke test for the new org.simocracy.gathering.context field."
}

Identifiers (so the record is auditable):

URIat://did:plc:awtx57rxuisy4sf4kzzr3uhf/org.simocracy.gathering/3lxtest19dc040f1c3 CIDbafyreihzvmqsamjpz5fnq3f2fvhz2qmdwmw4t6yvpwywijy22ojxzglfrm Live URLsimocracy.org/events/… JSON dumpassets/04-pds-record-get.json

2. UI walkthrough — production (simocracy.org)

Events gallery on simocracy.org with five gatherings listed
01Events gallery includes the freshly-created "Context Field Verification Event". Cards remain unchanged — context is intentionally not surfaced in the preview.
Event world with the senate building rendered (default image)
02Event world. The default senate background renders because the test record didn't set a custom buildingImageUrl. Click the building to open the Info sidebar.
Info sidebar with the new Context for Sims section
03The deliverable. The Info sidebar now carries a Context for Sims section between About and Council, with the book-open icon, the italic helper line ("Shared with sims when they participate in this event."), and the full briefing.

3. Indexer — simocracy-indexer on Railway

The Railway-hosted indexer keeps its own copy of the lexicon JSONs and registers them into the GraphQL schema via lex-gql. I synced all drift, redeployed via railway up (the service has no GitHub auto-deploy hook), and confirmed the new typed fields are live:

$ curl -s -X POST https://simocracy-indexer-production.up.railway.app/graphql \
    -H "Content-Type: application/json" \
    -d '{"query":"{ orgSimocracyGathering(first:1){ edges{ node{ simScope context } } } }"}' | jq

{
  "data": {
    "orgSimocracyGathering": {
      "edges": [{
        "node": {
          "simScope": "council",
          "context": "BRIEFING FOR SIMS\n\nMission: Evaluate whether the new gathering.context field …"
        }
      }]
    }
  }
}

Drift I also fixed in the same sweep:

4. ATProto network — lexicons published on gainforest.earth

All 10 org.simocracy.* lexicons are owned by the gainforest.earth account (did:plc:qoti4acfmc5wg6zzmtix6hse, PDS discina.us-west.host.bsky.network) as com.atproto.lexicon.schema records. The NSID is discoverable via DNS:

$ dig +short TXT _lexicon.simocracy.org
"did=did:plc:qoti4acfmc5wg6zzmtix6hse"

$ goat lex resolve --did org.simocracy.gathering
did:plc:qoti4acfmc5wg6zzmtix6hse

Installed goat (brew install goat, v0.2.3), loaded the gainforest.earth app password from .env.goat, and published the three updated schemas:

$ export GOAT_USERNAME=gainforest.earth GOAT_PASSWORD=…
$ goat lex publish --update lexicons/
  🟣 org.simocracy.gathering       # updated
  🟣 org.simocracy.interview       # updated (template ref)
  🟢 org.simocracy.interviewTemplate  # newly published

Post-publish status — every schema now in sync with the network:

  agents              🟢 org.simocracy.agents
  defs                🟢 org.simocracy.defs
  gathering           🟢 org.simocracy.gathering
  history             🟢 org.simocracy.history
  interview           🟢 org.simocracy.interview
  interviewTemplate   🟢 org.simocracy.interviewTemplate
  sim                 🟢 org.simocracy.sim
  skill               🟢 org.simocracy.skill
  star                🟢 org.simocracy.star
  style               🟢 org.simocracy.style

Findings

Three network layers in sync

End-to-end verified across all three layers: (1) user PDS writes the field → (2) Railway indexer exposes it as a typed GraphQL scalar → (3) gainforest.earth publishes the com.atproto.lexicon.schema record so other ATProto apps see the same shape. Next.js app renders the "Context for Sims" section on production, AI endpoints inject the briefing via the shared helper, lint + npm run build pass, and every existing record without context still renders and processes unchanged.

Drift in two sibling repos cleaned up in the same sweep

simocracy-indexer was missing sprite (sim, two days stale) and simScope (gathering, two days stale). interviewTemplate.json was missing the required "type":"object" on its record body, which blocked goat lex tooling. All three fixes shipped with the same deploy.

Senate hearing modal not gathering-aware

The senate hearing modal currently doesn't receive a gathering reference, so its fetch('/api/hearing') call doesn't pass gatheringContext. Marked with a TODO(gathering-context) in components/senate/senate-hearing-modal.tsx. Follow-up: thread the senate gathering record into the modal so its hearings get the same briefing as gathering-world hearings.

Railway auto-deploy not wired

The indexer's Railway service doesn't auto-deploy on GitHub push — railway up must be run from a dev machine. Consider adding a GitHub webhook. Until then, lexicon changes land in two commits: one to simocracy-indexer main, one railway up.

Reproducing

simocracy-v2 commits738542c (lexicon + UI) · cd8675c (AI plumbing) · e38adec (interviewTemplate type:object fix) simocracy-indexer commits9a76da1 (sync gathering fields) · 657e087 (sync sim.sprite + interviewTemplate) Quality gatesnpm run lint · npm run build · goat lex parse · goat lex status — all pass Indexer deploysTwo Railway deploys via railway up --ci --detach, both verified with __type(name:"OrgSimocracyGathering") schema introspection Test recordat://did:plc:awtx57rxuisy4sf4kzzr3uhf/org.simocracy.gathering/3lxtest19dc040f1c3 Live pagesimocracy.org/events/… PDS dumpassets/04-pds-record-get.json Lexicon registryat://gainforest.earth/com.atproto.lexicon.schema/org.simocracy.gathering (DID did:plc:qoti4acfmc5wg6zzmtix6hse, PDS discina.us-west.host.bsky.network) Implementation logtmux session gathering-ctx — product manager (this report) drove Claude Code (--dangerously-skip-permissions) through the two-commit plan in /tmp/gathering-context-brief.md; the network-layer sync was added on follow-up.