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
"""Engine orchestration — assemble the pure pipeline into a reconcile plan.
Ties the pure modules together: normalized `Flight`s → merged union → per-trip
chains → connection classification → planned legs → §B anchors → routed drives with
GPS-overlaid origins → desired blocks → the reconcile diff against the calendar.
Dependency-injected so the whole flow is testable headless: the caller resolves
everything that needs I/O — airport facts (byAir `get_airport`), routed drive
seconds (`maps_client`), the current calendar blocks, the live-location fix, and
the boarding-block / left-terminal predicates — and passes them in. `main()` wires
the real clients; this function stays pure over its inputs.
Per-leg endpoint resolution (the routing ↔ leave-by ↔ origin interplay of §A/§B):
- departure: dest = departure airport (fixed); origin = `position_at(anchor)`, then
routed, then GPS-overlaid when imminent, then re-routed from the resolved origin.
leave_by = arrive_by − routed drive.
- arrival: origin = arrival airport (fixed); dest = `position_at(depart_after)`,
which is home once a flight has landed at a home airport (#335). The drive
starts at depart_after; no GPS overlay.
Positions come from one `itinerary.Itinerary` per sweep, built from the TripIt
schedule and the merged byAir ∪ TripIt flights (`itinerary_for`), so a flight
only byAir tracks still moves the operator.
- transfer: both endpoints fixed airports; leave_by = window_end − routed drive.
A leg whose non-fixed endpoint cannot be resolved to an address (`position_at`
returns None off a configured home) is skipped with a diagnostic rather than
routed blind. A trivial routed drive with a boarding block present is suppressed.
"""
from __future__ import annotations
import sys
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
from pathlib import Path
# The itinerary ships in the co-located travel-core bundle; runtime mount,
# dev-clone sibling fallback for CI (the same pattern as `leg_anchor`).
_TRAVEL_CORE = Path("/home/node/.claude/skills/tessl__travel-core")
if not _TRAVEL_CORE.is_dir():
_TRAVEL_CORE = Path(__file__).resolve().parent.parent / "travel-core"
if str(_TRAVEL_CORE) not in sys.path:
sys.path.insert(0, str(_TRAVEL_CORE))
from block_codec import leg_identity # noqa: E402
from chain import LegKind, plan_chain_legs # noqa: E402
from chain_builder import build_pair_contexts, group_into_chains # noqa: E402
from flight_identity import Flight, MergedFlight, merge_flights # noqa: E402
from itinerary import CANCELLED, DIVERTED, Itinerary, Transport # noqa: E402
from leg_anchor import AirportFacts, BufferOverrides, ConcreteLeg, resolve_leg_anchor # noqa: E402
from position import position_at, resolve_leg_origin # noqa: E402
from reconcile import ( # noqa: E402
DesiredBlock,
HeldLeg,
ParsedBlock,
ReconcilePlan,
legacy_keys_for_airport_leg,
plan_reconcile,
)
from suppression import is_trivial_leg # noqa: E402
# No real drive to or from an airport runs this long; a longer one means the
# origin or destination resolved to the wrong city (a 16.8 h Schiphol→FCO
# "drive", #323). Such a leg writes no block and reports why.
MAX_AIRPORT_DRIVE = timedelta(hours=6)
RouteFn = Callable[[str, str], "timedelta | None"]
BoardingPresentFn = Callable[[MergedFlight], bool]
LeftTerminalFn = Callable[[MergedFlight, MergedFlight], bool]
@dataclass(frozen=True)
class AirportInfo:
"""Resolved byAir facts for one airport, keyed by IATA in the `airport_info` map."""
flag: str | None = None
delay_index: str | None = None
timezone: str | None = None # IANA tz — the airport block's local display tz
lat: float | None = None # byAir coordinates — the airport's routing point
lon: float | None = None
@dataclass(frozen=True)
class EngineResult:
plan: ReconcilePlan
skipped: tuple[str, ...] = () # human diagnostics for legs that couldn't be built
# Legs that exist but could not be computed this sweep; their blocks are kept.
held: tuple[HeldLeg, ...] = ()
origin_sources: tuple[dict[str, str], ...] = ()
# A builder's outcome: the block, or None with a diagnostic, and whether the leg
# is held (it exists but could not be computed) rather than deliberately absent.
Built = tuple["DesiredBlock | None", "str | None", bool]
def _facts_for(flight: MergedFlight, airport_info: dict[str, AirportInfo]) -> AirportFacts:
dep = airport_info.get(flight.dep_airport, AirportInfo())
arr = airport_info.get(flight.arr_airport, AirportInfo())
return AirportFacts(
dep_flag=dep.flag,
arr_flag=arr.flag,
delay_index=dep.delay_index,
dep_timezone=dep.timezone,
arr_timezone=arr.timezone,
)
def _airport_place(iata: str) -> str:
"""The display form of an airport endpoint, `"STN airport"`, written on the
block. Also the routing form when the airport has no coordinates: the maps
route can't reliably resolve a bare 3-letter code (#165)."""
return f"{iata} airport"
def _airport_point(iata: str, airport_info: dict[str, AirportInfo]) -> str:
"""Where the maps route goes for an airport: its byAir coordinates as
`"lat,lon"` when known, else the text `_airport_place`. Text is geocoded, and
the backup provider geocoded "SFO airport" as the town of Airport, MO (#334);
`reconcile_sweep.make_route` refuses a backup answer for a text airport."""
info = airport_info.get(iata)
if info is not None and info.lat is not None and info.lon is not None:
return f"{info.lat},{info.lon}"
return _airport_place(iata)
def _leg_past(kind: LegKind, concrete: ConcreteLeg, now: datetime) -> bool:
"""True when a leg's drive window has already closed. A departed/completed
flight needs no drive, and routing the operator's current home to a faraway,
already-past airport (e.g. home in TN → a London departure that flew last week)
has no driving route and just fails — so filter these before building instead
of letting each fail routing with noise (#165)."""
instant = concrete.window_end if kind is LegKind.AIRPORT_TRANSFER else concrete.anchor
return instant is not None and instant < now
def _dest_label(planned) -> str:
"""A human label for a resolved drive-home destination: 'home' only when it
really is the static home, else the lodging/anchor address (never a lie)."""
if planned.source == "home":
return "home"
return planned.address or "destination"
def _summary(leg: ConcreteLeg) -> str:
if leg.kind is LegKind.AIRPORT_DEPARTURE:
return f"Drive: → {leg.dest_airport}"
if leg.kind is LegKind.AIRPORT_ARRIVAL:
# Arrival's real destination is resolved later; _build_arrival overrides
# this with the actual place. Fallback label only.
return f"Drive: {leg.origin_airport} → destination"
return f"Drive: {leg.origin_airport} → {leg.dest_airport}"
def _legacy_keys(leg: ConcreteLeg) -> frozenset[tuple[str, str, str]]:
kind = (
"airport_departure"
if leg.kind is LegKind.AIRPORT_DEPARTURE
else "airport_arrival"
if leg.kind is LegKind.AIRPORT_ARRIVAL
else "airport_transfer"
)
return legacy_keys_for_airport_leg(kind, leg.flight.byair_flight_ids)
def _implausible(side: str, leg: ConcreteLeg, drive: timedelta, origin: str, dest: str) -> str:
hours = drive.total_seconds() / 3600
return (
f"{side} {leg_identity(leg)}: implausible {hours:.1f} h drive {origin} → {dest}, "
"not written — check the trip's lodging and transport times in TripIt"
)
def _build_departure(
leg: ConcreteLeg,
*,
itinerary: Itinerary,
now: datetime,
live_origin: str | None,
origin_sources: list[dict[str, str]],
route: RouteFn,
boarding_present: BoardingPresentFn,
airport_info: dict[str, AirportInfo],
) -> Built:
anchor = leg.anchor
assert anchor is not None and leg.dest_airport is not None
dest = _airport_place(leg.dest_airport)
dest_point = _airport_point(leg.dest_airport, airport_info)
planned = position_at(itinerary, anchor)
if planned.address is None:
return None, f"departure {leg_identity(leg)}: no origin (position_at unresolved)", True
approx = route(planned.address, dest_point)
if approx is None:
return None, f"departure {leg_identity(leg)}: origin route failed", True
leave_by = anchor - approx
resolved = resolve_leg_origin(
planned, now=now, leave_by=leave_by, drive=approx, live_origin=live_origin
)
origin_sources.append(
{
"identity": leg_identity(leg),
"kind": "airport_departure",
"source": "live_gps" if resolved.source == "live_gps" else "planned",
}
)
origin = resolved.address or planned.address
drive = route(origin, dest_point)
if drive is None:
return None, f"departure {leg_identity(leg)}: route failed", True
if drive > MAX_AIRPORT_DRIVE:
return None, _implausible("departure", leg, drive, origin, dest), True
if is_trivial_leg(drive, presence_block_present=boarding_present(leg.flight)):
return (
None,
f"departure {leg_identity(leg)}: trivial ({int(drive.total_seconds())}s), suppressed",
False,
)
leave_by = anchor - drive
return (
DesiredBlock(
identity=leg_identity(leg),
kind="airport_departure",
summary=_summary(leg),
start=leave_by,
end=anchor,
origin=origin,
destination=dest,
baseline_seconds=int(drive.total_seconds()),
anchor=anchor,
timezone=leg.timezone,
legacy_keys=_legacy_keys(leg),
),
None,
False,
)
def _build_arrival(
leg: ConcreteLeg,
*,
itinerary: Itinerary,
route: RouteFn,
boarding_present: BoardingPresentFn,
airport_info: dict[str, AirportInfo],
) -> Built:
anchor = leg.anchor
assert anchor is not None and leg.origin_airport is not None
origin = _airport_place(leg.origin_airport)
origin_point = _airport_point(leg.origin_airport, airport_info)
planned = position_at(itinerary, anchor)
destination = planned.address
destination_label = _dest_label(planned)
if destination is None:
return None, f"arrival {leg_identity(leg)}: no destination (position_at unresolved)", True
drive = route(origin_point, destination)
if drive is None:
return None, f"arrival {leg_identity(leg)}: route failed", True
if drive > MAX_AIRPORT_DRIVE:
return None, _implausible("arrival", leg, drive, origin, destination), True
if is_trivial_leg(drive, presence_block_present=boarding_present(leg.flight)):
return None, f"arrival {leg_identity(leg)}: trivial, suppressed", False
return (
DesiredBlock(
identity=leg_identity(leg),
kind="airport_arrival",
summary=f"Drive: {leg.origin_airport} → {destination_label}",
start=anchor,
end=anchor + drive,
origin=origin,
destination=destination,
baseline_seconds=int(drive.total_seconds()),
anchor=anchor,
timezone=leg.timezone,
legacy_keys=_legacy_keys(leg),
),
None,
False,
)
def _build_transfer(
leg: ConcreteLeg, *, route: RouteFn, airport_info: dict[str, AirportInfo]
) -> Built:
assert leg.origin_airport is not None and leg.dest_airport is not None
assert leg.window_start is not None and leg.window_end is not None
origin = _airport_place(leg.origin_airport)
dest = _airport_place(leg.dest_airport)
drive = route(
_airport_point(leg.origin_airport, airport_info),
_airport_point(leg.dest_airport, airport_info),
)
if drive is None:
return None, f"transfer {leg_identity(leg)}: route failed", True
if drive > MAX_AIRPORT_DRIVE:
return None, _implausible("transfer", leg, drive, origin, dest), True
leave_by = leg.window_end - drive
return (
DesiredBlock(
identity=leg_identity(leg),
kind="airport_transfer",
summary=_summary(leg),
start=leave_by,
end=leg.window_end,
origin=origin,
destination=dest,
baseline_seconds=int(drive.total_seconds()),
anchor=leg.window_start,
window_end=leg.window_end,
timezone=leg.timezone,
),
None,
False,
)
_LEG_KIND_NAME = {
LegKind.AIRPORT_DEPARTURE: "airport_departure",
LegKind.AIRPORT_ARRIVAL: "airport_arrival",
LegKind.AIRPORT_TRANSFER: "airport_transfer",
}
def _held_leg(concrete: ConcreteLeg, reason: str) -> HeldLeg:
when = concrete.window_end if concrete.kind is LegKind.AIRPORT_TRANSFER else concrete.anchor
return HeldLeg(
identity=leg_identity(concrete),
kind=_LEG_KIND_NAME[concrete.kind],
summary=_summary(concrete),
when=when,
reason=reason,
)
def itinerary_for(
flights: list[MergedFlight],
schedule: list[dict] | None,
*,
home_address: str | None,
home_airports: frozenset[str] = frozenset(),
unplaced: tuple[Transport, ...] = (),
) -> Itinerary:
"""The sweep's one itinerary: the schedule plus every merged flight, so a
flight byAir alone tracks still moves the operator (#335). Each carries
byAir's live times, its booked departure and its status: a cancelled
flight takes the schedule's copy with it, a diverted one lands at no known
airport. `unplaced` are byAir flights whose airports could not be read: he
still flew then, to somewhere the itinerary judges by the trip."""
transports = list(unplaced) + [
Transport(
f.effective_dep,
f.effective_arr if f.effective_arr is not None else f.effective_dep,
f.dep_airport,
None if f.status == DIVERTED else f.arr_airport,
scheduled=f.scheduled_dep,
status=f.status if f.status in (CANCELLED, DIVERTED) else None,
)
for f in flights
# A cancellation needs only its route and booked departure to take
# the schedule's copy with it; a flight that moves him needs a span.
if f.status == CANCELLED
or (f.effective_arr is not None and f.effective_arr > f.effective_dep)
]
return Itinerary(
schedule, home_address=home_address, home_airports=home_airports, transports=transports
)
def build_reconcile_plan(
*,
flights: list[Flight],
airport_info: dict[str, AirportInfo],
current_blocks: list[ParsedBlock],
route: RouteFn,
schedule: list[dict] | None = None,
home_address: str | None = None,
home_airports: frozenset[str] = frozenset(),
itinerary: Itinerary | None = None,
now: datetime,
live_origin: str | None = None,
boarding_present: BoardingPresentFn | None = None,
left_terminal: LeftTerminalFn | None = None,
overrides: BufferOverrides | None = None,
extra_desired: list[DesiredBlock] | None = None,
extra_held: list[HeldLeg] | None = None,
managed_legacy: frozenset[str] | None = None,
) -> EngineResult:
"""Run the full engine pipeline and return the reconcile plan + diagnostics.
`route(origin, dest)` returns the drive duration or None on failure (a failed
route skips the leg with a diagnostic, never a blind block). `boarding_present`
reports whether a flight's boarding block exists (for trivial suppression);
default assumes present. `left_terminal` supplies same-airport "did you leave"
geofence evidence. `extra_desired` folds in blocks from another source (the
meeting-leg source) so both are diffed against the calendar in one reconcile,
and `extra_held` the legs those sources could not compute, so their blocks are
kept (#334). `managed_legacy`, when given, is passed through to
`plan_reconcile` to scope which legacy generations the engine may converge /
orphan-delete.
A leg that could not be computed — no resolvable position, a failed or
implausible route, a drive window already past — is held, never dropped: its
existing block stays as it is. Only a deliberately suppressed leg (a trivial
drive) leaves the desired set, so its block is deleted.
`route` runs to completion — there is no routing deadline (#211). A hung
provider is bounded by the maps client's own per-call timeout (a failed leg
returns None and is skipped with a diagnostic), not by abandoning the plan.
"""
boarding_present = boarding_present or (lambda _flight: True)
if overrides is None:
overrides = BufferOverrides()
merged = merge_flights(flights)
# byAir governs status: a cancelled flight has no drive to or from it.
chains = group_into_chains([f for f in merged if f.status != CANCELLED])
if itinerary is None:
itinerary = itinerary_for(
merged, schedule, home_address=home_address, home_airports=home_airports
)
desired: list[DesiredBlock] = []
skipped: list[str] = []
held: list[HeldLeg] = []
origin_sources: list[dict[str, str]] = []
for chain in chains:
contexts = build_pair_contexts(chain, schedule=schedule, left_terminal=left_terminal)
planned_legs = plan_chain_legs(chain, contexts)
for planned in planned_legs:
if (
planned.kind is not LegKind.AIRPORT_DEPARTURE
and planned.from_flight is not None
and planned.from_flight.status == DIVERTED
):
# A diverted flight lands somewhere else: no drive leaves its
# booked destination until byAir names where it landed.
diverted = planned.from_flight
skipped.append(
f"{planned.kind.value} after diverted flight "
f"{diverted.dep_airport}-{diverted.arr_airport}: suppressed"
)
continue
# plan_chain_legs guarantees the right endpoint per kind, but narrow it
# explicitly here so _facts_for receives a non-None flight (no ignore).
if planned.kind is LegKind.AIRPORT_TRANSFER:
earlier, later = planned.from_flight, planned.to_flight
if earlier is None or later is None:
skipped.append("transfer leg missing a flight endpoint")
continue
concrete = resolve_leg_anchor(
planned,
facts=_facts_for(earlier, airport_info),
partner_facts=_facts_for(later, airport_info),
overrides=overrides,
)
if _leg_past(planned.kind, concrete, now):
diag = f"transfer {leg_identity(concrete)}: past, skipped"
skipped.append(diag)
held.append(_held_leg(concrete, diag))
continue
block, diag, hold = _build_transfer(
concrete, route=route, airport_info=airport_info
)
elif planned.kind is LegKind.AIRPORT_DEPARTURE:
flight = planned.to_flight
if flight is None:
skipped.append("departure leg missing a flight")
continue
concrete = resolve_leg_anchor(
planned, facts=_facts_for(flight, airport_info), overrides=overrides
)
if _leg_past(planned.kind, concrete, now):
diag = f"departure {leg_identity(concrete)}: past, skipped"
skipped.append(diag)
held.append(_held_leg(concrete, diag))
continue
block, diag, hold = _build_departure(
concrete,
itinerary=itinerary,
now=now,
live_origin=live_origin,
origin_sources=origin_sources,
route=route,
boarding_present=boarding_present,
airport_info=airport_info,
)
else: # AIRPORT_ARRIVAL
flight = planned.from_flight
if flight is None:
skipped.append("arrival leg missing a flight")
continue
concrete = resolve_leg_anchor(
planned, facts=_facts_for(flight, airport_info), overrides=overrides
)
if _leg_past(planned.kind, concrete, now):
diag = f"arrival {leg_identity(concrete)}: past, skipped"
skipped.append(diag)
held.append(_held_leg(concrete, diag))
continue
block, diag, hold = _build_arrival(
concrete,
itinerary=itinerary,
route=route,
boarding_present=boarding_present,
airport_info=airport_info,
)
if block is not None:
desired.append(block)
if diag is not None:
skipped.append(diag)
if hold:
held.append(_held_leg(concrete, diag))
if extra_desired:
desired.extend(extra_desired)
held.extend(extra_held or [])
held_keys = frozenset(leg.key for leg in held)
if managed_legacy is None:
plan = plan_reconcile(desired, current_blocks, held=held_keys, now=now)
else:
plan = plan_reconcile(
desired, current_blocks, managed_legacy=managed_legacy, held=held_keys, now=now
)
return EngineResult(
plan=plan, skipped=tuple(skipped), held=tuple(held), origin_sources=tuple(origin_sources)
).tessl-plugin
rules
skills
check-travel-bookings
drive-engine
expertflyer
flight-assist
scripts
nightly-travel-sync
sync-tripit