Harness Engineering / Workshop / One hour, one lab

How to grow from junior to senior in the age of AI

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.

seed repo · github.com/tacoda/fulcorum-junior-to-senior-workshop
who it's for · early-career engineers working with an AI coding tool, and the leads who mentor them
what you leave with · the comprehension card (one rule, five questions, five moves) and a habit you can run Monday morning
the seam · the agent produces plausible code fast; seniority is the judgment to tell plausible from correct
Feedforward
A rule the agent reads. Shapes the code before it is written. A nudge, not a guarantee.
Feedback
A hook that checks the result after and refuses a bad one. A guarantee, not a nudge.
The code
What already exists in the tree, which the agent imitates. The surface juniors forget.
0 Pre-flight

Before you start, get to green

You 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.

seed seed/ tool your AI coding agent
pre-flight — send it ahead in the calendar invitegreen before we start
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.

What you'll work on

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.

1 The method

Your tool: the comprehension card

The whole method on one page. You'll use it in the lab and keep it next to your keyboard after.

the comprehension cardprint it, keep it
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.
2 Baseline

Baseline, then spring the trap

Everything is in seed/. Copy-paste this block and read the comments as you go.

file seed/money.py plant patches/plausible-but-wrong.diff
seed/ — the trap in six linesgreen, and wrong
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:

The one idea

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.

3 Comprehension in

Run the five questions

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.

  1. Question 1 What does it do, in one sentence? round_cash now rounds to the nearest nickel, not down.
  2. Question 2 Where does it enter and leave? Trace it from app.py into money.py and back to the printed cash total.
  3. Question 3 Why is it written this way? The comment says "simplified." Rounding to nearest is the textbook cash scheme, but is it this business's policy? And why is there suddenly a float?
  4. Question 4 Is it consistent? The docstring still promises cash rounds "in the customer's favor," and the charter's iron law says integer cents only. The code now breaks both. Which do you trust?
  5. Question 5 What would you call slop? Name the exact line that is plausible, passing, and quietly wrong.
Deliverable

A filled card is your first deliverable, and everyone reaches it.

4 Comprehension out

Prove it wrong

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:

the test the shipped suite didn't havegoes red on the diff
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:

resetback to baseline
git apply -R patches/plausible-but-wrong.diff
5 The two loops

A rule and a hook

You caught the overcharge by hand. Now let the charter catch it for you, with the two mechanisms every charter is built from.

feedforward CLAUDE.md feedback .claude/hooks/

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.

1 · The rule steers (feedforward)

ask your agent, verbatimshapes the draft
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.

2 · The hook catches (feedback)

now ask it to write the bad version anywayrefused on save
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 — the refusal, told to the agentexit 2
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.

One rule, one hook

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.

6 The tradeoffs

How it works: the two knobs

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.

Feedforward — the rule's specificity

A rule you cannot fail is a rule that cannot steer.

seed/.claude/rules/
RuleWhat it saysTradeoff
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.

Feedback — the gate's position

Same check, different distance from the mistake. The earlier the loop closes, the cheaper the fix.

seed/.claude/hooks/
HookEventSpeed / 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.

When to reach for which loop

  1. Feedforward For contested decisions the model won't guess right. Rounding direction is one: left alone, a capable agent rounds to nearest (with a float) every time. The rule is what makes it round your way.
  2. Feedback For invariants the model must never violate, whether or not it usually gets them right. A hook is a guarantee; a rule is only a nudge.

Try the variants (optional)

Each swap is one step; undo it when done.

  • Weak rule. Replace the rounding line in 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.
  • Commit gate alone. Delete the 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.
Together

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.

7 Discussion

Think it through: the policy just changed

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.

1 · The rule (feedforward)

CLAUDE.md and .claude/rules/cash-concrete.md. Change "round down, customer's favor" to "round up, merchant's favor":

CLAUDE.mdfeedforward
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.

2 · The code (imitation)

seed/money.py. This is the one people forget, and it's the one that matters most. Change the implementation and its docstring:

seed/money.pythe surface the agent imitates
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

3 · The hook (feedback)

Both files in .claude/hooks/. Flip the check from floor to ceil so it now blocks anything that doesn't round up:

.claude/hooks/round-gate-*.pyfeedback
want = -(-total // 5) * 5      # was: total - (total % 5)
if got != want:                # block when the code failed to round up

Why you still have to change the code

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.

Where it bites hardest

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.

  1. The defect A rule is a promise about future code. It doesn't retroactively fix round_cash, which keeps rounding down in production until someone edits it.
  2. The pattern At scale, the code is what the agent imitates. Leave a round-down implementation in the tree and you've planted the pattern the next change copies. The more of it there is, the louder it gets.
The durable fix

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.

8 Bigger picture

Three surfaces, spent sparingly

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.

the behavior bandreliability is a tight band
  Instruction (charter prose) ─┐
  The code (imitation) ───────┼──▶  Agent  ──▶  Behavior band
  Feedback (sensors, gates) ──┘                 (how tight = reliability)
  1. Instruction The charter prose the agent reads: rules, CLAUDE.md. This is feedforward.
  2. The code What already exists in the repo, which the agent imitates. The surface juniors forget, and the one that dominates at scale.
  3. Feedback The sensors and gates that check the result: tests, hooks. This is feedback.

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.

The honest part

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.

What you leave with

  • The comprehension card: the daily rule, the five questions, the five moves.
  • A filled card applied to a real agent diff.
  • A test you wrote that catches the planted overcharge.
  • A working mental model: three surfaces bound an agent, the rule you write (instruction), the code it imitates, and the hook that checks it (feedback), and reliability is just a tight behavior band.

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.

  1. The shift From "does it work?" to "how would I know?" The junior question is did the tests pass. The senior question is what would have to be true for this to be wrong, and did I check it? Step 2 lived in that gap. Distrust the green until you've named the case it doesn't cover.
  2. The shift From "it's done" to "I can explain it." Working code you can't explain is a debt with your name on it. If you can't say what it does, where it enters and leaves, and why, out loud, in your own words, you haven't finished reading.
  3. The shift Confusion is a signal, not a shame. A stall means the charter never explained it (a gap the team owes you) or you haven't understood it yet (a gap you close by asking). Name which. Juniors hide it and merge; point at the line instead and say "I don't get this yet."
  4. The shift The agent is a tutor, not an oracle. Use it to explain, never to skip. Form your own answer before you accept its. Pasting a diff you couldn't defend hands it the judgment that was supposed to be yours.
  5. The shift You own the change the moment you merge it. The agent has no accountability. It doesn't get paged, doesn't sit in the review, doesn't answer for the overcharge in production. Once it lands under your name it's your code: your bug, your behavior, your 3 a.m. page. "The agent wrote it" is not a defense.
Three things to run this week

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.

9 Take-home

After the workshop: the take-home lab

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.

Why SQLite is the one to start on

  • Small enough to hold. One file's worth of public API, legible in an afternoon.
  • Famous for its tests. It ships far more test code than library code, and documents how it's tested, the discipline this workshop trains, at professional scale. (sqlite.org/testing.html)
  • The docs explain the why, which is question 3 on your card, answered for you.
  • Self-contained C. Legible and dependency-free; you see exactly where a change enters and leaves.

Source and docs: sqlite.org · source at sqlite.org/src · a readable mirror lives on GitHub.

Run the card on real code

  1. Rep 1 Read one design doc, explain it back. Pick a page from the SQLite docs, read it, then explain it to your agent in your own words. Where you stall is where you didn't understand.
  2. Rep 2 Comprehension pass. Choose one self-contained function. Run all five questions against it. Use the agent as a tutor, ask what, where, why, but form your own answer first.
  3. Rep 3 Predict the failure (move 4). Pick one test. Before reading it, predict what input would break the code it guards. Then read the test. Were you right?
  4. Rep 4 Catch the agent wrong (move 5). Ask your agent to "simplify" or "optimize" one small function. Run the five questions on its diff. Prove it correct, or prove it wrong with a test.
  5. Rep 5 Legibility read (move 3). Find the best-named and worst-named thing you can. Ask why the good one is good. That instinct is what you're building.
The habit

Repeat rep 4 weekly on any codebase you touch. The habit is the deliverable, not any one catch.