aaif-sync-chapters skill + Luma API integration (0.3.0 → 0.4.0) - #9
Conversation
- 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>
There was a problem hiding this comment.
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-chaptersskill + 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_jsonoutput splitting (avoidsplitlines()U+2028 corruption) and improveaaif-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.
- 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>
Respond-to-Comments ImprovementsCode changes (4 applied):
Tests added (2):
Commits: eb7b3b1 Remaining unresolved threads: none (6 of 6 resolved). All 48 unit tests and pre-commit pass. Generated using hero-skills. |
Self-ReviewFive review agents (code-reviewer, silent-failure-hunter, pr-test-analyzer, comment-analyzer, type-design-analyzer) ran against the full diff. Deduplicated findings: Critical (3)
Important (8)
Suggestions (7)
Strengths
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>
Self-Review — ImprovementsCritical (3 / 3 fixed):
Important (8 / 8 fixed):
Suggestions (7 / 5 fixed):
Skipped (with rationale):
Commits: 30a3164 All 90 tests across 4 suites pass; pre-commit green. Generated using hero-skills. |
What
Two releases in one PR:
0.3.0 — chapters-list sync + intake data-quality fixes
aaif-sync-chaptersskill: 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 atomicbatchUpdateon approval, idempotent; near-miss cities and unresolved-city rows are surfaced for a human, never written.gwsJSON output corrupted by U+2028 inside cell values (splitlines()→ split on\nonly) in the sync, create-chapter, and create-online-series engines. Hit by a real intake row.aaif-clean-datanow treats anyOther…city placeholder as unresolved (24 rows were invisible to scan) and the green provenance rule checks theOtherprefix; retired formulas tracked soinstall-colorsrefreshes 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 fromLUMA_API_KEYor theluma-api-keykeychain item; Luma Plus). Pure, unit-tested payload builders.--create/--apply, which the SKILL.mds instruct the agent to run only after the user explicitly approves the printed proposal/diff.aaif-create-event→luma_push.py(create page, cover upload, hosts, URL written back to tracker),aaif-update-event→luma_sync.py(field-level diff,--quietfor guest notifications, post-apply verify; no cancellation),aaif-event-status→luma_stats.py(read-only guest counts; nothing flows back into Intake Ops).Review fixes (Copilot, eb7b3b1)
api_key()guards the macOSsecuritycall; a missing binary now means "no key" (available()→ False) instead ofFileNotFoundError, preserving the connected-or-graceful contract everywhere.aaif-slug classification: replaced the substring heuristic with entity-lookup classification (newluma.NotAnEventUrl) inluma_push/luma_sync/luma_stats, so event pages whose slug starts withaaif-are no longer mistaken for chapter calendar links.Self-review fixes (5-agent review, 30a3164)
--create("couldn't verify — retry or--force") instead of silently falling back to the slug heuristic.luma.callretries transient errors only for GETs and 429s; a timed-outevents/create/updateis never re-sent (it may have gone through), and the error says to check Luma first.LumaErrornow carries.status.id/api_id; a failed re-fetch reports the id instead of crashing; a missing page URL is never fabricated into the tracker.event_timeshonors AM/PM ("6:00 PM" is 18:00, not 6 AM);parse_hostlevels are case-insensitive;upload_imagefailures wrap inLumaError; locked-keychain errors are distinguished from "no key stored".luma_statsexits 1 on partial digests and flags missing guest counts;luma_syncaborts cleanly on unresolvable URLs; sheet writes useRAW(no formula injection via intake names).Verification
luma.callretry contract andresolve_event_id; 8luma_push; 5 in newtest_luma_sync.py;sync_chaptersincl. exactapply_changesranges and a U+2028 regression test); pre-commit andclaude plugin validategreen.aaif-sync-chaptersverified live: steady-state zero changes, merge test restored a blanked cell in a single revision, second run idempotent. (Note: batchUpdate now usesRAW— new-row Luma links won't auto-hyperlink; cosmetic.)events/getresponse shape) pending a pilot calendar API key.🤖 Generated with Claude Code
Generated using hero-skills.