Six scripted pi sub-agent runs (3 tool regimes × 2 models) rehearsed the exercise; deterministic gate = solution test + approved file. Key results: mandated CLI triples cost on the strong model (4:06 vs 0:39, PASS both); weak model fails only under the mandate (A+Q), passes when free or optional. Drift documented with code: scorer.updateSelection left inside the extracted method (A+Q, C+Q). Harness in tools/rehearse; summary in rehearsal-findings.md; per-run evidence in rehearsal-runs/; cheat sheet in rehearse-with-agent.md corrected (lambda-typed parameters unsupported).
Exercise folder — Split Loop with Lambda (learning hour)
Contents:
java/— pre-factored exercise repo (prefactored-longrange; runs withmvn test)solution/— reference end state after step 3 (extracted method taking the lambda)rehearse-with-agent.md— how Willem rehearses this with qwen-code + java-refactoring-mcp
Reference solution
See solution/. End state of the production code (simplified):
public Map<IceCream, Integer> longRangeForecast() {
return longRangeForecast(daysForward -> {
scorer.updateSelection();
return scorer.lookupWeather(daysForward);
});
}
public Map<IceCream, Integer> longRangeForecast(
LongFunction<Boolean> dayOffsetToWeather) {
var expectedWeather = collectExpectedWeather(dayOffsetToWeather);
// ... reset loop + scoring loop, unchanged ...
}
private static List<Boolean> collectExpectedWeather(
LongFunction<Boolean> dayOffsetToWeather) {
var expectedWeather = new ArrayList<Boolean>();
for (Instant interestingDate : DATES) {
long daysForward = ChronoUnit.DAYS.between(ANCHOR_DATE, interestingDate);
expectedWeather.add(dayOffsetToWeather.apply(daysForward));
}
return expectedWeather;
}
Notes on the end state:
- Key move:
scoreris used only inside the lambda; the extracted methodcollectExpectedWeatherhas no dependency onScorerat all — all its inputs are parameters. - The public no-arg overload keeps callers unchanged; it injects the
"real" weather lambda. The test calls the overload with a pure
daysForward -> trueinstead. - The test is pure and glanceable:
alwaysSunnyweather gives{Strawberry:12, Chocolate:12, Vanilla:17, }(approved file included). scorer.updateSelection()turned out to be deletable (it only mutates staticScorer.flavourstate that this method never reads). Leaving it in the lambda is fine for step 3; deleting it is a debrief aha.