Back to projects
Active Started May 2026

AI User Personas

Local-first workspace to create and version structured user personas — goals, frustrations, evidence with provenance, and a synthetic-vs-real confidence meter.

Next.js 16 React 19 TypeScript Tailwind v4

The Problem

The stated problem is persona quality: teams can’t tell a grounded persona from a synthetic one, so personas drift from evidence and become marketing fiction nobody trusts. That’s the product surface.

The actual question this repo was built to answer sits one level down: how far can a fleet of coding agents get building a working product from a minimal brief, with the humans mostly out of the loop? The initial commit is literally titled “two-agent dogfood” — Claude Code and Codex were split into UI owner and architecture/data owner and pointed at Agent Rally Point, a coordination ledger, with almost no spec beyond an ownership split and a schema shape. The persona generator is what that coordination produced; the coordination protocol itself was the thing under test.

Approach

Two agents, one repo, no human in the merge path beyond starting sessions and reading handoffs. AGENTS.md and CLAUDE.md define the split: Codex owns the domain model, schema, persistence boundary, and API contracts (docs/architecture.md, docs/data-contracts.md, schemas/); Claude owns the UI shell, screens, and component behavior (app/, src/, components/, styles/). Coordination runs through Rally primitives — start, preflight, inbox, hook before-write --auto-claim, handoff, ack, stop — so each agent claims paths before writing and hands off contract changes instead of touching the other’s territory directly.

That two-agent run produced a working v1 (persona CRUD, JSON Schema validation, local file persistence) and a session-level feedback log (src/notes/RALLY_FEEDBACK.md) itemizing exactly where the coordination protocol helped and where it broke down. The experiment then scaled: a persona-lab plugin was built to turn “spin up personas to test UI” into a repeatable CLI/skill/agent workflow, and on 2026-06-09 the team ran a 10-agent Haiku-tier persona panel through rally-workflows — ten independent coding-agent sessions reviewing this same app’s own evidence model concurrently, coordinating writes through the same Rally ledger the two-agent run used. What was deliberately not built in this phase: no direct paid-model API calls from the app itself (docs/plans/persona-review-capability.md states this explicitly), and no seeded/fixture data standing in for real app state — empty JSON means an empty UI, not a demo.

What I Built

AI User Personas — persona list with cards for Dee Carter (support), Priya Shah (design systems), and Morgan Lee (RevOps), each showing role, summary, primary goal, a confidence meter, and tags ::border

A local-first workspace for creating, editing, filtering, and archiving structured user personas. Each persona is a validated record — archetype, role, summary, goals, frustrations, motivations, behaviors, needs, use-case scenarios, tags, and a draft/active/archived status — backed by a JSON Schema as the canonical domain model. A second surface, /councils, runs structured persona-panel reviews against the app (or any target repo) and stores rosters, runs, findings, and synthesis as the same kind of validated JSON record.

Evidence and Confidence

AI User Personas — persona detail for Morgan Lee with goals, frustrations, motivations, behaviors, needs, channels, a scenario, and a synthetic evidence item flagged at 45% confidence ::border

The differentiator is evidence provenance. Every persona carries evidence items tagged by source — interview, survey, analytics, or synthetic — and a ConfidenceMeter surfaces how much of the persona rests on real data versus assumption. A persona seeded from a product thesis reads as 45% confidence with its evidence flagged synthetic, so no one mistakes a hypothesis for a finding.

Architecture

The app splits into two connected pipelines behind one JSON Schema domain model: a persona-CRUD path (human-authored or locally-generated personas, stored as file-backed JSON) and a persona-council path (a fleet of coding-agent subagents reviewing a target artifact and writing findings back into the same store). Both paths share the PersonaRepository / CouncilRepository boundary defined in docs/architecture.md, and neither talks to a hosted database — this is explicitly local-first, with SQLite/Postgres deferred until local JSON “becomes a real constraint.”

flowchart LR
  U["User instruction"] --> API["POST /api/personas/generate"]
  API --> GEN["persona-generate.server.ts"]
  GEN -->|execFile| HARNESS["RossLabs Agent Harness CLI<br/>(local binary, no API key)"]
  HARNESS --> OLLAMA["Ollama: qwen2.5-coder:7b"]
  OLLAMA --> GEN
  GEN -.harness missing / unparsable.-> SCAFFOLD["Deterministic scaffold<br/>(no LLM, regex lens match)"]
  GEN --> DRAFT["Draft persona<br/>provenance=synthetic-assumed"]
  SCAFFOLD --> DRAFT
  DRAFT --> REPO["PersonaRepository"]
  REPO --> PJSON[("data/personas.json")]
  PJSON --> UI["Persona list / detail / editor"]

  RQ["Council review request"] --> CREPO["CouncilRepository:<br/>roster + run"]
  CREPO --> PLAN["persona-plan-adapter.ts"]
  PLAN --> RALLY["Agent Rally Point<br/>claims / handoffs / DAG"]
  RALLY --> FAN["rally-workflows fan-out:<br/>N concurrent Haiku-tier subagents"]
  FAN --> FIND["Per-persona findings JSON"]
  FIND --> SYN["Synthesis<br/>(orchestrating session)"]
  SYN --> RJSON[("data/council-runs.json")]
  RJSON --> CUI["/councils UI"]

Models — which, where, why:

StageModel / tierWhere it runsWhy there
Persona auto-generationOllama qwen2.5-coder:7b (local; overridable via HARNESS_MODEL)src/lib/persona-generate.server.ts, called through the RossLabs Agent Harness CLI (harness complete --schema)No API key, no cloud call — a small local model filling a deliberately bare JSON schema is enough for a synthetic draft, and the result is force-tagged provenance: synthetic-assumed regardless of output quality
Generation fallbackNone — deterministic scaffoldscaffoldFromInstruction(), same fileRuns when the harness binary is missing or its output fails to parse; regex keyword matching against the instruction (security/buyer/novice/power/a11y/developer) picks a lens so the generate action never hard-fails
Persona-council reviewClaude, tiered haiku | sonnet | opus | frontier | human (ModelTier, src/lib/council.ts); the one executed scale run used Haiku-tier agentsOutside the Next.js process entirely — Claude Code / Codex host subagents launched via rally-workflows, not an SDK call from the appThe app owns the roster/run/assignment/finding contract and validates + stores results; it does not hold a model API key. docs/plans/persona-review-capability.md states direct paid model calls from the app are out of scope for P0/P1
Base persona CRUDNone/api/personas, PersonaRepositoryDeterministic read/write/validate against schemas/persona.schema.json

Tools & infra — which, why:

  • Next.js 16 App Router + React 19 + TypeScript — UI shell and API routes (/api/personas/*, /api/councils/*).
  • Tailwind v4 — styling.
  • JSON Schema (schemas/persona.schema.json v1.1, persona-roster.schema.json, persona-review-run.schema.json) — canonical domain model; every write is validated against it before it lands.
  • Local file-backed JSON (data/personas.json, data/council-rosters.json, data/council-runs.json) — chosen over SQLite/Drizzle/Neon “until the local JSON adapter becomes a real constraint” (docs/architecture.md); writes go through a temp-file-plus-rename and reject stale updated_at transitions.
  • RossLabs Agent Harness — a separate local CLI binary (~/dev/git-folder/rosslabs-agent-harness) that wraps Ollama for the app’s one generation call; the app shells out to it via execFile, it isn’t a library dependency.
  • Agent Rally Point (Rally) — the actual subject under test: a coordination ledger (claims, handoffs, acks, preflight, DAG replay) that ran the original two-agent Claude/Codex dogfood and later the 10-agent Haiku scale test.
  • persona-lab plugin — a separate CLI (persona) plus a Claude Code/Codex plugin (skill, slash command, persona-panel-orchestrator and persona-perspective-reviewer agents) backing a global, cross-repo persona library at ~/.persona-lab/.
  • GitHub — source hosting (github.com/tyroneross/ai-user-personas). No live deployment: docs/deployment.md states the app is local/persistent-filesystem only until a durable storage adapter replaces local JSON, so it isn’t run as a serverless or immutable production service.

How it works

  1. A user submits a free-text instruction on /personas/new, which posts to POST /api/personas/generate.
  2. generatePersona() shells out to the RossLabs Agent Harness CLI with a bare JSON Schema and a system prompt instructing a single-JSON-object, no-prose response.
  3. The harness calls Ollama’s local qwen2.5-coder:7b; the response is stripped of markdown fences, parsed, and folded into a PersonaCreateInput with provenance forced to synthetic-assumed and confidence forced to 0.4 regardless of what the model returned.
  4. If the harness binary is missing, times out, or returns unparsable output, scaffoldFromInstruction() runs instead — a zero-LLM regex match against the instruction text picks an archetype/role lens (security, buyer, novice, power user, accessibility, developer, or a generic fallback) and returns an editable stub. The API route never hard-fails; it degrades to a scaffold and reports why in the response.
  5. The draft is returned to the form, edited by a human, and saved through POST /api/personas, which validates it against schemas/persona.schema.json and writes it into data/personas.json via PersonaRepository (temp-file write, atomic rename).
  6. /, /personas/[id], and /personas/[id]/edit read that same file through the repository boundary; archiving sets status: "archived" rather than deleting.
  7. Separately, a persona-council review starts from /councils/new: a CouncilRepository call builds a RepoPersonaRoster (via persona-plan-adapter.ts, deterministic — not an LLM call) and a PersonaReviewRun with one PersonaAssignment per persona.
  8. For a live run, rally-workflows fans the assignments out as N concurrent host subagents (Claude Code or Codex sessions), each claiming disjoint paths through Rally’s before-write hook before writing its finding. In the executed scale test this was 10 Haiku-tier agents coordinating under before-write --strict.
  9. Each subagent posts a claim and an artifact fact back to the Rally ledger; rally dag --run <run-id> reconstructs the full lineage. The orchestrating session reads all per-persona findings and writes a synthesis (converged themes, ranked recommendations) back through /api/councils/[runId]/synthesis.
  10. Findings and synthesis land in data/council-runs.json, validated the same way base personas are, and render on /councils/[runId]. Nothing here calls a model API directly from the Next.js process — every generative step happens in a subagent or local CLI the app shells out to or coordinates via Rally.

Tech stack

Next.js 16 App Router, React 19, TypeScript, Tailwind v4. Domain validation via JSON Schema (schemas/), no ORM. Storage is local file-backed JSON — no database; SQLite/Postgres explicitly deferred. Generation runs through the RossLabs Agent Harness (local CLI) over Ollama qwen2.5-coder:7b, with a deterministic no-LLM scaffold as fallback. Multi-agent coordination runs through Agent Rally Point and rally-workflows; the persona-lab plugin (CLI + Claude Code/Codex plugin) supplies the reusable persona/roster library. No hosted database, no model API key held by the app, no live deployment.

Results

  • Two-agent dogfood (Claude = UI, Codex = architecture/data): shipped a working v1 — persona CRUD, JSON Schema validation, local persistence — coordinated entirely through Rally claims and handoffs, no direct file conflicts recorded.
  • That same run surfaced ten concrete coordination-protocol gaps in src/notes/RALLY_FEEDBACK.md: an oversized JSON envelope on every poll, pending handoffs not surfacing in the agent-visible state, next reporting idle while handoffs sat unacked, redundant per-file claims under an already-claimed parent directory, no linkage between a stopped session and its successor, and a recommended_next_action field the agent learned to ignore because it stayed stale. These are the actual, lived findings of the experiment — not a retrospective audit.
  • 10-agent Haiku-tier scale test (haiku-scale-20260609-01): 10/10 subagents completed with zero failures and zero retries, 43–76s per-agent wall clock, ~7 minutes batch wall clock, ~463K tokens total, 20 Rally facts (10 claims + 10 artifacts) all landed, and no ledger contention across 10 concurrent writers. The run’s own write-up called this a GO for a 20-agent wave and estimated ~15–20 as the practical wave size at larger scale — bounded by host subagent concurrency, not by the Rally ledger itself. That’s the project team’s stated conclusion from one run, not an independently verified scaling law.
  • The panel’s actual output was 59 findings about this app’s own evidence-provenance model (source-URI requirements, synthetic-disclosure treatment, accessibility gaps in the persona form, novice onboarding). Several — provenance, anti_goals, job_to_be_done on the schema — were subsequently built into the app, so the coordination experiment fed back into the product it was testing.

Lessons

  1. The two-agent dogfood run generated genuine friction data (stale recommended_next_action, invisible pending handoffs, ambiguous claim-hierarchy semantics) that a solo build would never have surfaced — running the actual coordination protocol under real multi-agent load, not designing it in the abstract, is what produced usable findings.
  2. Scaling from 2 to 10 concurrent agents held with zero retries and no ledger contention, which is evidence the coordination primitives (claims, handoffs, DAG lineage) work at that scale — but it’s one run of one workload type (independent, disjoint-path persona reviews), not proof the same holds for agents that need to touch overlapping files or coordinate mid-task.
  3. The panel that reviewed the app was built from the same synthetic-persona machinery the app produces, and it reviewed the app’s own evidence-provenance weaknesses — a self-referential result worth flagging plainly: it’s a useful pressure test of the tooling and it did surface real, actionable gaps, but it is not independent third-party user validation of the persona-quality problem the product nominally solves.
  4. Keeping the app itself free of any model API key — pushing all generation through a local Ollama call or out to host coding-agent subagents — kept the “what does the app actually call out to” surface small and auditable, which mattered more here than generation quality, since generation quality was never the thing being tested.