Identity & Interviews
The identity system is how silicon-zack comes to know bio-zack β not just preferences, but values, stories, communication style, and the way he sees the world. It captures raw material from interviews, check-ins, and the conversation stream, synthesizes it into structured profile files, and exposes those files to every Claude session (via CLAUDE.md) and to a browsable HTML report.
This page covers the full pipeline: where raw observations land, how synthesis turns them into per-domain profiles, the communication-style ("voice") analyzer, the micro-grit and identity-assertion instrumentation, and how the data surfaces in the web UI.
Framing. Knowing bio-zack accurately enough that he'd say "yes, that's me" is the system's homeostasis target. Everything here is instrumentation for measured self-knowledge, not a static bio. The mechanism is reimplementable; the contents shown below are sanitized placeholders.
Architecture at a glance
CAPTURE SYNTHESIS CONSUMPTION
βββββββ βββββββββ βββββββββββ
interviews/transcripts/*.md β ββ CLAUDE.md voice section
observations.jsonl βββΊ /reflect --synthesize ββ€ (every Claude session)
quotes.jsonl β /identity-refresh βββΊ /who-am-i narrative
stories.json ββ€ (group, dedup, βββΊ data/reports/identity.html
writing corpus βββΊ voice βββ€ contradiction-check, β served at /api/reports/
pipeline β coverage-score) β identity/html β web /identity
micro-grit.json ββ β ββ coaching maps, sleep gather
βΌ
profile/identity.json (domains)
profile/communication.json (voice)
profile/timeline.json / relationships.json
data/identity/stories.json + state.json
There are two distinct synthesis loops that share data but solve different problems:
| Loop | Input | Output | Status |
|---|---|---|---|
Identity synthesis (/reflect, /identity-refresh, /who-am-i) |
observations, quotes, interview transcripts | profile/identity.json (per-domain coverage), timeline.json, relationships.json, stories.json |
active, Claude-driven |
Voice / communication-style (scripts/voice/cli.py) |
a writing corpus (telegram/signal/email/Claude/interviews) | profile/communication.json β CLAUDE.md |
pipeline dormant (last full regen Jan 2026); its product is live |
Commands
| Command | What it does |
|---|---|
/interview |
List interview prompts sorted by readiness; emits a voice briefing |
/interview --pick=ID |
Mark a prompt explored, output its briefing text |
/reflect |
Show domain coverage, pending observations, and unresolved contradictions |
/reflect --synthesize |
Run the synthesis algorithm, regenerate profile files |
/who-am-i |
Format the profile into a human-readable narrative |
/who-am-i --refresh |
Run synthesis first, then show the narrative |
/who-am-i --raw |
Dump raw JSON from the profile files |
/identity-refresh |
Process new transcripts + observations into all profile files; report coverage gaps |
/interview supports filter flags: --time=N (max minutes), --mode= (biographical / philosophical / challenge / exploratory), --setting= (walk / drive / desk), --mood= (reflective / contemplative / energized / curious).
Automatic triggers:
- End of
/checkinβ up to 1 observation captured. - End of
/interviewβ up to 3 observations captured. - "rememberβ¦" / "note thatβ¦" in any session β 1 high-confidence observation.
- Session start β the voice profile (already synced into
CLAUDE.md) shapes Claude's tone.
The data model
All identity data lives in two roots: profile/ (the synthesized representation) and data/identity/ (the raw feedstock and processing state).
Raw feedstock (data/identity/)
| File | Shape | Purpose |
|---|---|---|
observations.jsonl |
one JSON object per line | durable observations about bio-zack |
quotes.jsonl |
one object per line | verbatim quotes with context + theme |
stories.json |
{schema_version, source, stories[]} |
full narrative stories |
interviews.jsonl |
one object per line | interview session metadata |
events.jsonl |
one object per line | audit log of synthesis runs + rate-limit events |
question-history.json |
object | recently-asked identity questions (dedup) |
micro-grit.json |
object | streak/recovery analytics (see below) |
state.json |
object | synthesis processing state (processed_observations, dropped_observations, contradictions) |
INTERVIEW_GUIDE.md |
markdown | prompt-design strategy + domain coverage notes |
assertion-responses/ |
dir of weekly JSON | identity-assertion verification loop (see below) |
Synthesized profile (profile/)
| File | Purpose |
|---|---|
identity.json |
per-domain coverage, summaries, insights, confidence |
communication.json |
voice/style profile (feeds CLAUDE.md) |
timeline.json |
life events chronologically |
relationships.json |
people + connections |
identity-anchor.json |
vision / anti-vision / operational rules (see below) |
The
profile/directory holds additional domain files the identity system reads or maintains βbasics.json,books.json,goal-philosophy.json,lifestyle.json,parenting.json,places.json,preferences.json,work-philosophy.json,training_zones.json,gear.json. These are narrower fact stores feeding the same "know bio-zack" target; the identity report and synthesis steps draw on them.
Observation (data/identity/observations.jsonl)
{
"id": "<uuid>",
"ts": "2026-01-01T12:00:00.000000",
"type": "value",
"obs": "Prefers reasoning from first principles over received wisdom",
"evidence": ["'<short verbatim quote, <=100 chars>'"],
"confidence": "high",
"source": "interview_2026-01-01",
"v": 1
}
type is one of value, personality, preference, communication. Append with atomic_append() from scripts.utils β never hand-edit the JSONL.
Quote (data/identity/quotes.jsonl)
{
"id": "<uuid>",
"quote": "<verbatim sentence bio-zack said>",
"context": "Reflecting on a core belief",
"tags": ["philosophy", "work"],
"theme": "values",
"source": "interview_2026-01-01",
"ts": "2026-01-01T21:32:34.575798",
"v": 1
}
Identity profile (profile/identity.json)
{
"v": 1,
"generated_at": "2026-01-01T10:15:00.000000",
"domains": {
"childhood_family": {
"summary": "Grew up in [region]; [formative theme] ...",
"insights": ["<one-line distilled insight>", "..."],
"confidence": "high",
"coverage": 0.85
}
},
"gaps": []
}
The system tracks identity domains (the web type union lists ten; the synthesis skill scores nine β epistemology is present in the type system but not in the skill's coverage formula):
| Domain | Description |
|---|---|
childhood_family |
upbringing, parents, siblings, formative experiences |
values_beliefs |
core philosophy, decision frameworks |
relationships |
partner, friends, how connection works |
challenges_resilience |
hardships, failures, how setbacks are processed |
self_knowledge |
strengths, weaknesses, honest self-assessment |
legacy_mortality |
what to leave behind, perspective on death |
spirituality |
meaning, transcendence, flow |
work_purpose |
career, calling, what work means |
creativity_passions |
hobbies, flow activities, creative outlets |
epistemology |
how he knows things, relationship with truth (web-only domain) |
Timeline / relationships (profile/)
// timeline.json
{
"schema_version": 1,
"source": "interview_2026-01-01",
"events": [
{"date": "~YYYY", "age": "N", "event": "Family moved to [city]",
"significance": "Shaped early social environment", "note": "..."}
]
}
// relationships.json β keyed by role; values sanitized
{
"schema_version": 1,
"people": {
"partner": {"name": "Partner Name", "relationship": "spouse",
"context": "Met in YYYY", "location": "City",
"interests": ["..."], "notes": "..."}
}
}
The web UI buckets people into orbit categories: immediate_family, extended_family, close_friends, colleagues.
Identity anchor (profile/identity-anchor.json)
A small, deliberately-stable file that pins bio-zack's directional north star independent of the churn in the rest of the profile:
{
"v": 1,
"captured": "<iso timestamp>",
"statement": "<one-line identity statement>",
"vision": "<where he's heading>",
"anti_vision": "<the failure mode he's avoiding>",
"operational_rules": ["<rule 1>", "<rule 2>", "<rule 3>"],
"enabled": true
}
enabled gates whether the anchor is applied. It's a manually/skill-curated file, not a synthesis output.
Observation capture
Skill:
.claude/skills/observation-capture/SKILL.md
Observations are the atomic input to identity synthesis. They are captured conservatively so the store stays high-signal:
| Rule | Value |
|---|---|
| Max per session | 3 (excess is dropped and logged to events.jsonl) |
| Confidence | high for explicit statements; medium for repeated behavior / strong reactions |
| Evidence | up to 3 quotes, each β€ 100 characters, verbatim preferred |
| Never capture | task-specific instructions, project-specific prefs, temporary states |
Triggers: end of /interview (0β3), end of /checkin (0β1), explicit "rememberβ¦"/"note thatβ¦" (1, high confidence). Durable preferences are also captured as learnings, which can be promoted into CLAUDE.md.
The capture path is a plain append:
from scripts.utils import atomic_append, generate_uuid
atomic_append("data/identity/observations.jsonl", {
"id": generate_uuid(), "ts": ..., "type": "preference",
"obs": "...", "evidence": ["..."], "confidence": "high",
"source": "checkin_2026-01-01", "v": 1,
})
See Check-in System for where in-conversation capture fires.
Synthesis algorithm
/reflect --synthesize (and the synthesis step shared by /who-am-i --refresh and /identity-refresh) turns raw observations into profile data:
1. Load all observations NOT in state.processed_observations
2. Group by type: communication, preference, value, personality
3. For communication observations, detect contradictions:
same type + >=50% keyword overlap + opposite polarity β contradiction
4. Regenerate profile/identity.json and profile/communication.json
5. Update state.json with processed IDs
6. Append a synthesis event to events.jsonl
Contradiction resolution is interactive β when a conflict is detected, /reflect surfaces both sides and asks which reflects the current preference (keep A / keep B / both context-dependent / dismiss both). The decision updates state.json and may move observations to dropped_observations.
Coverage formula (per
identity-refreshskill): a domain'scoverage(0.0β1.0) is a weighted blend β observations 40%, quotes 20%, timeline 15%, relationships 15%, stories 10%. This quantifies "how well do I actually know bio-zack here?" and is the input to gap detection.
Gap priorities (drive what to interview next):
| Priority | Condition |
|---|---|
| high | domain coverage < 30% |
| medium | domain coverage < 50% |
| medium | timeline gap > 8 years |
| low | a relationship category with no people |
The voice / communication-style pipeline
Status: dormant pipeline, live product. The full corpus β profile run was last executed Jan 2026;
profile/communication.jsonis the live artifact it produced and is what shapes every session today.
This is a separate, statistical pipeline under scripts/voice/ that learns bio-zack's writing voice from real text rather than from interview answers. It is identity work that happens to live in the voice/ directory (distinct from the live voice-capture system below).
corpus sources analysis output
ββββββββββββββ ββββββββ ββββββ
telegram exports β
signal exports ββ corpus.py ββΊ patterns.py ββ
work/personal ββ€ normalize vocabulary.py βββΊ synthesize.py ββΊ communication.json
email β dedup (TF-IDF, β (validate.py) β
Claude sessions β clean hedges, β β sync_claude_md.py
interviews ββ signatures) β βΌ
.claude/CLAUDE.md (VOICE_PROFILE)
CLI (scripts/voice/cli.py):
uv run python scripts/voice/cli.py analyze # full pipeline
uv run python scripts/voice/cli.py parse # corpus -> data/voice/normalized.jsonl
uv run python scripts/voice/cli.py patterns # -> data/voice/patterns.json
uv run python scripts/voice/cli.py vocab # -> data/voice/vocabulary.json
uv run python scripts/voice/cli.py synthesize # LLM synthesis -> synthesis_draft.json
uv run python scripts/voice/cli.py validate # -> data/voice/validation_report.json
uv run python scripts/voice/cli.py integrate # -> profile/communication.json
uv run python scripts/voice/cli.py stats # corpus statistics
uv run python scripts/voice/sync_claude_md.py # communication.json -> CLAUDE.md
Pipeline modules
| Module | Role |
|---|---|
corpus.py |
parse platform-specific writing into a normalized NormalizedMessage (text, medium, modality, recipient_type, thread context); clean + dedup by content hash |
patterns.py |
sentence-length distributions, punctuation habits, capitalization, reply/initiation rates, medium-specific tendencies |
vocabulary.py |
TF-IDF word usage, hedges/fillers, signature phrases, use-vs-avoid lists, vocabulary by recipient |
synthesize.py |
LLM-combine patterns + vocabulary into a draft profile |
validate.py |
sanity-check the synthesized profile β validation_report.json |
integrate.py |
fold the validated draft into profile/communication.json |
sync_claude_md.py |
write the <!-- VOICE_PROFILE_START - DO NOT EDIT BELOW --> β¦ END block in .claude/CLAUDE.md |
models.py |
Pydantic models: NormalizedMessage, VoiceCharacteristics, MediumProfile, RecipientProfile, VocabularyProfile, CommunicationProfile |
prompt_builder.py |
score background facts against topics to build personalized interview prompts |
profile/communication.json
The synthesized voice profile. Shape (values illustrative):
{
"v": 1,
"generated_at": "2026-01-06T...",
"voice": {"primary": "Direct and analytical, biased toward action",
"traits": ["analytical", "pragmatic", "stoic"]},
"style": {"formality": "casual", "humor": "dry", "detail": "high"},
"by_medium": {
"chat": {"style": "casual and direct", "length": "brief, 5-15 words",
"conventions": ["lowercase throughout"]},
"email": {"style": "professional but warm"},
"spoken": {"style": "analytical and meandering"}
},
"by_recipient": {"close_friend": {...}, "professional": {...}, "assistant": {...}},
"patterns": ["Uses Socratic dialogue", "Thinks in systems"],
"vocabulary": {"use": ["actually", "frankly"], "avoid": ["amazing"],
"signature": ["i mean", "sort of"]},
"avoid": ["Excessive emotional validation", "Hand-holding"]
}
sync_claude_md.py renders this into the VOICE_PROFILE block of .claude/CLAUDE.md, which is how the voice reaches every session β including the Telegram agents (whose canonical formatting rules live in scripts/telegram_parallel/system_prompt.py).
Agent-prompt source and parity guard
The broader runtime identity source is .agents/identity.yaml (mirrored at
.claude/identity.yaml for harness compatibility). Running
scripts/generate_agent_prompts.py renders the Claude and aloop prompt surfaces
from that source. Hand-editing a generated prompt without porting the durable
change back to the YAML would otherwise create a silent next-regeneration
revert.
uv run python scripts/generate_agent_prompts.py --check # read-only parity check
uv run python scripts/generate_agent_prompts.py --write # regenerate CLAUDE.md
The /sleep finalize step runs --check after all other edits and records
prompt_parity in the overnight summary. A mismatch is loud but non-blocking so
a monitor false positive cannot wedge the nightly run. verify-fast also tests
the live parity contract. The repair is to move the intended generated-file edit
into identity.yaml, then regenerateβnot to weaken the check.
Micro-grit analytics
scripts/identity/microgrit.pyβdata/identity/micro-grit.json
Identity instrumentation for persistence: up to 3 small challenges set each morning and checked each evening, accumulating streak and completion data (current_streak, longest_streak, recovery-after-miss, milestones). It feeds the identity system as evidence for the challenges_resilience and self_knowledge domains and surfaces in Insights. It is a Python module API rather than a slash command.
Identity-assertion verification loop
data/identity/assertion-responses/β early rollout / experimental
A newer falsifiability loop: instead of only asking open questions, the system generates assertions about bio-zack and asks him to confirm, correct, or expand them. Confirmation rate becomes a calibration signal for how accurately the profile models him β the same homeostasis idea as Homeostasis predictions, applied to identity claims.
Per-week file (YYYY-Www-assertions.json):
{
"week_number": 9, "week_label": "2026-W09",
"generated_at": "...", "generated_by": "...",
"rollout_phase": "...",
"assertions": [
{"id": "...", "type": "...", "domain": "values_beliefs",
"text": "<assertion about bio-zack>",
"reaction_prompt": "<how to ask>",
"tests_observation_id": "<obs uuid>",
"reasoning": "...", "difficulty": "...", "domain_coverage": 0.6}
],
"telegram_message": "...", "status": "...", "responses": [...]
}
assertion-responses/state.json carries the running tallies (total_confirmations, total_corrections, total_expansions, response_rate, domains_validated) and a schedule ({day, time_pt, cadence, channel}). The loop is designed to run weekly over Telegram but is not yet wired to a scheduler task or skill β treat it as an in-progress instrument, not a running job.
silicon-zack custom model
bio-zack's voice also exists as custom fine-tuned weights, not just a prompt profile. The .aloop/config.json defines two modes backed by a Modal-hosted model:
"silicon-zack": {"model": "silicon-zack", "provider": "modal-silicon-zack",
"max_iterations": 1},
"silicon-zack-chat": {"model": "silicon-zack"}
scripts/inference/factory.py routes the silicon-zack provider to Modal (distinct from the OpenRouter routing the rest of the system uses). The non-chat variant caps at a single iteration. This is the most literal form of "voice as identity" β bio-zack's style encoded in weights. For how everything else routes, see Model Guidance.
Live voice capture
data/voice/βsession_store.py, plus the hardware/STT/TTS modules
Separate from the dormant style pipeline, there is a live turn-based voice-capture system. scripts/voice/session_store.py is a SQLite store (data/voice/sessions.db, tables sessions / turns / audio_files) with audio/ and originals/ directories and a tool-calls.jsonl log. Supporting modules:
| Module | Role |
|---|---|
session_store.py |
SQLite store for turn-based voice sessions |
transcript.py |
voice transcript logging + management |
intent_router.py |
classify voice intent (via OpenRouter) |
hardware_voice.py |
hardware pipeline: STT β intent β Telegram dispatch β device response |
tts.py |
text-to-speech (ElevenLabs, cloned voice) |
auto_title.py |
auto-title voice sessions |
usage.py |
voice API cost/usage tracking |
This is the operational surface for spoken interaction; deeper coverage lives in Voice & Location. Its existence means an interview can now happen partly in-system, though the structured interview workflow below is still transcript-import-based.
Interview workflow
The interview prompt mechanics are detailed in Interview Prompts; the data round-trip is:
- Select β
/interviewlists prompts (filterable by time/mode/setting/mood), sorted by readiness. - Brief β
/interview --pick=IDmarks the prompt explored and emits a voice briefing to paste into an external voice AI (Grok, ChatGPT, or other β the platform is a free field, not locked to one vendor). - Transcribe β export the conversation and drop it in
data/interviews/transcripts/namedYYYY-MM-DD_platform_type.md(e.g.2026-01-02_grok_risk.md). - Extract β
/identity-refresh(or/reflect --synthesize) processes the transcript: verbatim quotes βquotes.jsonl, narrative stories βstories.json, observations βobservations.jsonl, and domain coverage updates βidentity.json. - Correct β fix transcription artifacts (names especially) by editing the transcript or via the web UI feedback queue.
Interview processing details (file moving, metadata) live in the
interview-processingskill; processing supports any platform string, not just Grok.
Web UI: the Identity page
Important architectural fact. The web
/identityroute is not a React-rendered page.web/src/routes/identity/index.tsxis a 15-line component that embeds a server-rendered report:
function IdentityPage() {
return <iframe src="/api/reports/identity/html" className="w-full h-screen border-0" title="Identity" />;
}
The real consumption surface is the static HTML report at data/reports/identity.html, served through the generic reports endpoint:
| Endpoint | Server | Behavior |
|---|---|---|
GET /api/reports/identity/html |
API on port 33800 (api/src/routers/reports.py, get_report_html) |
reads data/reports/identity.html, records a view, returns raw HTML |
The web UI on port 33801 simply iframes that endpoint. (See Architecture for the port map: API 33800, web 33801, public read-only API 33802.) The report HTML is a self-contained page (charts rendered client-side from JSON baked in at generation time), regenerated when identity data is refreshed.
Orphaned React components
A full set of richly-interactive components still exists on disk under web/src/components/identity/ β IdentityConstellation.tsx, ValuesDNA.tsx, TimelineSpiral.tsx, RelationshipOrbits.tsx, DomainRadar.tsx, GapsDashboard.tsx, FeedbackFAB.tsx, QuickAddForm.tsx, and others β together with the useIdentity() hook (web/src/hooks/useIdentity.ts) and the web/src/types/identity.ts type definitions.
These are orphaned. They are exported from the directory's
index.tsbut imported by no live route. They represent an earlier component-driven version of the page that the iframe-to-HTML-report approach replaced. A reimplementer should treat them as reference, not the current render path.
Type definitions (web/src/types/identity.ts)
Still authoritative for the data shapes (used by the hook and report alike):
type IdentityDomain =
| 'childhood_family' | 'values_beliefs' | 'relationships'
| 'challenges_resilience' | 'self_knowledge' | 'legacy_mortality'
| 'spirituality' | 'work_purpose' | 'creativity_passions' | 'epistemology';
interface DomainData { summary: string; insights: string[];
confidence: 'high'|'medium'|'low'; coverage: number; }
interface IdentityProfile { v: number; generated_at: string;
domains: Record<IdentityDomain, DomainData>; gaps: string[]; }
interface IdentityGap { id: string;
type: 'missing_data'|'low_coverage'|'stale_data'|'contradiction';
domain?: IdentityDomain; description: string;
priority: 'high'|'medium'|'low'; action?: string; actionRoute?: string; }
Completeness calculation (calculateCompleteness())
The hook computes an overall 0β100 score (still used where the React types are consumed):
| Component | Weight | Target |
|---|---|---|
| Average domain coverage | 60% | β |
| Timeline events | 10% | 10+ |
| Relationships | 10% | 10+ |
| Quotes | 10% | 50+ |
| Stories | 10% | 10+ |
File locations
| Path | Purpose |
|---|---|
data/interviews/transcripts/*.md |
raw interview transcripts (YYYY-MM-DD_platform_type.md) |
data/identity/observations.jsonl |
all identity observations |
data/identity/quotes.jsonl |
verbatim quotes with context |
data/identity/stories.json |
narrative stories |
data/identity/interviews.jsonl |
interview session metadata |
data/identity/events.jsonl |
synthesis-run + rate-limit audit log |
data/identity/state.json |
synthesis processing state |
data/identity/question-history.json |
recently-asked questions (dedup) |
data/identity/micro-grit.json |
micro-grit streak analytics |
data/identity/assertion-responses/ |
weekly identity-assertion verification |
data/identity/INTERVIEW_GUIDE.md |
prompt-design strategy |
profile/identity.json |
synthesized per-domain profile |
profile/communication.json |
voice/style profile (β CLAUDE.md) |
profile/timeline.json / relationships.json |
life events / people |
profile/identity-anchor.json |
vision / anti-vision / operational rules |
profile/*.json (other) |
narrower fact stores feeding identity |
data/writing/corpus/ |
raw writing samples for the voice pipeline |
data/voice/normalized.jsonl |
normalized corpus output |
data/voice/sessions.db |
live voice-capture session store |
data/reports/identity.html |
the served Identity report |
scripts/voice/ |
voice/style pipeline + live capture modules |
scripts/identity/microgrit.py |
micro-grit analytics |
api/src/routers/reports.py |
serves /api/reports/{slug}/html |
web/src/routes/identity/index.tsx |
iframe wrapper for the identity route |
web/src/components/identity/ |
orphaned React identity components |
Known limitations
- Two render paths. The live page is an iframe to a static HTML report; the React component tree is orphaned. Adding a field means regenerating the report, not editing a component.
- Manual extraction. Transcript β quotes/stories/observations requires a Claude session; it is not auto-triggered on file drop.
- Simple contradiction detection. 50% keyword overlap + opposite polarity catches obvious conflicts only.
- No observation aging. Old observations carry equal weight to recent ones.
- Voice style pipeline is dormant.
communication.jsonis live but hasn't been regenerated since Jan 2026. - Assertion loop not yet scheduled. The weekly identity-assertion review has a schedule config but no wired scheduler task.
Where to go next
- Interview Prompts β prompt design, readiness scoring, the prompt bank
- Check-in System β where in-conversation observation capture fires
- Voice & Location β the live voice-capture / hardware pipeline
- Insights β where micro-grit and identity signals surface
- Homeostasis β the broader "is my model accurate?" instrumentation
- Model Guidance β how the silicon-zack model fits the routing table