Coaching a strategy — and a bug the harness fixed for free
The first tutorial described a strategy in your own words and let the skill turn it into a spec.
This one looks at a participant this repo already ships, algorithm-coached-claude, which sits at
a different point on the same spectrum: instead of a loose description, it’s handed a fully
worked-out procedure — exact index-derivation rules, not just an algorithm’s name. And because
this participant is old enough to predate this repo’s own harness migration, it comes with
something none of the earlier tutorials had available: a real bug, filed and explained in its
own README, from before the migration — and the chance to check, with today’s dryrun.py,
whether that bug survived into the generated-code version that replaced it. It didn’t, and why
it didn’t is the actual lesson.
What the coaching document says
participants/algorithm-coached-claude/AGENTS.md teaches selection sort by direct placement:
find the leftmost index p that isn’t yet in its final position, find m, the index of the
minimum of everything from p onward, and swap them — one swap makes one position final,
however far away m is. It also states, explicitly, a correctness argument: “Because i != j
is required by the protocol, step 1’s definition of p already guarantees m != p: if array[p]
were the minimum of its suffix, p would not have been chosen.”
That argument is correct. It didn’t stop a real fault from happening anyway.
The historical bug, kept honestly in the README
Before this repo’s harness migration, this participant worked the way Bring your own
participant online describes as the
now-superseded architecture: a live model call, once per round, coached by the strategy document
inlined into its system prompt — no generated code, no determinism guarantee. Run against a real
array containing a duplicate value ([16, 46, 23, 84, 46, 77], 46 twice), the model derived p
wrong and emitted a swap with i == j — exactly the case the coaching document says is
impossible if p is derived correctly. validateMove rejected it as a fault. A second run, on a
duplicate-free array of comparable difficulty, had zero faults — one data point each, not enough
to call it a duplicates effect on its own, but a real, reproducible failure, not a fluke: the
model re-derived the same wrong p consistently enough to cause a four-round oscillation
(inversionsOverTime: 9 → 4 → 7 → 4 → 7 → 4 → 1 → 0) before breaking out.
The README keeps this writeup deliberately, unedited, as real project history — see the file itself for the full account, including why “coaching improved the strategy; it did not make the participant immune to re-deriving a wrong answer consistently” is the honest verdict for that architecture.
What’s actually running today, and what a fresh test finds
That architecture is retired for this participant. algorithm-coached-claude now runs on the
same generated-code harness as bubble-sort-claude — the only other two-stage participant this
repo ships: AGENTS.md is the spec, generate.sh writes
participants/algorithm-coached-claude/generated/handler.py once, and handler.sh execs that
file — zero live model calls happen during a real run today. Reading the generated code shows the
same direct-placement logic the coaching document describes, but stated as plain, fixed Python.
Not every shipped participant migrated, deliberately: chaotic-claude, minimal-claude, and
verbose-reasoner-claude still make a live per-round claude -p call and have no generate.sh —
they’re the control group this repo keeps on purpose, showing what harness variation alone (not
code generation) does to the numbers. minimal-claude’s own README states this explicitly: it’s
“what you get with nothing added.” So “retired” describes this one participant’s own history, not
a repo-wide architecture change.
def find_placement(array):
n = len(array)
for p in range(n - 1):
m = p
for k in range(p + 1, n):
if array[k] < array[m]:
m = k
if m != p:
return p, m
return None
m is always the index of the earliest occurrence of the true minimum of array[p:] — not
“a” minimum, computed once, the same way, every time. That structurally forecloses the historical
bug: there’s no live judgment call left that could re-derive p inconsistently.
Tested this directly rather than take that reasoning on faith — dryrun.py, run today, against
the exact kind of array that caused the original fault:
[46, 16, 46, 84, 23, 77] (duplicate value, the original bug's own shape): rounds=5 faults=0 sorted=True
[5, 5, 5, 5, 5, 5, 5, 5] (all duplicates): rounds=1 faults=0 sorted=True
[3, 1, 3, 1, 3, 1, 3, 1] (alternating duplicates): rounds=5 faults=0 sorted=True
Reproducing this needs a generation first, and your numbers may differ from these. The
participant directory in the clone holds AGENTS.md and the scripts but no generated/handler.py
— that artifact is built, not committed, so a fresh clone cannot run these arrays until you run
generate.sh. And when you do, expect the faults to match and the round counts not necessarily
to: measured across 27 generations whose move mix was recorded, the same spec text produced
handlers that differ by a factor of 2.5 in rounds
(Change the skill has the measurement). Exact
round counts for generated code are a property of one draw; faults=0 and sorted=True are the
properties of the specification, and those are the ones worth checking.
Zero faults, on the specific array shape that used to fault, plus two harder duplicate-heavy
shapes for good measure. Five further random 12-element arrays, a reversed 24-element array, and
the singleton/pair edge cases all came back faults=0 sorted=True too, with comparisons=0 on
every single run — confirming the coaching document’s second claim (compare buys nothing when
you can already see the array) holds exactly as strongly as it did before the migration. The
correction probe came back the same NOTE result the last two tutorials explain: a handler this
deterministic never emits an invalid move to begin with, so it never receives a real correction to
react to.
The actual lesson: the bug didn’t move because the algorithm got smarter
It’s tempting to read “the bug is gone” as “the harness migration made this participant better at
selection sort.” That’s not what happened. The direct-placement algorithm was already correct —
the coaching document’s own correctness argument was right the whole time. What changed is who
carries out the arithmetic each round. Under the live-decision architecture, “derive p and
m from the current array” was a fresh judgment call a model made every round, and a judgment
call can be executed inconsistently even when the instructions describing it are exactly right.
Under the generated-code architecture, that exact same derivation is a fixed function, executed
identically every time it’s called — there’s no “mostly gets it right” left to fail on a
duplicate-heavy input, because nothing is being freshly decided anymore.
This is the same claim Why generate code, not live decisions makes in the abstract, but this tutorial gets to show it as a real before/after on one unchanged strategy, rather than argue it in general terms: same coaching document, same algorithm, same correctness argument, one real fault under one architecture and zero across 15+ fresh, targeted tests — including the exact array shape that used to fail — under the other.
The numbers, head-to-head
Five random 16-element seeds against handlers/reference-sorter.sh (insertion sort), budget
unlimited:
| Seed | coached rounds | reference rounds |
|------|-----------------|-------------------|
| 1 | 12 | 70 |
| 2 | 10 | 49 |
| 3 | 14 | 66 |
| 4 | 15 | 65 |
| 5 | 14 | 78 |
Roughly 5-6x fewer rounds — the largest margin of any participant covered in these tutorials, and
for a structural reason: direct placement needs at most n - 1 swaps, full stop, regardless
of how scrambled the array is, because every swap finalizes one position permanently. Change the
algorithm showed that any strategy built
from adjacent swaps costs inversions + 1 rounds no matter which algorithm chose them; direct
placement sidesteps that bound entirely by never doing an adjacent-only walk in the first place —
a stronger, more direct exploitation of the same non-adjacent-swap lever Non-adjacent
swaps used for comb sort, because every
swap here is guaranteed to finalize a position rather than merely reduce disorder by however much
the gap happens to buy.
Once it’s verified: go live
Same self-service path as every other tutorial here — a public waiting room, no operator file edit. See Join as a participant.
Prefer to try all of this without touching the live deployment? Run the arena locally brings up the same bridge on your machine in about a minute — every command in this tutorial works against it unchanged.