2026-08-18 — south_san_juans_3day: the full fix list¶
Companion to 2026-08-18-south-san-juans-manual-interventions.md, which records the
hand interventions. This one records the defects — everything that had to be
worked around to publish one report, and what each one actually needs.
Final tally: nine builds, seven research passes, nine prose passes, five hand-killed runs, ~14 hours across three days, for a three-day trip report whose research was correct on the first pass.
Two things caught the real defects: Kyle reading the published page, and Kyle noticing a gain figure that was physically impossible. Almost nothing here was caught by a gate before it shipped. That is the finding.
Legend: [FIXED] landed · [OPEN] still to do.
Status after the fix session (2026-08-19)¶
22 of 33 fixed. Kyle: "Fix the bugs. Use your own judgement and don't stop until everything is tested and pushed." Every fix carries its reasoning in-file and a test where the behaviour is testable; the fleet was re-run after each gate change.
Three more defects were found while cleaning up the published CalTopo map, and are folded in below as N-1 to N-3.
Still open, in the order they should be taken:
- F-5 the build writes its own output back over its input (the oscillation's root)
- F-10 each gate declaring the phase that OWNS what it checks, so a failure rewinds there — only the loop-stopping half is done
- N-3 duplicate track names on a CalTopo map make
caltopo_to_gpxambiguous - F-7
trim_spurscollapses a genuine out-and-back's return leg - F-3 (remainder) one declared schema shared by the report_author contract and the reader
- F-9, F-14, F-16, F-23, F-26, F-28
Tier 1 — corrupted the report on every build¶
F-1. sync_route_stats rewrote a trip's Day 1 heading with the trip total. [FIXED]
body_pat rewrote the FIRST bold stat line from the frontmatter totals, count=1.
Written for single-day reports, where that line IS the trip. On a trip it is Day 1's
heading, so finalize published "Day 1 — Bennett Pk: 20.9 mi · 7,550 ft" against a 6.1 mi
day. Five occurrences across two days. It runs in FINALIZE, after the prose agent and
after verification, so each time it looked like a fresh mistake by a different actor.
This single bug accounts for a large share of the rebuild count. Now skipped when the
report has days_detail or ## Day N headings.
F-2. check_report_stats accepted the trip total in a day heading. [FIXED]
A body stat line need only match "the total OR any day", so it could not tell which day a
heading belonged to. Its own docstring named this exact failure — "writing the trip total
into each day heading would satisfy the old rule and make the report wrong for a reader.
That is never the fix." — and then permitted it. Now a ## Day N heading binds its stat
lines to days_detail[N-1], and the failure names the day.
F-3. write_report crashed on the schema the research agent actually writes. [FIXED]
Agent writes {name, lat, lon, loj, pb, ele_ft}; the reader demanded elev_ft, cls,
rank, status, loj_id|id. It crashed on EVERY build of this slug, so no skeleton was
ever generated and a prose agent hand-authored the report each time — including the
version that was live for two days. Also silently broke assign_days, defaulting every
peak to Day 1 (the gold_dust bug, 2026-08-07, in a new key name). Fixed by enriching from
peak_db, which preflight already resolves. [OPEN] the contract and the reader still
need one declared schema; the agent invented a different days[] shape again afterward
(peaks: for objective_ids:, from_track: for method:/track:).
Tier 2 — made numbers wrong in ways nothing detected¶
F-4. Nothing checks that a route's gain is physically possible. [FIXED —
check_route_gain_plausible.py. Floor: gain >= trailhead-to-summit rise. Ceiling: a
single-summit out-and-back may not top out on its summit twice. The ceiling is geometric,
not a ratio — a first cut using a gain-to-rise ratio failed west_elk_pk, which
legitimately gains 2.0x crossing drainages. Fleet: 89 reports clean.]
Bennett shipped a heading of 12.2 mi / 4,322 ft — the day walked twice, ascent counted
twice. Frontmatter, day detail and prose all agreed, so every consistency gate passed it.
Kyle caught it in one line: "wait, the gain is roughly double the one way? no". This is
the highest-value gate we do not have: an out-and-back's gain must be near its one-way
net rise, never double it; and no route's gain may fall below summit − trailhead.
F-5. The build writes its own output back over its input. [OPEN — worked around]
The data phase syncs to the new CalTopo map and re-derives local source tracks from it, so
a report's inputs are replaced by its own outputs mid-build. Bennett's source flipped
between a 622-point one-way line and a 618-point round trip repeatedly, and the route
phase and data phase built from different versions of the same filename. That produced
four different distances for the same day — 4.1, 6.09, 8.24 and 12.2 mi — and drove
every check_route_determinism and pin_track_sigs failure. Worked around by pinning the
source under kyle_validated_bennett_fs265_roundtrip.gpx, outside the sync's namespace.
The loop itself is untouched.
F-6. Composed routes ignored mapped-OSM sources. [FIXED]
mapped_src was set only in the --from-track branch, so every composed route was treated
as recorded GPS. OSM ways step hundreds of feet by construction; those steps read as GPS
dropouts, the writer split the line, and stats summed within segments. Bennett lost 1.27 of
3.14 mi. Systematic: a composed OSM corridor always under-reports itself, so it always
loses to a recorded track on the very numbers used to choose between them.
F-7. trim_spurs collapses a genuine out-and-back. [OPEN]
It reads the return leg as a spur and cuts it, turning a 6.09 mi round trip into ~4.1 mi.
Now disabled per-day by hand; it should recognise a return along the outbound line.
F-8. check_route_summits passes at 295 ft. [FIXED — default 600 ft → 250 ft. Measured
rather than guessed: the worst legitimate approach in the fleet is 141 ft, every route
passes at 200 ft, so 250 ft keeps 109 ft of headroom and still fails the 295 ft miss.]
It asserts "summit reached" but accepted a route stopping 295 ft short and ~200 ft below
the top, while genuine routes on the same report land within 8–50 ft. Measuring far looser
than the standard it claims.
F-9. The console's route distance disagrees with the written GPX. [OPEN]
"Recommended route: 6.09 mi" printed while the file's own <name> said 4.1 mi. The report
follows the file; a human watching the build follows the console. They must not disagree.
Tier 3 — orchestration¶
F-10. A gate failure re-runs the phase it is in, not the phase that can fix it.
[PARTIAL — three consecutive identical blockers in one phase now halt the run and name the
rewind as the remedy, instead of spawning agents indefinitely. A different failure resets
the streak; completing a phase clears it. The other half — each gate declaring the phase
that OWNS what it checks, so the orchestrator rewinds THERE — is still open, and is the fix
that makes the counter unnecessary.]
Kyle's standing rule, 2026-08-18: "Every gate failure should cause a rewind for a fix
unless it's a judgment call." status_source is written in research and checked at the
prose gate, so a research defect re-spawned the prose agent, which cannot edit a research
field. Each gate must declare the phase that owns what it checks; failure rewinds there and
carries the verdict into that phase's task file. Escalate to Kyle only for a judgment call.
F-11. Stale-prose cleanup deletes the report on every prose re-entry. [FIXED — the flag was cleared only when there was a report to delete, so a crashed generator left it set and the NEXT entry ate the agent's fresh work. It now clears on the first prose entry after the rewind regardless.] Combined with F-3 this was a non-terminating loop: agent writes report (~10 min) → cleanup deletes it → generator crashes → gate says missing → spawn agent → repeat, until the 60-minute timeouts. Killed by hand. Needs the flag cleared after the first pass, plus non-progress detection (same phase, same blocker, N runs → halt).
F-12. --rewind does not refuse while a run is live. [FIXED — refuses with the other
orchestrator's pids and the command to confirm.]
A rewind issued while the previous --auto was still finishing was silently undone when
that process wrote its state, and the next run resumed on the old routes as if nothing had
happened. It reported success and was reverted seconds later, with no warning anywhere.
F-13. --advance runs exit criteria against a phase that never ran. [FIXED — the same
list is only called a rejection when the phase has actually produced an agent run to
reject; otherwise it is presented as what this run has to produce.]
It records a spurious failure, which is then quoted to the agent as "THE LAST ATTEMPT WAS
SENT BACK FOR EXACTLY THIS" when there was no last attempt.
F-14. write_report refuses to regenerate finished prose. [OPEN]
So a corrected input cannot reach the report without a full rewind — which is why a
status_source fix in peaks.yml had to be hand-copied into the artifact.
Tier 4 — status and provenance¶
F-15. orchestrator_exited is stamped while the orchestrator is still running. [FIXED —
and the real mechanism was worse than first written up: the atexit handler stamps EVERY
run in .rebuild/, so any incidental invocation (--render-status, --stopped, a
--help) declared an actively building run dead. It now skips runs with a live
build_report.py process for that slug.]
Set ~67 s into a phase that runs 15–20 minutes, so status.html shows a live run as dead
for the whole phase. Kyle asked "is it frozen?" twice. A live run and a dead run produce
byte-identical state; liveness had to be read with ps aux.
F-16. "Blocked on" renders the last gate verdict as a current halt. [OPEN] Nothing distinguishes "this stopped us" from "this is what the running agent was sent to fix."
F-17. check_route_determinism did not know a recipe could change. [FIXED]
It recognised new source tracks and changed builder code, but not Kyle changing the route
decision — so every deliberate trailhead or track change read as a nondeterministic
pipeline. Now fingerprints the peaks.yml recipe, reading the previous one from the
committed peaks.yml so existing reports benefit immediately.
F-18. A second route decision erased the first. [FIXED]
materialize assigned protected_deviation per entry, so with two route_shape entries
the newer silently overwrote the older in peaks.yml — the block the composer reads. Both
survived in deviations.yml, so nothing failed and nothing warned. Now merged.
F-19. The report was built by uncommitted tooling. [FIXED — --start lists dirty files
under scripts/ and stamps them on the run. A warning, not a refusal: fixing tooling
mid-session is often the right thing to be doing.]
For several hours the published report could not be reproduced from a clean clone, and
nothing said so. The orchestrator should refuse or loudly mark a run when scripts/ is
dirty, and record the tooling commit in build provenance.
Tier 5 — inputs, rails, and ops¶
F-20. The deviation registry had no writer. [FIXED]
Validator and materializer existed; every decision was hand-authored YAML appended by
heredoc. One went into the wrong file entirely (peaks.yml days[], which the agent
regenerates) and was correctly rejected as unauthorised, costing a full research pass.
record_decision.py now validates before touching disk and dry-runs by default.
F-21. status_source breaks on nearly every research pass. [FIXED — write_report now
STAMPS it from peak_db (naming the ids, the tally, and why a beta page cannot be the
source) and keeps any research note as a trailing remark. The agent can still be wrong
about it; it can no longer break the build with it.]
Three different wrong citation styles in three passes — peakbagger URLs (which cannot show
climbed status without a login), bare paths with no URL, then LoJ URLs. Checked only at the
prose gate. The climbed status should be stamped by the tooling from peak_db, never written
by an agent.
F-22. Sweep freshness is required even for a correction rebuild. [FIXED, partial]
PRESWEEP_HOURS = 12 with no exemption blocked a rebuild that needed no new source data,
while LoJ was logged out and Kyle was away. --accept-stale-sweep "<why>" now records the
reason on the run. [FIXED] it now lists any source tracks sitting in this run's
quarantine and absent from the working directory, at the moment the skip is chosen — the
sweep is also what restores them, and a recipe naming one otherwise fails much later as
"matched []" at the route phase. Remaining gap —
--start quarantines CalTopo tracks and relies on the sweep to restore them.
F-23. One logged-out source blocks the sweep of all three. [OPEN] 14ers and peakbagger were fine; LoJ was out; nothing was swept. Correct to refuse partial data silently, but it should be able to sweep what it can and record the rest absent.
F-24. caltopo_to_gpx.py --map has no scoping and no dry run. [FIXED — refuses to write
into gpx/<slug>/ without --force, and offers --dry-run.]
Run against a report directory it wrote 1,054 files — an entire regional map, under a
different naming convention than the originals — adding duplicate summit waypoints and
phantom route files that failed three gates. Cleanup needed rm, which is not allow-listed,
so Kyle had to run it.
F-25. list_source_tracks.py does not report route shape. [FIXED — every track now
carries round-trip or ONE-WAY (x2 = N mi), stating the implied day rather than leaving
it to be worked out. --help no longer crashes.]
It gives length and coverage but not whether a line is one-way, out-and-back or a loop.
Quoting a one-way length beside round-trip figures sent Kyle a misleading comparison, and he
chose a route on it. --help also crashes (IndexError, no argv guard).
F-26. The drawn-line test is a single binary signal. [OPEN] Timestamps present or absent, with no forensic tier between "recording" and "rejected". Kyle: "timestamps aren't reliable to measure if its actually a climb or not. it may have been converted and the timestamps dropped." The evidence that settled it — walking-cadence point density, irregular step sizes, stationary-start jitter, and time AND elevation absent together — is mechanically computable and was gathered by hand.
F-27. check_route_density recommends a key the registry forbids. [FIXED —
route_density_ok added to ALLOWED_PEAKS_YML_KEYS, so the escape hatch the gate names can
actually be recorded.]
It says to record route_density_ok: in peaks.yml; that key is not in
ALLOWED_PEAKS_YML_KEYS, so the escape hatch it names cannot be recorded.
F-28. compare_route_options.py --recommended silently draws nothing for trips. [FIXED
— falls back to day_*/leg_*_recommended.gpx, labels each, and says so on stderr when it
finds none rather than omitting them from the legend.]
It looks for <slug>_recommended.gpx, which trips do not have. No warning; the legend just
omits it. Also measured a file at 4.1 mi that the builder measured at 6.09.
F-29. CLAUDE.md's documented start command is stale. [FIXED]
Says build_report.py --slug <slug> --start; the real CLI is scripts/build_report.py
<slug> --start, and python* is denied. Caused a permission denial on the first command
of the session.
F-30. No allow entry for gh issue create. [FIXED — gh issue create/list/view added
to the allow list, so the retro's prescribed output can be filed on-rails in future.]
Found while cleaning the published map (2026-08-19)¶
N-1. Dedupe silently drops a recommended route that matches its source. [FIXED]
When a recipe uses a track verbatim — any from_track, or a single composed segment — the
recommended route is BY DEFINITION geometrically identical to its source, so dedupe scored
it 1.00 and skipped the upload. The published map had no magenta Bennett line at all: the
geometry was there, but only as a plain green source track. Kyle: "I only see the
recommended magenta route for conejos." Any verbatim-recipe report loses its route the
same way. Routes and drive lines are now never deduped.
N-2. A relabelled day leaves its route on the map twice. [FIXED]
stale_route_kills matched by geometry, so a route whose day LABEL changed was kept — the
line never moved. The map carried "Conejos day — recommended route" beside "Day 3 Conejos
Pk day — recommended route", both 664 points. Where several lines claim the same report
route, the one the report actually names is kept and the rest go.
N-3. A CalTopo map allows duplicate track names, and caltopo_to_gpx derives filenames
from them. [OPEN — this is F-5's concrete mechanism]
The map held three different tracks all named "Bennett Peak #1" — 618, 622 and 3,000
points. Deriving ..._bennett-peak-1.gpx from a non-unique title means which geometry you
get depends on ordering, which is why the source file kept flipping between phases and
produced four different distances for one day. Either the derivation must disambiguate, or
duplicate route-source titles must be refused on the map.
What this costs until it is fixed¶
Nine builds for one report. The research was right on the first pass; everything after was
the pipeline fighting itself. Three defects reached the published site and sat there for
two days. The gates that exist are good — check_route_gap_loss caught a 40% distance
loss I had talked myself out of — but the ones that matter most for a reader (is this day
physically possible? does this heading describe this day? do these directions go where I am
going?) either did not exist or measured looser than they claimed.
Acceptance test, unchanged: rebuild south_san_juans_3day from scratch with no hand
edits, no killed runs, no out-of-band gate runs, and no deviations — and get this report.
Today it needs six deviation entries, a pinned source file staged by hand, and nine builds.