Every morning at 2:00, a script wakes up
It files yesterday's quiz answers and updates a review schedule. It asks Claude to write today's lecture — two to three thousand words on one topic — plus ten new questions. It picks today's review questions from a bank of everything taught so far, using spaced repetition. Then it emails the lecture with a self-contained quiz page attached.
You wake up and answer Part 1 before reading. You will get most of it wrong; that is the point. You read the lecture. You answer Part 2 — review drawn from earlier editions. You tap one button to send your results back. Tomorrow morning the script files them and schedules the ones you missed to come back sooner.
Once a week there is a spoken teach-back: explain a concept out loud, no notes, and get graded honestly.
It is not a chatbot conversation. That distinction is the whole document.
Why it's built this way
Every design choice traces to a finding in the learning-science literature. The short version:
| Rule | Why |
|---|---|
| Guess before you read | The pretest effect. Guessing before instruction improves how well the instruction sticks — even when the guesses are wrong. |
| Recall after you read | Retrieval practice beats re-reading by a wide margin. Every edition tests both directions. |
| No multiple choice | Short-answer and explain-why transfer to being able to talk about a thing. MCQ transfers worst of all formats. |
| Gaps of 3, 7, 14, 30 | Successive relearning. Each correct answer pushes the next review further out. Miss it and it returns tomorrow with the streak reset. |
| Say it out loud weekly | Teach-back with no notes carries a solid effect size, and it is the only part that rehearses the actual target skill — speaking. |
| Build days | Six of the 36 editions replace reading with hands-on work. Productive failure transfers to novel problems in a way reading does not. |
And the honest scope line, kept in the design doc verbatim so nobody oversells it: sixty to a hundred and twenty hours buys conversational expertise and interview-credible articulacy, not researcher depth.
The one rule that makes it work
The runner owns the schedule. The model never remembers it.
This is the load-bearing idea. Everything else is implementation.
A language model is excellent at writing a lecture on quantization. It is terrible at remembering that you got question 7 wrong eleven days ago and it is due again on Thursday. If you make the model hold the schedule — in its context, in a chat history, in a "memory" — it will drift, and you will not notice until week five, when the whole thing has quietly become a content feed with no idea what you actually know.
So: deterministic state lives in flat files that a plain script owns. The model is called for exactly one thing — write today's content — and its output is merged into the bank by code. The scheduler is a pure function of items plus history. It can be unit-tested. It cannot hallucinate.
If you build one thing from this document, build that boundary.
Five files do all the work
None of them are large.
| Piece | What it is | Size |
|---|---|---|
CURRICULUM.md |
Thirty-six rows: topic and core sources per edition, grouped into phases. Written once, by hand, with Claude's help. | a table |
run.py |
The runner. Owns the item bank, the schedule, ingestion, orchestration. This is the brain. | ~470 ln |
content.js |
A small agent workflow: research, write the lecture, generate questions, adversarially verify them. Content only — it never touches the schedule. | ~190 ln |
send.py |
Emails the lecture and builds the quiz page from a fixed template. Deterministic; the model never writes the quiz page. | ~390 ln |
runtime/ |
The state directory. Plain text and JSONL. Inspectable with cat. |
a folder |
Plus one cron entry to fire the runner daily.
The cycle, start to finish
-
02:00
Cron fires the runnerTakes a lock file. Never two runs at once.
-
+0s
Drain the inbox into historyYesterday's answers get filed. If today is already sent, exit here.
-
+1m
Call Claude — the only model callResearch, write, generate ten questions, verify them. Produces the lecture and the new items. Nothing else.
-
+12m
Merge new items into the bankGiven ids like
e12-3. Append-only. -
+12m
Compute what's dueA pure function over history. No model involved. Build today's quiz and a manifest mapping question number to item id.
-
+13m
SendLecture email, quiz page attached. Write a sent-marker. Log the cost. Bump the edition counter.
-
morning
You answer Part 1 blind, read, answer Part 2One tap sends a single line of results back.
-
next 02:00
That line gets filedMisses return tomorrow. Correct answers move further out.
The human loop is one tap. Section 10 is about what happened to those taps.
The formats are deliberately boring
Every one of them can be read, fixed, or reconstructed by hand at eleven at night without a debugger.
QUIZ-2026-07-19: 1✓ 2✓ 3✗ 4✓ 5✓ 6✓ 7✗ 8✓ ... 16✓
The prefix is arbitrary — pick anything greppable and then never change it, because the parser and every line you have already sent depend on it. Numbering is global across both parts: questions 1 to 4 might be new material, 5 to 16 review. A separate manifest maps {"7": "e3-4"} so the runner knows question 7 was item e3-4. The parser accepts several tick and cross glyphs, because you will be tapping this on a phone.
{"id":"e1-1","edition":1,"topic":"The transformer, end to end",
"q":"...","a":"...","kind":"explain","pretest":true,
"created":"2026-07-14"}
e1-1 2026-07-14 1 e3-4 2026-07-19 0
Item, date, and 1 for correct. The entire schedule is derived from that file. Append-only means a bad run cannot corrupt it — worst case you get a duplicate row.
The quiz page is a single self-contained HTML file built from a fixed string template. Not written by the model. It works offline from an email attachment, reveals answers with native <details>, and generates the result line for you.
Seven prompts
These are written for Claude Code — the agent that reads and writes files — because the thing you are building is a set of files, not a conversation. Each prompt is one work session. Substitute your own subject wherever it says [your subject].
The evidence, before the design
I want to build myself a daily self-study curriculum on [your subject], aimed at conversational fluency — the level where I can hold my own in a technical discussion — in about ten weeks at 45 to 60 minutes a day.
Before we design anything, research what is actually supported about adult skill acquisition on that timescale: retrieval practice, spaced repetition intervals, the pretest effect, teach-backs, interleaving, and which formats transfer to speaking about a topic rather than merely recognizing it.
Write me DESIGN.md with a table of findings, each with a citation and a one-line "what this means for the build." Then propose a system. Be honest about the ceiling — tell me what 60 to 120 hours actually buys and what it does not.
You want the design constrained by evidence you can point at later, not by whatever the model finds rhetorically appealing. The honesty clause matters: ask for the ceiling explicitly, or you will get an implied promise of mastery.
The curriculum
Now write CURRICULUM.md: 36 daily editions covering [your subject], grouped into phases that build on each other.
Each row: edition number, topic, and two to four core sources — papers, docs, posts, real ones, with URLs. Mark six editions as build days where I make something instead of reading. Add a weekly teach-back prompt for each of the six weeks.
Sequence it so nothing depends on something taught later. Flag anything likely to be stale within a year so I know to re-verify it at runtime.
The curriculum is the one artifact you should review by hand. Everything downstream trusts it. The staleness flag is what later justifies telling the research agent to check live docs rather than trusting the file.
The scheduler — do this one carefully
Now the core. Write run.py. The rule I care about most: this script owns the schedule and the model never does.
State, all flat files in runtime/:
items.jsonl— the question bank, append-onlyhistory.tsv— item_id, date, 0 or 1 — tab separated, append-onlystate.json— current edition number and last sent dateWrite next_due(item, history) as a pure function:
- never answered — due created + 1 day
- answered correctly — expanding gaps by streak: 3, 7, 14, 30, 30…
- answered wrong — due tomorrow, streak resets to zero
- ignore any history event dated the same day the item was created. That is the pretest guess: priming, not retrieval practice. Record it, do not schedule on it.
Cap the daily review set at 12, oldest-due first, and log the pre-cap queue depth so a backlog is visible instead of silent.
Then write the run sequence: take a lock file, ingest the results inbox into history, bail if already sent today, call the content generator, merge new items, build today's quiz and a manifest mapping question number to item id, hand off to the sender, write a sent-marker, log the cost.
Write a test file that asserts next_due over every case above, including the pretest one. I want to run it before every deploy.
Naming the function as pure and demanding tests for it is the highest-leverage instruction in this document. A scheduler bug is invisible — it does not crash, it just quietly teaches you the wrong things in the wrong order for a month. The pretest rule and the cap both come from real failures; see section 09.
The content generator
Write the content step as a small multi-agent workflow: given an edition number and its curriculum row, produce lecture.html — two to three thousand words — and new-items.json with ten questions.
Four stages: brief, research the sources, write the lecture, generate the questions.
Two hard requirements. The research stage must re-verify anything volatile — prices, model specs, version numbers — against live docs rather than trusting the curriculum file. The items stage must end with a separate adversarial verifier whose job is to try to falsify each question and answer; anything it cannot fix gets dropped before it enters the bank.
Question format: short-answer and explain-why only. No multiple choice. Mark four of the ten as pretest items.
This workflow writes content and nothing else. It must not read or write the schedule, the history, or the item bank — the runner merges its output.
Unverified generated quiz items carry roughly a 22% factual error rate in published audits. An adversarial verifier is cheap and catches most of it. And stating the boundary out loud — content only — stops the model from helpfully improving your architecture by giving the writer access to the schedule.
The delivery surface
Write send.py: email the lecture and attach a self-contained quiz page.
The quiz page is built from a fixed template in this file — the model never writes it. Requirements, all load-bearing:
- One HTML file, no external assets, works offline from an email attachment
- Answers hidden behind native details/summary, not JavaScript, because Gmail strips scripts from attachments
- After revealing, I tap a tick or a cross per question
- At the bottom, auto-generate my result line with one-tap share
- Include a no-JS fallback block that hides itself when JS is available
- Refuse to send if the quiz file's date is not today; write a sent-marker so a double run cannot double-send
- Support a TEST_RECIPIENTS override so I can test without spamming myself
Copy rule: only promise a section that actually exists. If there are no review items today, render an explicit "nothing due today" note rather than pointing me at an empty section.
Every bullet there is a scar. The details/summary one especially — see section 09.
Automation and a health check
Schedule run.py daily at 02:00 local, with logs I can read.
Then write verify.sh that I can run any time: check every required file exists, check the cron entry is actually loaded, run the next_due test cases, syntax-check the workflow, and confirm the sender never calls a messaging API directly. Print a pass/fail tally.
Also add a PAUSED sentinel file that the runner checks at startup and exits cleanly on — I want to stop this for a week without unloading anything.
The verify script is what lets you touch the system in month three without fear. The sentinel file is there because "just unload the cron job" is a pause that lives nowhere in your source tree, and it will silently un-pause itself the first time you run any setup or repair script. Ask for it on day one.
The weekly teach-back
Once a week, on Sundays, the sender should append the week's teach-back assignment to the lecture email — one question I have to answer out loud, no notes.
Write the grading side too: given my recorded answer and the week's assignment, grade it in three or four sentences — what I got right, what I fumbled or skipped, one cue for next time. Honest, not kind. It should tell me when I hand-waved.
"Honest, not kind" is a real instruction that changes real output. Without it you get encouragement, and encouragement teaches you nothing.
Building it with IBM Bob instead
The design has exactly one model-shaped hole in it. Everything else — scheduler, bank, history, sender, quiz page — is plain Python that has never heard of a vendor. That is what makes the port small.
Bob is IBM's coding agent, generally available since 28 April 2026, replacing Watsonx Code Assistant. It has three modes — Ask for questions, Plan for strategy, Agent for read, write, and execute — an IDE integration and a CLI called Bob Shell, MCP support, subagents, and a Skills system for reusable instruction sets. Rather than exposing one model, it routes each task across Claude, Mistral, Granite, and fine-tuned models by accuracy, performance, and cost.
The one command that matters
bob run --format json --max-cost 30 --max-turns 40 \
"$(cat prompts/edition.md)" > out/edition.json
bob run is the non-interactive session. Piping works (echo "…" | bob run), @path references pull files into context, and output redirects normally. Critically, in non-interactive mode all tools are pre-approved — it will not sit at 2 AM waiting for someone to confirm a file write.
Where Bob is genuinely better for this
| Capability | Why it matters here |
|---|---|
| Hard ceilings | --max-cost and --max-turns are flags, not hopes. The cost figures in section 10 were observed; on Bob they are enforced. |
| Structured output | --format json and --format stream-json. Since 2.0.0 the JSON carries token counts, cache ratios, and cost — so your cost log populates itself instead of being scraped out of prose. |
| Audit by default | Every agentic action is recorded as it happens. For a job that runs unattended overnight, that is exactly the artifact you want at breakfast. |
| Governance in the path | Prompt normalization, sensitive-data scanning, and policy enforcement sit inline. If a teammate runs this on a corporate machine against internal material, this is the difference between sanctioned and not. |
| Model routing | Generating ten quiz questions does not need the same horsepower as writing the lecture. Bob makes that call per task without you wiring it. |
Where it will fight you
| Friction | What to do |
|---|---|
| Subagent approval | Subagents default to on, and each spawn is approved before it starts. Fine at a keyboard; wrong shape for a nightly job. Disable them and sequence the stages yourself — which you should do anyway, see below. |
| No model pin | There is no documented per-call model-selection flag on bob run; routing is Bob's decision, not yours. So do not write prompts that lean on one model's quirks. Assert on output shape instead. |
| Version churn | Bob Shell 2.0.0 arrived August 2026: settings reorganized, Code and Advanced modes consolidated into Agent, Skills added. Guides written against 1.x show a different command surface. Pin the version and read the changelog before trusting any flag you found in a blog post. |
| Cost is in bobcoins | Not dollars. The per-edition figure in this guide will not transfer. Calibrate your own over the first three runs before setting --max-cost. |
| Built for enterprise work | The centre of gravity is Java, COBOL, IBM i, and mainframe modernization. A personal curriculum on a laptop is off-axis, and the licensing and auth setup assumes a company rather than a person. |
Four changes that make it portable
Each of these improves the project regardless of which agent you use — which is the tell that they are the right changes.
- Extract a provider adapter. One module, one function:
generate_edition(edition, topic, sources)returns a lecture and a list of items. One environment variable picks the backend. Nothing else in the codebase ever learns which agent you use. - Sequence, do not fan out. Replace the four-agent workflow with four ordinary calls the runner makes in order — research, write, generate, verify. Portable to any CLI, cheaper, and far easier to debug when edition 23 comes out wrong.
- Demand JSON; never scrape prose. Make the adapter's contract "returns a validated object" and every backend has to meet it. Bob hands you
--format jsonfor free; others need a schema in the prompt. - Put the budget ceiling in the adapter. Bob enforces cost and turns natively; for backends that do not, enforce it in the runner. What you are willing to spend should not be a property of which vendor you picked.
Practical setup notes
API-key authentication for CI/CD arrived in 1.0.3 — use that for the scheduled job. The OAuth tokens in ~/.bob/settings/auth-secrets.json are for humans at keyboards and will strand an unattended run. Standing instructions go in AGENTS.md, loaded hierarchically from your home directory, the project root, and subdirectories; if you already keep a CLAUDE.md, that is where its contents move. Settings live in ~/.bob/settings/settings.json, where session.maxTurns defaults to 50 and session.subagents to true — set both deliberately before the first overnight run. Telemetry is on by default and collects usage patterns rather than prompt or file content; turn it off there if your team requires it.
Codex, and everything after it. The same adapter absorbs them. If swapping backends means touching more than one file, the boundary is in the wrong place — which is section 03 arriving from a different direction.
Sources IBM Bob product site and Bob Shell documentation, including the non-interactive session reference, configuration reference, and changelog (bob.ibm.com/docs/shell); IBM newsroom, 9 July 2026, on multi-agent capabilities and the premium modernization packages; TechTarget and DevOps.com coverage of the 28 April 2026 general-availability release. Verified 13 August 2026 — check the changelog before relying on any flag here.
These all cost real time
They are yours for free.
- Gmail strips JavaScript from attachments.
- The first quiz page revealed answers with a JS click handler. It worked perfectly on the desktop and was completely inert on the phone — which is the only place it was ever used. Native details/summary survives. Test on the surface you will actually use, not the one that is convenient.
- Copy that promises what the state cannot deliver.
- Editions 1 and 2 both said "now go do Part 2." There was no Part 2 — under the original rules the first review items did not come due until edition 3. The scheduler was working exactly as written; the copy was lying. Nothing was broken and it felt broken, which is worse. Make user-facing text conditional on actual state.
- Pretest guesses counted as recall.
- Four lucky day-one guesses pushed those items straight to a three-day gap, skipping their first real review entirely. Guessing before instruction is priming; it is not retrieval practice of studied material, and the gap intervals are calibrated on the latter. Record pretest marks, schedule on nothing but real reviews.
- A review cap that is too low breaks the thing you promised.
- With ten new items a day and a cap of eight, the queue saturates. Then "oldest debt first" means a freshly missed item — the one you most need to see tomorrow — sorts behind a month of old backlog. The expanding-gap schedule silently degrades into FIFO. Raise the cap above inflow, and log the queue depth so saturation is visible.
- Unverified generated questions are wrong about a fifth of the time.
- Adversarial verification stage. Not optional.
- Two agent fleets in one rate window is a cost multiplier.
- If you run more than one of these, stagger them by an hour. And never run the content fleet inside an interactive session "just to test it" — that is how a ten-minute experiment becomes a genuinely startling invoice.
The bill, and the one thing that broke
Those last two figures are the whole lesson. The quizzes were answered daily. Only three sets of answers ever reached the history file.
The ingestion step was the weak point, and it was weak for a specific reason: it asked a model to perform a deterministic write. The result line arrived in a chat thread, and the agent reading that thread was instructed to append it to a file. When it acknowledged the message without doing the append, the answers were simply gone — no error, nothing to notice at the time. A prefix mismatch between the documented format and what the parser actually accepted quietly ate more of them the same way.
Section 03 says the model must not own the schedule. This is the same rule with the edges filed off: the model must not own your writes either. Anything that has to happen exactly once, in a known format, belongs in code.
The fix is small and worth building on day one. Have the delivery surface write the result line to a file directly, or give the runner a parser it owns end to end — and make an unparseable line loud rather than a warning nobody reads. A scheduler that never sees your answers is an expensive newsletter, and it will not tell you that is what it has become.
If you only take three things
- The runner owns the schedule and the writes; the model only makes content. Deterministic state in flat files, one pure function you can unit-test, and no step that matters left to something that might merely say it did the thing.
- Retrieval, not reading. Guess before, recall after, short answers only, expanding gaps, out loud once a week.
- Build the pause switch, the health check, and the honest copy on day one. They are cheap now and expensive at week five.
Built with Claude Code over about two days. The curriculum was the slow part.
Figures are from a real eleven-edition run. Nothing here is hypothetical, including the parts that did not work.