The operating half of Ink, answering the owner's audit (2026-09-02):「do we have all the design decisions?」— the reader-facing half is decided (the essays · the look); this page proposes the four that weren't: the nightly pipeline, the slate policy (how many · which beats · which formats · who writes how often), where editions live, and the console's Ink tab. Recommendations are marked; the owner's calls are collected at the foot.
| Decision | State | Where |
|---|---|---|
| Feed layout + the day-seeded composer | decided · built | ink-design · lib/ink_compose.py |
| Article ceremony · peg strip + in-app peg card · ⋯ menu | decided · built | ink-design · v1042–v1044 |
| Formats (9, likeliness not assignment) · triggers (8 + the why-now contract) | decided | vibes-essays |
| First-hand boundary · vantage (alive now, remembering then) · anecdote budget + memory bank | decided | vibes-essays |
| Tone gate → THE EDITOR: the AI-tell lint (en + zh) · a RIVAL model's three positive passes · exemplars of the persona's own prose in the brief | BUILT 09-02 (§3f) · author = DeepSeek V4 Pro thinking, Flash failover; editor = Gemini 3.8 Flash (3.7 until 09-09) | this page §3f · lib/ink_lint.py |
| Hero art: DeepSeek art-directs → flash-image, batch mode | decided · proven | ink-design |
| Language: one essay, frozen renditions, switch in the ⋯ | decided · mocked | ink-design §Language |
| The door = a room seeded by the essay + the essay as a SPECIAL BUBBLE | BUILT 09-02 (v1054): one private room per reader per piece; the essay frozen in as the room's first linked page; its card as the author's first bubble (the link-bubble grammar; a tap reopens the reader) | vibes-essays · /api/ink/door |
| The nightly pipeline (stages · timing · failure rules) | decided · BUILT 09-02 (lib/ink_brew.py, §5) | this page §1 · §5 |
| The slate policy (count · beats · formats · cadence · language) | decided 09-02 · 16/day, the 8 beats stand | this page §3 |
| The rhythm (heavy · mid · light — the kicker) + the light formats | built into the editor's brief, the validator and ink_compose H6–H8 · funny still unproven | this page §3b · §5 |
| Topic granularity (coarse beats · fine tags) | tags minted per piece from the first brew; not yet reader-visible | this page §3c |
| The anecdote bank — built? backfill? | the ledger + the dealt hand are live; the bank is the Vibes voice card, extracted lazily on first commission | this page §3d · §5 |
| Where editions live on the box | open → proposed below | this page |
| The console's Ink tab | BUILT 09-02 (v1053) — editions · the brew · policy knobs · kill switch · Brew now | this page §4 |
state.json, in brew.log and as a notice on every admin's rail (ink_failed, one row per day — a second failure the same night replaces it; a pass that later publishes the day takes the row back). A day published below the floor knob raises the same notice (「published short: 4 pieces, floor 12」). The brew reaches the rail through one seam (ink_brew.NOTIFY): the bare timers write the durable rows straight into the DB; a console-triggered brew runs inside the app, which hangs its own callback on the seam so the notice also mirrors live over SSE and to the lock screen.batches.cancel) and is painted sync at full price — 16 pictures in parallel ×4 is ~2 minutes, worst case +70¢ on the day. If sync fails too (a Gemini outage), the composer demotes heroless pieces to compact rows and picks the lead among the pieces that have art — the edition still flips at 07:30. A brew never misses the deadline for art.ink_compose assigns roles by the day seed; the edition is written as edition-YYYY-MM-DD.json with prev chained, then edition.json flips to it (atomic rename) — the threshold archive builds itself./var/lib/mad/ink (beside rooms/ and the built personas), served by the existing /ink mount pointed at MAD_INK_DIR (default lib/ink for dev, which keeps the specimen editions in-repo as fixtures). Retained forever — the archive is the product's memory, and the threshold needs it. A nightly brew writes ~1 MB (8 heroes + JSON); a year is ~400 MB.| Axis | Policy (the validator enforces; the editor is briefed) |
|---|---|
| Count | 16/day (12–20). Commission N+2, ship N. A console knob — the owner reads all 16 for now and will size it by what holds up. |
| Beats (8 house beats) | World · Science · Tech & AI · Markets · Culture · Sports · China · Life & relationships. A day covers ≥6 beats, no beat more than 4 pieces; the beat least covered over the trailing week gets priority. (Owner 09-02: the combo stands; finer topics ride the tag layer, §3c.) |
| Register (new axis) | Every commission carries heavy · mid · light. Per 16: ~6 heavy · ~6 mid · ~4 light; never two heavies adjacent in the composed order; the piece before the band is light (the kicker); one light piece rides front-of-book right after the lead. §3b. |
| Formats | The plain essay ≤ 6 of 16; ≥4 distinct other formats a day; ≥2 light-format pieces a day (§3b); fable and notes ≤ 1 each per day; ≥1 dialogue a week, ≥2 advice columns a week once reader letters exist; the response (商榷) whenever a piece has provoked rooms. |
| Triggers | Per 16: ~7 breaking · ~4 calendar · ~2 reply/continuity · ~2 appointment columns (running-title recurring slots) · ~1 no-peg light piece (the kicker may be pegged to nothing but the day). |
| Language | A persona writes in its own tongue — owner's ruling 09-03, reversing 09-02's「everyone writes in English」: a Chinese writer (the card's writes: zh, else a Han-script name that is not Japanese — native_lang) writes in Chinese and is rendered INTO English by the same two-pass method under the AI-tell lint; everyone else writes in English and the Chinese reader gets the persona-voiced rendition (frozen at brew time, the honesty pill under the byline). 09-04: the rendition brief carries the reporter's notebook, the house glossary (lib/ink/glossary-zh.json) and the names rule (people · companies · products · publications in the original script; places and long-dead figures in Chinese); 09-06 — the slate fills (owner: 16 asked, 11 written, 8 published): the budget meeting used to be told only three days of titles, proposed ten subjects the shelf's cap then refused (once on the word「riddle」— a format, not a subject; once on one shared tag,「central-banks」), re-asked once and went ahead with 11; the gate then dropped three on triples a generic editor pass had left standing. Now the meeting reads the shelf's last four days with their SUBJECT WORDS named and only the pegs still open (the same rule the validator applies, told up front), commissions n + 4 (four spares), and the re-ask is spent only on a real shortage; the cap fires on two shared subject words, never a format or beat word; and the editor's two passes read the lint's own hits — the exact triple, the exact word — so nothing new is spent (owner:「the design is the solution, not more passes」). 09-05: a rendition that still fails the lint SHIPS, flagged lint_fail — never withheld (owner: every non-English piece gets its English); the「wrote this after」line is rendered with it. measured with exam/rendition_ab.py: the machine judge preferred the two passes with thinking, the owner's read preferred the cheap re-authoring arm (one call, thinking off, ~0.6¢, 20 s) for fluency — it ships, with the periodical rhythm (财新 · 三联: 20–45 字 sentences of two or three 分句, never three short ones in a row) in the brief (ink-quality §4, the pairs page). |
| Cadence | ≤2 pieces per persona per week · every public persona writes ≥1 a month (the whole cast stays exposed) · a new public persona debuts within 7 days · the used-anchor ledger caps repetition. |
| Lead | The editor's #1 by significance + art strength; never the same persona two days running; a zh lead at least weekly. |
The mechanism is the house one: these are constraints in the editor's brief (so it composes toward them) and checks in code (so a miss is caught, not hoped against) — one re-ask, then runner-up swaps. All numbers are console knobs, not constants.
| Publisher | The light slot | The rule we take |
|---|---|---|
| Broadcast news / the wire | the kicker —「and finally…」— the last item is light, by rule, every bulletin | the piece before the band is always light |
| Axios (AM · PM · every vertical) | ends every edition with 「1 fun thing」; the body is bulleted and heavy | the kicker is a fixed, named slot readers learn to expect |
| NYT The Morning | news → Morning Reads (lighter) → culture → Play (games) | the register descends through the edition; games at the tail — we have the toolbox |
| The New Yorker | Shouts & Murmurs (satire, ~600 words), cartoons and the caption contest sit between the long pieces; Talk of the Town opens light | light pieces are short and interleaved, never a ghetto at the end only |
| The Economist | the back half — Bartleby, Johnson, the obituary, Books & arts; Espresso's quote of the day | magazine anatomy: front of book · the well · back of book — light at both ends, the weight in the middle. The Ink Well already exists. |
| WeChat 公众号 · 人民日报 | 夜读 · 副刊 — the evening light read beside the day's news | a zh light slot is natural, not an import |
The proposal. Register becomes a third axis of every commission (beat · format · register), briefed to the editor and enforced by ink_compose as a harmony constraint: H6 never two heavies adjacent, H7 the kicker is light, H8 one light front-of-book. Four light formats join the nine:
| App | Coarse layer | Fine layer |
|---|---|---|
| Google News | 8 top-level sections | thousands of followable topics, machine-minted |
| Apple News | ~10 channels on the tab bar | hundreds of topics + publishers to follow |
| 小红书 | ~20 channel chips | open hashtags, the feed learns from them |
| Substack · Medium | ~30 categories | open tags per post |
| The Economist · NYT | a dozen sections, fixed for decades | subjects and series inside them |
The proposal: keep the 8 beats closed and coarse, and mint an open tag layer now. The beat is the editorial constraint (what a day must cover) and the reader's rubric (World · Essay); it can split later — when a beat exceeds ~30% of a month's pieces, it forks (Tech & AI → Tech · AI). Tags are the fine layer: 2–3 per piece, chosen by the editor from a house vocabulary file (lib/ink/tags.json) that grows — a new tag is allowed but must be normalized (Fed, not Federal Reserve and the Fed), and a tag becomes reader-visible only once three pieces carry it. Tags cost nothing at brew time and are expensive to add later (retro-tagging every edition), which is why they are minted from day one even though no reader sees them yet. They feed, in order: search · the Metrics tab (which subjects draw rooms) · follow-a-topic · personalization, when there are enough readers to personalize for. Coarse now, finer in the future — with the data already there.
Solved on paper, not in code. The five mechanisms on the essays page (the used-anchor ledger · the anti-echo block · method over memoir · the corpus grows in-world · the memory bank outside profile.md) are designed and two are proven in the Vibes engine; none runs for essays yet, because the brew doesn't exist. They ship as stage 0 of the brew, not as a separate project.
The corpus is mostly there; the bank is not. The repo holds 88 personas, 85 with a corpus/; the 27 app-built personas have none. So the backfill is not a research harvest — it is a distillation: run the existing vibes_extract anchor-bank pass over every corpus into anchors.json beside the profile (30–60 dated, sourced anchors each, tagged marquee / minor / method), one V4 call per persona ≈ 1–2¢ → ≈ $1.50 for the whole library, an hour off-peak. The three corpus-less repo personas and the app-built ones get the builder's research step (a few SERP calls) or, for invented personas, a frozen invented ledger — made once, canonical forever, so the character never contradicts itself.
Then the bank feeds itself. Each commission deals 2–4 unburned anchors (≤1 marquee), the ledger burns them with a 90-day cool-down, and the writer stage runs 1–2 SERP calls around persona × peg and files any new sourced fact into the bank — the corpus grows by writing. The arithmetic: 40 anchors, ~1.5 pieces a week at 16/day over 88 personas, ~2 anchors a piece → a first full cycle takes ~3 months, by which time the bank has grown and the persona's own published record is material (「as I wrote in September, and was wrong about」).
Yes — and half the machinery exists. Two stores already carry a persona's past: the live persona memory (docs/live-personas.html row ③, shipped v931 — what a persona remembers across chats) and the brew's ledger (what it published). The proposal joins them with one new artefact, the stance gist:
wrote), global to the persona (an essay is public; every user meets the same author), never per-user.personas-recent/<slug>.md, beside the「recent updates」— same dated-bullet grammar, told apart by tag: the record is the world's truth ([press · Forbes]), the stance is the persona's own opinion, developed by the model (- 2026-09-03 — [stance] I argued that… (The Drums at Hormuz) [ink · 2026-09-03-winston-churchill]). Built: the writer emits its stance in the same call (two lines, first person, present tense); publish appends the bullet (idempotent by piece id; a fixed-horizon persona gets the file created); the weekly refresh job never shows [stance] lines to its router, never counts them against the 60-bullet cap, never drops them; the chat loader excludes them from the「Since the record」block (its cite-only gloss is the opposite of a stance) — they load nowhere until their own block ships behind the baseline A/B. Shipped v1051: the profile's sub-page is「Lately」(owner's pick over「Since the record」/「On the record」; 近况) and shows both kinds — In the record (grouped by sweep, as before) and In Ink (the stances, newest first); a fixed-horizon persona gets the row too once it has a stance (the API's living gate became an empty-file gate). Owed: the stance block in the megaprompt with λ decay, the writer's stance block — both behind the baseline A/B.drafts/. The code picks: a draft that passes the machine check, then one with no disclaimer, then the fewest flagged habits, then (since 09-13) the fewest over-used phrases against the profile's list and the lowest share of paragraphs ending on a That is / It is gavel, then the lowest lint score — the over-use profile §7. No model scores a draft. A local test (four DeepSeek drafts, tonight's assignments) found the drafts far more different than re-rolls or five-in-one-call, and the picked pieces cleaner by count; most drafts ignored the memory they were dealt. At 1 the brew writes one draft, unchanged. Production cost with the Gemini draft: about 3.5¢ a piece for the writing.app.ink_interview.| Finding | Source | What Ink does now |
|---|---|---|
| The tells are a KNOWN LIST: the negation pivot (not X, but Y / it isn't A; it's B), em-dash cadence, the rule of three, the metronome (sentence lengths within a narrow band — GPTZero's original signal), self-answered questions,「here's the thing」, a vocabulary of ~300 words (delve, tapestry, testament, underscore…) | Wikipedia's Signs of AI writing (WikiProject AI Cleanup, ~15k words); the SlopMonster lint | lib/ink_lint.py: regexes only, English and Chinese, run on every piece and every rendition. Zero tolerance on the strong pivot; two plain not…, but contrasts fail (one is speech); three of anything else fails; the metronome alone fails. |
| 「Don't」instructions backfire — the pink-elephant problem; past ~40% negatives the output degrades. Positive instruction + exemplars is what works. | 16x Engineering; the negative-instruction cascade study (2026) | The writer's「avoid」list is gone. In its place: say each thing directly and once · vary your sentence length · end where the thought ends — and THREE PARAGRAPHS OF THE PERSONA'S OWN PROSE. |
| An editor pass works when it is a RIVAL model: lint → cleanse in three positive passes (vocabulary · structure · voice) → lint again; ship only clean.「A model is poor at hearing its own accent.」 | SlopMonster (lint→cleanse→re-lint, 3/5 → 5/5 on a live run) | The gate's editor is Gemini 3.8 Flash (3.7 until 09-09; the writer is DeepSeek): plain words → reshaped sentences → the voice pass with the exemplars; re-lint; still failing → dropped, the spare fills. ~1.5¢ a failing piece. |
| Stripping tells without restoring voice leaves sterile text; voice comes from real prose samples, not a description. | Sudowrite's voice matching; the two-classifier finding | exemplars() mines the persona's corpus for its own QUOTED spans (50–220 words, the researcher's notes filtered out) — 73 of 84 personas have three; they ride the writer's brief and the editor's voice pass. |
| The translator re-introduces the pivot even when the English is clean (Churchill's zh carried four 不是…而是 turns). | this brew's own renditions | The Chinese lint (不是…而是 · 与其说…不如说 · 不在于…而在于 · 不仅…更 · the note-taker's vocabulary · dashes · the metronome) runs on every rendition; a failing one goes through the editor's two zh passes; still failing → the rendition is not shipped (the piece ships English-only). |
The console's edition view shows each piece's lint score and the editor's before → after. The three proof editions were re-gated as the baseline (see the chat of 09-02).
<brew>/driven/<id>.json. About 13¢ and 6½ minutes a piece (the cost, step by step); the searches run one a second by design, so a morning of sixteen finishes inside twenty minutes. Off, the brew writes the old way: several drafts, the code picks and repairs. English commissions only; a Chinese one keeps the old path. The dealt hand, the interview and the shape menu are not used on this path.ink_lint.register: metaphor nouns per 1k · long words per 100 · first person per 100 · mean sentence length · a「The X of the Y」title, each one flag over its allowance), fewest flags first, ties in the editor's order; the top N ship. No model scores a piece in the brew; the IQS below stays a CLI and calibration tool. The same ruling subtracted the writer's brief — see the first-read page §4 for what shipped.| What the field measures | Source | Ink's axis |
|---|---|---|
| The Pulitzer test for editorial writing:「clearness of style, moral purpose, sound reasoning, and power to influence public opinion」; since 2026 folded into Opinion Writing —「well-reasoned and compelling arguments… originally researched and reported, or informed by personal experience」 | pulitzer.org | E · editorial — five lines, 1–5 each with one quoted line of evidence: clarity · reasoning · specificity · a position a reader could disagree with · carry (one thing the reader can repeat tomorrow). Judged by the rival family. |
| LLM judges are usable (~85% agreement with humans, above human–human) IF: a rubric with separated criteria; pairwise with position swap; length-controlled; never the writer's own family; calibrated against human labels monthly | the 2025–26 judge-bias literature | All judges are Gemini Flash (the writer is DeepSeek); the imposter test runs both orderings; the readers' 👍/👎 and door taps are the human labels the judges are calibrated to. |
| The authorship gap: personalized LLM text sits below the human cross-author floor (LUAR 0.48–0.51 vs 0.63) — closer to the model's own fingerprint than to any human. Single metrics lie (cross-metric |r| < 0.07); a judge that reads the same profile the writer read measures instruction-following, not voice (circularity). | PersonalBench (2026); Theory-grounded evaluation of LLM personalization (2026) | V · voice, three independent measures, none of which sees the profile: ① the imposter test — the judge gets the persona's REAL paragraphs and three unlabelled pieces (the persona's + two decoys from the same brew) and must pick the persona's from the writing alone; ② trait match — five checkable writing traits extracted once from the persona's real prose, scored yes/partly/no on the piece; ③ stylometrics in code — function words, punctuation, sentence shape, ranked against the cast (the content-independent authorship signal). The honest expectation from the literature: we will not cross the human floor; we can climb toward it and see which personas fall short. |
| Reported opinion beats asserted opinion: numbers, dates, names, attributed quotes | the Opinion Writing criteria; the specimen review | S · specificity — specifics per 100 words, in code; the reporter's notebook (the next build) is what will move it. |
| The tells are a hard gate, not a score | §3f | L · the lint — a piece with a tell is not scored; it is edited or dropped. |
IQS = 100 × (0.40·E + 0.40·V + 0.20·S), over pieces that pass the lint. The brew now commissions N+2, gates, scores every survivor, and ships the top N by IQS — a spare with a better score beats a weak main. The console's edition view shows each piece's IQS with its E · V · S and the editor's one-line verdict. Cost: about 0.7¢ a piece (four Flash calls; the traits once per persona).
The first reading (09-02, the 17 proof pieces, 12.6¢): IQS ran 47–92. Buffett's letter led (E 1.0 · V 0.87 · S 0.84); Li Ka-shing's letter and Jobs's essay followed; the bottom were Dylan's Shouts (V 0.47, stylo 0), Chestnut's fable (trait match 0.0) and Twain's kicker. Three things the numbers said at once: ① specificity is the weak axis across the board (S 0.10–0.84, most under 0.5) — the reporter's notebook is the lever; ② trait match is the discriminating voice signal (Jobs, Mouratoglou, Banksy 1.0; Churchill, Darwin, Einstein 0.2; Chestnut 0.0) and the judge's own words agree —「Churchillian pastiche whose rhetorical elegance outstrips its content」; ③ the imposter test was too easy (1.0 everywhere): with two other-persona decoys on other subjects the judge was picking by subject, not by voice. Second cut, the same piece rewritten by an anonymous columnist: still 1.0 — the judge picked「the one with a voice」. Third cut (shipped): the decoy is the same piece rewritten in another persona's voice from that persona's own paragraphs, so the judge must tell Churchill from Dylan on the same facts. Re-scored, the test still passes every piece — but a NEGATIVE CONTROL (the key set to Dylan's prose, the texts Churchill's piece, its Musk-voiced decoy and Dan Wang's) answers 0.5 with「none of the options match Dylan」, so the judge is reading style, not guessing: the personas' pieces really are in voices consistent with their samples, and the test is a FLOOR (a piece that failed it would be badly wrong), not a ladder. V is weighted accordingly — trait match ½ (the ladder: 0.0–1.0 across the cast), the imposter floor ¼, the stylometric rank ¼.
How it becomes an optimisation: every knob the brew has — the writer's brief, the exemplar count, the editor's passes, the model, the register mix, the notebook — is changed one at a time and read on IQS over a week of editions, with the readers' verdicts as the check that the judges still agree with people. The persona × topic map (IQS by persona and by beat) is the first thing to read: it says which crossovers work and which personas write as imposters.
python lib/ink_brew.py voices): for the historic greats — Ink's staples — the open web's PRIMARY text (Gutenberg's files, Wikisource, archive.org; a middle window of a long book, past the front matter) fetched raw and Flash copying out verbatim paragraphs written BY the persona; the exemplar bank grows (cap 24 since 09-03) and the persona's traits are re-extracted. Searching by name was the trap (09-03): Gutenberg's「Winston Churchill」author page is the American novelist (Coniston, Richard Carvel), and Twain's first hits are Paine's biography of him — so the greats now have a hand-listed shelf, lib/ink/voices.json (slug → the persona's own public-domain plain-text files; two windows from the middle of each book), which the harvest reads before it searches; the SERP path stays for everyone else. voices --force <slug> starts the bank over from the corpus (dedupe + the note filter — the old Churchill bank held four passages twice, once bare and once in quotation marks; Twain's carried the researcher's own「THE MOST IMPORTANT FILE in this corpus」as Twain). The method and the readings: the quality score.| Ink format | The field's proven length | Where the number comes from | Ink now (was) |
|---|---|---|---|
| Essay | 800–1,200 words; leaders and signed columns 850–1,000 | NYT Opinion's guest-essay guideline (800–1,200); the Guardian's comment page ~850; the Economist's leaders and named columns (Bagehot, Lexington, Charlemagne) ~900–1,000; USA Today op-eds 600–800 | 1,000–1,400 (600–900) |
| Notes | 800–1,200 | the Economist's Bartleby/Johnson columns; Matt Levine's numbered items; Munger's talks in list form | 800–1,200 (450–700) |
| Review | 800–1,200; a critic's essay 2,000+ | a book review is「around 1,000 words, 400–2,000」(Letter Review); NYer critics run longer | 900–1,300 (450–750) |
| Dispatch | 1,200–1,500 in a daily; 3,000+ in a magazine | NYT dispatches; the New Yorker's「Letter from…」; Dan Wang's annual letters run to 10,000 | 1,000–1,500 (450–750) |
| Letter | 700–1,100 | a published letter of counsel (Rilke's to the young poet ~1,000–1,500); NOT a letter to the editor, which the NYT caps at 150 words | 700–1,100 (350–600) |
| Advice | 800–1,200 for one letter answered | Dear Prudence answers 300–500 each (three a column; the question itself is capped at 150); Ask Polly and Dear Sugar answer one letter at 1,500–2,500 | 800–1,200 (400–650) |
| Scorecard | 700–1,100 | the forecasters' public post-mortems; a Levine item | 700–1,100 (350–600) |
| Dialogue | 900–1,400 | the New Yorker's「In Conversation」and the NYT's edited two-hander run 1,500–2,500 with the small talk in; a written dialogue keeps the argument only | 900–1,400 (450–800) |
| Fable | 250–600 | Thurber's Fables for Our Time (the New Yorker's fables) run 250–600 words; Aesop's mean is 44 words — a moral, not a read | 400–800 (350–600) |
| Shouts | 600–1,000 | the New Yorker's Shouts & Murmurs, as its submission trackers record it (600–1,000) | 600–1,000 (220–400) |
| Kicker | 80–150 spoken; 400–600 written | a broadcast「and finally」is 30–45 seconds of script; Axios's「1 fun thing」and the NYT Morning's closing item are 80–150 words; the shortest written equivalent — a Talk of the Town short — is 400–600 | 150–300 (70–150) — the broadcast tag, doubled; whether the slot survives is the owner's call (§the ranking) |
| Overheard | 500–800 | a Shouts-length dialogue; McSweeney's lists and two-handers 500–800 | 500–800 (220–400) |
| Riddle | 200–400 | a setter's preamble plus the puzzle (NYT Play, the Guardian's crossword blogs) | 200–400 (100–250) |
The engagement data agrees with the print numbers rather than with「write short」: Medium's completion peaks at a 6–8-minute read (1,000–1,200 words); Substack's most-engaged cluster is 800–2,500 words with 70–80% completion at 1,200–1,800 (Substack Course). The owner's ranking of the formats by read quality (09-03): essay ≈ numbered notes ≳ notes > letter ≈ fable ≫ kicker — recorded here; what it changes in the slate mix (fewer kickers, or none) is decided on the specimen brew at the new lengths. Ruling (09-03, late): the kicker stays for testing — dropped only if it keeps getting bad reviews. The specimen at the new floors (brew 2026-09-07, ten pieces): the writer does not obey a floor it is merely told — essays and notes landed at 700–1,060 words against 800–1,400, a scorecard at 570, a fable at 370; and the editor's passes trim further (Buffett 746 → 572). So a draft under 85% of its floor now gets one extension ask in the writer's own call — carry the argument further from the notebook (another reported fact, a second example, the objection and the answer), never pad; the longer draft stands (extended on the piece, shown on the brew log). Measured on brew 2026-09-08 (ten pieces): the drafts landed at the floors (Darwin 1,057 · Dan Wang 1,033 · Augustus 938 · Nolan 886 · Karpathy 875 words — against 700–1,060 the night before), but the editor cuts what the writer built (Nolan 886 → 631, Karpathy 875 → 701): a piece the editor trims under its floor is now extended once more and re-linted, where the words are last touched. The gate also dropped two edited pieces at exactly three soft tells (a contrast, a dash pair, a triple — what the majors carry at that length): the allowance is now three per thousand words inclusive, a fourth fails.
.tbl tables, the switch). The daily schedule lives here since 09-04 (owner:「let me set the start time in the Ink tab」): a Daily brew switch and three times — text starts (20:00), the art cutoff (07:15) and publish (07:30) — read every minute by the app's own scheduler thread (ink_brew.due_pass): the text pass writes the edition of the morning it will publish — a start after the publish time (20:00) writes tomorrow's, a start before it (03:00, the first setting on the box) writes today's; the first scheduled run filed a 03:00 brew as tomorrow and nothing showed in the app — and it submits the art batch; the dawn pass begins collecting half an hour before the cutoff (derived, never a fourth knob — the owner's catch), paints the stragglers at the cutoff and publishes at the publish time; each runs once a day (a stamp on the day's state), never while the kill switch is on. The two systemd timers in deploy/ are superseded — installed on the box but never enabled. Redesigned v1078 (owner, 09-04:「no small text · house inputs · a timeline of the three times · one row per day with the key facts · a tap opens the day's analysis · fix the costs, show the major ones」): the top card is four stat tiles (current edition · the brew's state · Daily brew · kill switch) and the Brew now row; The day is a timeline — Text starts → Art cutoff → Publish, each a house time input on its node — with pieces/floor/ceiling beneath; Editions is one row per brew day: pieces published of wanted · the day's three times as they actually ran (from the day's state and log) · IQS mean · cost · read-to-the-end · verdicts; a tapped day opens the analysis below the table — six stat tiles, the cost by task as bars (Writing · Editing · Judges · Research · Renditions · Art · Voice banks, the brew log's own arithmetic: the publish summary already holds the evening in a one-process day, a two-process day adds both), and one card per piece: hero thumbnail · title · author · beat · format · words · art style · the gate's word (lead / the well / published / held / dropped · flat / dropped · antithesis) · IQS · the piece's own cost · votes · readers · notes · Hold/Restore · Repaint. Since 09-19, a piece the persona drove carries one more line — the persona drove · 6/10 questions answered · 3 of its life recalled · point passed both tests · witness 5 found, 1 quoted · tells 10 → 15 → 9 · 5 fact flags — the brew's own summary of that morning (the tells are the rival reader's marks before each mend, then after; a gauge with false positives, never a score), the IQS number appears only where a score exists, and the day's tile counts how many pieces the persona drove. In the knobs row, drafts a piece and interview first grey out while the persona drives (the old path's knobs); the the persona drives switch saves on change. The unpublished days (a failed pass, a cancelled run) list too. Top card (as first built): the current edition · the brew's state (idle / brewing / last crash) · the kill switch (settings ink_knobs.kill — the timers exit at once while it's on) · Brew now with a piece count and a date (both passes at once, art painted sync, published the moment it's ready; polls to completion). The count beside the button is for that click only and saves nothing — its pill says so as it is typed (09-19); the standing number is the Pieces a day knob under The day, saved with its Save button. Slate policy: pieces a day (floor · ceiling), the art cutoff and the publish time — saved to settings and read by the brew itself, so the box's timers honour them (the units now call the passes without arguments). Editions: every edition on the shelf — date · pieces · lead · layout string · heavy/mid/light · art · 👍/👎 — and a row unfolds into its pieces: role · title + provenance · author · beat/format/register + tags · minutes · the gate's verdict (antithesis ×n, disclaimers, flat) · art · zh · votes, with json · hold / restore (the edition is rewritten and re-composed; the pointer follows when it's the current day) · repaint (one hero, sync). The brew: the last 14 days — written · chosen · dropped · no-art · cost · published — and the stage log on demand. Models & APIs carries one Ink card, headed app.ink, with a plain sub-head per brew task (09-02, owner:「radio button style, separate by tasks」; the fifteen cards combined into one on 09-09, the Studio's shape — the per-seat kinds are not named on this tab, because Usage & cost is where app.ink opens into app.ink_desk and the rest) — the editor · the research desk · the background brief · the writer · the copy editor · the cold reader · the voice judges · renditions · the art director · the paint · the anchor bank · the voice harvest · pegs (thirteen, after the 09-03 audit:「all cost items, all model selections」) — each a radio row over the models the brew can run for that seat (ink_brew.TASKS; the pick lands in settings ink_knobs.models and the brew's model_for() reads it, falling back to the seat's default for any stray id). Usage & cost books each task under its own ledger leaf, app.ink_<task> — the paint per image at the console's $/image (batch half, sync full — the console's one-hero repaint books too) and every SERP fee (pegs · the desk's three searches · the voice harvest's five) included, neither of which the old single app.ink row ever saw; and the ledger books one row per provider call (09-04, the owner:「why only 1 call for the writer?」 — a pass used to be summed per task into a single row, so a night's writing read as one call), and the write drains, so a one-process brew (Brew now, the dev brew) books the evening pass once. ⚠ The ledger is written AS THE MONEY IS SPENT (09-10, the owner:「I don't think the cost record for ink is complete and accurate」). The accumulator is memory only and book_to_db used to run once, after the pass — so anything that ended the process in between (a deploy's systemctl restart mad-room, a crash, the timer's kill) took every dollar in it, and the resumed pass, finding the pieces already on disk, booked only its own remainder. The box's own record of it: the 09-09 brew started 03:00, was restarted under that day's two deploys, finished 21 hours later and booked $0.0370 over two seats for a 20-piece day — where the undisturbed 09-08 brew booked $1.53 over eleven; its _usage.jsonl holds the 285 calls the ledger never saw. Three things now hold it shut: every call flushes within BOOK_EVERY_CALLS (8) or BOOK_EVERY_SECONDS (90) of being made; every stage ends on the books (run_stages books in a finally, so a dying stage — which re-raises past the pass's own booking — still pays); and a write the DB refuses puts its unwritten tail back at the head of the queue instead of dropping it. A row is stamped with the clock of its own call, so an edition written across midnight straddles two ledger days by design. Not built from the mock: Cast & beats and Sources (the code holds them; a later slice), rooms-per-slate (no doors yet). Endpoints: GET /api/admin/ink · GET /api/admin/ink/edition/{date} · POST /api/admin/ink/brew-now | knobs | hold | repaint · GET /api/admin/ink/log.The admin console gains an Ink tab in its own list-row grammar. Six sections down the left; the landing is today's edition with the brew's health strip on top. Everything the pipeline froze is inspectable here, and every policy number above is a knob.
Rows are pieces; the trailing verbs are the owner's hands on the edition: open (the reader, as users see it), regenerate hero (another batch/sync paint), re-translate, hold (unpublish one piece — the composer re-flows), read draft (a gate-dropped piece, with its meter score). The dot is the tone-gate score; the strip is the brew's health and the day's spend.
| Section | What it holds |
|---|---|
| Editions | Every edition by date: pieces, gate scores, art status (batch/sync/missing), renditions, peg ok, the background brief (built 09-03, stage 2¾ stage_briefs: one Flash call per Ink language from the notebook only, ~0.1¢; briefs --date backfills a day and re-freezes its Source cards without republishing) · what the writer read (built 09-02: peg fetched / walled · pages · facts · quotes · sources per piece — the honest answer to「how did the persona write from an empty excerpt」: the card is the reader's excerpt, the notebook is the writer's reading), rooms spawned; per-piece verbs above; whole-edition hold/re-publish. |
| The brew | Schedule (evening/dawn times, timezone), the kill switch, Brew now, the last run's stage log with timings and the stage that failed, per-stage models (editor · writer · translator · art director · painter · failover) drawn from the Models tab's roster, retry. |
| Slate policy | Every number in §3 as a knob: pieces/day, beat quotas, format quotas, weekly minimums, cadence caps, language minimums, gate threshold, length floors per language. |
| Cast & beats | Persona × beat affinities (editable), last-written date, pieces this month, opt-out (a persona that shouldn't write), debut queue for new personas. |
| Sources | Query recipes per beat, the calendar file (data days · anniversaries), blocked domains, the SERP engine chain — the same engines the dispatch uses. |
| Metrics | The four that matter: engaged reads (30s+), doors opened, rooms spawned per slate (the neutron-bomb number), return rate — per edition, per beat, per format, per persona. Cost per piece beside each. |
lib/ink_brew.py (2026-09-02)<ink_dir>/brew/<date>/, re-runnable and resumable. The constitution throughout: the LLM proposes, the code enforces.| Stage | What runs | The code's word | Artefact |
|---|---|---|---|
| pegs | the Vibes news harvest (14 beat queries, Serper news) + 18 extra beat queries via websearch + calendar.json; cleaned (entities, tags), home/section pages dropped | dedupe by url and title; pegs used in the last 3 days excluded (the ledger) | pegs.json |
| slate — the budget meeting | ONE V4 Pro thinking call (24k cap): every peg + the whole roster with cadence state + the last 3 days + the policy text → N+2 commissions in editorial order with beat · format · register · trigger · peg · title · dek · angle · tags · well hint | validate_slate: unknown persona, twice-a-day, resting, bad beat/format/peg, lead = yesterday's lead, <6 beats, >4 a beat, >6 essays, <3 lights, kicker not light, duplicate light formats → ONE re-ask, then trim_slate enforces the rest | slate.json |
| write | parallel V4 Pro thinking calls (6 at a time): the full profile + the house rules (boundary · vantage · ENGLISH · facts only from the peg card · style tells) + the commission + a DEALT HAND of ≤3 unburned anchors ranked against the peg + the persona's last 5 Ink pieces (anti-echo) | the voice card is extracted on first commission if missing (the lazy backfill); word count → minutes | pieces/<id>.json |
| gate | the antithesis meter (two English regexes + aitone's 不是…而是… net): ≥3 hits → one rewrite ask, still ≥3 → dropped; every LIGHT piece read cold by V4 Flash —「did it land?」— flat → dropped; empties dropped | survivors in editorial order, the spares fill; exactly N chosen | gate.json |
| renditions | the Flash tier (V4.1 Flash since 09-09), persona-voiced 简体中文 for every chosen piece | translated_from: en rides with it (the honesty pill) | renditions/<id>.zh.json |
| art | the art director (V4 Pro thinking, one call, 12 styles varied across the day, no text/likeness/photorealism) → Gemini batchGenerateContent keyed by piece id (50% off) | a piece with no prompt gets a plain one — never a missing hero by omission | art/prompts.json · art/batch.json |
| dawn · collect | poll the batch every minute; save heroes as JPEG (PIL) into img/<id>.jpg. The batch's own finish time is kept beside our collect time (09-04: Gemini's endTime said 03:47, seven minutes after submission, and the heroes sat until the 09:38 collect), and a batch that ends failed — the API says BATCH_STATE_* where the older docs said JOB_STATE_*, so the state is matched on its suffix — stops the polling instead of running to the cutoff | THE 07:15 RULE: at the cutoff, batches.cancel + sync generateContent ×4 in parallel for whatever is missing | art/batch.json |
| publish | ink_compose (H1–H8, two wells and two compact runs from 12 pieces) → edition-<date>.json chained by prev, pegs.json merged (frozen SERP cards), then the atomic pointer flip | a heroless piece never leads or wells; the ledger records cadence, burned anchors (90-day cool-down), the lead, the pegs; spend booked under call kind app.ink | edition.json · ledger.json · state.json |
python lib/ink_brew.py evening --n 16 # 20:00 CST (deploy/mad-ink-evening.timer) python lib/ink_brew.py dawn --cutoff 07:15 --publish-at 07:30 # 06:45 CST (deploy/mad-ink-dawn.timer) python lib/ink_brew.py brew --n 16 --sync-art # dev: both passes now, art painted sync, publish at once python lib/ink_brew.py status --date 2026-09-03 # what the day's brew holds
MAD_INK_DIR — the box's /var/lib/mad/ink (set in the box's .env beside the DB and rooms paths); dev launchers set rooms-dev\_ink. run_room serves /ink from it and seeds an empty dir from the repo's lib/ink once, so a fresh box shows the shipped editions until its first brew. The two systemd timers ride in deploy/.$MAD_ROOMS_DIR/_vibes/cards/<slug>.json, 20–30 anchors) is the bank; a persona with no card gets one extracted at first commission (~1¢). The ledger burns used anchors for 90 days and the writer is dealt at most 3, told to use at most 2 —「method, not memoir」.L r r W r r (Churchill leads on Hormuz, Buffett's letter takes the well, Twain's kicker closes). 33¢ all in, 7 minutes. Two traps met and coded around on the way: V4 Pro's「Server Overloaded」503 at peak (→ the Flash failover) and Flash STARVING on the 24k thinking budget (→ an empty answer flips the retry to thinking-off JSON mode).shelf_repeat, in the validator and in the editor's brief) — the way back to a running story is the second-day form, a Scorecard or a Response, or a continuity/reply trigger that names what moved.app.ink · the 12-piece light specimen read (funny is still unproven) · the door that seeds a room (step 1.5)./var/lib/mad/ink · ⑥ the register axis + the four light formats · ⑦ tags from day one · ⑧ the bank, lazily (no separate backfill run needed). Still yours: ⑤ the console's Ink tab (§4 mock) — build next? · the light specimen read: the first real edition IS the specimen — read its four light pieces and rule.