Releases: OPTIMETA/PAIDEIA
PAIDEIA 26.06.04
Update Catalog
26.06.04 update
A second reading of what the exam will ask
PAIDEIA has always answered one question — what is this exam actually going to test? — with a single signal: homework density. Every drill priority, every 🔥 on coverage.md, traces back to how often a topic surfaced in the problem sets. It is a good signal, because professors tend to assign what they mean to test. But it is one signal, and it is deaf to the half of a course that happens out loud. A topic the professor circled on the board three times and never assigned is, to homework density, invisible.
This release gives PAIDEIA a second, independent reading of the same question — and a single command to fold it in.
/paideia:alt
Exam Radar is OPTIMETA's Alt plugin. It reads your lecture recordings and ranks each topic by how hard the professor leaned on it out loud — a measure of exam probability that owes nothing to the problem sets. You triage the result into three zones — the gold zone you must study, the things you've already got, the things safe to drop — and hit copy. /paideia:alt ingests that export.
Paste the Exam Radar copy straight after the command, or save it to materials/radar.md and run the command bare. PAIDEIA parses the fixed exam-radar:v1 form — course, days-to-exam, and every topic with its exam-probability percentage and a 🎙 flag where the professor verbally stressed it — and folds the signal into course-index/.
Homework density stays primary. Emphasis is a second opinion.
The thing /paideia:alt deliberately does not do is overwrite your exam tiers. Homework density remains the authority behind every Exam tier on coverage.md. Lecture emphasis is laid down beside it as a second opinion — surfaced, never substituted. A single lecture signal does not silently promote an HW-based tier, for the same reason a user-declared weak spot doesn't: one source of evidence is not consensus.
What it does instead is make the two signals argue in the open. Where homework and lecture agree, you get corroboration — study that, both of us are sure. Where they diverge, you get the two cases that matter most:
- 🎙 emphasized, but never assigned — the oral-only material a homework-driven plan walks right past. This is the blind spot the second signal exists to catch.
- homework-frequent, but the lecture stayed quiet — quietly important, the kind of thing so foundational the professor stopped narrating it.
Both land in a divergence section for you to judge. PAIDEIA flags; you decide.
What it writes
course-index/radar.md— the canonical store of the lecture-emphasis signal, mirroring howcoverage.mdstores the homework signal. Overwritten on each import; it is a snapshot of Exam Radar's latest state.course-index/coverage.md— gains aLecture emphasiscolumn and a lecture-vs-HW divergence section, and the emphasis is folded into drill priority. The HW-basedExam tiercolumn is left exactly as it was. The merge is re-runnable — it replaces its own annotations rather than stacking duplicates.weakmap/weakmap_<ts>.md— a fresh weakmap seeded from the gold zone, treating lecture-hot and self-weak as declared weaknesses. Never overwritten; always a new timestamped file, so your weakness history stays intact.
The lecture-to-exam loop, closed
This is the piece that makes the three tools one workflow. Alt captures the lecture. Exam Radar turns the recording into a decision about what's worth your time. /paideia:alt carries that decision into the course graph, where PAIDEIA's drills, weakmaps, and cheat sheets already live. Recording → decision → study, end to end, without retyping a thing in the middle.
PAIDEIA is now an OPTIMETA project
PAIDEIA has moved to the OPTIMETA organization, where it now sits alongside PAIDEIA-codex and Exam Radar. Stars, forks, and history came with it, and every old TaewoooPark/PAIDEIA URL 301-redirects to the new home — existing clones, bookmarks, and marketplace add commands keep working untouched. Install URLs and badges in the README now point at OPTIMETA/PAIDEIA directly.
Mechanics
/paideia:alt— import an Exam Radar export; with no argument, readsmaterials/radar.md.- New:
plugins/paideia/commands/alt.md,plugins/paideia/skills/alt-import/SKILL.md. - Plugin and marketplace bumped to 0.9.0 (15 → 16 commands); both READMEs updated.
- Repository transferred to github.com/OPTIMETA/PAIDEIA; old URLs redirect.
- No course migration.
radar.md, thecoverage.mdannotations, and the gold-zone weakmap are all additive — a course that never runs/paideia:altis byte-for-byte what it was.
Notes
Pairs with Exam Radar (OPTIMETA's Alt plugin) and PAIDEIA-codex, both released today.
PAIDEIA 26.06.01
Update Catalog
26.06.01 update
Closing the gap between "cloned it" and "ran it"
The most expensive failure a tool like this can have is silent. A student clones the plugin the night before an exam, types /paideia:grade, and nothing useful happens — poppler was never installed, or the Ollama daemon is down, or a brew upgrade last week moved a binary, or the plugin got reinstalled at a new path and the statusline wiring now points at a file that no longer exists. None of these are loud errors. They are dead ends, and a dead end at 11pm before a final is the moment a tool loses a user for good.
init-course already checked most of these — but only once, at bootstrap, buried inside a markdown walkthrough with no way to run it again. The moment the environment drifted, there was no entry point to re-diagnose. This release adds one.
/paideia:doctor
doctor answers a single question: can paideia actually run here? It checks the install — Python deps, poppler, tesseract (+ the kor trained data), the Ollama daemon and the qwen3-vl:8b model — and, when run inside a course folder, the workspace too: the directory skeleton, .course-meta, errors/log.md, writable paths, and the statusline / SessionStart wiring in .claude/settings.json.
It runs in two modes, chosen automatically:
- Global mode — no
.course-metain the current directory. Checks only the system dependencies, then points the user at/paideia:init-course. This is the "cloned but can't run" first line of defense, the check you reach for before you've set up any course at all. - Course mode —
.course-metais present. Full check. OCR-dependency severity is graded against the course'sOCR_ENGINE:poppleris required for every engine (even the defaultclaudepath renders pages withpdftoppm), buttesseractonly blocksollamaandtesseract, and the Ollama daemon and model only blockollama. A course set to Claude native vision is never told it is broken because a local model it will never call is absent.
Every failing line comes with a copy-paste fix command, in the course's INTERFACE_LANG (en or ko). The bottom line is a single status: all clear, usable with warnings, or blocking issues — mirrored in the exit code (0 / 1 / 2) so it composes in scripts.
--fix, and the line it will not cross
/paideia:doctor --fix repairs the issues that never need elevated permission: it creates the missing course directories, seeds errors/log.md with the schema /grade and /weakmap expect, restores the +x bit on the plugin scripts, and rewrites the absolute paths in .claude/settings.json from CLAUDE_PLUGIN_ROOT — the single repair that fixes the "plugin moved, statusline vanished" failure mode that nothing else could.
It deliberately does not run brew, apt, or pip, and it does not guess .course-meta values. Those need sudo, or a human who knows the exam date. --fix prints them as commands and stops. The principle is the same one init-course already follows: the tool repairs what it can do safely and silently, and hands back everything that would require a decision or a password.
One consequence worth stating plainly: if --fix rewrites the statusline wiring, Claude Code must be fully quit and relaunched for the statusline to reappear — statusLine settings are read only at app startup, never on reload. doctor says so when it touches them.
Why this is the highest-leverage patch in a while
Everything else PAIDEIA does — pattern extraction, HW-density weighting, blind drills, hand-writing grade trails — assumes the tool runs at all. doctor is the floor under all of it. It is the most direct possible reduction of "I cloned it but couldn't get it to work," and it turns a class of silent dead ends into a diagnosed list with the next command already written out.
Internally it also consolidates the dependency-check logic that had been living inline inside init-course's markdown into one script, scripts/doctor.py, which is now the single source of truth for whether the workspace is runnable.
Mechanics
/paideia:doctor— diagnose;/paideia:doctor --fix— diagnose, then apply the permission-free repairs and re-check.- New:
plugins/paideia/scripts/doctor.py,plugins/paideia/commands/doctor.md. - Plugin and marketplace bumped to 0.8.0 (14 → 15 commands); both READMEs updated.
- No migration. Existing courses gain the command immediately; nothing in
.course-metachanges.
PAIDEIA 26.05.24
Update Catalog
26.05.24 update
Opening the tool, not translating it
PAIDEIA was written by a Korean student preparing for Korean exams in Korean classrooms, and that fact left traces in every corner of the codebase. The slash commands instructed the agent to write feedback "한국어로 짧게." The SessionStart hook printed "최다 실수 패턴." The vision-OCR prompt told Qwen3-VL that the prose it was transcribing would be Korean. The skill files described "Korean conventions" as if there were no other kind. For users whose courses are taught in another language, every one of those was a small wall — not a hard error, but a constant reminder that the tool was not really written for them.
This release introduces INTERFACE_LANG, a per-course en | ko field that controls the language of every author-written prompt, hook output, command instruction, and skill directive. New courses default to English. Existing courses, with no INTERFACE_LANG line in their .course-meta, continue to behave as English-default — to keep the Korean experience that was once unconditional, a Korean-speaking user adds one line: INTERFACE_LANG: ko. No re-init, no folder migration.
The change is not a translation pass. The Korean strings the tool used to emit unconditionally are now one of two paths, and the English path was written from scratch in voice — not machine-translated from the Korean, not paraphrased, but composed as if English had always been the source. The Korean strings were left structurally intact where they were already idiomatic, and rewritten where they had been awkward translations into Korean from an earlier draft. The pair, at every site, reads as if either language could have been the original.
What this fixes
The richest cases are the ones where an English-medium user had been silently dropped through a Korean assumption:
- Audit a
/paideia:graderun on a Quantum Mechanics course. In 0.6.x the grade table printed Korean column headers and the appendederrors/log.mdnarration came back in Korean even though the student's handwriting was English and the course materials were English. 0.7 reads the language from.course-meta(INTERFACE_LANG: enby default for any new course) and writes the rendered table, the verdict line, and the log narration in English. Korean-medium courses behave unchanged: a singleINTERFACE_LANG: koline and the entire grade trail switches back. - Audit a
/paideia:blindstrategy walk by a non-Korean student. PAIDEIA still has only two interface languages, but the content the student writes — the strategy prose, the variable names — was always going to be whatever the student writes. The fix is that the agent's frame around that content (the verdict line, the "what to fix next" hint, the error-log key descriptions) now matches the chosen interface language rather than always being Korean. A student pickingengets English scaffolding while their own prose is preserved verbatim in the log. - Audit a vision-OCR run with
--engine=tesseract. The Korean tesseract model (kor) used to load unconditionally aseng+kor, which adds ~300 MB of memory and noticeable per-page latency. 0.7 picksengforINTERFACE_LANG: enandeng+korforINTERFACE_LANG: ko— courses that will never contain Korean handwriting no longer pay for the Korean trie. - Audit a fresh
/paideia:init-coursein either language. The bootstrap walk now opens with a language picker (Step 0, English-fixed,endefault withkooption), writes the choice into.course-meta, and uses it for every subsequent step — Step 3's diagnostic prompt, Step 5's pattern seeding voice, Step 11's "✅ ready" confirmation. A user who pickskosees Korean from Step 1 onward; a user who picksensees English throughout. The two paths are not translations of each other — they were drafted side by side in their own voices.
How it is used
For new courses, /paideia:init-course asks. The picker is itself in English — the one fixed-language surface in the tool, because the user has not yet picked. Hit enter to accept English; type ko to switch. The choice is written as INTERFACE_LANG: en or INTERFACE_LANG: ko into .course-meta alongside the existing COURSE_NAME, EXAM_DATE, EXAM_TYPE, WEAK_ZONES, and OCR_ENGINE fields.
For existing courses set up under 0.6 or earlier, nothing happens automatically — those .course-meta files have no INTERFACE_LANG line, so every consumer falls back to the English default. A user who has been running PAIDEIA in Korean for months and wants to keep it that way adds one line to .course-meta:
INTERFACE_LANG: ko
No other migration. No re-init. The SessionStart hook, the statusline, the slash commands, the vision-OCR prompt, and the skill files all read the field on the next invocation.
The field also accepts an inline trailing comment, the way the rest of .course-meta does — INTERFACE_LANG: ko # main course language is parsed as ko rather than as the literal ko # main course language (which would silently fall back to en because it would not match the allowed set). Both the SessionStart hook and vision_ocr.py strip # and everything after it before validating, so a course author who annotates their config to remind themselves what each field does does not pay for the annotation with a silent revert to English.
Language is content, not contract
The most consequential decision in this release was what to leave English regardless of INTERFACE_LANG. Slash command names (/paideia:weakmap), pattern IDs (P1, P2, P3, …), tier markers (🔥🔥 / 🔥 / 🟡 / ⚪), YAML keys in errors/log.md (pattern:, error_type:, problem_id:), file paths (course-index/patterns.md, errors/log.md, weakmap/weakmap_*.md), and section anchors that other commands regex against (## One-line verdict, ### Top miss) all stay English even when INTERFACE_LANG: ko. They are not content — they are contract, internal symbols that the rest of the tool reads programmatically.
This split matters because the alternative — translating the contract surface too — would have been a quiet correctness disaster. A weakmap.md file written with a Korean section header ## 한 줄 요약 would be invisible to the SessionStart hook's regex ^##\s*One-line verdict. A log entry whose key was 패턴 rather than pattern would silently drop out of the top-miss counter. The Korean experience is in the narrative the tool addresses to the user; the contract that lets the tool's parts talk to each other lives in symbols, and symbols are not addressed to the user. They are infrastructure.
The rule applied throughout the diff: if a string is parsed by another part of the tool, it is English. If a string is read by the user, it is INTERFACE_LANG-bound. The boundary is sometimes subtle — the statusline's phase=cram token is parsed by no one, but it shows on the bar, and so it stays English (the bar is shown to the user, but the tokens are technical identifiers like setup/diag/drill/mock/cram; their English-ness is the same kind of contract as the slash command names). The grade table's column headers (Problem, Pattern, Error type, Notes) are addressed to the user and so they localize. Splitting on this axis kept the i18n diff confined to two scripts, four skill files, and a handful of command markdowns — no command renaming, no key renaming, no schema migration.
What this does not change
Everything that worked in 0.6 continues to work. The 14 slash commands have the same names, the same arguments, the same outputs in shape. The three OCR engines (claude / ollama / tesseract) have the same selection mechanism. The error log YAML schema is byte-identical. The statusline rendering, the SessionStart hook's trigger condition, the cache key at ~/.cache/paideia/, the /paideia:grade archive step from 0.6 — all unchanged.
For a user who only ever runs PAIDEIA in Korean, the user-facing behavior is identical to 0.6 only if they add INTERFACE_LANG: ko to their .course-meta. Without that one line, the default English path kicks in and the SessionStart hook would print top-miss pattern: P3 rather than 최다 실수 패턴: P3. The migration cost is one line per course; the upside is that the tool now ships with English as a first-class language for the first time, rather than as an unsupported-but-it-might-work side path.
The vision pipeline's Qwen3-VL prompt continues to use the same per-language prose rule (Prose stays in its original language for en, Korean prose stays as Korean prose for ko). The contract with the model — LaTeX math, no grading, no interpretation, [?] for ambiguity, no commentary — is byte-identical. Users of the native-vision grade path (OCR_ENGINE=claude, the default) see no change to the transcription prompt at all; the language switch lives in the agent's framing around the transcription, not inside the VLM call itself.
The plugin contract is the same. No new runtime dependencies, no new files under ~/.cache/, no changes to the .claude/settings.json slot wiring. A 0.6 → 0.7 upgrade is git pull and nothing else; courses that don't add INTERFACE_LANG: ko will switch to English narration on the next session, and a one-line .course-meta edit is the only thing required to keep Korean.
Technical notes
plugins/paideia/scripts/session_start.pygains a_MSGdict keyed by message id with{"en", "ko"}value bundles, and at(key, lang, **kw)helper that falls back to English on a missing key or empty translation (theorrather thanis Noneis deliberate — an explicitly empty translation should not produce a blank line in the SessionStart output). All five phase-appropriate next-step hints, the D+N past-exam suffix, the exam-day phrase, and the top-miss line route throught(); the rest of the function is unchanged.- The
.course-metaparser at theINTERFACE_LANGconsumpt...
PAIDEIA 26.04.24
Update Catalog
26.04.24 update
Closing the drill loop
PAIDEIA has always described itself as a formation cycle: ingest → analyze → drill → grade → weakmap → cheatsheet, with the graded errors feeding back into the next drill. The cycle has been visible from the start — every command writes to disk, every artifact persists, every phase of prep leaves a markdown trail you can git log months later. But three of the feedback arrows in that cycle were quietly lossy. A /paideia:blind error wrote a YAML entry whose keys the statusline and weakmap couldn't read. A seeded course-index/patterns.md flipped the statusline into drill phase before the student had solved a single problem. And fresh clones opened with errors/log.md listed in .gitignore, so the learning record that every downstream command depends on was thrown away by the one tool that should have been preserving it.
This release closes those arrows. The error log now has one shape across every writer. The statusline now reflects activity rather than the mere existence of empty files. The artifacts that accumulate the most signal — the error log, the answer keys, the generated solutions — are the ones git now actually commits.
What this fixes
The richest cases are the ones where the loop was visibly there on disk but silently carried no data:
- Audit a blind-drill error. Running
/paideia:blind hw3-p2and failing on the pattern axis now writes the samepattern:/error_type:keys that/paideia:gradewrites. The statusline's top-miss counter picks it up immediately; the next/paideia:weakmapincludes it without manual edits. In 0.5.x those entries were orphaned — present on disk, invisible to every consumer. - Audit a phase transition. Seeding
course-index/patterns.mdno longer puts the statusline intodrill. Drill only fires when patterns exist ANDerrors/log.mdhas at least one graded- problem_id:entry. The mock phase enforces the same rule — a mock-sourced entry must appear in the log, not just a file undermock/. An artifact that was never acted upon does not move the phase forward. - Audit a fresh clone.
git cloneof a course folder now brings the entire learning history — every error, every seeded answer key, every generated solution for a twin or a chain. The old.gitignoretemplate treated those as "generated output, throwable away"; 0.6 treats them as "the study graph, version-controlled." - Audit a cross-course OCR prompt. The local-inference path (
--ocr=ollama) used to inject the phrase "Discrete Mathematics" into every VLM prompt regardless of course, because the prompt was a hard-coded string. It now resolves the course from--course-name, then.course-meta'sCOURSE_NAME, then a generic"math / physics"default — so a Complex Analysis folder no longer transcribes under a Discrete Mathematics framing.
How it is used
Existing users run /paideia:init-course once inside any course folder to pick up the new SessionStart hook, the updated .gitignore, and the rewritten scripts/statusline.py. No content-side migration is required: the statusline regex still accepts the legacy pattern_missed_initial: key alongside the canonical pattern:, so a log written under 0.5.x keeps rendering in 0.6 without manual cleanup.
In a fresh session:
- The statusline's
<phase>is now computed from three orthogonal signals — artifact-exists, has-graded-entry, mock-was-graded. Create a patterns file but never quiz:diag. Grade one problem:drill. Grade a mock:mock. Drop acheatsheet/final.md:cram. Delete them back: the phase regresses. Unlike 0.5.x, the display tracks what you did, not what you declared. - The
P<k> ↑marker accepts both schema forms in parallel — it seespattern:from/gradeandpattern_missed_initial:from any pre-0.6/blindentries as equivalent. A user migrating from 0.5 sees no gap in the top-miss counter even if half the log is in the legacy form and half is in the canonical. - The
SessionStarthook prints a two-line reminder at the top of every new conversation — course name with D-N, current phase, and the top-miss pattern with a suggested next command. The statusline and the hook are belt-and-suspenders for the same signal: one in the status bar, one inline in the prompt. First-turn context tends to land before the eye hits the statusline, so the hook catches the user where they're already looking.
The statusline caches by mtime, not by timer
Claude Code re-renders the statusline on every prompt. In 0.5.x that meant every turn re-scanned the course folder, glob-matched the weakmap/ directory, read errors/log.md end-to-end, and re-parsed YAML — on a mature course folder with fifty weakmaps and a two-thousand-line error log, that was real wall time multiplied by the length of the conversation.
0.6 introduces a disk cache at ~/.cache/paideia/<sha1(cwd|session)>.json that memoizes the rendered line. The cache key is the (cwd, session_id) pair; the invalidation predicate is a dict-equality comparison of the mtimes of every file whose content affects the display — .course-meta, course-index/patterns.md, cheatsheet/final.{md,pdf}, errors/log.md, and the newest file under weakmap/, mock/, and quizzes/. One stat() per watched path, zero reads when nothing has moved.
Time-based TTL was considered and rejected. A short TTL would re-scan too often on idle sessions; a long TTL would leave the line stale immediately after a /paideia:grade run — the user adds a new error, the display still shows yesterday's top miss until the clock expires. Mtime-based invalidation is exact in a way a timer can't be: the line is stale iff a watched file has actually moved, and fresh otherwise.
The second-order effect is that the statusline now scales with the depth of the conversation rather than the size of the error log. A student finishing their twentieth turn in a course folder with three months of prep history pays the same rendering cost as a student on their first turn.
What this does not change
The markdown in answers/converted/, the YAML in errors/log.md, the coloring of the statusline, the 14 slash commands, the three OCR engines (claude / ollama / tesseract), and the /paideia:ingest vision pipeline all behave identically to 0.5.x. A course folder set up under the old version continues to work without any migration beyond a single /paideia:init-course rerun to wire the new hook.
The vision-ocr prompt, for courses that actually are discrete math, now says "discrete math" only because .course-meta says so; the contract with the Qwen3-VL model (LaTeX math, Korean prose, no grading, no interpretation, [?] for ambiguity) is byte-identical. Users who never used --ocr=ollama see no behavioral difference on that path.
The native-vision grade path (--ocr=claude, the default) now resizes every rendered PNG to ≤1800 px before the Read-tool loop, matching what /paideia:ingest has always done. Because the transcription prompt is unchanged, the grading output is unchanged; the visible effect is cheaper image-token usage on 10+ page scans. The original PDF is also moved to answers/_archive/<stem>_<ts>.pdf after grading succeeds, so the next invocation's "most recently modified in answers/" resolver stops re-picking the same stale file. The converted markdown stays in answers/converted/ and remains version-controlled, so the grade trail is preserved.
Technical notes
plugins/paideia/scripts/statusline.pygains_PATTERN_RX, a regex that accepts bothpattern:(canonical) andpattern_missed_initial:(legacy/blindpre-0.6). Phase detection is split into single-responsibility helpers —_quiz_problems_exist(which excludes_answers.mdsiblings so a seeded answer key alone doesn't tripdrill),_has_error_entries,_mock_was_graded. Caching is implemented by_render/_read_cache/_write_cacheand is transparent to callers:main()short-circuits on cache hit, writes on cache miss, and falls through on anyJSONDecodeErrororOSErrorwithout surfacing the error to the UI.plugins/paideia/scripts/vision_ocr.pyextracts the Ollama prompt intoPROMPT_TEMPLATEwith a{course}placeholder, resolved bybuild_prompt(course_name). The CLI gains--course-name=<name>; the model-warmup timeout is separated from the per-page timeout (60 s warmup, 1800 s per page) so a long per-page ceiling no longer masks a hung daemon at startup. Dedup is hardened with_is_noise_sentence(Korean + English stopword prefixes for VLM self-doubt — 잠깐 / 음 / wait / actually / hmm / …) and_strip_ngram_tail(trims trailing n-gram loops that escape sentence-level dedup because Qwen3-VL's thinking-mode drift sometimes repeats a 5-token window three or more times mid-sentence).plugins/paideia/scripts/session_start.pyis new. Invoked by thehooks.SessionStartentry that/paideia:init-coursewrites into.claude/settings.jsonwithmatcher: "startup|resume". Silent (exit 0, no output) when CWD has no.course-meta, so it's safe to leave wired when the usercds elsewhere. Reads the same inputs as the statusline and chooses between a weakmap verdict, a top-miss hint, and a phase-appropriate next-step hint — whichever is the most specific signal available.plugins/paideia/commands/grade.md§2a gains a Pillow resize heredoc that matches/paideia:ingest's ≤1800 px rule, and a §8 archive step that moves the graded PDF intoanswers/_archive/<stem>_<ts>.pdfafter the grade table and theerrors/log.mdappend both succeed. The archive guard skips cleanly if the PDF is already gone, so a re-run of/paideia:gradedoes not crash.plugins/paideia/commands/init-course.md's.gitignoreseed now explicitly does not ignoreerrors/log.md,answers/converted/*.md,quizzes/*_answers.md, `mock/...