Skip to main content
Status: draft · Version 0.2 · Filed 2026-04-25

SPEC-036 v0.2 — Scaffolder Persona-Leak Fix

Status: draft Version: 0.2 Authors: Lola (claude.ai), Frank Tewksbury Date: 2026-04-25 Related: SPEC-021 (Session Bootstrap Enforcement), commit 09e53ae (persona registry + runtime identity), commit c439ebf (frontmatter strip for Prism project)

1. Summary

The compose_prism() function in mcp/sync_helper.py bakes a persona: <n> line into every scaffolded PRISM.md’s YAML frontmatter. Because CLAUDE.md’s boot sequence reads PRISM.md (step 1) before calling prism_start (step 3), agents absorb the frontmatter persona as their identity before the MCP identity-resolution chain has a chance to fire. This spec defines the fix: remove the persona line from composed frontmatter, default all persona references to “Bot” for consistency, and clarify the boundary between the project.persona DB field (project-level metadata for historical/reporting purposes) and runtime identity (session-level, resolved by prism_start).

2. Problem Statement

2.1 Observed Symptoms

Two independent failure modes confirmed by different agents on different machines: Offline mode (Lofanda, signal 12de5278): Candi’s Windows Claude Code session launched with MCP offline. The agent read PRISM.md’s persona: Lola frontmatter and adopted Lola as its identity for the entire session, bypassing persona-registry elicitation entirely. No self-correction occurred because prism_start never ran. Online mode (Donna, signal 924ca769, mini2): Frank ran coder.ps1 -as Candi on a fresh Windows local install. All containers were healthy. Claude Code booted, read PRISM.md’s persona: Lola frontmatter at step 1 of the BIOS sequence, and greeted as Lola. After prism_start eventually fired at step 3, the agent self-corrected to Candi — but the initial greeting and early turns used the wrong identity.

2.2 Root Cause

mcp/sync_helper.py:163 — the compose_prism() function emits:
f"persona: {persona}\n"
into the YAML frontmatter header of every composed PRISM.md. This line is populated from the project.persona database column, which defaults to "Lola" (models/project.py:22). The CLAUDE.md boot sequence (Ring 1 BIOS, §Boot Sequence step 1) instructs agents to “Read ./PRISM.md” as their first action. Agents treat YAML frontmatter as authoritative metadata. By the time step 3 fires (prism_start), the agent has already internalized the frontmatter persona.

2.3 Why the Existing Identity Resolution Doesn’t Help

The prism_start identity resolution chain (commit 09e53ae) works correctly:
  1. Explicit identity argument (re-entry after elicitation)
  2. PRISM_AGENT_IDENTITY env var (from coder --as <n>)
  3. Structured identity-elicitation response with prism_whois results
All three paths fire only at BIOS step 3 (prism_start MCP call). The static file read at step 1 preempts them. In offline mode, step 3 never fires at all.

2.4 Scope of the Leak

The persona value flows through six code paths into composed frontmatter:
LocationDefaultRole
models/project.py:22"Lola"DB column default
schemas/project.py:13 (ProjectCreate)"Lola"API schema default
routers/projects.py:24 (BootstrapRequest)"Lola"HTTP endpoint default
bootstrap_service.py:43"Lola"Service layer default
scaffold_service.py:252 (build_manifest)"Donna"Scaffold default (inconsistent)
sync_helper.py:163 (compose_prism)from callerFrontmatter emitter (the bug)

3. Design Decision

3.1 Chosen Approach: Option D (immediate symptom fix) + default alignment

Two changes ship together:
  1. Strip the persona: {persona} line from compose_prism()’s YAML frontmatter header. The function signature retains the persona parameter for backward compatibility — callers still pass it, but the value is no longer emitted into the composed file. This fixes both the online and offline failure modes with no migration or schema change.
  2. Align all persona defaults to "Bot" across the codebase. Every location that currently defaults to "Lola" or "Donna" changes to "Bot". This ensures that when the field appears in DB records, API responses, or logs, it reads as a clearly non-identity placeholder — no one will mistake it for a real agent persona.

3.2 Rationale

The persona line in PRISM.md frontmatter served a purpose in the pre-registry era: it was the only mechanism for conveying “which agent owns this project” to offline readers. With the persona registry (commit 09e53ae) and runtime identity resolution now live, this static-file signal is redundant and actively harmful — it creates a race condition between static-file identity (step 1) and runtime identity (step 3). Defaulting to "Bot" rather than any real persona name eliminates confusion: if an agent ever encounters persona: Bot in metadata, it’s clearly a system default, not an identity to adopt. Precedent: Lofanda’s commit c439ebf already stripped the persona: line from the Prism project’s own PRISM.md and context/_ACTIVE_CONTEXT.md with no breakage. This spec generalizes that fix to the scaffolder so all future projects are clean.

3.3 The project.persona DB field — kept for historical/reporting purposes

Per Frank’s ruling: the project.persona column in the projects table stays. Its role is historical attribution and future reporting — e.g., which persona originally bootstrapped a project, join queries across projects and sessions for long-term memory analysis. It is explicitly NOT runtime identity. The authoritative runtime identity backbone is the Redis session plane (persona registry + prism_start identity resolution). No ADR needed for the field itself — it keeps its current role, just stops leaking into agent-visible files. If a future rename to project_owner or similar is desired for clarity, that’s a cosmetic migration that can ride any convenient release.

4. Implementation

4.1 Change 1: Strip persona from compose_prism header (REQUIRED)

File: mcp/sync_helper.py, function compose_prism() Remove the line:
f"persona: {persona}\n"
from the header string in compose_prism(). The composed PRISM.md frontmatter will contain project, pid, ptype, methodology_version, composed_from, composed_at, and override_chain — but no persona. The persona parameter stays in the function signature. No callers need to change.

4.2 Change 2: Align all persona defaults to “Bot” (REQUIRED)

Update the default value from "Lola" or "Donna" to "Bot" in these locations:
FileLineCurrentNew
backend/app/models/project.py:22default="Lola""Lola""Bot"
backend/app/schemas/project.py:13persona: str = "Lola""Lola""Bot"
backend/app/routers/projects.py:24persona: str = "Lola""Lola""Bot"
backend/app/services/bootstrap_service.py:43persona: str = "Lola""Lola""Bot"
backend/app/services/scaffold_service.py:252persona: str = "Donna""Donna""Bot"
mcp/server.py (bootstrap_project)persona: str = "Lola""Lola""Bot"
mcp/server.py (prism_clone)persona: str = "Lola""Lola""Bot"
mcp/server.py (prism_create)persona: str = "Lola""Lola""Bot"
mcp/server.py (prism_sync_bios fallback)"Lola""Lola""Bot"
No database migration needed — existing rows keep their current persona values (which correctly reflect who bootstrapped them). Only new projects and API calls without an explicit persona will default to "Bot". After the code changes ship, running prism_sync_bios(files="prism") on any project will recompose its PRISM.md without the persona line. For immediate unblock on affected machines (e.g., mini2), a manual one-line edit to strip persona: Lola from the existing PRISM.md frontmatter is equivalent.

4.4 Change 4: context/_ACTIVE_CONTEXT.md template (VERIFY — already clean)

The _active_context_md() function in scaffold_service.py does not currently emit a persona line — confirmed clean. No change needed.

5. Verification

5.1 Test: Fresh scaffold produces no persona in frontmatter

After shipping Changes 1-2:
  1. prism_create(name="TestNoPersona")
  2. Read the scaffolded PRISM.md
  3. Assert: YAML frontmatter contains no persona: key
  4. Assert: DB record has persona = "Bot"
  5. prism_destroy(pid=<new_pid>, delete_dir=True) to clean up

5.2 Test: Re-sync strips persona from existing project

  1. Manually add persona: TestLeak to an existing project’s PRISM.md frontmatter
  2. prism_sync_bios(pid=<pid>, files="prism", force=True)
  3. Read the recomposed PRISM.md
  4. Assert: YAML frontmatter contains no persona: key

5.3 Test: Identity resolution works end-to-end post-fix

  1. Launch coder --as Candi on a project whose PRISM.md has no persona in frontmatter
  2. Observe: agent reads PRISM.md at step 1 — no identity signal absorbed
  3. Observe: prism_start fires at step 3, reads PRISM_AGENT_IDENTITY=Candi from env
  4. Assert: agent greets as Candi from the first turn, no identity confusion

5.4 Test: Offline mode with no persona in frontmatter

  1. Launch Claude Code with MCP server unreachable (offline mode)
  2. Agent reads PRISM.md — no persona in frontmatter
  3. Agent falls through to offline-mode fallback (PRISM.md §6)
  4. Assert: agent does NOT adopt any specific persona name from static files

6. Migration

No database migration required. No schema changes. The project.persona DB column and all associated schema/router/service code remain untouched — they continue to serve their role as historical project metadata. Existing rows keep their current values. Only the defaults change (to “Bot”), affecting new projects created after the fix ships. Existing projects with persona: in their PRISM.md frontmatter will self-heal on the next prism_sync_bios run.

7. Resolved Questions

  • Q1 (resolved): The project.persona DB field stays. Its purpose is historical attribution and future reporting/joins — not runtime identity. The Redis session plane (persona registry + prism_start) is the authoritative runtime identity backbone. No ADR needed.
  • Q2 (resolved): All defaults align to "Bot" — a clearly non-identity placeholder that no agent will mistake for a real persona. Ships as part of this spec, not deferred.
Status: draft
Last modified on April 27, 2026