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
96%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
Documents the on-disk state files the drive-planner skill reads and writes. Per coding-policy: stateful-artifacts.
drive-planner (this tile) is the sole owner. Only this skill migrates schema_version. The sweep is both writer and reader; no other skill reads or writes these files.
/workspace/state/drive-planner/DRIVE_PLANNER_STATE_DIR environment variableskip-state.jsonThe user's "skip this meeting" decisions, with per-skip expiry. Owned by skip_state.py.
{
"schema_version": 1,
"skips": {
"evt_42": "2026-07-01T17:00:00-05:00"
}
}Fields:
schema_version (int, required) — currently 1skips (object, required) — map of meeting_id → ISO-8601 expiry timestamp (tz-aware). The skip is active while its expiry is strictly after now; once expired it is dropped on the next read/prune and the meeting re-enters needs_decision.Writer / reader contract:
apply.py remove, which derives the expiry: the request's meeting_end when present, otherwise the latest of the deleted blocks' arrive-by values — both lapse the skip once the meeting is past. add_skip(meeting_id, expires=, now=) records it; clear_skip(meeting_id, now=) undoes it; prune(now) reclaims disk.load_active_skips(now) and passes the result to scan(skip_state=...). scan.py consumes the returned {meeting_id: expiry} mapping; it never touches the file.Tolerance:
schema_version, or a schema_version below the current floor) raises SkipStateError rather than being silently treated as "no skips" — silently resetting would resurrect every skipped meeting as a nag.schema_version newer than this tile reads as "no usable prior state" (an empty map) on the read path (load_active_skips), per coding-policy: stateful-artifacts — the reader is lagging, not awaiting migration, and an empty map is the safe, non-disruptive fallback (worst case the sweep re-asks; it never escalates work). The fix is to update the tile to accept the new version. On the write path (add_skip / clear_skip / prune) a newer file is refused with SkipStateError — the no-prior-state fallback is read-only, and writing would downgrade the future-version file to v1 and clobber a newer writer's state.Migration:
schema_version 1 is the initial version; no migration exists yet. A future shape change bumps the version and adds the owner-side upgrade-on-read per coding-policy: stateful-artifacts. A version below the current floor has no migration path (v1 is first) and is refused; a version above is treated as no-usable-prior-state until the tile is updated to accept it.A created drive block has no local record — the calendar event itself IS the state (Epic #59 §4). The recheck poll re-fetches the near-term window by a direct API call and reads each of its own blocks back off the event. There is no blocks.json; the only local state file is skip-state.json above. Owned by block_props.py (build_block_args / build_description write, parse_block reads).
All state lives in the event description — the live Composio v3 calendar toolkit exposes NO writable extendedProperties on any create/patch/update action (verified against the NAS during Phase 1), so the description is the only durable, writable surface. It carries three parts:
Drive: <summary>;[drive-planner:meeting=<id>:dir=<dir>] — scan.py reads it to recognize the planner's own blocks (idempotency, lombot #50); pinned against scan._MARKER_RE by a test;<!--dp:{...}--> JSON comment (compact, hidden in most calendar UIs) with the machine state:| state key | meaning |
|---|---|
v | record schema version (currently 2) |
b | routed drive seconds captured at creation (recheck baseline) |
a | arrival-anchor timestamp, ISO-8601 — the hard arrival deadline for outbound / bridge; for a return leg it is the leg end (informational, the poll never rechecks returns) |
o / d | the routed leg endpoints (the poll re-routes exactly this pair) |
al | comma-joined record of alerts already pushed — growth and/or leave_now — so a later poll never re-pings the same condition |
The leg direction and served meeting id come from the marker; the block's start/duration carry the times (CREATE uses flat start_datetime + event_duration_*).
Writer / reader contract:
apply.py create (idempotent: finds existing markers first via GOOGLECALENDAR_FIND_EVENT, never double-books). When an alert fires, the recheck poll emits a patch and the recheck SKILL.md applies it via apply.py suppress AFTER the send; the patch carries the full rebuilt description with only al updated (GOOGLECALENDAR_PATCH_EVENT supports a partial description update).parse_block(event); a non-block or malformed event yields None (never raises), so one bad event can't abort the poll. Only arrival-anchored legs (outbound / bridge) are rechecked; a return leg is created for visibility but not watched.Migration (per coding-policy: stateful-artifacts):
v 2 is the current version. v 1 was the original extendedProperties.private string-map shape — defunct: the live v3 toolkit has no writable extendedProperties, so no v1 record was ever written, and the description-based parser cannot read that shape anyway (it carries no <!--dp:--> comment). Bump on any further shape change to the description state JSON and add the owner-side upgrade in parse_block. A record stamped NEWER than this tile supports — or carrying a non-int v — parses to None (no-usable-prior-state, the safe non-disruptive fallback). A missing v is treated as the current shape for back-compat.Tolerance:
None and is treated as "not a block I recheck" — never raised on.composio-fetch and fetch_events.py.skills
check-travel-bookings
drive-planner
drive-planner-recheck
flight-assist
nightly-travel-sync
sync-tripit