# Plan: Universe Onboarding How a real user joins a deployed demarkus universe and starts using its worlds from a coding agent (initially Claude Code, eventually Cursor / Aider / Continue / etc.). The "last mile" between the broker we shipped in Phase 6 and the user's actual editor session. This plan is the unbuilt other half of the original /roadmap.md §6.4 ("user install flow") which got reframed into kind topology examples; we shipped the topology examples (Stages 1-4 of the kind harness, PRs #126-#134) and now circle back to the actual user-onboarding piece. ## Goal A user with `claude` on PATH and the demarkus Claude Code plugin installed can join a customer's universe with one slash command: ``` /soul-join https://demarkus.acmecorp.com ``` After running it, they are signed in via their corp IdP, have working MCP entries for every world they're authorized for, and never need to think about token refresh, expiry, or per-world configuration. Adding/removing worlds on the broker side propagates silently. Revocation works. ## Non-Negotiables (Fritz directive 2026-05-15) - **No MVPs, no shortcuts.** Build the right thing the first time. Refresh tokens, proper RFC 8628 device flow, content negotiation, agent-agnostic core. Do not ship "minimal viable" placeholders intending to harden later. - **No core changes.** Zero protocol or demarkus-server changes. All work lives in `tools/demarkus-broker/`, `tools/demarkus-join/` (new), `plugins/claude-code/`, `deploy/helm/demarkus-broker/`. Same precedent as the existing Claude Code plugin, the Obsidian plugin, the broker itself. - **Agent-agnostic core.** The OIDC dance + token persistence + universe state lives in a binary (`tools/demarkus-join`), not in plugin shell. Claude Code plugin is a thin wrapper. Cursor / Aider / Continue plugins reuse the same binary later. - **Single-broker only for now.** A user is in exactly one universe at a time. Multi-broker support is explicitly deferred (see §Out of Scope). ## User Experience (the canonical flow) 1. **Customer admin gives the user one URL:** `https://demarkus.acmecorp.com` — the broker. Nothing else. 2. **User runs in Claude Code:** ``` /soul-join https://demarkus.acmecorp.com ``` 3. **Claude Code shows:** ``` Open this URL in your browser to sign in: https://demarkus.acmecorp.com/device?code=WDJB-MJHT (Browser opening automatically.) Waiting for sign-in... [polling] ``` `tools/demarkus-join` shells `open` / `xdg-open` / `start` to launch the URL automatically; falls back to print-only when no GUI is detected. 4. **Browser side:** - Broker's `/device` page shows `WDJB-MJHT` already filled in. - User clicks "Sign in" → bounces through broker's existing `/auth/login` → IdP (Google / Okta / Entra / Auth0) → `/auth/callback`. - Broker associates the resulting id_token + email with the device_code. - Page shows: "Done. You can close this tab and return to your editor." 5. **Back in Claude Code:** ``` Connected to Acme Corp universe. Authorized worlds: - platform - data - design Restart Claude Code to start using them, or run /restart. ``` 6. **After restart:** Claude Code has three new MCP servers wired up: `demarkus-platform`, `demarkus-data`, `demarkus-design`. `mark_fetch`, `mark_publish`, etc. work transparently. 7. **Hours later, access tokens expire** (24h TTL). User sees nothing — `tools/demarkus-join` runs as a background renew (cron-style or daemon) using the long-lived refresh token, mints fresh access tokens, updates `.claude.json`. User never re-authenticates in a browser unless their refresh token expires (90 days default). 8. **Admin adds a new world** (`worlds:` list edit + Argo sync). On next refresh tick, `tools/demarkus-join` notices the new world in `/me/install` response and adds the MCP entry. Claude Code shows "added new world: research" on next plugin event. Admin removes a world: same shape but reversed. 9. **User leaves the company:** admin disables the IdP account; refresh fails; plugin shows "your tokens are no longer accepted" and prompts `/soul-leave` to clear local state. 10. **User wants to switch universes:** `/soul-leave` then `/soul-join `. Single broker at a time per the non-negotiable. ## Architecture ``` ┌──────────────┐ ┌──────────────────────────┐ ┌────────────┐ │ Claude Code │ shells │ tools/demarkus-join │ HTTPS │ broker │ HTTPS ┌─────┐ │ slash cmd │ ──────► │ (Go binary, OIDC │ ──────► │ /device/* │ ──────► │ IdP │ │ /soul-join │ │ device-flow client) │ │ /me/* │ └─────┘ └──────────────┘ │ │ │ /.well-known │ ~/.config/demarkus/ │ └─────┬──────┘ │ ├── broker.toml │ │ │ └── refresh_token │ QUIC│ (cluster-internal, └────────┬─────────────────┘ │ cross-namespace │ │ RBAC for the │ writes via `claude mcp add` │ per-world ▼ │ tokens Secret) ~/.claude.json ▼ (mcpServers entries ┌──────────────┐ per world) │ world-a sv-r │ │ │ world-b sv-r │ ▼ │ world-c sv-r │ demarkus-mcp ──── QUIC ──────────►└──────────────┘ (one process per (private network, MCP entry, runs as token-auth, no subprocess of public DNS) Claude Code) ``` ### Layer responsibilities | Layer | What it owns | What it does NOT own | | --- | --- | --- | | **Claude Code plugin** | The `/soul-*` slash commands. Argument parsing. Invoking `tools/demarkus-join`. Showing the user the verification URL + status. | OIDC. Token storage. World discovery. `.claude.json` patching schema. | | **`tools/demarkus-join`** | OIDC discovery via broker `/.well-known/openid-configuration`. Device flow polling per RFC 8628. Refresh token persistence. `/me/install` JSON consumer. Driving `claude mcp add` per world. Background refresh loop (cron-friendly subcommand). | Knowing about Claude Code's slash command machinery. UI polish (it's a CLI, not a TUI). | | **Broker** | `/device/*` (RFC 8628 device authorization endpoints). `/me/install` (content-negotiated). `/.well-known/openid-configuration` (proxies IdP discovery, overrides the device-related endpoints to point at the broker). Refresh token mint + grant. Per-world `WorldConfig.PublicURL`. | Hosting the UI page rendering library (server-rendered HTML for `/device` is fine; no SPA). Knowing about Claude Code. | | **`/me/install` response** | Per-world `{name, publicURL, accessToken, expiresAt}` JSON for programmatic consumers. Text-shellscript variant for `curl ... \| sh` fallback users (non-plugin). | Refresh token (returned separately by the device-flow `/device/token` response, not by `/me/install`). | ### Why broker is the device-flow shim, not plugin going IdP-direct The plugin sees one API regardless of which IdP the customer wired up (Google, Okta, Entra, Auth0). The broker handles the OIDC dance internally — it already does for the existing browser flow; device flow is a second front-end into the same machinery. Customer's IdP choice is invisible to every plugin we ever ship. If we went IdP-direct from the plugin, every plugin would need: per-IdP discovery, per-IdP device flow quirks (e.g., Google's device endpoint is different from Auth0's), per-IdP scope strings, per-IdP error handling. Multiplied by N coding agents we eventually support. Hard no. ## Network Topology (recommended deployment) | Component | Visibility | Why | | --- | --- | --- | | **Broker** | Public (or behind corp WAF) | User must reach it from wherever they sign in. OIDC-fronted; rate-limited; every request authed. Hardened in §6.3. | | **Worlds** | Private (corp network / VPN / Tailscale) | Knowledge is internal; no reason to expose to the public internet. ClusterIP Service, no Ingress, customer's ops team picks how corp users route to them. | **Critical invariant:** **world URLs never appear in any unauthenticated broker response.** Discovery doc lists broker endpoints only. Per-user world list comes from authenticated `/me/install`. Customer can additionally split-horizon DNS the world hostnames so they don't even resolve from the public internet. This is the same posture as e.g. GitHub Enterprise (`gh.io` public for SSO, private repos behind VPN). demarkus does not invent its own private networking — that's the customer ops team's call (VPN, Tailscale, Cloudflare Tunnel, VPC peering). ## Decisions (locked in) 1. **OIDC device code flow per RFC 8628** as the auth mechanism. Standard pattern, every major IdP supports it, terminal-friendly. 2. **Broker hosts the device-flow shim**, not plugin-IdP-direct (rationale above). 3. **Refresh tokens with silent renewal**, ~90 day TTL. User does not re-auth daily. `tools/demarkus-join refresh` is a separate subcommand that drives the renewal; can be invoked by cron, systemd timer, plugin's startup hook, or LaunchAgent. 4. **Single broker per user.** Multi-broker support is deferred. 5. **`tools/demarkus-join` is a new binary** in `tools/`. Same release shape as `demarkus-token` / `demarkus-publish` (binary archives via goreleaser; no Docker image; no helm chart). 6. **Plugin is a thin shell wrapper** around `tools/demarkus-join`. New `commands/soul-join.md`, `commands/soul-leave.md`, `commands/soul-refresh.md` per the existing Claude Code plugin slash-command pattern. 7. **`.claude.json` is patched via `claude mcp add`** subcommand, not direct JSON manipulation. Claude Code owns its config schema; we use the official surface. 8. **Per-world `WorldConfig.PublicURL`** new required-when-installable field. Optional in chart values; if unset for a world, that world is skipped in `/me/install` with a comment ("world X has no publicURL configured — skipping"). 9. **`/me/install` is content-negotiated.** `Accept: application/json` returns structured JSON; default returns shell script for `curl ... | sh` users (non-plugin terminal use case). 10. **Broker `/.well-known/openid-configuration`** is the discovery surface. We extend the standard OIDC document with broker-hosted device endpoints rather than inventing a `/.well-known/demarkus-broker`. Plugins use standard OIDC client libraries. 11. **MCP server name format:** `demarkus-`. World names are already validated for k8s namespace shape (DNS-1123 subdomain), which is also a valid MCP server name. 12. **Refresh token storage:** `~/.config/demarkus/refresh_token` mode 0600. Broker URL + per-world labels in `~/.config/demarkus/broker.toml` mode 0644 (no secrets). XDG_CONFIG_HOME respected. ## Open Questions (resolve before / during work) 1. **Does mock-oauth2-server (navikt) support device code flow?** Need to verify before kind harness Stage 5 work. If not, we either find a different mock IdP or skip the kind end-to-end for device flow specifically (broker-side tests still cover it via fakeVerifier). Verification path: check navikt/mock-oauth2-server v2.x source for `/device_authorization` endpoint. 2. **Refresh token TTL.** Lean: 90 days, matching most enterprise SSO defaults. Customer-overridable via chart value. 3. **Refresh token revocation surface.** Broker-side `POST /token/revoke` per RFC 7009? Or just "refresh tokens expire and aren't renewable post-expiry"? Lean: ship revocation now (it's load-bearing for "user leaves the company → admin revokes IdP → refresh fails fast" UX). +50 lines broker. 4. **`claude mcp add` exact invocation shape.** Need to verify against current Claude Code CLI: does it support `--scope local`, `--env KEY=val` flags, idempotency on re-add? If `claude mcp add` is not idempotent, `tools/demarkus-join` does `claude mcp remove 2>/dev/null; claude mcp add ...`. 5. **Browser-launch UX when no GUI is available.** SSH session, tmux/screen, headless container. Detection: `[ -n "$DISPLAY" ] || [ "$(uname)" = "Darwin" ]` heuristic. Fallback: print URL, no auto-launch, user copies into a browser on another machine. 6. **Plugin background refresh trigger.** Three options: (a) Claude Code plugin lifecycle hook on each session start, (b) systemd timer / launchd plist installed by `tools/demarkus-join setup`, (c) cron job. Need to check what the existing plugin does for periodic work. Lean: (a) on session start as the primary; (b) as opt-in for users who want it more frequent. 7. **Per-world MCP entry naming collision.** What if user's `.claude.json` already has a `demarkus-platform` entry from a different source? `tools/demarkus-join` should namespace its entries under a recognizable prefix (`demarkus-soul-platform`?) OR detect and skip non-broker-managed entries. Lean: namespace under `demarkus--` where universe-slug is derived from broker host (`acmecorp` from `demarkus.acmecorp.com`). ## Sub-Phases (PR sequence) Each PR is independently reviewable, testable in isolation, and ships a coherent slice. Order is dependency-respecting; do not parallelize. ### PR1 — Broker config foundations - `tools/demarkus-broker/internal/broker/config.go`: add `WorldConfig.PublicURL string \`yaml:"publicURL"\`` (optional; validation only requires it when used). - `deploy/helm/demarkus-broker/values.yaml`: add `worlds[].publicURL` example with comment. - `deploy/helm/demarkus-broker/templates/secret-config.yaml`: render the new field. - `deploy/helm/demarkus-broker/tests/secret-config_test.yaml`: add helm-unittest assertions. - `tools/demarkus-broker/internal/broker/config_test.go`: add unit test for the new field round-tripping through YAML. - **Scope:** ~80 lines code + 30 lines tests. Single-day PR. ### PR2 — Broker discovery doc - `tools/demarkus-broker/internal/broker/server.go`: new route `GET /.well-known/openid-configuration`. Proxies the IdP's discovery doc but overrides `device_authorization_endpoint`, `token_endpoint` (broker-hosted), and `issuer` (broker URL). Public, unauthed, cacheable. - `tools/demarkus-broker/internal/broker/discovery.go` (new file): the proxy + override logic. Cached at startup with refresh on TTL. - Tests: assert overridden fields, assert proxy of un-overridden fields (jwks_uri, userinfo_endpoint). - **Scope:** ~150 lines + tests. Single-day PR. ### PR3 — Broker device flow (RFC 8628) - `tools/demarkus-broker/internal/broker/device.go` (new file): - `POST /device/authorize`: returns `{device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval}`. In-memory store keyed by device_code; user_code is short human-readable (e.g., 8 chars from base32 alphabet). - `GET /device`: server-rendered HTML page with a form for user_code. On valid user_code, sets a cookie linking the device_code, then 302 to existing `/auth/login`. The existing `/auth/callback` handler grows a branch: if the device_code cookie is present, associate the resulting id_token + email with the device_code, then return a "you can close this tab" page (instead of the normal JSON tokens response). - `POST /device/token`: polling endpoint per RFC 8628. Returns `{error: "authorization_pending"}` while waiting, `{error: "slow_down"}` if polled too fast (configurable interval), `{error: "expired_token"}` after expiry, `{error: "access_denied"}` if user denies in browser, success `{access_token, id_token, refresh_token, token_type, expires_in}`. - In-memory store: `map[deviceCode]deviceCodeState` with periodic janitor goroutine. - `tools/demarkus-broker/internal/broker/server.go`: route registration. - Tests: full RFC 8628 happy-path + every error code + concurrency safety on the device_code store. - **Scope:** ~600 lines + tests. ~2 day PR. ### PR4 — Broker refresh tokens - `tools/demarkus-broker/internal/broker/refresh.go` (new file): - Mint refresh token alongside id_token in the device-code success path. Refresh token is opaque random (32 bytes hex), stored in a new broker-namespace Secret (`demarkus-broker-refresh-tokens`) with the user's email, issuance timestamp, expiry. - `POST /device/token` with `grant_type=refresh_token`: looks up the refresh token, mints a fresh id_token (re-fetches user info from IdP via stored refresh-on-IdP token, OR re-uses cached claims with a TTL — design decision: revisit user info every refresh OR cache for some window). Returns new access_token + id_token (not a new refresh_token by default; rotation is an open question). - `POST /token/revoke` per RFC 7009: drops the refresh token from the broker-side store. Called by `tools/demarkus-join leave`. - Sweeper integration: existing broker sweeper sweeps expired refresh tokens too. - New chart value `server.refreshTokenTTL` (default 90 days). - Tests: mint, refresh, revoke, expiry, concurrent refresh. - **Scope:** ~400 lines + tests. ~1.5 day PR. ### PR5 — Broker `/me/install` - `tools/demarkus-broker/internal/broker/server.go`: new route `GET /me/install`, wrapped in `requireAuth`. - `tools/demarkus-broker/internal/broker/install.go` (new file): - Calls `s.issuer.List(claims.Email)` for existing issuances; if empty for any allowed world, mints fresh per existing `Issuer.Mint`. - Filters worlds: only include those where `WorldConfig.PublicURL != ""` AND the user is in `Allow`. - Content negotiation on `Accept` header: - `application/json` (or `*/*` from a programmatic UA): structured `{worlds: [{name, publicURL, accessToken, expiresAt}], universe: {name, brokerURL}}`. - `text/x-shellscript` or curl default: shell-script body that the user can `curl ... | sh`. Script does the same world-detection + `claude mcp add` invocations the binary would do. - Tests: JSON shape, shell shape, auth required (401 without bearer), worlds-without-publicURL skipped with comment, mint-on-empty path, multi-world fan-out. - **Scope:** ~300 lines + tests. ~1 day PR. ### PR6 — `tools/demarkus-join` binary - `tools/demarkus-join/` (new module-resident package): - `main.go`: subcommand router (`join`, `refresh`, `leave`, `status`, `version`). - `internal/oidc/discovery.go`: standard OIDC discovery against broker. - `internal/oidc/device.go`: RFC 8628 device flow client. Polls with proper backoff per server's `interval` + `slow_down` handling. - `internal/install/`: `/me/install` consumer. JSON parser for the world list. Per-world `claude mcp add` invocation (shell out, capture errors, idempotent via remove-then-add). - `internal/state/`: `~/.config/demarkus/broker.toml` + `refresh_token` persistence. XDG_CONFIG_HOME aware. Mode 0600 on refresh_token. - `internal/browser/`: `open` / `xdg-open` / `start` launcher with no-GUI fallback. - `tools/.goreleaser.yml`: add `demarkus-join` build entry (linux + darwin, all archs). - `tools/.goreleaser.yml`: add archive entry so users can install standalone. - Tests: each internal package; integration test against a fake broker (httptest). - **Scope:** ~1200 lines + tests. ~3 day PR. ### PR7 — Plugin slash commands + Stage 5 kind harness - `plugins/claude-code/commands/soul-join.md`: new slash command. Body: shell script that locates `demarkus-join` (download via plugin's existing install logic if missing — bump TOOLS_VERSION pin), then invokes `demarkus-join join `. - `plugins/claude-code/commands/soul-leave.md`: invokes `demarkus-join leave`. - `plugins/claude-code/commands/soul-refresh.md`: invokes `demarkus-join refresh`. - `plugins/claude-code/scripts/lib.sh`: bump `TOOLS_VERSION` per the plugin pin rule; add `demarkus-join` to the binaries downloaded. - `plugins/claude-code/.mcp.json`: no changes (the new MCP entries are added by `claude mcp add`, not the plugin's bundled config). - `deploy/kind/up.sh`: new Stage 5 path (`--with-soul-join` flag) that, after Stage 4 mint validation, also drives `tools/demarkus-join` from a debug pod to assert the install flow lands MCP entries. Dependency: requires mock-oauth2-server device code support (open question above). - **Scope:** ~200 lines plugin scaffolding + ~150 lines kind harness + tests. ~1 day PR. ### PR8 — Documentation - `docs/deployment/onboarding.md`: customer-facing user-onboarding doc. The user-experience narrative from this plan, formatted for end users + admins. - `docs/deployment/security-topology.md`: operator-facing topology doc. Public broker + private worlds pattern, recommended VPN/Tailscale integrations, the "world URLs never in unauthenticated responses" invariant, attack-surface analysis. - `/roadmap.md`: mark §6.4 user-install-flow as done; reference this plan. - `/index.md`: add link to this plan under Active Plans (move to Completed Plans on close). - **Scope:** content-only; ~500 lines markdown. Single-day PR. **Total: 8 PRs, ~10-12 working days end to end if done sequentially.** ## Out of Scope (explicit) - **Multi-broker support.** A user is in exactly one universe. If they want to switch, `/soul-leave` then `/soul-join `. Multi-broker requires plugin state for tracking N broker registrations + merging N world lists into one `.claude.json`; defer until a real customer asks. The `tools/demarkus-join` config schema (`broker.toml`) should be a single-broker shape; multi-broker is a schema migration when that day comes. - **Other coding agents.** Only Claude Code is in scope for this slice. Cursor / Aider / Continue plugins reuse `tools/demarkus-join` later — same binary, different per-agent shell wrapper around it. Track those as separate plans. - **Web UI on the broker.** The `/device` page is server-rendered HTML, intentionally minimal (one form, one button). No SPA. No admin UI, no per-user settings page. The broker stays a protocol surface. - **Self-service world discovery / signup.** A user cannot "discover" universes they're not invited to. They get the broker URL out-of-band (HR email, admin invite, etc.). - **Token rotation policies.** Refresh tokens have a TTL, no automatic rotation. If customer needs rotation (refresh-token-rotation per OAuth 2.1 best practice), it's a follow-up. - **Audit log surfacing in the plugin.** Broker logs every mint / refresh / revoke; users don't see this in Claude Code. Surfaced in admin tools (out of scope here). - **Offline mode.** Plugin assumes internet access during `soul-join` and refresh ticks. Air-gapped deployments would need a different shape (manual token import?) — defer. ## Constraints Summary - ✓ No protocol changes - ✓ No demarkus-server changes - ✓ All work in `tools/demarkus-broker/`, `tools/demarkus-join/` (new), `plugins/claude-code/`, `deploy/helm/demarkus-broker/`, `deploy/kind/` - ✓ Plugin remains a thin wrapper; logic lives in the binary - ✓ Agent-agnostic core (binary works for any agent that supports MCP + has its own config-edit CLI) ## Risks - **mock-oauth2-server device code support unverified.** If absent, kind end-to-end Stage 5 doesn't validate the device path. Mitigation: broker tests with a fake IdP in-process (httptest) cover the broker-side correctness; kind harness can validate `/me/install` + plugin slash command independently of device flow with a manually-issued bearer token. - **`claude mcp add` schema may change.** Claude Code's CLI surface is not a stable API contract from our perspective. Mitigation: pin to a minimum Claude Code version in the plugin's manifest; on `mcp add` failure, fall back to JSON patch with `jq` and emit a warning telling the user to upgrade Claude Code. - **Refresh token storage in plain-text file.** Industry-standard for CLI tools (gh CLI, aws CLI, gcloud all do this), but it's a real exposure. Mitigation: mode 0600, document the threat in `docs/deployment/security-topology.md`, optional-future-work to integrate keychain (macOS Keychain, Linux Secret Service, Windows Credential Manager) — note as backlog, not blocking. - **Background refresh requires the user to keep the plugin's renewal process running.** If the user only opens Claude Code once a week and tokens expire daily, every Monday they hit "tokens expired" until the plugin's startup-hook refresh kicks in. Mitigation: longer access token TTL in the broker's defaults (configurable, but default 7d is reasonable for a coding-agent workflow); refresh-on-startup hook in the plugin so the first Mon-morning request always works. - **Discovery doc cache invalidation.** If broker caches the IdP's discovery doc for a long TTL and IdP rotates JWKS, broker rejects valid tokens until cache refresh. Mitigation: 5-minute TTL on the discovery cache + on JWKS-key-not-found error, force-refresh once before failing. - **Browser auto-launch fails in some environments.** SSH-without-X-forwarding, headless CI, exotic terminals. Mitigation: print the URL prominently regardless of whether `open` succeeds; the auto-launch is a convenience, not a requirement. ## Success Criteria A non-trivial end-to-end test exists: 1. Customer setup: ApplicationSet templates 3 worlds with `publicURL` set; broker installed with values pointing at a real IdP (or mock-oauth2-server with device flow if PR7 risk panned out). 2. End user runs `/soul-join https://broker.test` → browser flow completes → 3 MCP entries appear in `.claude.json` → Claude Code restarts and `mark_fetch` against each world returns 200. 3. Wait past access-token TTL → background refresh tick fires → MCP entries' tokens are updated → Claude Code's next request succeeds without user intervention. 4. Admin removes one world from `worlds:` list → ApplicationSet syncs → next refresh tick removes the corresponding MCP entry from `.claude.json`. 5. Admin disables user's IdP account → next refresh fails with `invalid_grant` → plugin shows "your tokens are no longer accepted" + suggests `/soul-leave`. If the kind Stage 5 harness can't validate (4) and (5) end-to-end, those become integration tests in `tools/demarkus-join/` with a fake broker. ## Sequencing & Status Plan v1, 2026-05-15. Approved by Fritz directive: "no MVPs, no shortcuts" + "single-broker for now." Pending: - All 8 PRs unstarted. - Verify mock-oauth2-server device code support (PR7 risk). - Verify `claude mcp add` shape (PR6 risk). When picking this up cold: read this plan in full, fetch `/index.md` + `/patterns.md` + `/guidelines.md` per project preflight, fetch `/journal/2026-05-14.md` for the prior-day Stage 1-4 context that this plan continues from. Resume at PR1 unless you see merged commits suggesting otherwise — in which case, find the next-uncommitted PR by inspecting git log + this plan's checkboxes. ## Status — CLOSED (verified 2026-05-31) This supersedes the stale "Sequencing & Status" section above (frozen at Plan v1, "all 8 PRs unstarted" — the body was never re-edited after the strategy pivot, which was recorded in `/index.md` and the gateway plan instead). Ground truth: - **PR1-PR4** — shipped 2026-05-15 (#137/#138/#139): broker config, discovery, RFC 8628 device flow, refresh tokens. - **PR5** (`/me/install`) — shipped 2026-05-20 (#141). - **PR6** (`tools/demarkus-join` binary) — **CANCELED** 2026-05-20 in favor of the MCP Gateway. No `tools/demarkus-join` exists. - **PR7** (plugin command + kind stage) — **ABSORBED** into Gateway Slice 8: join ships as `/knowledge-join` (#152), with the smoke stage as `--with-mcp-smoke` in `deploy/kind/up.sh` (#151). - **PR8** (docs) — **partially absorbed** into the broker chart README + `tools/demarkus-broker/MCP-API.md`. Authoritative absorption record: `/plans/broker-https-gateway.md` (v7) and `/index.md`. The personal-soul `/soul-join` command never shipped by design — the flow pivoted to the organizational `/knowledge-join` shape; a future direct-QUIC `/soul-join` is a separate deferred plan, not unfinished work here. **Remaining: doc debt only** — the two standalone docs `docs/deployment/onboarding.md` and `docs/deployment/security-topology.md` were never written (functionally covered by the chart README + `MCP-API.md`). Low-priority; not blocking closure. ## Related documents - [Broker HTTPS gateway](/plans/broker-https-gateway.md): the authoritative absorption record for PR6 and PR7 - [PR3 broker device flow](/plans/universe-onboarding-pr3.md): the RFC 8628 sub-plan of this plan - [PR4 broker refresh tokens](/plans/universe-onboarding-pr4.md): the refresh-token sub-plan of this plan - [PR5 broker /me/install](/plans/universe-onboarding-pr5.md): the install-endpoint sub-plan of this plan