Travel assistant for NanoClaw: byAir flight notifications (delay, gate, connection risk, inbound aircraft delay, time-to-leave, arrival logistics), traffic-aware drive planning for in-person meetings (auto drive blocks + leave-by traffic rechecks), travel-booking gap checks, and nightly TripIt sync. Per-chat overlay plugin.
72
90%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
Reference for the data.events payload the precheck script emits on wake_agent: true. Each event is {"flight_id": int | null, "event": {...}}. flight_id is null only for the source-level events (byair_unreachable, byair_recovered, sync_internal_error, sync_internal_recovered). Consumed by SKILL.md Step 3 to compose user-facing notifications.
Flight poll events also carry an opaque notification_id. The precheck persists it before advancing the snapshot. Acknowledge each ID only after send_message confirms delivery, following skills/flight-assist/references/notification-delivery.md. Source-level sync events and other callers may omit the ID.
wake_rules.py)cancelled{"reason": "cancelled"}Fires on computed_status transition into cancelled, OR on first cycle if the flight is already cancelled.
diverted{"reason": "diverted"}Same trigger as cancelled but for diverted status.
gate_change{"reason": "gate_change", "side": "dep" | "arr", "from": "B25", "to": "B7"}to may be null (gate removed). from is always non-null (first publication of a gate doesn't fire).
delay{"reason": "delay", "delay_minutes": 22, "new_dep_time": "2026-05-17T13:00:00-07:00"}
{"reason": "delay", "delay_minutes": 31, "new_dep_time": "2026-06-04T21:11:00+02:00", "schedule_slip": true}delay_minutes is the size of the delay in minutes (positive = later than before, negative = moved earlier); new_dep_time is the departure to show. The optional schedule_slip: true flag marks a delay already present on the first poll; compose it identically to a plain delay. Firing thresholds and the slip-vs-schedule rule live in wake_rules.py (detect_wake_events; constants in the module docstring).
inbound_delay_predicted{"reason": "inbound_delay_predicted", "delay_minutes": 35, "predicted_time": "2026-05-17T13:00:00-07:00"}delay_minutes is the predicted inbound-aircraft delay; predicted_time is the new estimated departure. The precheck derives that clock from the flight record's scheduled departure plus the current positive inbound delay, preserving the scheduled offset. It enriches only the notification projection; the persisted schema-7 snapshot has no new field. Firing threshold and dedupe live in wake_rules.py (detect_wake_events).
inbound_delay_retracted{"reason": "inbound_delay_retracted", "prev_delay_minutes": 95, "new_delay_minutes": null}prev_delay_minutes is the delay last surfaced to the user; new_delay_minutes is the current prediction (null when retracted entirely). Send the all-clear even if an earlier surface escalated to "rebook now" — without it, silence reads as still-delayed. Firing conditions live in wake_rules.py (detect_wake_events).
An unsent prediction that recovers produces no all-clear. If an earlier prediction was delivered, the clear refers to that last confirmed prediction. A more recent confirmed all-clear supersedes older confirmed predictions.
boarding_started{"reason": "boarding_started"}Fires on transition into actual boarding, gated on the real-boarding signal rather than the computed_status label alone — byAir labels the phase boarding before boarding starts. The real-boarding predicate is also held against flight-assist's own planned boarding window, the one its boarding block uses (#295), each snapshot judged at its own poll instant. First-cycle "already boarding" does not fire. An undelivered notice retries while the live snapshot still reflects boarding. The precheck sets phase_markers.boarding_fired only from a confirmed receipt and suppresses subsequent transitions on that consumed flag; existing true legacy flags remain consumed. Firing conditions and the real-boarding predicate live in wake_rules.py (detect_wake_events, is_real_boarding); the window in phase_markers.py (boarding_window_open).
carousel_revealed{"reason": "carousel_revealed", "baggage": "CLM1"}Fires when baggage transitions null → populated.
connection_risk.py)connection_at_risk{
"reason": "connection_at_risk",
"leg1_code": "AA100",
"leg2_code": "AA200",
"leg1_flight_id": 12345,
"connecting_airport_id": 28,
"transfer_minutes_remaining": 32,
"missed_connection": false,
"scheduled_layover_minutes": 70,
"min_transfer_minutes": 45,
"projected_leg1_arr_time": "2026-05-17T13:28:00-07:00",
"scheduled_leg2_dep_time": "2026-05-17T14:00:00-07:00"
}Fires when the projected transfer window between leg-1 arrival and leg-2 departure falls below min_transfer_minutes (configurable via config.json:min_transfer_minutes, default 45). The event is keyed to leg-2's flight_id (the at-risk downstream leg) so the once-per-flight phase marker (connection_at_risk_fired) survives leg-1 landing.
missed_connection is true when transfer_minutes_remaining is at or below zero — i.e., leg-1 is projected to arrive at or after leg-2 has already departed, so the connection is structurally lost. SKILL.md branches on the flag to render a "rebook required" message instead of a sub-zero "boards in N min" string.
Suppression rules:
landed (outcome observable), cancelled, or diverted (a more specific alert path fires)connection_at_risk_fired is already true on the leg-2 record (once-per-flight)phase_markers.py)day_before{"reason": "day_before", "scheduled_dep_time": "2026-09-10T19:50:00-05:00", "hours_until_dep": 7, "day_label": "today", "day_label_tz": "America/Chicago"}Fires once per flight. The firing logic lives in phase_markers.py (check_day_before, DAY_BEFORE_HOURS); a flight first tracked late can fire the same local day.
hours_until_dep — the real whole hours from the poll to scheduled departure (24 at the exact threshold, 7 in the example); never the constant 24.day_label — today, tomorrow, or a YYYY-MM-DD date, resolved by the precheck against the operator's local date through the core current-tz reader. Render it verbatim.day_label_tz — the IANA zone the label was resolved in; null when no operator zone was available, in which case day_label is the explicit date scheduled_dep_time carries in its own offset.time_to_leave{"reason": "time_to_leave", "leave_by": "2026-05-18T16:15:00+00:00", "travel_time_minutes": 30, "scheduled_dep_time": "2026-05-18T17:00:00+00:00"}Fires when now + travel_time + 15min buffer ≥ scheduled_dep_time. travel_time_minutes is the in-traffic estimate from Google Maps Distance Matrix. Suppressed once the snapshot shows the flight really boarding or already gone — departed, en_route, landed, cancelled, or diverted (phase_markers.is_boarding_or_gone) — the leave-by alert is moot by then (#102).
gate_assignment{"reason": "gate_assignment", "dep_gate": "E16", "dep_terminal": "2"}One-time departure gate + terminal readout, fired the first cycle a gate exists inside the pre-boarding window. The window bound lives in phase_markers.check_gate_assignment (GATE_ASSIGNMENT_WINDOW_LEAD_MINUTES). dep_terminal is null when byAir hasn't published a terminal. Gate info before the window is recorded to state silently; gate moves after this readout surface as gate_change (#103).
arrival_logistics{"reason": "arrival_logistics", "scheduled_arr_time": "2026-05-18T20:00:00+00:00", "minutes_until_arr": 15}Fires once per flight at T-arr − 15min.
removed_upstream{"reason": "removed_upstream"}Fires when byAir returns 404 for a previously-tracked flight. The agent should surface this to the user (don't auto-delete state — let the user confirm).
skills/flight-assist/sync_tripit.py)The daily sync script also emits via the same data.events shape so SKILL.md's composition table has a single contract.
tracked_flight_added{
"reason": "tracked_flight_added",
"code": "XX123",
"scheduled_dep_time": "2026-05-18T17:00:00+00:00",
"scheduled_arr_time": "2026-05-18T20:00:00+00:00"
}Fires when a flight appeared in byair_list_trips that wasn't on the active-flights index. The sync script writes the initial state record; the event carries the flight code + scheduled times so the agent has notification context without re-reading state.
tracked_flight_removed{
"reason": "tracked_flight_removed",
"code": "XX123",
"scheduled_dep_time": "2026-05-18T17:00:00+00:00",
"scheduled_arr_time": "2026-05-18T20:00:00+00:00"
}Fires when a previously-tracked flight is no longer in byair_list_trips (e.g., the trip expired or the user untracked it upstream). The sync script deletes the per-flight state file — the event payload captures the prior-state metadata BEFORE the delete so the agent's composition template has code to render. Reading state by flight_id after this event arrives returns None (state already deleted).
skills/flight-assist/sync_tripit.py, #326)These concern byAir itself, so the wrapper carries "flight_id": null. There is no per-flight state to read for them.
byair_unreachable{"reason": "byair_unreachable", "consecutive_failures": 3, "error": "session_missing"}Fires once per failure run, when consecutive sync passes that reached byAir and failed hit the threshold (_OUTAGE_ALERT_THRESHOLD in skills/flight-assist/sync_tripit.py). error is the latest byAir error type (transport_error, session_missing, …). Until the sync recovers, new or changed trips do not reach the tracking index. Polling of already-tracked flights is separate; its failures show as error: "byair_poll_failed" in the flight-assist precheck payload.
byair_recovered{"reason": "byair_recovered", "failures_before_recovery": 5}Fires on the first clean sync pass after a byair_unreachable alert.
sync_internal_error{"reason": "sync_internal_error", "consecutive_failures": 3, "error": "ValueError"}Fires once per failure run, when consecutive passes that died on a local fault (configuration, state, or a bug) hit the same threshold. error is the exception class. The traceback is in the sync-tripit precheck's stderr in the host log. byAir itself is not implicated.
sync_internal_recovered{"reason": "sync_internal_recovered", "failures_before_recovery": 4}Fires on the first pass that produces a result after a sync_internal_error alert. It means the local fault cleared, nothing more: that same pass may still fail at byAir and advance the separate byAir run.
flight_id arrive in one cycle (then merge per the ordering rule in SKILL.md Step 3).flight_id: null) are never merged, with each other or with flight events: each gets its own notification line.reason. Render <missing> or skip the substring rather than inventing a value..tessl-plugin
rules
skills
check-travel-bookings
drive-engine
expertflyer
flight-assist
scripts
nightly-travel-sync
sync-tripit