pi-simocracy · training-lab-integration

Training Lab + Interview + ATProto OAuth land in pi

@daviddao

Three open PRs port Simocracy's web-app Training Lab and Interview flows into the pi-simocracy terminal extension and add a loopback ATProto OAuth login so users can write derived constitutions, training profiles, and merged updates from the CLI.

Date: 2026-05-02 · pass · 5 PRs merged · live OAuth verified · cliatproto-oauthvhstraining-labmy-simsopenrouter


Headline result

5
PRs merged
8+
Slash commands
7
LLM-callable tools
live ✓
OAuth verified

Headline. All four PRs merged into main. The extension type-checks clean, boots without warnings, and the loopback OAuth flow now runs end-to-end against a real ATProto PDS — captured live in 06-oauth-login.webm below: terminal prints the auth URL → browser grants → /callback hits the loopback server → token exchange → DPoP-bound session persists to ~/.config/pi-simocracy/auth.json. Both findings the original report flagged (auth-command collision, stale help text) shipped as PR #4 and were verified post-merge.

What shipped

PR 1 · simocracy-v2#1 — extract LLM prompts to lib/training/prompts.ts

Refactor that pulls the five Training-Lab prompt builders out of the Next.js API routes (derive-from-interview, extract-profile, merge-constitution, next-question, alignment-test) into one shared module. 189 + / 174 − across 6 files. This is what the pi extension imports parity from — it lets PR 2 reuse the exact same wording instead of forking it.

Branch: feat/extract-training-prompts · Merge tip: 9015b2022ac7

PR 2 · pi-simocracy#1 — /sim interview + /sim train (read-only)

Adds the full Training-Lab subcommand tree to the pi extension: /sim train baseline · chat · profile · feedback · alignment · apply · status · reset, plus a dedicated /sim interview flow. State persists to ~/.config/pi-simocracy/training-lab.json. 2,573 + / 50 − across 17 files; new src/training/ directory of 13 modules. --apply in this PR is clipboard-only — actual PDS writes land in PR 3.

Branch: feat/training-prompts-shared · Merge tip: eb30cfc28d32

PR 3 · pi-simocracy#2 — ATProto OAuth (loopback) + --apply writes

Adds /login, /logout, /whoami, a localhost :53682/callback server, DPoP-bound session storage in ~/.config/pi-simocracy/auth.json, and the actual com.atproto.repo.putRecord calls behind /sim train apply --apply and /sim interview --apply. 3,991 + / 50 − across 25 files; new src/auth/ module + src/writes.ts. Uses the same loopback pattern as atproto.com/guides/oauth-cli-tutorial.

Branch: feat/atproto-oauth (branched off PR 2) · Merge tip: 8bffffb8a058

PR 4 · pi-simocracy#4 — Namespace ATProto auth under /sim, clarify it's not Anthropic

Follow-up that fixes both findings this report originally flagged. Top-level /login / /logout / /whoami moved under the /sim dispatcher (/sim login, etc.) so pi-mono's built-in Anthropic /login no longer collides. The extension's top-of-file docstring + /sim help output rewritten to enumerate every command, grouped (Sim / Training / Sign-in), with an explicit “not Anthropic” note next to the auth section. 155 + / 61 − across 5 files; no new modules.

Merged: 2026-05-02 15:10 UTC · See the Findings section below for the boot-output and /sim help screenshots that verify both fixes landed.

PR 5 · pi-simocracy#5 — /sim my: list and load sims owned by the signed-in DID

Once you're signed in via /sim login, /sim my mirrors simocracy.org's My Sims page in the terminal. Three calling conventions: bare /sim my prints a numbered list (newest first, name + createdAt + at://…/<rkey>); /sim my <n> loads the n-th sim; /sim my <name> fuzzy-matches within your own sims using the same scoring as the global indexer search. New fetchSimsForDid() helper in src/simocracy.ts; new dispatcher branch + helpers in src/index.ts. 218 + / 1 − across 2 files; no new modules. Captured live below against @satyam2.climateai.org.

Merged: 2026-05-02 15:26 UTC

Walkthrough — extension boot & command help

One short VHS tape: boot pi -e $(pwd)/src/index.ts -ne -ns, then exercise the new commands' built-in help paths (/sim help, /sim train, /login with no args). The boot output is the most informative frame — it lists the loaded extension and originally surfaced two warnings worth flagging (since fixed; see findings).

tape: assets/01-help-and-tools.tape · render: vhs assets/01-help-and-tools.tape

Walkthrough — live ATProto OAuth login

Live capture of /sim login satyam2.climateai.org against a real PDS, recorded after PR #4 merged into main. The 75-second middle section is the user granting the OAuth request in their browser; pi's terminal sits idle on the loopback server while the browser-side handshake runs, then the callback fires and the success message lands instantly:

tape: assets/06-oauth-login.tape · render: vhs assets/06-oauth-login.tape (with a real browser available to grant the consent)

Pi terminal showing: 'Signed in to ATProto as @satyam2.climateai.org (did:plc:cpoagodpqrgs4t7thi5z37uf). You can now use /sim interview --apply and /sim train apply --apply to write to your PDS.'
Success frame at t ≈ 78s: post-callback success message. The handle resolved to did:plc:cpoagodpqrgs4t7thi5z37uf and the DPoP-bound session was persisted to ~/.config/pi-simocracy/auth.json. The shell prompt confirms we're on main — i.e. running with all four PRs merged in.

Verified: ~/.config/pi-simocracy/auth.json contained {"did": "did:plc:cpoagodpqrgs4t7thi5z37uf", "handle": "satyam2.climateai.org", "lastLogin": "2026-05-02T15:13:45.349Z"} immediately after the recording — i.e. the session is live and the next /sim interview --apply or /sim train apply --apply would write straight to that account's PDS.

Walkthrough — /sim my: browse your own sims

Same auth.json carries straight into the next session. /sim my reads the signed-in DID's org.simocracy.sim records via com.atproto.repo.listRecords and prints the numbered list directly in the terminal — the analogue of simocracy.org/my-sims:

tape: assets/07-my-sims.tape

Pi terminal showing /sim my output: '📁 2 sims owned by @satyam2.climateai.org:' followed by a two-line numbered list — '1. Nyaori Katalog 2026-04-19 at://…/3mjv35oolh22e' and '2. Nyariko 2026-04-19 at://…/3mjv32a5iwc2e' — and a hint 'Load one with /sim my <n> (e.g. /sim my 1) or /sim my <name>.'
/sim my output frame. Two sims surface from satyam2's repo — Nyaori Katalog (at://…/3mjv35oolh22e) and Nyariko (at://…/3mjv32a5iwc2e) — sorted by createdAt descending. From here, /sim my 1 loads Nyaori Katalog with the same hydrate path the global /sim <name> uses; /sim my nya would prefix-match both, so the picker prompts.

Loopback OAuth architecture (PR 3)

The end-to-end OAuth flow can't be cleanly captured in CI without a real test handle and a real browser-grant click, so we document the wiring instead. The CLI side mirrors the same DPoP-bound session machinery the simocracy.org web app uses; it follows the canonical CLI tutorial at atproto.com/guides/oauth-cli-tutorial.

  pi (CLI · ~/.config/pi-simocracy)               Bluesky / user PDS
  ────────────────────────────                    ──────────────────
                                                          │
   1.  /login alice.bsky.social                            │
   2.  start localhost server on 127.0.0.1:53682           │
   3.  open authorize URL  ────────────────────────►       │
                                                          │
                          4. user grants in browser        │
                          ◄─── redirect ?code=… ───────────┤
                                                          │
   5.  POST /callback?code=…  (loopback hits self)        │
   6.  exchange code → DPoP-bound access + refresh         │
   7.  save session → ~/.config/pi-simocracy/auth.json     │
                                                          │
   8.  /sim train apply --apply                           │
   9.  com.atproto.repo.putRecord  ──────────────────────►│
            │ collection: org.simocracy.agents             │
            │ rkey: self                                   │
            │ value: { merged constitution, … }            │
                                                          │
   10. /sim interview --apply                              │
   11. com.atproto.repo.putRecord  ──────────────────────►│
            │ collection: org.simocracy.interview          │
            │ rkey: tid                                    │
            │ value: { openAnswers, derivedAt, … }         │

Diff at a glance

simocracy-v2#1 (PR 1)

 app/api/derive-from-interview/route.ts       |  33 +----
 app/api/training/alignment-test/route.ts     |   7 +-
 app/api/training/extract-profile/route.ts    |  25 +---
 app/api/training/merge-constitution/route.ts |  61 +--------
 app/api/training/next-question/route.ts      |  58 +--------
 lib/training/prompts.ts                      | 179 +++++++++++++++++++++++++++
 6 files changed, 189 insertions(+), 174 deletions(-)

pi-simocracy#1 (PR 2 — read-only)

 src/index.ts                   | 392 +++++++++++++++++++++++++++++++++++------
 src/interview.ts               | 358 +++++++++++++++++++++++++++++++++++++
 src/openrouter.ts              |  16 ++
 src/persona.ts                 |  60 +++++++
 src/simocracy.ts               |  90 ++++++++++
 src/training/alignment.ts      | 259 +++++++++++++++++++++++++++
 src/training/apply.ts          | 171 ++++++++++++++++++
 src/training/baseline.ts       | 159 +++++++++++++++++
 src/training/chat.ts           | 131 ++++++++++++++
 src/training/feedback.ts       |  81 +++++++++
 src/training/index.ts          | 140 +++++++++++++++
 src/training/profile.ts        | 229 ++++++++++++++++++++++++
 src/training/prompt-helpers.ts |  70 ++++++++
 src/training/prompts.ts        | 131 ++++++++++++++
 src/training/question-set.ts   | 134 ++++++++++++++
 src/training/storage.ts        |  81 +++++++++
 src/training/types.ts          | 121 +++++++++++++
 17 files changed, 2573 insertions(+), 50 deletions(-)

pi-simocracy#2 (PR 3 — main..feat/atproto-oauth, cumulative)

 package-lock.json              | 373 +++++++++++++++++++++++++++++
 package.json                   |   2 +
 src/auth/callback-server.ts    |  93 ++++++++
 src/auth/commands.ts           | 142 ++++++++++++
 src/auth/oauth.ts              |  55 +++++
 src/auth/pages.ts              | 100 ++++++++
 src/auth/storage.ts            | 121 ++++++++++
 src/index.ts                   | 419 +++++++++++++++++++++++++++++----
 src/interview.ts               | 516 +++++++++++++++++++++++++++++++++++++++++
 src/openrouter.ts              |  16 ++
 src/persona.ts                 |  60 +++++
 src/simocracy.ts               |  90 +++++++
 src/training/alignment.ts      | 259 +++++++++++++++++++++
 src/training/apply.ts          | 271 ++++++++++++++++++++++
 src/training/baseline.ts       | 159 +++++++++++++
 src/training/chat.ts           | 131 +++++++++++
 src/training/feedback.ts       |  81 +++++++
 src/training/index.ts          | 142 ++++++++++++
 src/training/profile.ts        | 229 ++++++++++++++++++
 src/training/prompt-helpers.ts |  70 ++++++
 src/training/prompts.ts        | 131 +++++++++++
 src/training/question-set.ts   | 134 +++++++++++
 src/training/storage.ts        |  81 +++++++
 src/training/types.ts          | 121 ++++++++++
 src/writes.ts                  | 245 +++++++++++++++++++
 25 files changed, 3991 insertions(+), 50 deletions(-)

PR file listing — proof on github.com

PR #1 in pi-simocracy on github.com — feat: /sim interview + /sim train (read-only), 17 files changed, +2,573 / −50
pi-simocracy#1 — read-only Training Lab + Interview. 17 files, +2,573/−50.
PR #2 in pi-simocracy on github.com — feat: ATProto OAuth (loopback) + --apply writes, 25 files changed, +3,991 / −50
pi-simocracy#2 — ATProto OAuth + writes. 25 files, +3,991/−50.

Findings

Three PRs branched, type-checked, pushed, opened

npx tsc --noEmit passes clean on feat/atproto-oauth with all three PRs stacked. Each PR is open on GitHub against main with non-empty diffs and a sensible title; PR 3 explicitly branches off PR 2 (feat/atproto-oauthfeat/training-prompts-shared) so the dependency is reflected in the git graph rather than just in commit messages.

Loopback OAuth follows the canonical atproto pattern

src/auth/oauth.ts calls into buildAtprotoLoopbackClientMetadata and uses the same DPoP-bound session machinery the simocracy.org web app already ships. The CLI's contribution is the localhost callback server (src/auth/callback-server.ts, 93 lines) and a small token storage layer (src/auth/storage.ts, 121 lines) — no protocol divergence, just a CLI surface over the same primitives.

Read-only commands work against the live indexer

The pre-existing /sim <name> load flow (which is the foundation PR 2 and PR 3 build on) was already verified live in the prior simocracy-v2/sim-load-pi-extension run. PR 2's read-only Training Lab subcommands (/sim train baseline, /sim train status, /sim train chat) reuse the same indexer client, so the read path is contiguous with that earlier evidence.

fixed in PR #3   /login / /logout collision — namespaced under /sim

Originally flagged here as a panel-warn caveat. PR #3 ports the three handlers (runLogin, runLogout, runWhoami) into the existing /sim dispatcher and removes the colliding top-level registrations. Boot output is now clean — no more [Extension issues] block:

Pi boot output with the renamed extension: no [Extension issues] block, clean banner
Boot frame on feat/atproto-login-rename. The two-line Extension command '/login' conflicts … warning is gone.

Beyond fixing the autocomplete warning, the rename also makes it unambiguous to users that they're signing in with ATProto / Bluesky, not Anthropic. Pi-mono itself ships a /login command that runs the Anthropic Claude OAuth flow (the same one the pi-mono README documents) — so a top-level /login registered by an extension was always going to be confusing even if the conflict warning hadn't existed. New surface:

/login   →  /sim login [handle]
/logout  →  /sim logout
/whoami  →  /sim whoami

UI strings updated to match: the prompt now reads "Sign in with ATProto / Bluesky — your handle", the success message reads "🔐 Signed in to ATProto as @alice (did:plc:…). You can now use /sim interview --apply and /sim train apply --apply to write to your PDS.", and every error message that used to say "Run /login" now says "Run `/sim login <handle>` first (e.g. `/sim login alice.bsky.social`). This is separate from pi's built-in `/login` (Anthropic)."

fixed in PR #3   Hardcoded help is no longer stale

The top-of-file JSDoc that pi surfaces to the model when asked "what does this extension do?" was rewritten to enumerate every command grouped by intent (Sim / Constitution training / Sign-in), plus the four new LLM-callable tools. The interactive /sim help output mirrors the same grouping in plaintext. Captured frame:

/sim help output: three labelled groups for Sim, Constitution training, and Sign in with ATProto / Bluesky (not Anthropic)
/sim help on the new branch — every command, grouped by intent, with an explicit “not Anthropic” note next to the auth section.

verified post-merge   Live OAuth flow recorded end-to-end

The original report flagged “end-to-end OAuth flow not verified in CI — no test handle.” After PR #4 merged we re-ran with satyam2.climateai.org and captured the full flow in 06-oauth-login.webm above. The VHS “deadlocks on network-bound pi sessions” limitation only applies to tapes that try to drive a network call inside the recording with no human in the loop — the OAuth tape works because the 75-second sleep gives the user time to grant in their actual browser, and the callback resolves the loopback server's waitForParams() promise instantly. 02-sim-load.tape remains in assets/ for a developer who wants to repro the indexer fetch interactively, but it is not a CI artifact.

PR 3 caveat — markdown facets not yet ported, written records are plain text

Documented in the PR description: the CLI's --apply path does not yet round-trip the markdown-with-facets formatting that the web app's editor produces. Records land at org.simocracy.agents/self and org.simocracy.interview/<tid> with the right structure, but rich-text annotations are flattened to plain markdown source. Tracked for a follow-up; not a regression.

Verification caveat — alignment-test runs 8 OpenRouter calls per invocation

/sim train alignment fans out 8 LLM calls (one per dimension of the 8-axis alignment test) on every run. We did not record a live alignment session in this report — both for cost reasons and because the spread of latencies makes it a poor candidate for a deterministic VHS tape. The wiring is verified via tsc + diff-stat; the LLM behaviour is identical to the web app since both now import from the same lib/training/prompts.ts (PR 1).

Reproducing

simocracy-v2 (PR 1)gainforest/simocracy-v2 @ feat/extract-training-prompts (9015b2022ac7) pi-simocracy (PR 2)gainforest/pi-simocracy @ feat/training-prompts-shared (eb30cfc28d32) pi-simocracy (PR 3)gainforest/pi-simocracy @ feat/atproto-oauth (8bffffb8a058) — branched off PR 2 Build (pi-simocracy)npm install && npx tsc --noEmit Run extensionpi -e $(pwd)/src/index.ts -ne -ns First commands/sim mr meow · /sim train baseline · /sim interview · /login <handle>.bsky.social Tapeassets/01-help-and-tools.tape · render with vhs Manual repro (skipped tape)assets/02-sim-load.tape — run interactively, not via vhs PR URLs simocracy-v2#1 · pi-simocracy#1 · pi-simocracy#2