What changed, and why
Glassbox exists to make hard ideas learnable, yet nothing measured that. The other content loops check adjacent things — content-accuracy whether the prose is true, lab-fidelity whether the labs are real, collection-coherence whether the structural beats are present — but none asks whether a lesson actually teaches. This loop does.
It is qualitative by design. The tempting version — have an agent predict a lab's output and check it against the engine — measures the agent's competence, not the teaching: a capable model computes a Bloom filter's false-positive rate from its own knowledge no matter how well or badly the lesson explains it. So instead a panel reads each lesson as a newcomer and scores it against a fixed rubric of named teaching practices.
The design's whole challenge is the one constraint that makes a quality loop usable: it must converge to a quiet PASS, not generate advice forever. Three mechanisms below do that — the fixed rubric, the majority threshold, and the per-evaluator honesty rules. Read them in that order.
The rubric is the cap
The rubric is the loop's entire universe of judgement. Evaluators may only speak to these named practices — they cannot invent new criteria, which is what stops the review from drifting into endless taste. It's a plain committed module: edit it to move the bar, deliberately.
A binary check per practice, not a vibe. Eight items; two shown.
agents/pedagogy-rubric.ts · 70 lines
⋯ 12 lines hidden (lines 1–12)
⋯ 30 lines hidden (lines 41–70)
The panel, and the majority threshold
Each lesson is reviewed by an independent panel (default 3), run in parallel. The evaluators never see each other; the only thing that makes a finding is agreement.
One pool slot per lesson; inside it, a parallel panel of read-only evaluators.
agents/pedagogy.ts · 323 lines
⋯ 281 lines hidden (lines 1–281)
⋯ 20 lines hidden (lines 304–323)
Aggregation is deterministic — no model sits in the scoring. A rubric item becomes a reported gap only when a strict majority of the panel marks it weak or missing, and only if it isn't accepted for this lesson. Everything else is either clear or a non-blocking note.
threshold = floor(panel/2)+1; a finding needs >= threshold flags AND no accept entry.
agents/pedagogy.ts · 323 lines
⋯ 198 lines hidden (lines 1–198)
⋯ 90 lines hidden (lines 234–323)
Keeping each evaluator honest
The panel only helps if each member is calibrated against false positives. The system prompt injects the rubric and three rules that bias hard toward silence: default to "satisfied" unless you can cite a specific shortfall, cite-or-omit, and judge the lesson as written for its chosen form (a declared survey legitimately makes different moves).
The rubric is injected from the module; the hard rules make a flag expensive.
agents/pedagogy.ts · 323 lines
⋯ 145 lines hidden (lines 1–145)
⋯ 153 lines hidden (lines 171–323)
Each evaluator returns a strict JSON verdict block, which the harness parses defensively — a malformed or absent block abstains rather than crashing the run, and an unknown verdict falls back to "satisfied". So a flaky evaluator can only ever lower the panel's confidence, never manufacture a finding.
Tolerant parsing: bad input → abstain or default-satisfied, never a false flag.
agents/pedagogy.ts · 323 lines
⋯ 116 lines hidden (lines 1–116)
⋯ 188 lines hidden (lines 136–323)
What it found, and how it stays quiet
Calibrated live on the two gold-standard lessons. bloom-filters → PASS, 8/8 items clear; its survey-dense back third surfaced only as a non-blocking note. paxos → one finding: the hardest step is scaffolded, flagged by 2 of 3 evaluators at Binding.jsx:46-49 — the protocol's crux ("why the highest ballot, not the most common?") asserted as "a short argument shows…" rather than shown. One high-confidence, cited finding — the exact cliff a by-hand analysis predicted, caught mechanically.
Convergence is the accept-list's job. A gap the steward judges intentional (an essay that's passive by design) is recorded in agents/pedagogy-accept.json, keyed lesson → rubric-item → reason, and the loop falls silent on it. RESULT: PASS when every item is clear or accepted; otherwise it exits non-zero with the cited gaps. It maps — it never edits a lesson.
Starts empty; each entry is a deliberate, diffable 'this is intentional'.