Skip to content

Sync Agent Connector — Design Brief

For: Implementation chat building the first Sync ↔ DTA integration From: Jonathan + co-founder design conversation, May 2026 Status: Design locked. Phase 1 (profile-consulting) ready for implementation. Phase 2 (agent-mediated gameplay) explicitly deferred per §16.

This brief is the consolidated output of a multi-session design conversation. It is meant to be the single source of truth for the connector implementation. Anything not in this brief either belongs in a separate brief, an existing ADR, or an open-question item in §15.


1. Context & goals

Sync is a Next.js + Supabase app that captures a user's judgment layer — how they reason through trade-off decisions — via gameplay rounds. Each round records a scenario, a chosen option, and (most importantly) a written rationale. Over time, twelve behavioral signals (ADR-024) compose into a profile and a "sync score."

There are two possible directions for an agent integration:

  1. Agent consults the profile — the user's digital twin agent (DTA) — Hermes today, Ironclaw / NemoCloud / others later — reads the user's judgment profile and uses it to embody the user's reasoning when handling real-world conversations and decisions. This is the unique value prop and the foundation of the Beacon thesis.
  2. Agent mediates gameplay — the user plays Sync through their agent (Telegram, Slack, etc.) instead of (or in addition to) the website.

Phase 1 (this brief) = direction #1 only. The agent is a profile consumer, not a profile producer. The user plays Sync on the website / mobile web; the agent benefits from the resulting judgment file.

Phase 2 = direction #2, deferred. Documented in §16 with full design captured, but explicitly out of scope for v1. Justification for deferral: the bulk of architectural complexity in earlier drafts existed to support agent-mediated gameplay (provenance, idempotency, kill switch, agent rate limiting). The unique value prop does not require it. We add gameplay mediation later only if user demand justifies it.

Net effect: smaller surface, sharper product story, faster path to a defensible benchmark.


2. Architectural decision: REST as substrate, MCP as a thin wrapper

We considered three options:

  • A. MCP-only — native to the agent ecosystem, but locks the data substrate to a moving spec, harder to debug, awkward to host alongside the existing Next.js app, and wrong-shape for non-agent clients (web dashboards, mobile, partner integrations).
  • B. REST-only — operationally mature in every dimension, reusable across client classes, but pushes too much onto hand-written skills.md that has to teach every new agent runtime how to call the API. Brittle across model families.
  • C. REST as substrate, MCP as a thin wrapper — REST is the source of truth; MCP is a small translation layer that maps tool calls to HTTP requests. Both surfaces derived from one typed data contract.

Decision: C. REST inside the existing Next.js app on Vercel, with a small MCP server wrapping it.

  • The data substrate is the moat. Coupling it to MCP — a still-evolving protocol that only one class of client speaks — is an architectural mistake when web/mobile/partner clients also need the data.
  • The MCP wrapper for a 2-3 endpoint surface is genuinely small (~150 LOC using @modelcontextprotocol/sdk).
  • Open ADR-038 (or next available) capturing this decision once implementation begins.

3. Data contract (typed source of truth)

Both REST and MCP derive from this. Define once in TypeScript with Zod validators in a new module src/lib/agent/contract.ts.

3.1 Schema reality check (May 2026)

The judgment layer is not a flat list of numeric signals. It lives on public.player_decision_profiles with rich typed structures per signal — reasoning_style_distribution, attention_distribution, confidence_calibration, timing_signature, persuadability, peer_reading, dissent_profile, authenticity, learning_rate, occasion_noise, self_awareness, name_bias_signal, plus category_patterns, context_rules, option_preferences. See src/types/database.ts PlayerDecisionProfile for the canonical shapes.

The agent-facing Profile is a distilled view of this richer structure — not a wire-format copy of it. Most agents don't need (and shouldn't see) the full nested distributions. The agent gets a stable, summarized projection.

Themes (the canonical settings/genres) and categories (the decision domains) are also distinct columns: - Theme (scenarios.theme): boardroom | frontier | rebuild | court | underground | custom — surface flavor. - Category (scenarios.category): governance | values-culture | resource-allocation | team-dynamics — decision domain. - "Personal" is a new theme, not a new category. Personalized scenarios are still classified into one of the existing decision-domain categories.

3.2 Wire types

// What the agent reads to embody the user's judgment.
// This is a distilled projection of player_decision_profiles, not a 1:1 copy.
export type AgentProfile = {
  user_id: string;
  sync_score: number;                 // from profiles.sync_score
  games_played: number;               // from player_decision_profiles.games_analyzed
  model_confidence: number;           // 0-1, how reliable is the profile
  signals: AgentSignalSummary[];      // distilled from the 12-signal structures
  category_tendencies: CategoryTendency[];  // distilled from category_patterns
  context_rules: AgentContextRule[];        // distilled from context_rules
  summary_text: string | null;        // LLM-generated prose summary
  summary_generated_at: string | null;
  summary_model_version: string | null;
  agent_context_block: string | null; // pre-composed system prompt block
  schema_version: "v1";
};

export type AgentSignalSummary = {
  signal: string;                     // "self_awareness", "confidence_calibration", etc.
  description: string;                // human-readable description of the signal
  state: "rich" | "developing" | "sparse"; // how much data backs it
  one_line: string;                   // LLM-friendly observation, e.g. "Tends to under-confidence on team-dynamics decisions"
};

export type CategoryTendency = {
  category: "governance" | "values-culture" | "resource-allocation" | "team-dynamics";
  primary_driver: string;             // e.g. "team_harmony", "pragmatism"
  secondary_drivers: string[];        // sorted by weight
  risk_tolerance: "low" | "medium" | "high";
  decision_speed: "deliberate" | "balanced" | "quick";
  games_in_category: number;
  recent_trend: string | null;
};

export type AgentContextRule = {
  trigger: string;                    // e.g. "crisis_mentioned", "team_conflict"
  behavior_shift: string;             // e.g. "shifts toward caution"
  confidence: number;                 // 0-1
};

// Optional second endpoint — recent decisions for richer context
export type RecentDecision = {
  scenario_id: string;
  scenario_title: string;             // not full description; agent gets a label
  category: string;
  theme: string;
  chosen_label: string;               // 1-line label of chosen option
  rationale: string | null;           // user's actual rationale (may be null on older rows)
  recorded_at: string;
};

Two structural choices preserved:

  • Profile is read-only over the wire. No mutation endpoints. Structural — there is no agent-side write path in Phase 1 because agents do not submit decisions.
  • agent_context_block is composed server-side. Sync owns the formatting. When we improve the composition, every agent benefits without redistributing skills.md.

Distillation policy: the projection from PlayerDecisionProfile to AgentProfile lives in one place (src/lib/agent/distill.ts). Changes to internal signal shapes don't break the wire format unless we explicitly bump schema_version.

Cold start semantics: - User with no rows in player_decision_profiles: model_confidence: 0, signals: [], category_tendencies: [], summary_text: null, agent_context_block: null. - Agents treat summary_text: null as "profile not yet trained — encourage user to play." - skills.md (§7) explicitly handles this case.


4. REST API surface

Inside the existing Next.js app, under /api/agent/v1/. Auth via long-lived API keys in Authorization: Bearer <key>.

Why long-lived keys, not session JWTs: Supabase JWTs expire on the order of an hour. An agent running on the user's laptop for weeks/months can't be re-authenticating constantly. Long-lived API keys are the standard pattern for this — minted by the user from a settings page, scoped to agent-read-only, listable, revocable.

Key model: - New table agent_api_keys (id, user_id FK, key_hash, name, last_used_at, created_at, revoked_at). - Settings page lets the user mint a new key, see existing keys (last 4 chars + last_used_at), and revoke any key. - Keys are SHA-256 hashed at rest; the plaintext key is shown to the user once at mint time and never again. - Each request looks up the key by hash, joins to user, enforces user-scoped data access.

Validation: every request validates the key is non-revoked, updates last_used_at, and resolves to the owning user before any handler runs. Standard middleware pattern.

Method Path Purpose
GET /api/agent/v1/profile Read the user's current profile (with summary + context block).
GET /api/agent/v1/profile/recent-decisions?limit=N Optional: read recent decisions for richer context. Limit 1-20.

Explicitly excluded from Phase 1: - POST /decision — agents do not submit decisions. - GET /scenario/next — agents do not fetch scenarios. - Any mutation endpoint. - Multi-user / team endpoints.

Rate limiting: per-token, generous (e.g., 600 reads/hour). Reads don't mutate state, so abuse risk is low.

Error vocabulary: - 400 invalid_request — schema/parameter violation. - 401 unauthorized — bad/expired token. - 403 profile_access_disabled — user has revoked agent access (see §11 kill switch). - 404 not_found — profile doesn't exist (shouldn't happen for authed users). - 429 rate_limited — too many reads in window. Returns retry_after_seconds. - 500 internal_error — generic.

Errors return { code, message, retry_after_seconds? }.


5. MCP wrapper

A small server exposing the REST API as MCP tools. Transport: streamable HTTP, not stdio. Stdio limits to local agents; streamable HTTP works for Hermes-on-laptop and Ironclaw-in-cloud uniformly.

Tool surface (1:1 with REST):

  • sync.get_profile()Profile
  • sync.get_recent_decisions({ limit })RecentDecision[]

Auth: pass the user's bearer token through. Do not reimplement auth in the wrapper.

Validation: validate against the same Zod schemas the REST layer uses. Don't drift the validators.

Hosting: prefer co-located with the Next.js app on Vercel if streamable HTTP support is stable enough at build time; otherwise a small Fly / Railway service. Decision deferred to implementation; flag in §15.


6. skills.md (agent-orientation layer)

Distinct from the protocol layer. MCP tells the agent how to call; skills.md tells the agent when and why to call, and how to integrate the result into the conversation.

Lives in the agent's runtime (Hermes' skills directory, Claude Code's ~/.claude/skills/, etc.).

Distribution mechanism (v1): a docs page on the Sync site (e.g., /docs/agent-skill) hosts the canonical skills.md content with copy-paste install instructions per agent runtime. Versioned filename (e.g., sync-skill-v1.md) so future updates don't silently break existing installs. The user copies the file into their agent's skills directory and restarts the agent.

Why this and not auto-distribution: users should be able to inspect, customize, and remove the skill. Auto-pushing updates would also be invasive across runtimes we don't control. A docs page + copy-paste is the lowest-friction path that respects user agency.

Pre-implementation verification (Day 1 task): confirm Hermes's actual skill file format and load mechanism by reading its docs / examples before writing v1. We've assumed Hermes loads markdown skills the same way Claude Code does; if the format differs, adjust accordingly.

The non-negotiable rule (Phase 1):

You are the user's digital twin agent. You have access to their Sync judgment profile via the sync.get_profile() tool. You consult this profile to embody their reasoning in real-world decisions and conversations. You do not play Sync on the user's behalf. You do not submit decisions, fetch scenarios, or mediate gameplay through this skill. If the user wants to play Sync, direct them to the website. The judgment layer is built there; you benefit from it here.

What skills.md teaches the agent to do:

  • When to consult the profile. When the user is wrestling with a decision in conversation, when drafting something on their behalf, when asked "what would I do in X situation," when the conversation involves trade-offs that the profile likely has signal on.
  • How to consume the profile. Prefer agent_context_block (pre-composed prose) for system prompt injection. Use structured signals only when reasoning programmatically.
  • What to do at cold start. When summary_text is null, do not pretend to know the user's judgment — instead, say so plainly and suggest they play a few Sync rounds to train the profile.
  • Transparency. When using the profile to shape a recommendation, be willing to say "based on your Sync profile, you tend to..." — don't pretend the suggestion came from nowhere.
  • What not to do. Don't try to play Sync. Don't reach for tools that don't exist. Don't fabricate signals or invent profile content.

skills.md should be short and load-bearing — not a manual. The protocol carries the mechanics; the skill carries the orientation.


7. Personal context — the editable text field

A single text field, owned by the user, that powers two distinct uses:

  1. Eval harness input (§9) — fed to baseline / treatment agents so the comparison against Sync is honest.
  2. Scenario personalization (§8) — fed to the scenario authoring pipeline to generate scenarios themed to the user's actual world.

Schema: - users.personal_context: text — user-owned, editable, default "". No upload pipeline, no encrypted blob storage, no normalizer. Just a text field. - The user pastes their AI memory export, writes their own context, or leaves it blank.

Right to deletion: delete = empty the field. Done. If signals were ever shaped by the field's content, those signals stay (they're aggregate / non-recoverable from the source). Document this asymmetry in the consent surface.

Eval reproducibility: snapshot at prediction time per §9.3 (evaluation_predictions.context_snapshot).

Onboarding integration:

  • Not part of required onboarding. Mobile keyboard friction (and the difficulty of getting AI memory out of a mobile app) would hurt completion.
  • Post-onboarding lightbox on the homepage. Triggers once, immediately after the user completes onboarding. Copy emphasizes pasting from their main LLM's memory: "Sync can personalize scenarios to your actual life. Paste your AI memory (from ChatGPT, Claude, etc.) so we can theme rounds to your world."
  • CTAs: "Add now" → settings page with the field focused / "Skip" → dismiss persistently.
  • One-shot. Skipping marks the user as having seen the prompt; the lightbox does not reappear on subsequent visits.
  • Locked "Personal" category card (§8) is the secondary conversion point. Users who skip the lightbox encounter the greyed card later when picking a quick-round category, with the same prompt to fill in context.
  • Settings page input design. Single large textarea, visible affordance for "paste your AI memory export here," autosave on blur, no validation, no required fields. Same target on mobile and desktop; users will mostly fill in on desktop where copy-paste from another app is easier.

Profile-wide use, not agent-specific. Personal context benefits the website too (since personalized scenarios are a website feature in Phase 1).


8. Personalized scenarios — "Personal" as a quick-rounds theme

A new theme (alongside boardroom | frontier | rebuild | court | underground | custom) in the quick-rounds picker. Personalized scenarios are quick-rounds only — campaigns remain canonical to preserve narrative continuity.

Theme vs category distinction: theme is the surface flavor (setting/genre); category is the decision domain (governance, values-culture, resource-allocation, team-dynamics). "Personal" is a theme — personalized scenarios still classify into one of the existing decision-domain categories so they roll up into normal signal coverage.

8.1 UI states

  • personal_context empty: card is greyed out / disabled. On hover or tap: "Fill in your personal context to unlock scenarios about your life. [Add context →]" with link to the settings field.
  • personal_context filled: card is active, clickable. Optional micro-text: "Last updated: 3 days ago" so the user knows what context will be used.
  • Footnote on personalized scenarios: "Personalized using your profile context. [Edit →]" — lightweight transparency.

8.2 Generation flow

When the user clicks "Personal":

  1. Server reads users.personal_context. If empty, return 400 (defensive — UI should prevent).
  2. Server picks a target decision domain — calibration, trust, resource_allocation, etc. Default selection rule: rotate across domains weighted by which of the user's 12 signals has the least data, so personal rounds also serve signal balance.
  3. Call the scenario-generation LLM with a structured prompt:
  4. Target domain.
  5. User's personal_context.
  6. Constraint scaffold (ADR-037) — explicit guardrails, real trade-offs, mandatory none-of-the-above option (ADR-017), content guardrails (ADR-027).
  7. Validate the generated scenario (soft quality check):
  8. At least 3 options + none-of-the-above.
  9. Each option has label and description.
  10. Constraints are non-empty.
  11. If validation fails, regenerate once. If still fails, fall back to a canonical scenario from the same domain and log the failure.
  12. Persist with theme: "personal", personalized_for_user_id, personalization_inputs_hash, and category set to the chosen decision domain.
  13. Return through the normal scenario API.

8.3 Design rules confirmed in design

  • System picks the domain, not the user. Keeps it simple, lets domain rotation serve signal balance. Sub-picker for domain control can be added later if requested.
  • No inline display of personal_context per scenario. Just a footnote linking to settings.
  • Personalization is contextual flavor, not narrative steering. Same trade-off mechanics as canonical scenarios. Only the surface (settings, characters, vocabulary) personalizes. The structure stays canonical.

8.4 Persistence and replay

  • Each click on "Personal" generates a fresh scenario.
  • Once generated, persisted — user can complete the round if they refresh or close.
  • After completion, recorded like any other decision with personalized_for_user_id set.
  • No deduplication or re-use across users.

8.5 Rate limiting

Each personal round costs an LLM call. Apply a daily cap per user (default: 20 personal rounds/day). Canonical rounds remain unlimited. Tunable via config.

8.6 Quality safeguards

  • Constraint scaffold in the prompt is the primary safeguard.
  • Soft quality check before serving (above).
  • Future: LLM-as-judge pass if quality issues surface. Not in v1.

9. Shadow-evaluation harness

The always-on benchmark for the judgment layer. Runs as a parallel, observational system. Never tampers with profiles.

9.1 Mechanism (per real decision submission, regardless of source)

  1. Snapshot the user's profile state before applying this round's update. Persist to evaluation_snapshots keyed by decision ID.
  2. Asynchronously run prediction agents on the scenario, each conditioned on a different context bundle (§9.2).
  3. Log each prediction (choice + rationale + model + condition + context_snapshot) to evaluation_predictions.
  4. Continue the live decision recording exactly as before. The user's profile builds normally; nothing is held back or filtered.

9.2 Three-condition design

The honest baseline is not "no context" — it's a modern LLM with the user's existing AI memory. Therefore:

Condition Context bundle
A: Personal context only User's personal_context field. No Sync profile.
B: Sync judgment only Sync profile snapshot. No personal context.
C: Personal context + Sync judgment Both.

The interesting comparisons:

  • A vs C — does Sync add value beyond what the user's AI already knows? This is the claim that matters.
  • B vs A — is Sync alone competitive with personal context alone? Useful for positioning.
  • B vs C — additive, or does one dominate?

9.3 Eval-model logging discipline

Each prediction logs: - eval_model (e.g., "gemini-flash-2.0") - eval_model_version (specific version string) - context_snapshot (the personal_context as it was at prediction time) - profile_snapshot_id (FK to evaluation_snapshots)

Trend integrity rules: - Pick one stable eval model and hold it. Don't switch mid-run unless explicitly running a model-comparison study. - When you must switch, re-run baselines on the new model on the same scenario set. - Optionally (later): run two eval models in parallel for findings that generalize across models.

9.4 Scoring

  • Choice agreement — top-1 accuracy.
  • Rationale similarity — embedding cosine + LLM-as-judge for substantive overlap.
  • Calibration — for predictions with probability distributions, did the user's actual choice rank where expected?

Internal dashboard surfaces: - Rolling agreement rate per condition. - Per-domain breakdown. - Per-user gap to test-retest ceiling. - Personalized vs. canonical breakdown — does personalization actually produce richer signal, or just more engagement? - Per-eval-model breakdown — needed for trend integrity.

9.5 Cost discipline

Three conditions × LLM calls × N users × M rounds is real money on paid inference.

  • Sample at 1-in-N (config knob, default 1-in-5) when paid.
  • Always evaluate rounds where the user's choice surprised the live model (high-information events).
  • Skip eval on test accounts unless explicitly enabled.

9.6 Pattern 2 (true holdout) — separate, occasional

For external claims (pitch decks, partner conversations), run periodic opt-in holdout studies: freeze the last K rounds of N consenting users, train profile to the cutoff, predict the held-out rounds, write up the result. Not always-on. Not in v1.


10. Operational concerns

10.1 Kill switch

Admin-controlled flag at the user and global level: agent_profile_access_enabled. Lives in a feature-flags / config table. Same pattern as decay_enabled from migration 007.

When disabled: API returns 403 profile_access_disabled. skills.md teaches the agent to surface this gracefully.

10.2 Rate limiting

  • GET /profile: 600 reads/hour per token.
  • GET /profile/recent-decisions: 200 reads/hour per token.
  • Personalized scenario generation: 20/day per user.

All tunable via config. Counters in Redis or Supabase row-level state.

10.3 Logging

  • All /api/agent/v1/* requests logged with: user_id, token prefix (not full token), request path, response code, response time, MCP-or-direct flag.
  • Separate "agent wire log" table or tag — high enough volume that it shouldn't pollute general app logs.
  • Correlation IDs from MCP requests preserved through to the underlying REST call.

10.4 Profile summary regeneration

  • Generated by an LLM pass at profile-update time, not at API-request time (avoid latency on agent calls).
  • Stored in profiles.summary_text, profiles.summary_generated_at, profiles.summary_model_version.
  • Regenerated when any signal moves by ≥0.05 since last summary, with a debounce of 10 minutes — at most one regeneration per user per 10-minute window, even if multiple eligible signal changes occur. Prevents runaway LLM cost during active play sessions.
  • Same LLM also generates agent_context_block.

11. Migrations

Confirmed against actual schema (May 2026). Migrations file format is migrations/NNN_description.sql with BEGIN; ... COMMIT; transaction wrapper. Latest existing is 075_scenario_constraint_audit.sql.

Migrations to write (one file per logical concern, numbered 076+):

076_agent_personal_context.sql - ALTER TABLE profiles ADD COLUMN personal_context text NOT NULL DEFAULT '' - ALTER TABLE profiles ADD COLUMN onboarding_lightbox_dismissed_at timestamptz NULL — tracks the post-onboarding lightbox one-shot dismissal.

077_agent_profile_summary.sql - ALTER TABLE player_decision_profiles ADD COLUMN summary_text text NULL - ALTER TABLE player_decision_profiles ADD COLUMN summary_generated_at timestamptz NULL - ALTER TABLE player_decision_profiles ADD COLUMN summary_model_version text NULL - ALTER TABLE player_decision_profiles ADD COLUMN agent_context_block text NULL - Note: summary lives on player_decision_profiles (the actual judgment layer), not profiles (user metadata).

078_agent_personal_theme.sql - Extend ScenarioThemeId to include 'personal' (TypeScript enum update; SQL is just ALTER TABLE scenarios if there's a CHECK constraint to relax — verify). - ALTER TABLE scenarios ADD COLUMN personalized_for_user_id uuid NULL REFERENCES auth.users(id) ON DELETE CASCADE - ALTER TABLE scenarios ADD COLUMN personalization_inputs_hash text NULL - ALTER TABLE scenarios ADD COLUMN target_domain text NULL — only if not already used; otherwise reuse category. - Index on personalized_for_user_id for cleanup / per-user queries.

079_agent_api_keys.sql - New table agent_api_keys (id uuid PK, user_id uuid FK, key_hash text NOT NULL UNIQUE, name text NOT NULL, last_used_at timestamptz NULL, created_at timestamptz DEFAULT now(), revoked_at timestamptz NULL) - RLS: users can only see/insert/revoke their own keys. - Function validate_agent_api_key(key_hash text) — returns user_id if valid, NULL otherwise. SECURITY INVOKER, called by API middleware after middleware-side hash.

080_agent_feature_flags.sql - New table agent_feature_flags (key text PK, value jsonb NOT NULL, updated_at timestamptz DEFAULT now()) - Seed agent_profile_access_enabled = {"global": true, "user_overrides": {}} for the kill switch.

081_agent_eval_harness.sql - New table evaluation_snapshots — id uuid PK, choice_id uuid FK to choices, user_id uuid FK, profile_state jsonb (frozen PlayerDecisionProfile at time of choice), personal_context_snapshot text, created_at timestamptz. - New table evaluation_predictions — id uuid PK, snapshot_id FK, condition text CHECK in ('A','B','C'), predicted_option_id text NULL, predicted_rationale text NULL, eval_model text NOT NULL, eval_model_version text NOT NULL, choice_agreement boolean NULL (computed post-hoc), rationale_similarity numeric NULL, created_at timestamptz. - RLS: only service role and the snapshot's owner can read. - Indexes: (user_id, created_at DESC), (choice_id), (condition, created_at DESC).

082_agent_security_hardening.sql (optional, separate cleanup) - ALTER TABLE scenario_character_names ENABLE ROW LEVEL SECURITY (no policies = anon blocked, service role bypasses). - This is the one-line fix for the existing RLS advisory, included as part of the connector migration set since we touch security posture anyway. Skippable if Jonathan prefers a separate cleanup.

Pattern across all migrations: - Wrapped in BEGIN; ... COMMIT;. - Idempotent where possible (IF NOT EXISTS, ON CONFLICT DO NOTHING). - Includes RLS policies in the same migration that creates the table. - Comments explain why each table/column exists (load-bearing for future archeology).


12. Testing plan

  • Unit: Zod validators for every type. Profile composition logic. Cold-start handling. Error vocabulary mapping.
  • Integration: Mock MCP client exercising every tool. Mock Hermes runtime. Snapshot-then-update path with synthetic decisions.
  • End-to-end: Staging Sync + real Hermes-on-Gemini. Jonathan installs skills.md, Hermes consults his profile in a real conversation, behavior changes observably.
  • Eval harness validation: Synthetic users with known preferences. Verify with-profile predictions beat without-profile on held-out scenarios. If they don't, the harness is broken before any real users touch it.
  • Adversarial spot-checks: Profile read with expired token (expect 401). Profile read for disabled user (expect 403). Empty personal_context generating personalized scenario (expect 400). Quality-check failure path on scenario generation.

13. Implementation sequencing

Ordered by dependency:

  1. Schema survey (list_tables) — confirm column names and avoid migration collisions.
  2. Hermes skill format check — read Hermes docs / examples to confirm skill file format and load mechanism before writing skills.md.
  3. Define the data contract — TypeScript types + Zod validators in a shared module.
  4. Migrations — per §11.
  5. Agent API key minting + settings UI — table + middleware + settings page to create / list / revoke keys.
  6. REST endpointsGET /profile first (most load-bearing), then GET /profile/recent-decisions.
  7. Profile summary regeneration — LLM pipeline that populates summary_text and agent_context_block on signal change, with the 10-minute debounce.
  8. Personal context field UI — settings page textarea, autosave, paste-friendly.
  9. Post-onboarding lightbox — homepage prompt with Add now / Skip CTAs.
  10. MCP wrapper — thin translation over REST. Same validators.
  11. skills.md v1 + docs page — copy-paste install instructions for Hermes; versioned filename.
  12. Personalized scenarios — "Personal" category, gated UI, generation flow with constraint scaffold.
  13. Shadow eval harness — Condition B first (Sync profile only). Validates snapshot path end-to-end.
  14. Shadow eval harness — Conditions A and C (now that personal_context exists, run all three).
  15. Internal dashboard — agreement rates, per-domain, per-eval-model, personalized-vs-canonical, gap-to-ceiling.
  16. First end-to-end test — Jonathan asks Hermes a real-world decision question. With skills.md installed and sync.get_profile() available, Hermes's response references his Sync judgment patterns specifically (e.g., "based on your profile you tend to weight long-term over short-term — does that apply here?"). Without skills.md or with profile access disabled, the response is generic. Difference observable in chat output.

14. Relationship to existing ADRs

  • ADR-024 (12 behavioral signals) — defines what the signals array contains.
  • ADR-027 (scenario content guardrails) — generated personalized scenarios must obey these.
  • ADR-037 (constraint discipline in scenario authoring) — generated scenarios must include explicit guardrails and real trade-offs.
  • ADR-017 (none-of-the-above) — generated scenarios must include this option.
  • ADR-007 (privacy model) — must be checked against personal_context handling, including delete-by-emptying semantics.
  • ADR-020, ADR-026 (campaign arcs / reactive campaigns) — referenced for the decision to scope personalization to quick rounds only in v1.
  • ADR-029 (chapter synopsis dual-source / scenario rewrite audit) — relevant context for why generated scenarios need a constraint scaffold rather than free-form authoring.
  • New ADR-038 (or next available) — open at implementation start to capture the REST + MCP architectural decision.

15. Open questions (flag during implementation)

  • MCP transport hosting. Streamable HTTP support on Vercel stable enough as of build time, or separate Fly / Railway service? Decide based on actual SDK behavior.
  • Test-retest reliability ceiling. Schedule a small ceiling study (10 users, 5 reruns each) before publishing any agreement-rate numbers externally.
  • Rationale similarity scoring. Embedding cosine + LLM-as-judge weighted; tune in dashboard layer.
  • Personalization aggressiveness. Full reskin per user vs. thematic anchoring with canonical settings recognizable? Implementation default: full reskin within the constraint scaffold.
  • Personalized scenarios in multiplayer. Excluded by category scoping (personalized = quick rounds only) — confirm no multiplayer leak vectors exist.
  • Sampling rate for paid inference. Default 1-in-5 placeholder. Calibrate when paid provider is in the loop.
  • Score-gaming mitigations. Deferred until score has external value (e.g., gating, governance). Not relevant in early adopter phase.
  • Onboarding lightbox copy. UX detail to resolve during implementation. Brief specifies the trigger (post-onboarding, homepage, one-shot), the two CTAs, and the emphasis on pasting from LLM memory; exact wording is the implementer's call.
  • Quick-setup question path (potential enhancement, not v1). If the lightbox + locked-card combo produces low conversion in practice — particularly on mobile where paste is hard — consider adding a 4-question fallback ("What do you work on?" / "What kinds of decisions come up?" / "What domains interest you?" / "Anything else?") that produces a usable personal_context paragraph from short tap-and-type answers. Decide based on observed conversion data, not a priori.
  • ADR-038. Open at implementation start. Reference this brief.

16. Phase 2 — agent-mediated gameplay (deferred)

Documented here so the design isn't lost. Do not build in Phase 1.

If/when user demand justifies, Phase 2 adds:

  • POST /scenario/:id/decision — agent submits a decision on the user's behalf.
  • GET /scenario/next — agent fetches a scenario.
  • decisions.submitted_via: text'web' / 'agent_telegram' / 'agent_other' (engagement metadata).
  • decisions.idempotency_key: text — agents retry; need de-dup.
  • agent_submissions_enabled feature flag (kill switch for write path).
  • skills.md additions: how to present scenarios faithfully, how to capture rationale verbatim.

Non-negotiable rule even in Phase 2: the agent does not draft the rationale. It can ask clarifying questions but the words submitted must be the user's words. The agent is a UI for Sync, not a player. This rule prevents signal contamination.

Why deferred: - Most architectural complexity in earlier brief drafts existed to support this. Removing it shrinks Phase 1 surface dramatically. - The unique value prop (judgment layer in real-world contexts) does not require it. - Mobile web is already accessible for casual play. - We add gameplay mediation later only if demand justifies.


17. What this brief explicitly does not decide

  • Which agent runtimes get first-class skills.md treatment beyond Hermes.
  • The pricing / freemium gating model.
  • Whether to open-source the MCP wrapper or skills.md.
  • Multiplayer-via-agents protocol semantics.
  • The "team brain" enterprise integration shape.
  • Profile augmentation (merging personal_context into profile output) — explicitly out of scope; positioning is "complement, not absorb."
  • Phase 2 timing or trigger conditions.

These belong in their own briefs / decisions. Do not let them creep into the Phase 1 connector implementation.