

Lab / Tool
The lab journal
While a feature is built, the working agent logs each decision and dead end, hooks count prompts and tokens with no model involved, and Jev sorts the record at wrap-up. A writer agent drafts the Lab Project from the journal and checks every sentence against it. Along the way the house voice moved into one file, with a gate at save time.
- Published
- 27 September 2026
- Built with
- Claude Code, TypeSafe Jev, Payload CMS+3
- Read
- 39 MIN
Context
A document for every feature
At the end of a session reviewing the sas-cms MCP server for places TypeSafe could cut agent token use, Miles asked for the findings as a document with his prompt, the model and the token usage, because that work would become a Lab Project entry.
The question
Then he asked whether an agent could do that for every feature: stay with the work across days and sessions, and record the key decisions, challenges, insights, models, token usage and prompt history. In his words:
is this something an agent would be able to do going forward if i want to begin work on a feature with the end goal of documenting the entire process for a new lab project entry in my cms including key decisions, challenges, insights, ai model and token usage used, prompt history, etc.?
He accepted the design Claude recommended in his next message, and asked for TypeSafe too:
i want this built - and i want you to also find opportunities to use /typesafe:typesafe-ai for this if relevant to save usage and improve workflow
What a published Lab Page already carried
Two Lab Pages were already published: From Webflow to Payload and Building a shader studio in Payload CMS. A later audit of the writer counted what is on them. The pages carry 7 to 8 diagrams each. The Webflow page adds 10 code listings and 14 admin screenshots with client names blurred, the shader page 4 charts and 2 bespoke figures, all of it placed by hand in sessions that predate the journal.
The problem
Three constraints
The repository is public, so nothing typed can be committed as typed. A subagent cannot watch a session: it runs in its own context, sees only what it is handed and returns one message. And the hooks run on every prompt, so they had to be instant, offline and unable to fail a session. What went wrong inside those lines follows.
The repository is public
Found while building: gh repo view reports the repository as public. A committed prompt file would publish everything Miles types, including anything pasted by accident.
What went wrong
Two bugs no test caught
The first live run captured the wrong session
The start command backfills the running session's prompts from its transcript. The first version found that transcript as the newest file in the project's transcript folder. Miles had a second session open in the same checkout, and it had written more recently, so the journal took that session's prompt and token counts.
It was caught on the first real run, by reading the session id in the output instead of trusting the summary line. Claude Code names the session in every tool call's environment as CLAUDE_CODE_SESSION_ID, so that became the source and the newest file only a fallback. The same run named the private folder with-vercel-website, because package.json still carries the template's name. The name now comes from the git origin. Unit tests had passed. Both bugs lived in the part that touches the real machine.
One session, two features
The tooling's first real use was wrong in a way no test would catch. Claude started one journal, named for the MCP feature, and logged both the MCP review and the building of the journal into it, with the whole session's tokens. Miles caught it after the push: they are different features.
The design had assumed a session belongs to one feature. Now a session row can carry a window, set with pnpm lab:journal window and honored by every recount, by the prompt capture and by the digest. The session was split at 16:33:08 UTC, the prompt where the conversation turned, so its tokens sum to the session total across the two journals instead of counting twice. Nothing detects a change of subject yet, so the skill tells the agent to ask.
Checkouts
A feature's sessions move between checkouts
Claude Code files a transcript under the path of the directory the session ran in, and the tooling looked only under the current directory and its checkout root. From a worktree, the recount and the digest would never have found the sessions that ran in the main checkout. They now look under every checkout that git worktree list names.
Conductor needed one more step. A Conductor workspace is a worktree of Conductor's own clone, so from there git worktree list never names the main checkout. Transcripts are now found by session id in any Claude Code project folder, since ids are unique. Tested in a workspace chat, Conductor ran the prompt hook but showed no SessionStart note. The guide now says to run pnpm lab:journal status rather than read a missing note as a failed hook.
Description and steps
A decision flow for finding the transcript of the session running a command. If Claude Code has put the session id in the environment, the transcript is looked for by that id, first in every checkout that git worktree list names, then in every Claude Code project folder. Only when there is no id, or the id is found nowhere, does it fall back to the newest transcript. The takeaway is that the newest file, which caused the first bug, is now the last resort.
- Session id set? (decision)
- Every checkout
- Every project folder
- Newest file
- Transcript (start or end)
Connections
- Session id set? to Every checkout: yes
- Every checkout to Transcript: found
- Every checkout to Every project folder: not found
- Every project folder to Transcript: found
- Every project folder to Newest file (optional or async)
- Session id set? to Newest file: no (optional or async)
- Newest file to Transcript (optional or async)
The writer
The writer drafted words only
Before the writer was extended to media, an audit held its brief against the two published pages. The brief asked for one chart, tokens per session, and said the agent could not see or create images, so it would only place media ids a person handed it. That line was out of date. The agent has every tool, and Read shows it images.
A second gap sat under the first. pnpm cms:upload sent a file to CMS_UPLOAD_SERVER, else NEXT_PUBLIC_SERVER_URL, which in that workspace was localhost, while the sas-cms MCP writes to production. An uploaded id would name a local media document that the production draft could not resolve.
The figure plan
Asked what a diagram could show, Jev said yes to every entry
The figure plan adds four questions to the digest's request for each journal entry: screenshot, diagram, code listing and chart. The first wording asked whether the entry described a mechanism a diagram could show. Jev answered 0.84 to 0.97 for all 17 entries. Rewritten as a score from 0 to 3, 15 of the 17 scored 2.9 or higher. A plan that lists everything is not a plan.
TypeSafe's own limitations page for Jev 1.13 names the cause: it reads a question literally, and literally anything could be drawn. The fix asks what the entry is mainly about, and writes the near miss into the false side of the question. With that wording the diagram answers spread from 0.16 to 0.88, and five entries pass 0.5. Finding it took three full reruns of the digest at about 55,000 input tokens each.
Wording
Entries put forward, of 17
Description and data table
A horizontal bar chart of three wordings of one question over the same 17 journal entries. Asked whether a diagram could show the entry, Jev put forward all 17. As a score from 0 to 3, 15 scored 2.9 or higher. Asked whether the entry is mainly an explanation of a mechanism, 5 passed. The takeaway is that the wording, not the model, decided whether the plan was useful.
| Wording | Entries |
|---|---|
| Could it be drawn? | 17 |
| Score of 2.9+ | 15 |
| A mechanism? | 5 |
Decisions
A skill and hooks, with one agent at the end
Nine decisions. The first four settle who writes each part of the record and where it lives. The rest decide how the writer reaches the CMS, what its copy follows and what one entry covers. Each says what it rejected and whose call it was.
Miles asked for an agent that stays with a feature across days and sessions. Claude recommended against a subagent for that part, and Miles accepted. A subagent runs in its own context, sees only what it is handed and returns one message, so it cannot watch a session, and keeping it informed would mean resending the context each time.
So the work splits three ways. A skill tells the main session what to log and when, because only that session has the context. Hooks capture prompts and token counts with no model involved. One subagent runs at the end, the lab-project-writer, where a fresh context helps: it reads the journal, not five days of conversation.
Description and steps
A flow from the build to the draft. The working session logs each decision to journal.md. Hooks, with no model, capture every prompt and every session's token counts. At wrap-up Jev's digest reads the journal and the prompts and hands a brief to the writer agent, which also reads the token counts and writes the Lab Project draft, and the draft goes through the draft check. The takeaway is that the only model watching the build is the session doing the work. Everything else is captured by code or read once at the end.
- Working session
- journal.md
- Hooks
- Jev digest
- Writer agent
- Draft (start or end)
- Draft check (start or end)
Connections
- Working session to journal.md: logs
- journal.md to Jev digest
- Hooks to Jev digest: prompts
- Jev digest to Writer agent: brief
- Hooks to Writer agent: tokens (optional or async)
- Writer agent to Draft
- Draft to Draft check
Jev reads the record once, at wrap-up
Miles asked for TypeSafe wherever it saves usage. Claude kept Jev, TypeSafe's model, out of the hooks, because they have to be instant, offline and unable to fail a session. Jev runs once instead, in a digest step at wrap-up, and reads the raw record so the writer does not have to.
The record
Prompts stay on the machine
The journal, sessions.jsonl and meta.json are committed. Prompts go, redacted with the same redactFreeText the Ask assistant uses, to a folder under ~/.claude/lab-journals that every workspace and worktree on the machine shares.
Rejected: everything in the repository, which travels with the branch and publishes raw prompts, and a gitignored folder in the checkout, which a second Conductor workspace would not see. The cost is that prompt history does not follow the work to another machine. It was Claude's call, made without asking because it is the safe default and reversible, and flagged to Miles.
Never journal main
A review of the docs found that a journal listed on main captured every session anyone ran there, whatever feature it was for. Miles's call: main is never live, and start and resume refuse on it. Rejected: only removing main from the journals' meta.json, since the next resume on main would bring it back. The cost is that a journal cannot follow work done directly on main. Branch first.
The same change strips the block Conductor adds to a chat's first prompt, which the capture had kept. Both fixes are a string compare and a regex, with no extra git calls or model requests. Miles had asked for exactly that:
make sure we do not do anything here that causes excessive token usage or make things more costly.
Media
Screenshots from Miles's own Chrome
Miles's call: the writer takes its screenshots through the Claude in Chrome extension, in the Chrome window he is already signed into. For admin shots he signs in first and the agent moves around the admin. A reference site uses the same browser.
Rejected: a Playwright capture script with its own admin login, which needs a stored password or a saved login state on every machine, and fixture data unless it points at production. The extension shares the browser's login state, and its screenshot tool can save the image to disk, so the file goes straight to pnpm cms:upload. The cost is that the writer needs a session started with claude --chrome and a visible Chrome window.
The scripts and the MCP agree on one site
An upload has to land on the site the MCP drafts on, or its media id names nothing there, or another image. The first fix was scripts/cms-target.ts, one place that resolves the key and the site for every agent script. It refuses a local site unless --local is passed, and the upload prints the site it went to. Rejected: asking the MCP server which site it is, which a script cannot see, and defaulting to production, since agent uploads land public-approved and a contributor testing locally would publish by accident. Miles confirmed production as the target.
That version still made a person copy the MCP key and the production URL into .env. Miles pointed out that the sas-cms MCP is already configured for the whole machine. The scripts now read the site and the key from the sas-cms entry in ~/.claude.json, so they cannot disagree with the MCP about which database they are in. Environment variables still win, for CI, Codex and Cursor.
Voice
The house voice, in three layers
Miles asked that every piece of text the writer produces follow the editorial guide's voice, tone and style, with TypeSafe wherever a judgment fits. He noted the guide was current on voice and stale on composition: its page structure describes service pages built with an older block set.
Three layers, each catching what the one before cannot. The rules move to one file, docs/editorial/voice.md, and a test keeps the code's phrase lists identical to it. A Payload plugin refuses, by path, any save from an API key that holds an em dash or a banned phrase. A check reads the rest, with code for what is exact and Jev for what is a judgment. Only the gate can promise anything. The other two report.
Rejected: rewriting the guide, because the ask was copy that follows it. A Claude review of each draft was rejected too, since it costs what the draft cost and judges with the writer's own bias. People editing in the admin are not gated, because a quotation or a client's own words may hold a phrase the doc bans.
Description and steps
One file, docs/editorial/voice.md, holds the house voice. A test keeps voice.ts, the same rules as code, identical to it, and one line of the rules goes into the Ask assistant's system prompt. From voice.ts the rules reach the save gate, which refuses an API key's save that holds an em dash or a banned phrase on every collection, and the check, pnpm editorial:voice, whose report also ends the draft check. The takeaway is that the voice work reaches past the lab pipeline: the gate covers the whole CMS, and the Ask assistant reads the same rules.
- voice.md (start or end)
- voice.ts
- Ask prompt
- Save gate
- Voice check
- Draft check
Connections
- voice.md to voice.ts: tested
- voice.md to Ask prompt: one line
- voice.ts to Save gate
- voice.ts to Voice check
- Voice check to Draft check
One Lab Project, the voice work included
When the voice work began, the journal noted that Miles had opened it as a continuation of this feature, that the save-time plugin and the Ask prompt reach beyond the journal, and that pnpm lab:journal window could split it off if it became its own Lab Project. On 2026-09-27, before this write-up, Miles settled it:
scope is one lab project no splitting.
The case for one entry: the voice check is the writer's quality gate. The cost is that this entry has to say where else the voice work reaches. The save-time gate sits on every collection in the CMS. And the same rules, cut to one line, sit in the Ask assistant's system prompt, so what a visitor reads there follows them too.
Headings
Beat headings come from the record
A Story beats block printed a heading only when the page set one, by design. Miles's call reversed that design note and the agent's recommendation, which was to write a page heading on each block that needed one, taken from the record by hand. In his words:
incorrect it should fallback to heading only overrides can "override" it. story beat headings would need to follow proper heading hierarchy with correct heading type set. use /typesafe:typesafe-ai to classify this automatically in the future.
The rejected option put the same words in two places and left 64 blocks on the two Lab Pages to fill in by hand each time a page is composed. The chosen rule has a cost of its own. The Prose heading usually prints the first beat's heading already, so something has to decide when a beat heading only restates the heading above it, and at what level a beat sits. That judgment went to Jev.

A beat carries its own public heading on the record.

On the page, the heading field only overrides the record.
Build
Four hooks, and what each costs
Built in the order below, in seven sessions between 16:33 UTC on 2026-09-21 and 01:56 UTC the next morning: the journal tooling first, run on its own build, then the writer's figures, screenshots and draft check, then the voice layers and record beat headings. Two pull requests carried it to main.
Four Claude Code hooks are registered in .claude/settings.json, SessionStart, UserPromptSubmit, Stop and SessionEnd, and each runs scripts/lab-journal/hook.ts. Between them they capture each prompt, redacted, and each session's token counts, with no model involved.
Measured on this machine, one run each: a trivial TypeScript file starts in 52 ms under Node's native type stripping and 164 ms under tsx, and the real prompt hook took 163 ms end to end. tsx was kept. It matches every other script in the repository, so the hook reuses Ask's redaction instead of a copy. The prompt and Stop hooks run async, so those 163 ms never delay a reply. Only SessionStart is waited for, once per session.
Run
Milliseconds
Description and data table
A horizontal bar chart of three timed runs. A trivial TypeScript file starts in 52 milliseconds under Node's native type stripping and in 164 under tsx. The real prompt hook, under tsx, took 163 milliseconds end to end. The takeaway is that tsx costs about three times as much to start, which does not matter here because the prompt hook runs async.
| Run | Start to exit |
|---|---|
| Native, trivial file | 52 |
| tsx, trivial file | 164 |
| tsx, prompt hook | 163 |
Two of the four entries in .claude/settings.json. Stop and SessionEnd run the same script.
First version
Built, then run on its own build
By 16:47 UTC the first version existed: scripts/lab-journal with its library, hook, command line and digest, ten unit tests, the lab-journal skill, the lab-project-writer agent and the four hooks. Every hook event was exercised by hand with real payload shapes, and start, log, status and the digest ran for real against the session that built them.
The milestone also listed what was not verified. The hooks had not fired from Claude Code itself, which needed a new session. The writer had never run. The digest thresholds were untuned defaults.
A guide the team can follow
Miles asked for a guide he and the team could follow in order, ending with the feature as a Lab Project and a new Lab Page. It is docs/lab-journal/README.md, beside the journals: setup per machine, the order of a feature, the eight finishing steps from final milestone to publish and wrap, team rules, a symptom table and a checklist. The skill stays the agent's contract, and the guide points at it.
Writing the steps down exposed two gaps. Nothing could capture a session the hooks had missed, so pnpm lab:journal sync now takes a session id. The writer said nothing about images. One claim in the first draft was also wrong: a wrapped journal can be resumed, so wrapping stops capture instead of ending it for good.
Wrap-up
The digest and its figure plan
The digest, scripts/lab-journal/digest.ts, reads the raw record once and asks Jev about each item on its own. A prompt gets three questions: what it was doing, how much of the story it tells, and whether it must not be published. A journal entry gets its story section, and in the same request whether it could carry a screenshot, a diagram, a code listing or a chart. For each agent message of 280 characters or more, four questions ask whether it reads as a decision, a problem, a measurement or a lesson, so an unlogged moment can be found without rereading transcripts.
Raw probabilities are cached, the thresholds live in one constant, and an item already judged is never sent again. The writer starts from what comes out: the prompts worth quoting and the ones held back, each entry's story section, a figure plan, and candidate moments to check against the journal.
Description and messages
A sequence between the digest script, Jev and the private digest file. For each prompt the digest asks Jev what it was doing, how much of the story it tells and whether it is sensitive, and learns whether to offer it for quoting or hold it back. For each journal entry it asks for the story section and the four figure questions, and gets a section and a place in the figure plan. For each long agent message it asks whether it reads as a moment nobody logged, and gets a decision, problem, measurement or lesson score. Then it writes the brief to digest.md, outside the repository. The takeaway is that every item is judged on its own, in one pass, before the writer starts.
- Digest script to Jev: Each prompt: what, how much, sensitive?
- Jev to Digest script: Quote it, or hold it back (reply)
- Digest script to Jev: Each entry: section, four figures?
- Jev to Digest script: Section and figure plan (reply)
- Digest script to Jev: Each long message: a missed moment?
- Jev to Digest script: Decision, problem, measurement, lesson (reply)
- Digest script to digest.md: Write the brief
Checking the draft
A draft check held against the journal
pnpm lab:journal:verify holds a Lab Project draft against the journal. Code goes first, because code is exact: it lists every em dash and every number in the draft that the record does not hold. Then Jev takes each sentence with the three journal entries that share the most words with it and answers supports, contradicts or says nothing, following TypeSafe's citation check cookbook. A second question over the same state asks whether the sentence states a checkable fact at all, so a line of connective prose is not reported as unsupported.
The idea came from Miles asking whether TypeSafe could make the writer's media work cheaper and better. Jev 1.13 takes text only, so it cannot look at a screenshot, and that check stays with Claude. What it can do is plan the figures before the writer starts and hold the finished draft to the record.
Description and steps
A flow for one Lab Project draft. The draft is split into sentences. Code lists every em dash and every number the record does not hold. Jev then judges each sentence against the three journal entries nearest to it, with a verdict and whether it states a checkable fact. A checkable sentence is listed when the record contradicts it, says nothing about it, or supports it with low confidence. Anything else is skipped as connective prose. What is listed goes into verify.md, which ends with the voice check. The takeaway is that code handles what is exact and Jev only what needs judgment.
- Draft (start or end)
- Sentences
- Code: dashes, numbers
- Jev: verdict
- Checkable? (decision)
- Listed
- Skipped (start or end)
- verify.md (start or end)
Connections
- Draft to Sentences
- Sentences to Code: dashes, numbers
- Code: dashes, numbers to Jev: verdict
- Jev: verdict to Checkable?
- Checkable? to Listed: yes
- Checkable? to Skipped: no
- Listed to verify.md
The writer, rewritten for figures and screenshots
By 19:56 UTC the writer could plan figures, take screenshots and check its own draft. The digest writes a figure plan. pnpm lab:journal shot files a screenshot in the journal's private media folder while the feature is built. The writer's brief was rewritten around a setup check, the figure bar of the two published pages, screenshots through Miles's Chrome, and a verify run and a preview check before it reports. The capture steps went into the article-authoring skill, so any article can use them.
The Chrome capture itself was not verified. The Conductor session that built it had no browser tools, and whether a subagent gets them was an open question.
Voice
One file for the voice
docs/editorial/voice.md now holds the house voice: how it should feel, sentences, punctuation, the language to favor and to avoid, the contrast frames, the flattened claims, and a table of what is checked where. The editorial guide's voice section points at it, and a note at the guide's top marks its composition sections as the service-page system that predates Sections, Story beats and Lab Pages.
src/features/editorial/voice.ts holds the same rules as code. Its test reads voice.md and fails if the two lists differ, so the two cannot drift.

One table in voice.md says which rule is enforced where.
A gate at save time
src/plugins/house-style adds a beforeChange hook to every collection. A save from an API key that holds an em dash or a banned phrase is refused, every problem in one error with its path, the way the figures plugin refuses a bad spec. Autosave and team members in the admin are exempt. Nine unit tests cover it, and the MCP server instructions mention it in the room their 2048 character limit leaves.
When it shipped, the refusal was covered by those unit tests only. The workspace had no dev server of its own running to try it against.
Voice check
Jev reads for what code cannot
The check, pnpm editorial:voice, runs on any Lab Project, any document by collection and id, a file or a string, and pnpm lab:journal:verify ends with the same Voice section. Code lints what is exact: banned phrases, contrast frames, semicolons, paragraph length. Jev scores each paragraph on the guide's own terms: specific or generic, composed or inflated, human or formulaic, and whether it closes on a punchline. For headings, Jev names each one's grammatical form and code flags a run of one form.
Jev cannot read taste. The check finds the faults the guide names, not whether a line is good.
The strict pass on the published pages
With the check built, Miles asked for a strict pass on the published Lab Pages. The check's lists were read in full. Then every passage of Lab Projects 2 and 3 and Lab Pages 1 and 2 was read against voice.md by hand, because Jev lists what the doc names and a pass has to read the rest.
Nine lines changed, as drafts. One used a banned verb with an ending the gate's first pattern had missed, so the gate now catches a banned single word in any form, with a test. Others were a ranking with no particular behind it, a stock phrase used twice, a rhetorical setup and a caption that repeated a line the page already carried. The lines Jev had read as generic stayed. Each was an abstract line the studio means, such as "The website used to be the thing we owned. Now it is the first thing we do with what we own."
Headings
Headings judged when the page is saved
At render, src/blocks/shared/story-headings.ts finds the Section's first Prose heading and its level. Each beat prints its record heading one level under it, is hidden when it repeats the opener exactly, and gives way to the block's own heading when the page overrides it. Nothing at render calls a model.
What code cannot read goes to Jev when the page is saved. A plugin on the collections that offer the block, Lab Pages and Work Pages, asks two questions about each beat that code has not settled: does its heading only restate the opener, and does it open a subsection or a short passage. A subsection sits one level under the opener, a passage two. The answers are stored on the block with a hash of what was judged, and the renderer trusts one only while its hash matches. Autosave is exempt, and a failure leaves the beat to the code rule, so a save never fails for it.
The stored answers needed one new json column on the Story beats tables of both collections and their version tables, so the change waited on a migration, Miles's call.
Description and steps
A decision flow run at render time for each beat that has a heading on its record. If the page sets its own heading, that override prints. Otherwise, if the answer Jev stored at the last save still matches what it read, the heading prints or hides at the level Jev chose. If there is no matching answer, code hides a heading that exactly repeats the Section's opener and prints any other one level under it. The takeaway is that nothing at render calls a model: Jev judges once, when the page is saved, and its answer is used only while it is still current.
- Beat heading (start or end)
- Override? (decision)
- Override (start or end)
- Jev's answer? (decision)
- Jev's call (start or end)
- Repeat? (decision)
- Hidden (start or end)
- Printed (start or end)
Connections
- Beat heading to Override?
- Override? to Override: yes
- Override? to Jev's answer?: no
- Jev's answer? to Jev's call: current
- Jev's answer? to Repeat?: no
- Repeat? to Hidden: exact
- Repeat? to Printed: no

The record's beat heading prints one level under the Section's opener.
Shipping
Two pull requests, two merge commits
PR 18 went to main at 23:08 UTC on 2026-09-21, after Miles started a new Conductor workspace and found none of the work in it, since a workspace starts from main. His call was to merge then and keep working after. It went in as a merge commit, not a squash, so the branch and main share history. GitHub's merge call answered with a server error twice and then an empty body, and the merge had gone through anyway. The pull request's state was the thing to check.
PR 19 followed at 01:56 UTC with the voice layers, the strict pass and the record beat headings, again as a merge commit. The schema check found the one additive column, heading_auto. A migration with exactly that change was already in the tree when pnpm migrate:create ran, the command added nothing, and pnpm check:migrations and the drift check both passed against it.
Working with AI
How the work was split
Miles set the direction and made the calls the journal marks as his: build it, use TypeSafe, never journal main, screenshots from his own Chrome, merge PR 18 early, headings from the record, one Lab Project. Claude designed and built the tooling, wrote the docs, logged the journal entries, and made the calls the journal marks as Claude's, such as keeping prompts out of the repository. Jev judged the record and the drafts. No model ran in the hooks.
Seven Claude Code sessions built it, between 16:33 UTC on 2026-09-21 and 01:56 UTC the next morning. Two models shared them. The model claude-fable-5-1 carried the three long sessions: 2,668 input, 318,339 output, 1,191,464 cache write and 47,836,354 cache read tokens. The other, claude-opus-5, ran four short sessions and part of a long one, for 152 input, 41,453 output, 319,049 cache write and 5,330,506 cache read. Cache reads are most of both totals, and they do not compare with output.
The write-up runs on claude-opus-5-5 in further sessions on 2026-09-27, counted in the same file and not in these sums. Jev's runs are logged apart, in jev.jsonl. The journal puts the first digest at 17 requests and 13,556 input tokens, each of three figure-plan reruns at about 55,000 input tokens, and the planted draft check at 5 requests and 6,052. Four of those rows were restored by hand after a careless git checkout dropped them, with approximate times. The voice calibration and the heading judgments ran outside that file, and their costs are in their own entries.
Description and messages
A sequence between Miles, Claude, the hooks and Jev. Miles sends a prompt or makes a call. The hooks save the prompt, redacted, and Claude logs the journal entry. When a turn stops, the hooks count its tokens. At wrap-up Claude asks Jev to digest the record and gets back story sections, quotable prompts and a figure plan, then asks Jev to check each draft sentence and gets supports, contradicts or says nothing. Claude hands Miles drafts that are never published, and Miles reviews and publishes. The takeaway is that the only model watching the build is the one doing the work, and a person publishes.
- Miles to Claude: A prompt, or a call to make
- Hooks, to itself: Save prompt, redacted
- Claude, to itself: Log the entry
- Hooks, to itself: Count tokens on Stop
- Claude to Jev: Digest the record
- Jev to Claude: Sections, quotes, figure plan (reply)
- Claude to Jev: Check each draft sentence
- Jev to Claude: Supports, contradicts, says nothing (reply)
- Claude to Miles: Drafts, never published
- Miles, to itself: Review, then publish
- Output
- Cache write
- Cache read
Session
Tokens
Description and data table
A horizontal bar chart of nine finished sessions in date order, each with its output, cache write and cache read tokens. Cache reads dominate every session. The longest build session, starting 23:50 UTC on 21 September, read 20.8 million tokens from cache and wrote 154 thousand output tokens. The first, at 16:33, read 16.4 million and wrote 103 thousand. Three short sessions on 21 September read under a million each. The takeaway is that cache reads, the context read again on every message, are nearly all of the volume and are not comparable to output.
| Session | Output | Cache write | Cache read |
|---|---|---|---|
| 21 Sep 16:33 | 103.2K | 158.7K | 16.4M |
| 21 Sep 17:53 | 4K | 47.9K | 867.9K |
| 21 Sep 18:05 | 1.4K | 39.5K | 256.3K |
| 21 Sep 18:11 | 23.3K | 104.2K | 2.7M |
| 21 Sep 18:32 | 71.8K | 478.1K | 11.9M |
| 21 Sep 19:28 | 2.1K | 41.9K | 215.1K |
| 21 Sep 23:50 | 153.9K | 640.2K | 20.8M |
| 27 Sep 17:00 | 1.5K | 52.6K | 534.9K |
| 27 Sep 17:03 | 11.7K | 100.9K | 1.7M |
Outcome
What was measured
What was measured while it was built, and how. Each number is a smoke test on one journal, two approved projects or one page, and none is an accuracy figure.
The first digest, on this journal
The first digest ran on the record of the session that built it: 4 prompts, 10 journal entries and 3 agent messages long enough to judge. It made 17 Jev requests on jev-1.13.0, 13,556 input tokens, in 1.2 seconds of wall clock, from the script's own output. It offered 3 of the 4 prompts for quoting, held none back, placed all 10 entries in a story section, and flagged 3 agent messages as candidate moments, all already logged.
What the same pass would cost Claude was not measured: one session is too small to show it. The digest behind this entry judged 37 entries in 61 requests and 57,759 input tokens.
The draft check on a planted draft
The first test of pnpm lab:journal:verify was a five sentence draft with three planted faults: an em dash in the title, an invented result of 83 percent across 412 sessions, and a reversed decision claiming every prompt was committed to the repository. Code caught the dash and both numbers. Jev marked the reversed decision as contradicted at confidence 1.00 and the three true sentences as supported at 0.81 to 0.99, for 5 requests and 6,052 input tokens.
The invented result came back as says nothing at 1.00, and was hidden at first. The second question, whether the sentence states a checkable fact, scored it 0.47 against a gate of 0.5. The gate is now 0.25. One planted test is not an accuracy figure, and the thresholds stay untuned until a real write-up has been through it.
Calibration
The voice check against approved copy
pnpm editorial:voice ran on 2026-09-21 against copy Miles had approved, Lab Project 3 with 70 paragraphs and Lab Project 2 with 48, and a planted four paragraph agency draft written to break every rule in voice.md. Code found no em dash and no banned phrase in any approved copy field, and refused the planted draft at every paragraph.
At the first thresholds Jev listed approved paragraphs too. The approved passages it read as inflated were the confident assertions the doc asks for, at 0.60 to 0.78, against 0.87 to 0.94 for the planted hype. Approved copy read formulaic at 0.68 at most, the planted draft from 0.79. So the thresholds moved into the gaps: inflated 0.8, formulaic 0.75, generic 0.5.
After the move Lab Project 3 lists 7 generic passages and nothing else, Lab Project 2 lists nothing, and the planted draft still lists 3 of 4 as generic and 4 of 4 on the other two. The runs cost 119 requests and 76,854 input tokens for Lab Project 3, 71 and 47,517 for Lab Project 2, and 8 and 4,987 for the planted draft.
- Lab Project 3
- Lab Project 2
- Planted draft
Share of paragraphs listed
Listed as
Description and data table
A grouped bar chart of the share of paragraphs Jev listed at its first thresholds, for two approved Lab Projects and a planted agency draft. The planted draft was listed at every paragraph as generic, inflated and formulaic. Approved copy was listed far less: 14 percent of Lab Project 3 and 4 percent of Lab Project 2 as generic, and between 1 and 6 percent as inflated or formulaic. The takeaway is that approved and planted copy scored apart, and the thresholds were moved into that gap.
| Listed as | Lab Project 3 | Lab Project 2 | Planted draft |
|---|---|---|---|
| Generic | 14.3% | 4.2% | 100% |
| Inflated | 1.4% | 6.3% | 100% |
| Formulaic | 4.3% | 6.3% | 100% |
Headings
Beat headings on Lab Page 2
The heading rule and the save-time judgment were run over Lab Page 2, From Webflow to Payload, on 2026-09-22: 24 Sections and 48 beats. Of those, 33 beat headings print and 15 are hidden. Code settled 13, all exact repeats of the Section's Prose heading and all the first beat of their Section. Jev was asked about the other 35, one request each.
It differed from the code rule on two, both paraphrases code cannot see, and both right: "Every case study in the same template" under "One template for every case study", and "A Content Hub and a website" under "Two layers: a Content Hub and a website". Every other beat read 0.05 to 0.29 against a threshold of 0.6. On level, Jev called 22 beats a subsection and 13 a passage: the passages are the short single-idea beats, the subsections the multi-paragraph steps. One page is not an accuracy figure.
Outcome
Beat headings
Description and data table
A horizontal bar chart of the 48 beats on Lab Page 2. 33 headings print. 13 are hidden by code as exact repeats of the Section's opener, and 2 more are hidden by Jev as paraphrases. The takeaway is that code settles most of the hidden headings, and Jev adds the two paraphrases code cannot see.
| Outcome | Beats |
|---|---|
| Printed | 33 |
| Hidden by code | 13 |
| Hidden by Jev | 2 |
The strict pass, checked line by line
The nine edits were saved as drafts: six beats on Lab Project 3, one beat and the medium summary on Lab Project 2, and one caption on Lab Page 2. A dump of every passage before and after showed exactly those lines changed and nothing else: 236 lines on Lab Project 3, 140 on Lab Project 2 and 164 on the page, the same counts before and after.
On the rerun nothing was refused and no passage read as inflated or formulaic. Jev was asked only about the changed passages, 6, 2 and 1 requests, because its cache is keyed on the passage text. By 2026-09-27 Miles had published all three.
Learnings
What we took from it
Five lessons, each from a moment logged above, and what the build left open.
The bugs lived where the tests did not reach
Unit tests passed before the first live run, and it still took the wrong session and named the private folder after the template. Both bugs lived in the part that touches the real machine: two sessions open in one checkout, a package name nobody had changed. The mixed journal was the same kind of fault, one no test would catch, found by a person reading what the tool had written.
Read the state, not the summary
The wrong session was caught by reading the session id in the output instead of trusting the summary line. The first merge answered with two server errors and an empty body, and had succeeded all the same. In both cases the useful check was the thing itself: the id in the output, the state of the pull request.
Ask Jev what an entry is mainly about
Jev reads a question literally. Asked whether a diagram could show an entry, it said yes to every one, because anything could be drawn. Asked what the entry is mainly about, with the near miss written into the false side, the answers spread and five entries passed.
Jev lists, a person decides
Against approved copy, Jev's generic list held abstract thesis sentences the studio means. It cannot tell a studio's own idea stated plainly from agency copy, so that list is one to read, not one to cut. The strict pass kept every one of those lines and changed nine others, found by reading every passage by hand.
Only the gate promises anything
The voice rules reach an agent in the server instructions, in the skills and in the check. Only the save-time gate can promise anything, and only for what is exact, an em dash or a banned phrase. The other two layers report. Even the exact part needed care: a banned verb with an ending slipped past the gate's first pattern, in copy that had already been approved.
What is still open
The figure plan's threshold of 0.5 and the draft check's 0.25 and 0.6 are untuned defaults, waiting on a real write-up. Nothing detects that a session has changed subject. Prompt history does not follow the work to another machine, so the digest and the writer should run where most of the work happened. Lab Page 1 was last saved before the heading hook shipped, so only the exact-repeat rule applies there until it is saved once. When the record closed, the lab-project-writer had not had its first real run.
About this project
- Status
- In progress
- Built with
- Claude CodeTypeSafe JevPayload CMSTypeScriptModel Context ProtocolClaude in Chrome