Learns how you brief your AI agents, then briefs them for you.
An understudy sits in the wings, watches every performance, learns the blocking, and goes on when the lead can't. This is that, for the way you talk to agents.
Understudy reads the session logs already sitting on your disk — Claude Code, Codex, OpenClaw, and whatever else it finds — keeps only the messages you typed yourself, and distills them into a portrait of how you actually instruct agents: how long your prompts are, when you show evidence instead of describing a problem, when you put on the brakes, and which habits you're missing.
git clone <this repo> && cd understudy
python -m understudy runNo dependencies. Nothing leaves your machine. Nothing you own is modified — every source is opened read-only.
out/holds your entire working history: client names, internal hostnames, tokens, private repo paths, unreleased product details. It is in.gitignoreand must stay there. Do not commit it, upload it, or paste it into a third-party service. This repository ships code only.
role: "user" is not the same as a person said this. Every agent runtime
stuffs machine-generated text into the user slot — skill injections, cron
heartbeats, replayed history, sub-agent hand-offs, tool output fed back in. On
one real machine that noise was the majority of user-role records. A
corpus built without removing it is a portrait of the harness, not of you.
Removing it correctly is the whole job, and it has exactly one rule:
Noise is detected by structure, never by writing style.
"You are an agent that…"is something you write. It is precisely the technique this corpus exists to capture. Any filter that guesses from tone deletes your best material. Only envelopes, injected markers, and record-level flags may disqualify a message.
Understudy uses a distiller's vocabulary, because the process is the same shape — you run the whole batch through and keep only the middle fraction:
| stage | file | what it is |
|---|---|---|
| heads | out/heads.jsonl |
the foreshots you throw away — machine-injected text, agent-to-agent sessions, protocol boilerplate. Kept, with a reason on each, so the call stays auditable |
| hearts | out/hearts.jsonl |
the cut you keep — every session, every message you actually sent, full text, never truncated |
| cut | out/cut.md |
the purest fraction — your prose only, no slash commands, no pasted blocks. This is what you feed a model |
| proof | out/proof.md |
how pure it came out, and what was removed to get there |
understudy discover # what agents does this machine have?
understudy extract # read each one, keep only what you typed
understudy merge # combine, re-judge every message, write the heartcut
understudy run # all three
understudy proof # print the purity report
understudy discover runs three passes:
- Adapters — each adapter offers candidate paths; the ones that exist and
pass
detect()get read. - Known, no adapter yet — a catalogue of tools we know where to find but can't parse yet. Finding one prints an invitation to write an adapter.
- Blind scan — the catalogue is never complete. Understudy sweeps your home and platform data directories for anything holding chat-shaped records and reports it.
That third pass is not decoration. On the machine this project was built for, it turned up six agents beyond the three it was written for — including one whose transcripts were byte-identical to a format already supported, which became a working adapter in six lines.
| tool | where it looks | native session titles | status |
|---|---|---|---|
| Claude Code | ~/.claude/projects/**, ~/.claude/history.jsonl |
yes (ai-title / custom-title) |
✅ adapter |
| Codex CLI / IDE | ~/.codex/sessions/**, archived_sessions/, session_index.jsonl |
yes (thread_name) |
✅ adapter |
| OpenClaw | ~/.openclaw*/agents/*/sessions/* |
no | ✅ adapter |
| Qoder | ~/.qoder/projects/** |
yes | ✅ adapter |
| Gemini CLI, Qwen Code, Grok CLI, Kimi CLI / Code, Cursor CLI, Continue, Goose, opencode, Crush, Amp, Zed, Aider, VS Code–family chat storage | see discover.py |
— | 🔎 found, adapter wanted |
The most valuable thing you can contribute is not code — it's the noise rules you had to work out for your tool. See ADAPTERS.md.
Each adapter documents its own rules in its module docstring. The three recurring problems, in every runtime:
- A sub-agent's instructions are also
role: user. The parent agent talking to its child looks exactly like you talking to the agent. Detected by record flags (isSidechain, sub-agent originators), never by content. - History gets replayed into new sessions. Forks, resumes and sub-agent spawns copy earlier conversations forward — sometimes with fresh timestamps, so time-based dedup silently fails. Understudy dedups by record uuid, or tracks a current-thread cursor while streaming, depending on what the format allows. Without this, one sentence of yours can appear in dozens of files.
- Real messages arrive wrapped. Resident agents and chat bridges (Feishu,
Slack, QQ…) wrap your sentence in metadata headers, sender prefixes, queue
markers and async-completion notices. Judging the wrapped whole throws your
sentence out with the envelope, so adapters peel first (
unwrap()) and re-test the payload.
Three message kinds survive into the corpus, re-decided uniformly at merge time:
prompt— what you wrote. The core material.relay— a long structured block you pasted in (another agent's answer, a log, a spec). Your behaviour, but not your prose.slash— a slash command you typed.
Plus two flags that mark rather than delete: template (an integration's
boilerplate where only a title line is yours) and chore: true (continue,
push it, a bare number). Nothing is deleted for being short or routine —
handing off without re-specifying is itself an interaction trait, and proof.md
reports the length distribution both with and without them.
Any statistic in proof.md built from keyword matching is a lower bound, not
a measurement. "Only ask for falsifiable answers 0.7% of the time" means
0.7% said so in a way a regex could see. Phrase it differently and it isn't
counted. To get real proportions, feed out/cut.md to a model for semantic
classification — that is the intended next step, and PROMPT.md describes it.
out/is gitignored, and so issources.json— discovery results name your projects.- The repository contains no corpus samples. Every example in the docs is invented.
- Sources are opened read-only; SQLite is opened with
mode=ro&immutable=1. - Contributing an adapter must never include real captured messages.
A clean corpus is the foundation, not the goal. The path from here to something that can draft an instruction the way you would — reproducible technique analysis, a semantic labelling pass, outcome pairing, and the projection itself — is laid out in ROADMAP.md.
MIT. See LICENSE.