REPL-Driven Flight Tracking

Table of Contents

1. Thesis

The best interface between an agent and a complex system is a REPL. Not a test suite, not a script, not a notebook — a live environment where each evaluation informs the next hypothesis and intermediate state persists across the entire session.

This page demonstrates the claim using multi-source flight tracking. Four independent data sources converge on the same airspace with different latency, cost, and consistency guarantees. The divergence windows are the signal. The REPL is how you find them — it is the only interface that accumulates session state across the exploration, where each evaluation builds on the last rather than starting from scratch.

This is one of three REPL-driven case studies applying the CPRR methodology (Conjecture, Proof, Refutation, Refinement):

  • Flight tracking (this note) — multi-source consistency across metered APIs
  • REPL-driven compliance — one spec, 29 implementations, production log as oracle
  • REPL-driven feed crawling — format polymorphism across 60 heterogeneous sources

1.1. Data Sources

Data sources: AeroAPI, ADS-B, FAA ATCSCC, Weather — consistency model per source

Source Protocol Cost Update latency Provides
AeroAPI v4 REST/JSON $0.005/call Seconds Flight status, track, route
ADS-B Receivers SBS/TCP Free Real-time Position, altitude, velocity
FAA ATCSCC HTML (scrape) Free Minutes Ground stops, flow management
METAR/SIGMET Text/XML Free Hourly Surface obs, convective alerts
FA Web Scraper HTTP/HTML Free Minutes Gate info (ahead of API)

This is a distributed systems problem in miniature — replicas of the same logical state with no coordination protocol between them. The interesting question is not whether they agree but when they converge and what the divergence window reveals.

1.1.1. Contract Verification

The contract verifier diffs canonical output across sources:

(def contract-keys [:ident :registration :status :gate_destination :estimated_in])

;; Callsign normalization: BA48 ≡ BAW48 ≡ BAW48Q
(defn normalize-ident [s]
  (cond
    (re-matches #"[A-Z]{2}\d+.*" s)
    (str ({"BA" "BAW" "AA" "AAL" "DL" "DAL"} (subs s 0 2)) (subs s 2))
    :else s))

A key finding: the gate_origin field is visible on the FlightAware website before it appears in the API. This is eventual consistency with a visible convergence lag — the web frontend reads from a different replica (or cache layer) than the API.

2. Case Studies

Component architecture diagram

2.1. BAW48Q (Speedbird 48 Quebec Heavy)

British Airways 48, Boeing 777-300ER, KSEA → EGLL. Daily transatlantic service.

BAW48 route from Seattle to London via great circle over Greenland

2.1.1. Route Analysis

Filed route: KSEA ALPSE YDC → oceanic fixes → JELCO GRIBS KETLA → Greenland crossing → NUGRA2H STAR → EGLL

Metric Value
Direct distance 4,791 nm
Filed distance 4,948 nm
Dogleg 157 nm
Peak latitude 63°N (Greenland)
Filed speed 459 kt
Block time ~9h 15m
Aircraft B77W (777-300ER)
STAR NUGRA2H into Heathrow

2.1.2. ADS-B Coverage Transition

The route passes through Davis Strait (~60-63°N) where terrestrial ADS-B coverage transitions to space-based surveillance (Aireon). The update_type field in AeroAPI track positions encodes this:

Code Source Coverage
A ADS-B terrestrial Ground station range
S Space-based (Aireon) Global/polar
Z ATC radar Controlled airspace
P Projected Everywhere (estimated)

A naive tracker reports "gap" when surveillance source changes. A correct tracker reports "handoff."

2.1.3. Finding: No A→S Transition Observed

Analysis of 658 track positions from the 2026-05-17 departure showed all positions as type A (ADS-B) or Z (radar) — no S (space-based). This suggests either continuous terrestrial ADS-B coverage on this routing or that AeroAPI's Personal tier does not surface the S distinction. An open question for further investigation.

2.2. May 19 Storm

On May 19, 2026, a thunderstorm knocked out power to 51,000 homes in Boston (Universal Hub). Our local ADS-B receiver captured the aviation impact in real-time — a dataset ripe for REPL-driven exploration.

2.2.1. The Signal: Message Volume Drop

ADS-B message volume showing 37% drop during storm

At 22:00 UTC (6 PM ET), ADS-B message volume dropped 37% — from 141,751 to 88,880 messages/hour. Not receiver failure; aircraft absence. This drop occurred 30 minutes after the first FAA ground stop (see FAA correlation below).

2.2.2. The REPL Session

This analysis was not planned. It emerged step by step — each evaluation informed the next hypothesis.

;; STEP 1: Load and count
(def storm-data (load-data "2026-05-19" [21 22 23]))
(count storm-data)
;; => 352,003 messages in 3 hours

;; STEP 2: Message type distribution
(->> storm-data (map :msg-type) frequencies)
;; => {"3" 27782, "4" 39241, "7" 205789, "8" 68109}
;; MSG,3 = positions, MSG,1 = callsigns

;; STEP 3: Correlate callsigns to positions via ICAO hex
(def icao->cs (build-callsign-map storm-data))
;; => 203 unique aircraft

;; STEP 4: Hypothesis — are aircraft holding?
(def by-icao (group-by :icao (filter :lat storm-data)))

;; STEP 5: Calculate geographic spread per aircraft
(defn spread-nm [positions]
  (let [lats (map :lat positions)
        lons (map :lon positions)]
    (apply max (map #(haversine-nm (mean lats) (mean lons) (:lat %) (:lon %))
                    positions))))

;; STEP 6: The threshold wasn't known until step 5 revealed the distribution
(->> by-icao
     (map (fn [[icao ps]]
            {:callsign (get icao->cs icao icao)
             :positions (count ps)
             :spread (spread-nm ps)
             :alt-range (- (apply max (keep :altitude ps))
                           (apply min (keep :altitude ps)))}))
     (filter #(< (:spread %) 15))
     (filter #(< (:alt-range %) 3000))
     (sort-by :positions >)
     (take 10))
;; => JBU834 (347 pos, 13.5nm), AAL2595 (295, 11.3nm), SWR52 (261, 14.3nm)

The filter predicate at step 6 — < 15nm spread, < 3000ft altitude range — could not have been written in advance. The thresholds came from inspecting the distribution at step 5. This is the non-zero-shot argument: the analysis responds to its own output.

2.2.3. Holding Patterns: Where Were They?

Aircraft positions showing holding patterns northwest of Logan

Position data revealed 56 aircraft in holding patterns northwest of Logan, centered on the BOSCO/ROBUC holding fixes at ~42.45°N, 71.08°W.

Callsign Airline Positions Spread Altitude
JBU834 JetBlue 347 13.5nm 5-7k ft
AAL2595 American 295 11.3nm 4-6k ft
SWR52 Swiss 261 14.3nm 6k ft
SWA1797 Southwest 126 5.7nm 6k ft

Swiss 52 reported 261 positions with only 25-foot altitude variance — textbook holding at assigned altitude.

2.2.4. The 8,000 ft Marker

Altitude band distribution during storm

102 aircraft descended through the 8,000 ft marker during the storm window. The approach band (5-12k ft) shows aircraft stacking up in holding patterns rather than completing their descents to the terminal area. Terminal operations (<5,000 ft) dropped 60%. Cruise traffic (>28,000 ft) continued unimpeded — the signature of a ground stop.

2.2.5. FAA Ground Stop Correlation

The FAA ATCSCC issued multiple advisories for BOS on May 19. The timeline shows our ADS-B data captured the effect of the ground stop, not a precursor:

Advisory Time (UTC) Time (ET) Event
085 19:22 3:22 PM BOS arrival delays begin
118 21:30 5:30 PM First BOS Ground Stop
129 21:49 5:49 PM Extended flight plan drop times
146 22:23 6:23 PM Ground stop extended
160 23:28 7:28 PM 3,277 delay-minutes accumulated
167 23:52 7:52 PM Ground stop cancelled

The ADS-B message drop at 22:00Z occurred 30 minutes after the first ground stop (advisory 118 at 21:30Z). This is the expected propagation delay — departures stop, then airborne traffic clears.

Source: Advisory 160 — 3,277 total delay-minutes, ground congestion on taxiways after brief reopenings.

ATCSCC daily advisory counts Jan-May 2026 showing May 19 anomaly

May 19 produced 167 ATCSCC advisories — nearly double the 90/day baseline. The chart shows it in context: a clear anomaly, though March 16 (216) was worse. Other airports also ground-stopped, but those are outside our receiver range.

2.3. January Winter Storm (stub)

On January 26-27, 2026, a winter storm hit the northeast. The FAA issued BOS ground stops across both days (7 BOS advisories on Jan 26, 4 on Jan 27, within 95 and 69 total advisories respectively).

Unlike the May 19 thunderstorm, winter storms produce a different ADS-B signature: extended ground stops (hours, not the 2.5-hour thunderstorm window), deicing delays visible as long gate holds, and reduced but not eliminated traffic — airlines cancel flights proactively rather than holding airborne.

2.3.1. Storm Impact Summary

Data sourced from Boston.com and NBC Boston:

Metric Value
Snow at Logan 18.6 inches
Flight cancellations (Logan) 508-971
US-wide cancellations 3,921
US-wide delays 1,655
Ground stop No official stop — "few airlines operating"
Runway goal 1 runway open for emergencies

Massport advisory: "Due to winter storm clean up, few airlines are operating at Logan today. Please check with your airline before coming to the airport."

2.3.2. Weather Data Sources

The following APIs provide weather data for KBOS correlation:

Source Endpoint Historical Notes
NWS Observations api.weather.gov 3 days Free, no auth
AWC Archive aviationweather.gov 30 days METAR/TAF
NCEI CDO ncei.noaa.gov 5 years Request via AIRS
NWS FTP tgftp.nws.noaa.gov Current Raw METAR

For data older than 30 days (like January 2026), use the NCEI Archive Information Request System (AIRS). Select "Land-Based" → "Service Records Retention System (SRRS)" and request KBOS data for the date range. Results are emailed within ~30 minutes.

2.3.3. REPL Session: Querying NWS API

The following demonstrates fetching current KBOS weather via the NWS API. This same pattern works for historical analysis once data is retrieved from NCEI.

;; REPL session: 2026-05-23T19:45:00Z
;; Query NWS API for KBOS current observations

(require '[babashka.http-client :as http])
(require '[cheshire.core :as json])

;; Fetch latest observation
(def kbos-obs
  (-> (http/get "https://api.weather.gov/stations/KBOS/observations/latest"
                {:headers {"User-Agent" "flight-tracking/1.0"}})
      :body
      (json/parse-string true)))

;; Extract key fields
(def weather
  {:station (get-in kbos-obs [:properties :station])
   :timestamp (get-in kbos-obs [:properties :timestamp])
   :temperature-c (get-in kbos-obs [:properties :temperature :value])
   :wind-speed-kmh (get-in kbos-obs [:properties :windSpeed :value])
   :visibility-m (get-in kbos-obs [:properties :visibility :value])
   :description (get-in kbos-obs [:properties :textDescription])})

weather
;; => {:station "https://api.weather.gov/stations/KBOS"
;;     :timestamp "2026-05-23T23:45:00+00:00"
;;     :temperature-c 13
;;     :wind-speed-kmh 9.252
;;     :visibility-m 16093.44
;;     :description "Clear"}

The NWS API returns GeoJSON with nested properties. Key fields for flight impact assessment:

  • visibility — below 1600m triggers IFR, affecting approach rates
  • windSpeed / windDirection — crosswind limits vary by runway
  • textDescription — human-readable conditions

2.3.4. REPL Session: Historical METAR Fetch

For January 26 data, the Aviation Weather Center archive provides 30-day lookback. Beyond that, NCEI AIRS is required:

;; Fetch METAR from AWC archive (within 30 days)
(defn fetch-metar-archive [station date]
  (let [url (format "https://aviationweather.gov/api/data/metar?ids=%s&date=%s"
                    station date)]
    (-> (http/get url {:headers {"User-Agent" "flight-tracking/1.0"}})
        :body)))

;; For Jan 26 (>30 days old), we need NCEI AIRS
;; Submit request at: https://www.ncdc.noaa.gov/has/HAS.DsSelect
;; Select: Land-Based → SRRS Text Products
;; Station: KBOS, Date: 2026-01-26 to 2026-01-27

;; Once retrieved, parse the SRRS format:
(defn parse-metar-line [line]
  (let [[_ time wind vis wx temp dew altim]
        (re-matches #"KBOS (\d{6}Z) (\S+) (\S+) (\S*) (\S+)/(\S+) A(\d+)" line)]
    {:time time :wind wind :visibility vis :weather wx
     :temp temp :dewpoint dew :altimeter altim}))

;; Example METAR from Jan 26 blizzard:
;; KBOS 261456Z 02025G35KT 1/4SM +SN BLSN VV008 M04/M06 A2974
;; Wind 020 at 25 gusting 35kt, 1/4 mile vis, heavy snow, blowing snow

2.3.5. FAA Timeline

Advisory Time (UTC) Facility Action
Jan 26 BOS/ZBW 7 BOS advisories within 95 total
Jan 27 BOS/ZBW 4 BOS advisories within 69 total

2.3.6. What's Missing

  • ADS-B receiver archive availability for Jan 26-27 (not yet confirmed)
  • NCEI SRRS data request pending — submit via AIRS for historical METAR
  • If no ADS-B archive: this case study stays FAA + weather-only, demonstrating ground stop recovery from public data alone

2.3.7. Data Source Verification

Sources for this section:

2.3.8. Open Question

Do winter ground stops produce the same altitude-band signature as thunderstorms? The hypothesis: no. Winter operations keep aircraft on the ground (deicing, cancellations), so the approach band shouldn't show holding patterns. The REPL test:

;; If archives exist: compare altitude distributions
(def winter-bands (altitude-histogram (load-data "2026-01-26" (range 24))))
(def storm-bands  (altitude-histogram (load-data "2026-05-19" [21 22 23])))

;; Are the distributions different?
(chi-squared-test winter-bands storm-bands)

2.4. March 16 — Worst Day, No BOS Stop (stub)

March 16, 2026 was the worst day of the year for the NAS: 216 ATCSCC advisories (z=+3.69, the highest z-score in 141 days of data). Nine of those advisories mentioned ZBW (Boston Center), but no BOS ground stop was issued.

This is the interesting negative case: the system was under extreme stress, Boston Center was affected, but Logan itself stayed open.

2.4.1. What the Advisory Data Shows

Metric May 19 (storm) Mar 16 (peak)
Total advisories 167 216
BOS ground stops 4 (118, 146, 160, 167) 0
ZBW advisories 14 9
z-score vs baseline +2.26 +3.69

Source: FAA ATCSCC Advisories 2026-03-16

2.4.2. What the REPL Would Test

The hypothesis: if BOS wasn't ground-stopped, ADS-B message volume should be normal or slightly depressed — not the 37% drop seen on May 19. The REPL test:

;; Compare message volumes
(def mar16-volume (hourly-counts (load-data "2026-03-16" (range 24))))
(def may19-volume (hourly-counts (load-data "2026-05-19" (range 24))))
(def baseline    (hourly-counts (load-data "2026-05-15" (range 24))))

;; Normalize to baseline
(map (fn [h] {:hour h
              :mar16 (/ (get mar16-volume h 0) (get baseline h 1.0))
              :may19 (/ (get may19-volume h 0) (get baseline h 1.0))})
     (range 24))
;; Expected: mar16 ≈ 1.0, may19 << 1.0 in hours 21-23 UTC

2.4.3. Why This Matters

The negative case strengthens the storm case study. If Mar 16 — a worse day by advisory count — shows normal ADS-B volume near BOS, then the May 19 drop is specifically attributable to the BOS ground stop, not to general NAS stress. Without the negative control, the May 19 analysis could be explained by "the whole system was busy."

2.4.4. What's Missing

  • ADS-B archives for Mar 16 (not yet confirmed)
  • The specific ZBW advisories on Mar 16 (route restrictions? flow constraints? not ground stops) — worth fetching for comparison
  • If no ADS-B data: this case study demonstrates hypothesis formation from advisory data alone, with the REPL test as a specification of what we'd check

2.5. ASA355 BIKF→KSEA (Record-Longest US 737, Launch Week)

Alaska Airlines 355, Boeing 737 MAX 8, BIKF → KSEA. Daily transatlantic service launched 2026-05-28 — the longest 737 nonstop ever scheduled by a US carrier. Captured Days 2 and 3 of operation end-to-end through a launchd-driven AeroAPI watcher, then explored interactively through the entire flight's window using one persistent REPL session.

ASA355 polar route from Keflavík to Seattle via 65°N apex over Hudson Bay

The chart above is the filed polar route, in the same projection style as the BAW48 case study so the two transatlantic shapes are directly comparable. BIKF/KEF sits at the right (red), arcing up to the 65°N latitude band over AVPUT, holding that line across Davis Strait and Hudson Bay, then descending past YDC into KSEA on the lower left (green). The light-blue band above 55°N is where Aireon space-based ADS-B becomes the surveillance source — the same coverage transition BAW48 documented, but in reverse direction. The cyan dashed marker at 65.6°N is the pure great-circle vertex; the filed route tops at 65°N — a 0.6° rounding choice that buys ETOPS-friendly land coverage at the cost of ~25 nm.

2.5.1. The Execution-Context Argument

This case study is the model. When an agent works with a complex external system, the execution context of its code matters more than the code itself. Specifically:

  • A bash subprocess loses cd on exit. The next CLI call starts in the parent shell's directory, not where you left off.
  • A bash subprocess loses environment-variable mutations on exit. Set AEROAPI_KEY in one call; the next call needs it set again.
  • Each bash -c '...' invocation is a fresh execution context. Any loaded data, parsed JSON, or computed intermediate is gone.
  • Each bb -e '(...)' invocation reloads namespaces from disk. The cost is ~500 ms × N calls for a session of N interactions.

The agent fights these every turn unless something holds the context.

A tmux session + a Clojure REPL inside it is that something. The tmux pane keeps shell state (CWD, env, history) across an unbounded number of agent-issued commands. The REPL inside it keeps language state — defined vars, loaded namespaces, in-memory data structures — across an unbounded number of agent-issued evaluations. Together they mask the subshell-statelessness problem that otherwise dominates agent-CLI-driven work.

The flight-tracking case below was executed under exactly that context. Eight hours of incremental work, all in one tmux session named flight-repl, hosting a bb repl process with the captured flt variable loaded at minute 12 and still accessible at minute 480. Every CLI tool we shipped (bin/watch-status, bin/closest-pass, bin/north-of, bin/overhead, bin/deviation) is a consolidation of forms that already worked in that REPL — not a freshly-authored script.

The flat alternative (a series of bb -e '(...)' one-shots) would have produced the same artifacts but at perhaps 10× the wall-clock cost: every probe reloads everything, every probe re-parses the snapshot, every probe is a fresh JVM/bb startup. The interactive context isn't faster because computers are faster; it's faster because the agent stops re-establishing context on every call.

2.5.2. Route Analysis

Polar filed route: BIKF → RALOV → 64°N/30°W → 65°N/40°W → 65°N/50°W → AVPUT (65.03°N, 60°W) → 65°N/70°W → 64°N/80°W → 62°N/90°W → 59°N/100°W → 55°N/110°W → BINVO → YDC → JAKSN → GLASR3 → KSEA

Metric Value
Filed distance 3,622 mi / 5,829 km
Block time 7h 55m scheduled (7h 51m actual)
Aircraft Boeing 737 MAX 8 (B38M)
Tail N812AK (Day 3)
Filed altitude FL340 cruise
STAR GLASR3 into Seattle
Service launched 2026-05-28 (Thu); we caught Days 2 + 3
Operator parallel Icelandair 2× daily + seasonal 3rd

2.5.3. Block Timing (Captured Day 3)

Event Scheduled Actual Δ
Gate-out 12:45:00Z 12:44:00Z -1 min
Wheels-up 12:55:00Z 12:58:48Z +3:48 (taxi)
Wheels-down 20:15:00Z 20:27:11Z +12:11
Gate-in 20:40:00Z 20:35:00Z -5:00 EARLY

Phase decomposition: taxi-out 14 min · airborne 7h 28m · taxi-in 7 min. The headline 4-minute improvement came entirely from airborne time; both taxi phases ran the assumed budget.

2.5.4. Deviation From the Geodesic (D3)

Filed polar route bends 200 km south of the pure great circle between endpoints. Quantified by ~geo.crosstrack/track-deviation over the 631 captured ADS-B/radar fixes — a single REPL form, against the same flt that's been loaded all day:

(r/deviation-from-route "ASA355" "2026-05-30" {:tier :adsb-only})
;; → {:n 562
;;    :max 360.8        ; km, at [65.053°N -59.308°W] (just past AVPUT
;;                      ;  over Davis Strait)
;;    :mean 158.9       ; km
;;    :argmax [65.053 -59.308]}

Switching :tier :all → :adsb-only filters out the radar tail (562 ADS-B vs 631 total). The max survives the filter — the canonical deviation extremum lands on an ADS-B fix. Useful tier-gating invariant for any claim asserted to better than ~50 km.

2.5.5. Runway-From-Procedure-Turn (The Lesson)

Initial single-heading inference said RWY 16 (south landing): final captured heading on approach was 176°–177° true. Wrong: the plane continued south past KSEA to lat 47.12°N, then reversed through 336° → 359° → 001° on the procedure turn and landed RWY 34 (north).

The fix came from track replay in the REPL — possible only because the captured fixes were already loaded:

(->> (w/positions "ASA355")
     (take-last 20)
     (mapv (fn [p] [(:timestamp p) (:heading p)])))
;; → 222° → 198° → 177°   ← downwind (looks like final)
;;   → 222° → 336° → 359° → 001°  ← 180° reversal = procedure turn
;;   → final, runway 34 northbound

Without the persistent REPL state, this debugging move would have needed a custom script reading the snapshot from disk, re-parsing JSON, then projecting fields. With it: one form, three seconds.

Generalizable rule for any landing-runway inference: never key on a single heading point; require the stable post-reversal segment.

2.5.6. Observer-From-Origin

Three "where is the plane relative to a fixed observer" queries, all answered from the captured tracklog. Observer here is KBOS (Boston Logan) as a stand-in for "any East Coast position":

Query Result
Closest pass to KBOS (great-circle) 1,336 nm N at 16:24Z
Meridian crossing (directly north of KBOS) 15:56Z at 64.90°N
Cross-track to ideal GC(BIKF,KSEA) from KBOS 2,760 km S (foot at 65.98°N -84.76°W)

The polar route never comes near the observer — the closest fact ("1,336 nm North") is geometrically right but operationally meaningless. The meridian-crossing fact is the right question to ask: "when does the plane cross your line of longitude?" That's bin/north-of in the toolset. The distinction between these two answers comes from naming the question precisely — the same discipline the project's geo.crosstrack spec applies to the four flavors of "deviation."

2.5.7. Equal-Time Geometry (KBOS ↔ KSEA Bisector)

KBOS↔KSEA annotated projection: geodesic, 4,000 km isochron, equal-time bisector, and bisector airport waypoints over an airport-density base layer

The chart above is the same projection toolchain run against the KBOS↔KSEA endpoint pair, overlaying the geodesic (green), the 4,000 km isochron from KBOS (orange dashed — Keflavík sits on the same shell as Seattle, ~131 km off), and the perpendicular bisector great circle (purple dashed) with M / M' anchors. The bisector threads through KTVF / BGQQ / MMTO / ULDD / KG-0001 / SCIP / VOMY — seven airports spanning every continent except Antarctica, all equidistant from Boston and Seattle in still air.

The captured endpoint pair (KBOS, KSEA) defines an equal-time locus — the perpendicular bisector great circle through midpoint M (47.87°N, -95.44°W) and antipode M' (47.87°S, 84.56°E). Closest scheduled airport to M is KTVF Thief River Falls at 60 km; most-balanced major hub is KICT Wichita (|d_BOS - d_SEA| = 3.5 km); most-balanced globally is Jalal-Abad, Kyrgyzstan at imbalance 0.7 km but 10,000 km out — a stretched-bisector point appearing at antipodal distance.

Refutation included in the toolset: equal-time = equal-distance only in still air. ASA355's own 360 km D3 deviation is the proof-of-concept that real flight time is wind-warped, not geometric.

2.5.8. Annotation: Simple Flying (Day 4)

Captured as a structured annotation in research/annotations.ndjson, linked to all three fa_flight_ids of the captured saga (Day 2 outbound

  • Day 3 outbound + Day 3 inbound):
{ "fa_flight_id": "ASA355-1779955973-airline-1213p",
  "kind": "article",
  "title": "3,600 Miles: Alaska Airlines Launches Record-Longest
            Boeing 737 Flight By A US Carrier",
  "source": "simpleflying.com",
  "author": "Abid Habib",
  "published": "2026-05-31T16:00:00Z",
  "also_relates_to": [
    "ASA355-1779860627-airline-1198p",
    "ASA354-1779860627-airline-1197p" ],
  "facts_asserted": {
    "route_distance_mi": 3622,
    "service_launched": "2026-05-28",
    "previous_record_holder": "Alaska ANC→JFK",
    "world_longest_737": "GOL MCO→BSB at 3,778 mi"
  },
  "url": null,
  "url_status": "not provided in source paste" }

URL deliberately null per the project's URL-hygiene rule (no fabricated placeholders; cite by author/title/date until verified).

2.5.9. Building the Visualization (Toolchain)

The annotated projection above is the artifact of a small Makefile chain that composes three live ingredients: the OurAirports EDN cache, the geo.crosstrack primitive ns, and a bb render script that calls those primitives to produce SVG. Each step is reproducible from a clean checkout; the static data (airports) ships in the repo so the chart rebuilds offline.

docs/case-studies/airport-projection-annotated.svg: \
    resources/airports.edn \
    bin/render-projection-annotated.bb \
    src/geo/crosstrack.clj
        bb bin/render-projection-annotated.bb $@

docs/case-studies/airport-projection-annotated.png: \
    docs/case-studies/airport-projection-annotated.svg
        rsvg-convert -w 2400 $< -o $@

airport-projection-annotated: docs/case-studies/airport-projection-annotated.png
.PHONY: airport-projection-annotated

The bb script (bin/render-projection-annotated.bb) is a thin composition of REPL forms — every overlay corresponds to one ct/* call, all evolved interactively before being persisted to the file:

(require '[geo.crosstrack :as ct]
         '[flight-tracking.repl.geo :as g])

(def BOS [42.3656 -71.0096])
(def SEA [47.4502 -122.3088])

;; the three geometric overlays:
(ct/gc-polyline BOS SEA 120)              ; geodesic (green)
(ct/isochron-circle BOS 4000.0 360)       ; 4,000 km from BOS (orange dashed)
(ct/perpendicular-bisector BOS SEA 240)   ; equal-time locus (purple dashed)

;; anchors:
(def M    (ct/gc-midpoint BOS SEA))       ; → [47.87°N -95.44°W]
(def M-A  (ct/antipode M))                ; → [-47.87°N  84.56°E]

;; airport DB for the base layer + bisector waypoint dots:
@g/airports-db    ; 6,198 entries; OurAirports, large + medium +
                  ; small-with-scheduled-service, keyed by ICAO

The base-layer airport plot uses a straightforward equirectangular projection (lon → x, lat → y), drawn in size order so large hubs sit on top of medium hubs sit on top of small. The polyline / circle / bisector overlays handle longitude wraparound at ±180° by splitting into segments. rsvg-convert -w 2400 renders the SVG to a 2400px PNG at the end.

What's interesting from the REPL discipline angle: every overlay function is a pure one-form computation against the same loaded state (BOS, SEA, M, the airports-db ref). The render script is a consolidation, not a translation — same code runs from the REPL when you're exploring ((spit "preview.svg" (page (overlay-svg ...))) and from the Makefile when you're shipping.

The full source for the bb script is in the project repo at bin/render-projection-annotated.bb; the bisector-equidistance invariant the chart depends on ((apply max (map abs (haversine-imbalances bisector-pts))) → 0.0) is one of the L1 properties the L0 geo.crosstrack ns asserts in its verify fn.

2.5.10. What This Case Adds Beyond BAW48 + Storm

Three new REPL patterns surfaced here:

  • Live-flight-data-as-REPL-state. flt defined at minute 12 and still accessible at minute 480 of the session. Every new function was probed against the same captured snapshot without restart or re-parse. The cost of loading the JSON snapshot is paid once.
  • Track-replay-as-debugging. The runway-from-single-heading mistake was a snapshot-only error; one REPL form scanning the last 20 fixes surfaced the procedure-turn signature retroactively. The persistent REPL state made this an interactive examination, not a script-write.
  • Property verification via single-form REPL probes. (geo.crosstrack/verify) → :ok covers 9 invariants + 3 golden fixtures including this flight's D3-max value. The same form is cheap from the REPL, cheap from CI later, written once.

The thesis the case demonstrates is in the first section: an agent working a real external system is much faster, and much harder to break, inside a tmux + REPL context than running short CLI commands in succession. The flight-tracking work is concrete enough — and free of secrets — to ship as the canonical demonstration of that model.

2.5.11. Architecture

C4 context diagram

Observability pipeline

3. Agentic REPL Harness

3.1. Agentic REPL Harness

For AI agents driving the REPL through tmux, the key pattern is eval-wait-capture with state detection.

3.1.1. Core Pattern

eval_repl() {
    local code="$1" timeout="${2:-3}"
    tmux send-keys -t repl "$code" Enter
    sleep "$timeout"
    tmux capture-pane -t repl -p | tail -20
}

3.1.2. Common Mistakes

  1. Not waiting for output — capture happens before eval completes
  2. Assuming REPL state — sent Clojure to a shell prompt
  3. Namespace not loaded(r/help) fails without require
  4. Working directory wrong — file operations fail silently
  5. Quoting issues — shell interprets quotes before tmux sees them

3.1.3. Robust Eval with Completion Detection

eval_with_wait() {
    local code="$1" max_wait="${2:-30}"
    local marker="__DONE_$$__"

    # Send code wrapped with completion marker
    tmux send-keys -t repl \
      "(do $code (println \"$marker\"))" Enter

    # Poll for marker
    local elapsed=0
    while [[ $elapsed -lt $max_wait ]]; do
        if tmux capture-pane -t repl -p | grep -q "$marker"; then
            tmux capture-pane -t repl -p | grep -B 20 "$marker" | head -19
            return 0
        fi
        sleep 1
        ((elapsed++))
    done
    echo "TIMEOUT" && return 1
}

3.1.4. State Detection

detect_repl() {
    local output=$(tmux capture-pane -t repl -p | tail -5)
    if echo "$output" | grep -qE '=>|user>|λ'; then
        echo "clojure"
    else
        echo "shell"
    fi
}

3.1.5. Hypothesis-Driven Workflow

# 1. Observe shape
./repl-eval '(keys (first data))'

# 2. Sample distribution
./repl-eval '(->> data (map :type) frequencies (sort-by val >))'

# 3. Bind result
./repl-eval '(def errors (->> data (filter #(= :error (:type %)))))'

# 4. Refine hypothesis
./repl-eval '(->> errors (group-by :source) (map (fn [[k v]] [k (count v)])))'

The REPL maintains state across evaluations — intermediate results persist for the entire session. This is the key advantage over scripts: each eval builds on previous work without re-computation.

3.1.6. Daemon Architecture (Recommended)

The optimal setup: JVM nREPL runs as a daemon, Babashka scripts query it.

┌─────────────┐    Bencode/TCP    ┌─────────────────┐
│  Babashka   │ ←───────────────→ │   JVM nREPL     │
│  (10ms)     │    port 1667      │   (daemon)      │
│  Scripts    │                   │   Full Clojure  │
└─────────────┘                   │   AeroAPI libs  │
                                  │   Stateful      │
                                  └─────────────────┘

Why this architecture:

  • JVM startup cost paid once (daemon)
  • Babashka scripts start in 10ms
  • Full Clojure available (specs, test.check, Java interop)
  • State persists across queries (defs, loaded namespaces)
  • LAN-accessible for remote agents
# Start daemon once
clj -M:dev -m nrepl.cmdline --bind 0.0.0.0 --port 1667 &

# Fast queries via Babashka (111ms vs 2.5s)
bb scripts/nrepl-query.bb '(count (all-ns))'
bb scripts/nrepl-query.bb '
  (require (quote [aeroapi.core :as api]))
  (def client (api/from-env))
  (count (:arrivals (api/request client {:method :get :path "/airports/KBOS/flights/arrivals"})))
'

3.1.7. Babashka nREPL Client

Babashka speaks raw Bencode to the JVM daemon:

#!/usr/bin/env bb
;; nrepl-query.bb - Fast queries to JVM nREPL daemon

(defn bencode [data]
  (cond
    (integer? data) (str "i" data "e")
    (string? data) (str (count (.getBytes data "UTF-8")) ":" data)
    (map? data) (str "d" (apply str (mapcat (fn [[k v]] [(bencode k) (bencode v)])
                                             (sort-by key data))) "e")))

(defn nrepl-eval [code]
  (let [sock (java.net.Socket. "localhost" 1667)
        out (.getOutputStream sock)]
    ;; Clone + eval + collect responses
    (.write out (.getBytes (bencode {"op" "clone"})))
    ;; ... parse bencode responses ...
    ))

(when (seq *command-line-args*)
  (println (:value (nrepl-eval (first *command-line-args*)))))

3.1.8. Performance Comparison

Benchmark: 100 iterations of (+ 1 2) against running JVM nREPL daemon.

Client Min Max Avg Speedup
bb nrepl-query.bb 103ms 184ms 115ms 21x
clj -M:dev -e 2400ms 2500ms 2462ms baseline
=== Benchmark Results (hydra, FreeBSD 14.3) ===

Simple query '(+ 1 2)' - 100 iterations:
  Min: 103ms  Max: 184ms  Avg: 115ms

API query with AeroAPI call - 10 iterations:
  Avg: 1081ms (includes ~500ms network latency)

JVM client - 3 iterations:
  Avg: 2462ms

Key insight: The Babashka client overhead is ~115ms. The JVM client pays ~2400ms of startup cost every invocation. For agent automation where you may issue hundreds of queries, this 21x speedup adds up.

The daemon architecture gives agents full JVM Clojure power with Babashka-level startup times. Best of both worlds.

3.1.9. Considerations

  1. Daemon lifecycle — Start nREPL on boot or first use. It holds state (loaded namespaces, defs) across queries. Restart to reset.
  2. Connection pooling — Each nrepl-query.bb call opens a new connection. For burst queries, consider keeping a session open.
  3. Error handling — Bencode parse errors are silent. Add logging in production scripts.
  4. LAN security — Binding to 0.0.0.0 exposes nREPL. Use firewall rules or bind to specific interface (Tailscale IP recommended).
  5. Memory — JVM daemon holds loaded code in memory. For long-running daemons, monitor heap usage.

3.1.10. Direct nREPL (JVM Client)

For JVM-to-JVM communication (editor integration):

(require '[nrepl.core :as nrepl])
(with-open [conn (nrepl/connect :host "hydra" :port 1667)]
  (let [client (nrepl/client conn 5000)
        session (:new-session (first (nrepl/message client {:op "clone"})))]
    (doseq [msg (nrepl/message client {:op "eval" :code "(+ 1 2)" :session session})]
      (when (:value msg) (println (:value msg))))))

3.1.11. Method Comparison

Method Sync Structured Startup Best For
tmux send-keys No No 0 Human-in-loop
bb → JVM nREPL Yes Yes 10ms Agent automation
JVM → JVM nREPL Yes Yes 2400ms Editor/CIDER
bb –nrepl-server Yes Yes 10ms Babashka-only projects

3.1.12. Skill Reference

Full skill with references: gist:nrepl-bencode-experience

3.2. Resources