The agent changes what it means to be junior more than what it means to be senior. A senior already has the judgment the tool can't supply. A junior is building that judgment at the exact moment the tool offers to skip it: it hands you working code faster than you can understand it, and the whole pull is to accept the diff and move on. This workshop is the structured refusal to skip.
github.com/tacoda/fulcorum-junior-to-senior-workshopYou can read code and you've used an AI coding tool once. That's enough. Run this first and confirm a green suite. Nobody debugs their environment on lab time.
git clone https://github.com/tacoda/fulcorum-junior-to-senior-workshop.git
cd fulcorum-junior-to-senior-workshop/seed
pip install pytest && pytest # 3 passed — you're ready
⟲ No local setup? Pair with someone who's green, or run it in any browser Python sandbox. The seed is two small files.
For step 5 you'll also use your AI coding tool (Claude Code, for example)
opened on the seed/ folder. Have it installed and working before the session,
not during it.
The seed is a tiny cash-register service: it settles a bill for cash and prints a receipt. Pennies are discontinued, so cash totals round to a nickel, and the decision that matters is which way they round. The charter's policy is round down, in the customer's favor: they never pay more than the marked total. That single policy is what makes a plausible-but-wrong change catchable.
The whole method on one page. You'll use it in the lab and keep it next to your keyboard after.
THE DAILY RULE
Don't merge a change you can't explain — to the agent, out loud, in your own words.
Green pipeline = permission to proceed. Red = a lesson before a human had to teach it.
FIVE QUESTIONS (comprehension in — ask these of any diff)
1. What does this do, in one sentence?
2. Where does the change enter the system, and where does it leave?
3. Why is it written this way? (If neither a rule nor a doc answers, you found a charter gap.)
4. Is it consistent with the rest of the codebase? Which nearby code disagrees?
5. Which part would I call slop if an agent wrote it — plausible, passing, quietly wrong?
FIVE MOVES (comprehension out — the junior→senior cases)
1. Characterize before you change — pin current behavior in a test before you touch it.
2. Rename in anger — fix the worst-named thing everywhere; watch it clarify.
3. Make it boring — rewrite the clever version as the one you'd rather debug at 3 a.m.
4. Predict the failure — write how you expect it to fail, then run it. Were you right?
5. Catch the agent being wrong — find the confident, fluent, wrong answer. Prove it.
Everything is in seed/. Copy-paste this block and read the comments as you go.
cd seed
pytest # 3 passed
python app.py # cash total $10.80 ← rounded down, customer's favor
git apply patches/plausible-but-wrong.diff # a diff "an agent handed you" — looks cleaner
pytest # still 3 passed ← the trap
python app.py # cash total $10.85 ← a nickel overcharged
Look at those last two lines. The test suite is green, and yet the customer is now billed a nickel more than the correct cash total. This is the one idea the whole session is built on:
Green means "the tests that exist passed," not "the code is correct."
A nickel doesn't sound like much. It's a nickel on 40% of cash totals, every register, every
day, taken from customers who trusted the marked price. That's the bug that becomes a refund
program. The shipped suite only used totals that round the same way either direction, so it
never noticed the flip. The diff rounds to the nearest nickel, the textbook
cash-rounding scheme, written with a float, instead of down, the
charter's customer's-favor policy.
Leave the diff applied. You'll investigate it next.
Open seed/money.py and read the change you just applied. With a partner if
you can, work down the five questions from the card, out loud. Write your answers down.
round_cash now rounds to the nearest nickel, not down.
app.py into money.py and back to the printed cash total.
float?
A filled card is your first deliverable, and everyone reaches it.
Suspicion isn't a catch. Turn it into a test that fails.
A $10.83 total is 1083¢. Customer's favor rounds down to 1080
($10.80). The "simplified" code rounds to nearest: 1085 ($10.85), a nickel the
customer never owed. Write one test asserting cash never rounds up, using totals
whose last digit forces the direction, the case the shipped suite skipped:
def test_cash_never_rounds_up():
for total in [1083, 1084, 999]:
assert round_cash(total) == total - (total % 5)
Run pytest. It goes red on the diff. You just caught a
confident, fluent, wrong answer and proved it. That red test is the day you stop being
junior, in miniature. Reset before the next step:
git apply -R patches/plausible-but-wrong.diff
You caught the overcharge by hand. Now let the charter catch it for you, with the two mechanisms every charter is built from.
Feedforward is a rule in CLAUDE.md that shapes the
code before it's written. Feedback is a hook in
.claude/hooks/ that checks the result after and refuses a bad one.
Both are already on. Open the seed/ folder in your agent and run two quick
prompts.
Show me how you'd implement `round_cash(total_cents)` — round a cash total
to a nickel now that pennies are gone.
With the rounding rule in CLAUDE.md, it rounds down, in the
customer's favor (total_cents - total_cents % 5). Without that rule a capable
agent rounds to the nearest nickel, the overcharge from step 2. The rule is the
difference.
Simplify `round_cash` to round to the nearest nickel: `round(total_cents / 5) * 5`.
The moment it saves money.py, the hook fires and blocks it:
round-gate (edit): round_cash(1083) = 1085, but the customer's-favor
amount is 1080 — cash rounded against the customer.
Told why, the agent puts it back. The same gate also blocks at
git commit, so a bad version can't be saved or shipped.
The rule shaped the draft, the hook caught the one that slipped. That's the whole toolkit a charter uses to bound what an agent does.
You just used one setting of each loop. Both have a design choice, and the seed ships two examples of each so you can feel the tradeoff, not just read it.
A rule you cannot fail is a rule that cannot steer.
| Rule | What it says | Tradeoff |
|---|---|---|
cash-vague.md |
"Round cash fairly to a nickel." | Cheap, universal, ages well, and exerts almost no force. Nearest-nickel reads as compliant. |
cash-concrete.md |
"Round down to the nickel; 1083¢ settles at 1080, not 1085; integer math." | More work, narrower scope, but it decides a contested choice the agent would otherwise make for you. |
Same check, different distance from the mistake. The earlier the loop closes, the cheaper the fix.
| Hook | Event | Speed / consequence |
|---|---|---|
round-gate-edit.py |
PostToolUse on Edit/Write |
Fast. Fires the instant money.py is saved; agent corrected mid-task, fix is local. The bad code did exist on disk for a moment. |
round-gate-commit.py |
PreToolUse on git commit |
Late. Fires only at ship time; nothing bad ever lands, but the mistake may be buried under later work, so the fix costs more. |
Each swap is one step; undo it when done.
CLAUDE.md with the line from .claude/rules/cash-vague.md, restart the agent, and re-run the first prompt above. Watch the vague rule fail to steer.PostToolUse block from .claude/settings.json so only the late gate is wired, then re-run the second prompt. The bad edit now sails through and is caught only at commit. That gap between "caught on save" and "caught at ship" is the cost of a slow loop.A rule without a hook is a suggestion; a hook without a rule is a gate no one explained. Together, guidance you write and enforcement you run, they're the smallest whole unit of a charter, and the two mechanisms to reach for first.
The company changes its mind: cash should now always round up, the merchant's favor, never the customer's. You want the agent to write round-up code from now on. What has to change? Talk it out before you read the answer.
The tempting answer is "edit the rule." That is necessary but not sufficient, and the gap is the whole lesson. Three things describe the rounding policy, and all three have to move together.
CLAUDE.md and .claude/rules/cash-concrete.md. Change "round down, customer's favor" to "round up, merchant's favor":
Rounding policy: cash totals round UP to the next nickel (5 cents), in the
merchant's favor. Never round down, and never round to the nearest nickel.
seed/money.py. This is the one people forget, and it's the one that matters most. Change the implementation and its docstring:
def round_cash(total_cents):
"""Round a cash total UP to the next nickel (5 cents), in the merchant's favor."""
return -(-total_cents // 5) * 5 # integer ceil to a nickel
Both files in .claude/hooks/. Flip the check from floor to ceil so it now blocks anything that doesn't round up:
want = -(-total // 5) * 5 # was: total - (total % 5)
if got != want: # block when the code failed to round up
At the size of this seed the rule is right in front of the agent and the contradiction is glaring, so a capable agent follows the rule and even flags the stale code as wrong. (We tested exactly this contradiction on fresh agents, rule says up, code says down, and they rounded up every time, one of them explicitly calling out the old code as non-compliant.) The imitation problem is a scale effect. In a real codebase, thousands of lines, the rule one entry in a long charter, round-down rounding repeated across dozens of call sites, the prevailing pattern in the code becomes the strongest signal the agent has, and it copies that pattern past a rule it barely weighs. The lab is too small to reproduce this; the takeaway is what it points at.
Rounding direction is a simple, local decision the model can derive from the rule alone, which is exactly why the lab agents got it right. The rules imitation actually defeats are the design-level ones, where most of a charter's rules live: "use ports and adapters," "no business logic in controllers," "depend on interfaces, not concretes." There is no one-line correct answer to copy; the agent infers "how we do it here" from the surrounding code. If most of the codebase ignores the rule, the counterexamples are the pattern, and the agent replicates them. The more non-compliant code, the stronger the pull. The cost of a rule the code contradicts isn't a wrong penny; it's an architecture that drifts further from its own charter with every change.
round_cash, which keeps rounding down in production until someone edits it.
The hook removes the gamble: it blocks any round-down result regardless of what the agent weighed, in the lab and at scale alike. But the hook is a backstop. The durable fix is to make the code correct. When nothing in the tree rounds down, there is no contradictory pattern to imitate and no defect left in production. Feedforward, feedback, and the code itself have to agree.
Step back from the pennies. Everything in this session is about the surfaces that bound what an agent does. There are three, and you just met all of them.
Instruction (charter prose) ─┐
The code (imitation) ───────┼──▶ Agent ──▶ Behavior band
Feedback (sensors, gates) ──┘ (how tight = reliability)
CLAUDE.md. This is feedforward.
Each surface narrows the behavior band, the range of things the agent might do. Reliability is just a tight band. Add a surface and the band gets narrower.
This lab is a toy. A two-line round_cash does not deserve a rule and two
hooks. In real life you'd read it once and move on; you would never gate something this
small. Every rule and hook costs effort to write and, worse, to keep true as the code
changes. So the goal is never the most constraints. It's the fewest
constraints that buy a tight-enough band: you add one only when something real
needs it. Money, security, data integrity, a policy with legal or financial weight.
Where does that pay for itself? Not here, in a production app. Many files, many tests, many policies and requirements, many contributors, an agent making changes all day. There the code surface is huge, no human reads every diff, and comprehension alone can't scale. A few well-placed rules and hooks on the things that actually carry risk keep the band tight when nothing else can. That is when the two mechanisms you practiced today earn their cost, and knowing when not to reach for them is as much the senior's job as knowing how.
But the card is only the method. What carries it into Monday is a handful of shifts in how you think, and not one of them is about typing faster. The agent already types faster than you. Your edge is everything it can't do: knowing what "correct" means here, and noticing when the fluent answer isn't it.
Start from what the job actually is. Writing code was never the point, it was always the means. The job is solving a business problem: someone needs a bill settled correctly, a customer not overcharged, a policy honored. Code is how we've done that, but the "round down, customer's favor" policy is the why the code exists at all. The agent commoditizes the means, it produces the code faster than you can, which doesn't shrink your job, it reveals it. What's left when the typing is free is the part that was always the real work: understanding the problem well enough to know what correct even means. Seniority was always that understanding, not the typing speed. The agent just made it impossible to hide behind the keyboard.
Explain-back before merge (say what a diff does in one sentence; where the sentence goes vague is where you don't understand it). Predict, then run (write how you expect it to fail first; being wrong on paper is free). One move a day (the habit is the deliverable, not any single catch).
And the highest-leverage hour you have: pair with a senior, and mine the why, not the answer. The what is on the screen; the why is in their head. Bring a filled card and ask about the question you stalled on, not a blank "explain this." Watch what they distrust: seniors flinch at certain diffs before they can say why, and that trained suspicion is exactly what you're building. Study their rejections; a diff sent back is a free worked example of the plausible-but-wrong. Then reverse the chair and explain your change to them. Where you start hand-waving is where your understanding runs out, and now someone's there to catch it before production does. You bring comprehension; the team keeps the charter and harness good enough to guard you. Pairing is where both halves meet.
The session gives you the card and one rep on a toy repo. Judgment comes from reps on real code you didn't write. This lab is yours to run afterward: no instructor, no answer key. Do it on a project you'll never ship to, so you can be wrong for free.
Why an open-source project. A toy seed can't teach you scale, history, or the weight of code other people depend on. A real project can: it's readable at your own pace, its commit history records why every line is the way it is, and its test suite shows what "proven correct" actually looks like. The five questions and five moves are the same; only the code got real.
Source and docs: sqlite.org · source at sqlite.org/src · a readable mirror lives on GitHub.
Repeat rep 4 weekly on any codebase you touch. The habit is the deliverable, not any one catch.