CtrlK
BlogDocsLog inGet started
Tessl Logo

jbaruch/nanoclaw-travel

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 tile.

77

Quality

96%

Does it follow best practices?

Impact

No eval scenarios have been run

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

reconcile.pyskills/flight-assist/scripts/

#!/usr/bin/env python3
"""Run one calendar reconciliation cycle and print a JSON summary.

Invoked by SKILL.md on the wake cycle (the byAir change events that already
wake the agent — delay / gate_change / schedule_slip / cancelled / diverted,
plus the boarding window). Deterministic glue, no LLM: it resolves the
calendar IDs, fetches the current calendar state via Composio, runs the pure
planner, executes the resulting ops, and writes the owned event IDs back into
each flight's `calendar_events` ledger. See `calendar_reconcile.py`.

Usage:
    reconcile.py

Output: single-line JSON summary on stdout —
    {"status": "...", "byair_calendar_id": "...", "planned": N,
     "executed": N, "archived": N, "failed": [...], "airport_drive": {...}}

Some keys vary by `status`: `byair_calendar_id` and `archived` are present only
when a cycle actually ran (`ok` / `no_flights`) and are omitted on `no_calendar`.
`airport_drive` is present on every NON-error summary (`ok` / `no_calendar` /
`no_flights`) — it runs independently, see below — but is absent on the early
`{"status": "error", "error": "credentials" | "state"}` setup-failure exits,
which return before it runs (no Composio client / unreadable state).

`status` is `ok` (a cycle ran), `no_calendar` (no flight calendar resolved
from config — reconciliation disabled, like maps with no key), or
`no_flights` (nothing tracked to reconcile). Per-op failures are collected in
`failed`, not fatal — a failed Composio call defers that op to the next
cycle. Exit 0 when a cycle ran (even with collected per-op failures); exit 1
on a setup failure that makes the run meaningless (missing credentials,
unreadable state).

`airport_drive` carries the parallel reconcile of the airport drive blocks
(#90) on the primary calendar — independent of the byAir flight calendar, so it
runs even when `status` is `no_calendar`. It stays a dormant idle summary when
routing inputs are absent (no Maps key / byAir URL / tracked flights), and a
transient byAir/Maps/Composio failure during it is logged and recorded as
`{"status": "error"}` without failing the rest of the cycle.
"""

from __future__ import annotations

import json
import sys
import urllib.error
from datetime import datetime, timezone
from pathlib import Path

_BUNDLE_DIR = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(_BUNDLE_DIR))

from airport_drive_reconcile import run_airport_drive_pass  # noqa: E402
from byair_client import ByAirError  # noqa: E402
from calendar_reconcile import run_reconcile  # noqa: E402
from composio_client import ComposioClient, ComposioError  # noqa: E402
from maps_client import MapsError  # noqa: E402
from state import StateError  # noqa: E402


def main() -> int:
    # Scope the credential catch to construction ONLY. run_reconcile raises
    # ValueError subclasses of its own (PlanError, DispositionError,
    # NormalizeError) and from state writes; folding those into this handler
    # would mislabel a data/validation bug as a credentials failure and point
    # the operator at the wrong fix. Keep the two failure surfaces separate.
    try:
        client = ComposioClient.from_env()
    except ValueError as exc:
        # Missing / empty Composio credentials — a setup failure. Actionable
        # message to stderr, safe-shape JSON to stdout, non-zero exit.
        print(f"flight-assist reconcile: {exc}", file=sys.stderr)
        print(json.dumps({"status": "error", "error": "credentials"}, separators=(",", ":")))
        return 1

    now = datetime.now(timezone.utc)
    try:
        summary = run_reconcile(client, now=now)
    except StateError as exc:
        # On-disk state is corrupt / unreadable — reconciliation cannot run
        # this cycle. Surface it; do not pretend a clean no-op.
        print(f"flight-assist reconcile: {exc}", file=sys.stderr)
        print(json.dumps({"status": "error", "error": "state"}, separators=(",", ":")))
        return 1
    # Any other exception (incl. a ValueError from the reconcile itself)
    # propagates: a non-zero exit with a traceback on stderr is visible
    # failure under its real cause, per `coding-policy: error-handling`
    # (catch specific types; let unexpected exceptions propagate).

    # Airport drive blocks (#90) reconcile on the PRIMARY calendar, independent
    # of the byAir flight calendar above — run them even when that returned
    # no_calendar. A transient byAir / Maps / Composio failure, or a StateError
    # from reading active-flights / per-flight state, is caught here, logged, and
    # recorded on the airport_drive sub-result without failing the rest of the
    # cycle's single-line JSON.
    try:
        summary["airport_drive"] = run_airport_drive_pass(client, now=now)
    except StateError as exc:
        # The pass reads active-flights / per-flight state, so a corrupt file
        # raises StateError here — after run_reconcile already produced a summary.
        # Record it on the airport_drive sub-result and keep the cycle's
        # single-line JSON intact, rather than letting it escape past the
        # credentials/state handler above and surface as a raw traceback.
        print(
            f"flight-assist reconcile: airport-drive state error — drive blocks skipped this "
            f"cycle. Check active-flights.json / flight-*.json for corruption. Cause: {exc}",
            file=sys.stderr,
        )
        summary["airport_drive"] = {"status": "error", "error": "state"}
    except (ComposioError, ByAirError, MapsError, urllib.error.URLError) as exc:
        print(
            f"flight-assist reconcile: airport-drive pass failed — deferred, retried next cycle. "
            f"If it repeats, check Composio / byAir / Maps connectivity and credentials. "
            f"Cause: {exc}",
            file=sys.stderr,
        )
        summary["airport_drive"] = {"status": "error"}

    print(json.dumps(summary, separators=(",", ":")))
    return 0


if __name__ == "__main__":
    sys.exit(main())

CHANGELOG.md

README.md

tile.json