Skip to content

aaif-sync-chapters skill + Luma API integration (0.3.0 → 0.4.0) - #9

Merged
rparundekar merged 4 commits into
mainfrom
feat/sync-chapters-and-luma
Jul 7, 2026
Merged

aaif-sync-chapters skill + Luma API integration (0.3.0 → 0.4.0)#9
rparundekar merged 4 commits into
mainfrom
feat/sync-chapters-and-luma

Conversation

@rparundekar

@rparundekar rparundekar commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

What

Two releases in one PR:

0.3.0 — chapters-list sync + intake data-quality fixes

  • New aaif-sync-chapters skill: push organizer decisions (Accepted / Existing (from MLOps), exact strings) from the Intake Ops sheet into the Chapters List — merge names into each city row's Organizers column, append rows for net-new cities with their Luma link. Report-and-propose by default, one atomic batchUpdate on approval, idempotent; near-miss cities and unresolved-city rows are surfaced for a human, never written.
  • Fix: gws JSON output corrupted by U+2028 inside cell values (splitlines() → split on \n only) in the sync, create-chapter, and create-online-series engines. Hit by a real intake row.
  • Fix: aaif-clean-data now treats any Other… city placeholder as unresolved (24 rows were invisible to scan) and the green provenance rule checks the Other prefix; retired formulas tracked so install-colors refreshes without stacking duplicates.

0.4.0 — Luma API integration (proposals 2–4)

  • lib/aaif_meetups/luma.py: stdlib client for the Luma public API (per-calendar key from LUMA_API_KEY or the luma-api-key keychain item; Luma Plus). Pure, unit-tested payload builders.
  • Connected-or-graceful: every script detects whether Luma is connected; when not, it degrades to manual instructions (push → manual-creation details, sync → manual checklist, stats → skipped with a note) instead of erroring.
  • Confirmation-gated writes: live writes only happen behind --create / --apply, which the SKILL.mds instruct the agent to run only after the user explicitly approves the printed proposal/diff.
  • aaif-create-eventluma_push.py (create page, cover upload, hosts, URL written back to tracker), aaif-update-eventluma_sync.py (field-level diff, --quiet for guest notifications, post-apply verify; no cancellation), aaif-event-statusluma_stats.py (read-only guest counts; nothing flows back into Intake Ops).

Review fixes (Copilot, eb7b3b1)

  • Non-macOS safety: api_key() guards the macOS security call; a missing binary now means "no key" (available() → False) instead of FileNotFoundError, preserving the connected-or-graceful contract everywhere.
  • aaif- slug classification: replaced the substring heuristic with entity-lookup classification (new luma.NotAnEventUrl) in luma_push / luma_sync / luma_stats, so event pages whose slug starts with aaif- are no longer mistaken for chapter calendar links.
  • Tests added for both.

Self-review fixes (5-agent review, 30a3164)

  • Duplicate-create protection is now fail-closed: a failed entity lookup on the LUMA URL aborts --create ("couldn't verify — retry or --force") instead of silently falling back to the slug heuristic.
  • No blind retry of writes: luma.call retries transient errors only for GETs and 429s; a timed-out events/create/update is never re-sent (it may have gone through), and the error says to check Luma first. LumaError now carries .status.
  • Post-create hardening: event id read as id/api_id; a failed re-fetch reports the id instead of crashing; a missing page URL is never fabricated into the tracker.
  • Correctness: event_times honors AM/PM ("6:00 PM" is 18:00, not 6 AM); parse_host levels are case-insensitive; upload_image failures wrap in LumaError; locked-keychain errors are distinguished from "no key stored".
  • Fail-loud reporting: luma_stats exits 1 on partial digests and flags missing guest counts; luma_sync aborts cleanly on unresolvable URLs; sheet writes use RAW (no formula injection via intake names).

Verification

  • 90 tests across 4 suites pass (55 lib incl. the luma.call retry contract and resolve_event_id; 8 luma_push; 5 in new test_luma_sync.py; sync_chapters incl. exact apply_changes ranges and a U+2028 regression test); pre-commit and claude plugin validate green.
  • aaif-sync-chapters verified live: steady-state zero changes, merge test restored a blanked cell in a single revision, second run idempotent. (Note: batchUpdate now uses RAW — new-row Luma links won't auto-hyperlink; cosmetic.)
  • Luma scripts dry-run verified against the fixture tracker, including the not-connected paths (this machine has no key). Live end-to-end test (incl. events/get response shape) pending a pilot calendar API key.

🤖 Generated with Claude Code

Generated using hero-skills.

rparundekar and others added 2 commits July 6, 2026 22:35
- New aaif-sync-chapters skill: sync Accepted / Existing (from MLOps)
  organizers from the Intake Ops sheet into the Chapters List — merge into
  each city's Organizers column, append rows for net-new cities. Report &
  propose by default; one atomic batchUpdate on approval; idempotent.
- Fix gws JSON parsing corrupted by U+2028 inside cell values (splitlines).
- Fix aaif-clean-data missing the long 'Other (PLEASE TELL US WHERE...)'
  city placeholder in scan and in the green provenance rule; refresh rules
  without stacking via LEGACY_COLOR_FORMULAS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- lib/aaif_meetups/luma.py: stdlib client for the Luma public API with pure,
  unit-tested payload builders. Detects whether Luma is connected (per-calendar
  API key via LUMA_API_KEY or keychain); when not connected every script
  degrades gracefully to manual instructions instead of erroring.
- aaif-create-event/luma_push.py: create the live event page from the tracker
  entry (times/venue/capacity, description md, banner cover upload, hosts),
  write the URL back to the tracker. Proposal by default; --create only after
  explicit user approval.
- aaif-update-event/luma_sync.py: field-level diff vs the live event; --apply
  pushes only changed fields (--quiet suppresses guest notifications) and
  re-verifies. Cancellation deliberately not automated.
- aaif-event-status/luma_stats.py: read-only guest counts and registration
  state; Luma data never flows back into the Intake Ops sheet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 6, 2026 17:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR delivers two releases worth of functionality for the AAIF Meetups Toolkit: (1) a new aaif-sync-chapters skill to sync accepted/existing organizers from Intake Ops into the Chapters List with guarded, atomic writes, and (2) a Luma Public API integration that can push/sync events and read RSVP stats while degrading gracefully when no API key is configured. It also includes several data-quality and idempotency fixes across existing spreadsheet/docx tooling, plus a version bump and changelog updates.

Changes:

  • Add aaif-sync-chapters skill + engine + pure-logic tests to sync organizer decisions from Intake Ops → Chapters List with proposal-first, atomic apply, and guardrails.
  • Add Luma Public API client (lib/aaif_meetups/luma.py) + scripts for push (luma_push.py), sync (luma_sync.py), and read-only stats (luma_stats.py) with confirmation-gated live writes.
  • Fix gws_json output splitting (avoid splitlines() U+2028 corruption) and improve aaif-clean-data “Other…” city handling + idempotent rule refresh.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
skills/aaif-update-event/SKILL.md Documents optional live Luma sync flow with confirmation gate.
skills/aaif-update-event/scripts/luma_sync.py New script to diff/apply tracker → Luma updates with --apply gating.
skills/aaif-sync-chapters/SKILL.md New skill documentation for Intake Ops → Chapters List sync workflow.
skills/aaif-sync-chapters/scripts/test_sync_chapters.py New unit tests for pure sync/merge/slug logic.
skills/aaif-sync-chapters/scripts/sync_chapters.py New engine to compute proposals and apply atomic Sheets updates via gws.
skills/aaif-event-status/SKILL.md Documents read-only Luma RSVP stats capability.
skills/aaif-event-status/scripts/luma_stats.py New script to fetch and print Luma guest counts (read-only).
skills/aaif-create-online-series/scripts/create_series.py Fixes gws_json parsing to avoid U+2028 corruption.
skills/aaif-create-event/SKILL.md Documents Luma push flow and key setup with --create gating.
skills/aaif-create-event/scripts/test_luma_push.py New tests for Luma push helper logic.
skills/aaif-create-event/scripts/luma_push.py New script to propose/create Luma events and write URL back into tracker.
skills/aaif-create-chapter/scripts/create_chapter.py Fixes gws_json parsing to avoid U+2028 corruption.
skills/aaif-clean-data/scripts/test_clean.py Updates tests for new “Other…” handling + legacy rule staleness.
skills/aaif-clean-data/scripts/clean.py Treats any “Other…” placeholder as unresolved + tracks legacy formulas as stale.
lib/aaif_meetups/tests/test_luma.py New tests for Luma client payload builders and availability behavior.
lib/aaif_meetups/luma.py New minimal stdlib Luma API client + pure payload/diff utilities.
CHANGELOG.md Adds entries for 0.3.0 and 0.4.0.
.claude-plugin/plugin.json Bumps plugin version to 0.4.0 and updates description.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/aaif_meetups/luma.py
Comment thread skills/aaif-update-event/scripts/luma_sync.py
Comment thread skills/aaif-event-status/scripts/luma_stats.py
Comment thread skills/aaif-create-event/scripts/luma_push.py Outdated
Comment thread skills/aaif-create-event/scripts/test_luma_push.py
Comment thread lib/aaif_meetups/tests/test_luma.py
- luma.api_key(): guard the macOS `security` call so a missing binary on
  non-macOS returns "no key" instead of raising FileNotFoundError, keeping
  the available()-never-raises contract; test added
- replace the `aaif-` substring heuristic with entity-lookup classification
  (new luma.NotAnEventUrl error) so event pages with aaif- slugs are not
  misclassified as chapter calendar links in luma_sync, luma_stats, and
  luma_push; already_pushed() keeps the heuristic only as offline fallback
  (safe: --create requires the key and always takes the lookup path)
- tests for already_pushed() with an aaif- event slug (mocked lookup)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rparundekar

Copy link
Copy Markdown
Collaborator Author

Respond-to-Comments Improvements

Code changes (4 applied):

  • lib/aaif_meetups/luma.py:47 — unguarded macOS security call could raise FileNotFoundError on non-macOS — wrapped in try/except OSError so api_key() falls through to the normal LumaError and available() keeps its never-raises contract; also added a NotAnEventUrl subclass of LumaError raised by resolve_event_id() for structured non-event classification.
  • skills/aaif-update-event/scripts/luma_sync.py:29aaif- substring heuristic wrongly aborted sync for event pages with aaif- slugs — replaced with entity-lookup classification; aborts cleanly on NotAnEventUrl.
  • skills/aaif-event-status/scripts/luma_stats.py:42 — same heuristic wrongly reported aaif--slug events as "not pushed" — now attempts the lookup and only prints "not pushed" on NotAnEventUrl.
  • skills/aaif-create-event/scripts/luma_push.py:39 — same heuristic could bypass the duplicate-create safety abort — already_pushed() now classifies via entity lookup when connected (--create always is), keeping the slug heuristic only as an offline fallback for keyless dry-runs.

Tests added (2):

  • lib/aaif_meetups/tests/test_luma.py — missing security binary → available() is False, api_key() raises LumaError not FileNotFoundError.
  • skills/aaif-create-event/scripts/test_luma_push.py — mocked-lookup tests: aaif- event slug counts as pushed; calendar link (NotAnEventUrl) does not.

Commits: eb7b3b1

Remaining unresolved threads: none (6 of 6 resolved). All 48 unit tests and pre-commit pass.


Generated using hero-skills.

@rparundekar

Copy link
Copy Markdown
Collaborator Author

Self-Review

Five review agents (code-reviewer, silent-failure-hunter, pr-test-analyzer, comment-analyzer, type-design-analyzer) ran against the full diff. Deduplicated findings:

Critical (3)

  • [silent-failure, type-design, comment-analyzer] skills/aaif-create-event/scripts/luma_push.py:56already_pushed() fails open: except LumaError: pass swallows any failed entity lookup (404, auth, exhausted retries) and falls back to the slug heuristic even under --create, so an aaif--slugged event page can be misclassified as not-pushed and duplicated — the docstring's "always takes the lookup path" claim is false on exactly the failing path.
  • [silent-failure] lib/aaif_meetups/luma.py:82call() retries transient 5xx/network errors for all methods, including non-idempotent events/create / events/update / hosts/add; a timeout after the server processed the request silently re-POSTs, which can create duplicate events or double-send guest notifications.
  • [silent-failure, code-reviewer] skills/aaif-create-event/scripts/luma_push.py:131-133 — post-create fragility: create_event(payload)["id"] raises KeyError if the response uses api_id (a variance resolve_event_id explicitly handles); an uncaught LumaError from the follow-up get_event() crashes after the event exists; and the url fallback fabricates https://luma.com/evt-... and writes it into the tracker, poisoning later sync/stats resolution. In all three cases a live event is orphaned and a re-run duplicates it.

Important (8)

  • [pr-test-analyzer] lib/aaif_meetups/luma.py:167_TIME_RE only parses 24-hour times: a tracker cell like "6:00 PM — 9:00 PM" silently creates a 6 AM public event.
  • [type-design] skills/aaif-create-event/scripts/luma_push.py:30parse_host matches levels case-sensitively: a@b.co:MANAGER / :Check-In silently becomes a display name with default access.
  • [silent-failure] lib/aaif_meetups/luma.py:47api_key() discards the security CLI's stderr: a locked keychain / denied ACL is misreported as "no key found", degrading to "Luma NOT connected".
  • [silent-failure] lib/aaif_meetups/luma.py:140 — the pre-signed PUT in upload_image() raises raw HTTPError on non-2xx, making the r.status not in (200,201,204) check dead code and bypassing every except LumaError handler.
  • [type-design] skills/aaif-update-event/scripts/luma_sync.py:26find_event_id gives a friendly ABORT for NotAnEventUrl but lets a plain LumaError (e.g. 404 on a stale URL) escape as a raw traceback.
  • [type-design] lib/aaif_meetups/luma.py:85LumaError is string-only: callers can't distinguish a definitive 4xx from a transient network failure.
  • [silent-failure] skills/aaif-event-status/scripts/luma_stats.py:54,83 — per-event errors print !! but the script exits 0 even when every lookup failed; missing guest_counts silently prints "going: 0".
  • [pr-test-analyzer] untested live-write surfaces: sync_chapters.apply_changes A1-range/value construction, luma.call retry contract, resolve_event_id, luma_sync.find_event_id (no test file), and no regression test for the U+2028 splitlines() fix.

Suggestions (7)

  • [silent-failure, type-design] luma.call(..., retries<=0) and sync_chapters._gws silently return None if retries is ever ≤0.
  • [silent-failure] retry backoff sleeps (up to ~20s) with no output — looks like a hang.
  • [code-reviewer] sync_chapters.py batchUpdate uses USER_ENTERED: an organizer name starting with =/+/-/@ becomes a formula in the shared sheet; use RAW.
  • [silent-failure] sync_chapters.luma_status catches broad Exception; narrow to (URLError, TimeoutError).
  • [type-design] make already_pushed's connected parameter keyword-only.
  • [comment-analyzer] doc rot: luma_push's "Nothing is sent." (dry-run still makes read-only calls when connected); sync-chapters SKILL.md hardcodes "23 MLOps + 6 Accepted" (goes stale) and omits two hard-required headers (Run events before?, Why organize / ties); event_payload should list the detail keys it consumes.
  • [pr-test-analyzer] diff_payload has no test where start_at genuinely changed.

Strengths

  • The LIVE-WRITE CONTRACT is stated and uniformly enforced: propose-first everywhere, writes only behind --create/--apply, --create additionally gated on the key.
  • sync_chapters write path: recompute from a fresh read → single atomic batchUpdate → re-read verify that exits non-zero.
  • Pure payload builders cleanly separated and well-tested (midnight rollover, tz conversion, ms-normalized diffs, accent-folded dedupe).
  • NotAnEventUrl(LumaError) is the right error shape; the U+2028/splitlines() fix and its comments are exemplary.

Generated using hero-skills.

Critical:
- luma_push.already_pushed now fails CLOSED: a failed entity lookup raises
  instead of falling back to the slug heuristic, so --create aborts on an
  unverified LUMA URL (retry or --force) rather than risking a duplicate
- luma.call no longer blind-retries non-idempotent POSTs on transient
  errors (a timed-out create may have gone through); GETs and 429s still
  retry, write errors carry a check-Luma-before-retrying hint
- luma_push post-create hardening: id extracted as id/api_id, re-fetch
  failure no longer orphans the event unreported, and a missing URL is
  never fabricated/written into the tracker

Important:
- event_times honors AM/PM ("6:00 PM" is 18:00, not a silent 6 AM event)
- parse_host matches manager/check-in case-insensitively
- api_key distinguishes a locked/denied keychain from "no key stored"
- upload_image wraps PUT failures in LumaError (handlers + no dead code)
- luma_sync.find_event_id aborts cleanly on any LumaError, not traceback
- LumaError carries .status (HTTP code or None) for callers
- luma_stats: missing guest_counts is reported (not fake 0), per-event
  errors counted and exit 1 so a partial digest can't read as complete

Suggestions:
- sync_chapters batchUpdate uses RAW (no formula injection via names)
- luma_status catches only URLError/TimeoutError, not bare Exception
- retries<=0 can no longer silently return None (luma.call, _gws)
- retry backoff prints a notice to stderr instead of hanging silently
- connected is keyword-only; docstring/SKILL.md rot fixed (dry-run
  read-only calls, stale intake counts, missing required headers)

Tests: luma.call retry contract, resolve_event_id, AM/PM, keychain-denied,
fail-closed already_pushed, new test_luma_sync.py, apply_changes ranges/RAW,
U+2028 gws_json regression, genuine diff change, upload ext rejection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rparundekar

Copy link
Copy Markdown
Collaborator Author

Self-Review — Improvements

Critical (3 / 3 fixed):

  • skills/aaif-create-event/scripts/luma_push.py:39 — fail-open duplicate-create guard — already_pushed() now raises on a failed lookup instead of guessing; main() aborts with "couldn't verify" unless --force, so --create can never proceed on an unverified LUMA URL.
  • lib/aaif_meetups/luma.py:83 — blind retry of non-idempotent POSTs — call() now retries transient errors only for GETs (and 429s, which arrive before any work); write errors carry a "may still have gone through; check Luma before retrying" hint.
  • skills/aaif-create-event/scripts/luma_push.py:127-150 — post-create fragility — id extracted as id/api_id, a failed re-fetch prints the event id instead of crashing, and a missing URL is reported for manual recording instead of fabricating https://luma.com/evt-... into the tracker.

Important (8 / 8 fixed):

  • lib/aaif_meetups/luma.py:186event_times honors AM/PM markers ("6:00 PM" → 18:00; noon/midnight edge cases tested).
  • skills/aaif-create-event/scripts/luma_push.py:31parse_host matches manager/check-in case-insensitively.
  • lib/aaif_meetups/luma.py:59-66api_key() surfaces the security CLI's stderr when the failure isn't item-not-found, so a locked keychain no longer reads as "no key stored".
  • lib/aaif_meetups/luma.py:155-160upload_image PUT failures wrapped in LumaError; the status check is no longer dead code.
  • skills/aaif-update-event/scripts/luma_sync.py:36-38find_event_id aborts cleanly on any LumaError instead of a raw traceback.
  • lib/aaif_meetups/luma.py:37LumaError.status carries the HTTP code (None for network failures).
  • skills/aaif-event-status/scripts/luma_stats.py — missing guest_counts prints an explicit note (not a fake 0); per-event errors are counted, summarized, and exit 1 so a partial digest can't be mistaken for complete.
  • Test gaps closed: luma.call retry contract (6 tests), resolve_event_id (3), find_event_id via new test_luma_sync.py (5), apply_changes exact ranges/column order/RAW, U+2028 gws_json regression, keychain-denied path, AM/PM, genuine diff_payload change, upload extension rejection.

Suggestions (7 / 5 fixed):

  • sync_chapters.apply_changes uses valueInputOption: RAW — names/cities starting with =/+/-/@ stay text (note: new-row Luma links will no longer auto-hyperlink; cosmetic).
  • sync_chapters.luma_status catches (URLError, TimeoutError) instead of bare Exception.
  • luma.call/_gws can no longer silently return None on retries<=0.
  • Retry backoff prints a stderr notice ("retry 2/3 in 4s") instead of hanging silently.
  • connected is keyword-only; doc rot fixed (luma_push dry-run wording, SKILL.md stale intake counts and two missing required headers, event_payload consumed-keys list).

Skipped (with rationale):

  • read_intake fixture test — worthwhile but larger; city-resolution logic is exercised indirectly and verified live; follow-up candidate.
  • luma_sync update-body extraction + test — needs a main() refactor; the behaviors (never send visibility, --quiet mapping) are small and inline-documented.
  • cover_url sentinel restructure in luma_sync — works and is guarded; the verify pass already excludes cover_url.
  • _norm(None, ...) sentinel split — stylistic; the pattern is documented at both call sites.
  • event_payload(view)event_payload(details) signature change — API churn across all callers; documented the consumed keys in the docstring instead.
  • events/get response-shape live verification — pending the pilot calendar API key (already noted in the PR's verification section).

Commits: 30a3164

All 90 tests across 4 suites pass; pre-commit green.


Generated using hero-skills.

@rparundekar
rparundekar merged commit a9df76f into main Jul 7, 2026
2 checks passed
@rparundekar
rparundekar deleted the feat/sync-chapters-and-luma branch July 7, 2026 05:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants