From e0e2e57efdcc3e65150a6d48c6fa1f4b3ba33584 Mon Sep 17 00:00:00 2001 From: Rahul Parundekar Date: Tue, 30 Jun 2026 16:26:28 +0530 Subject: [PATCH 1/2] fix: address Copilot review feedback from PR #1 - Quote argument-hint as a YAML scalar in 8 SKILL.md files (was a bare flow sequence [...], violating CONTRIBUTING.md frontmatter convention) - intake.py: fail with a clear error on missing/invalid gws JSON instead of an unhelpful ValueError/JSONDecodeError stack trace - clean.py: tighten the Issues "bad email" rule from SEARCH("@") to a REGEXMATCH so foo@ / foo@bar are flagged - check_frontmatter.py: read via a context manager (no ResourceWarning) Co-Authored-By: Claude Opus 4.8 --- scripts/check_frontmatter.py | 3 ++- skills/aaif-announcement-post/SKILL.md | 2 +- skills/aaif-attendee-reminder/SKILL.md | 2 +- skills/aaif-carousel-copy/SKILL.md | 2 +- skills/aaif-clean-data/scripts/clean.py | 2 +- skills/aaif-dayof-slides/SKILL.md | 2 +- skills/aaif-luma-description/SKILL.md | 2 +- skills/aaif-recap-post/SKILL.md | 2 +- skills/aaif-speaker-bio/SKILL.md | 2 +- skills/aaif-speaker-invite/SKILL.md | 2 +- skills/aaif-triage-intake/scripts/intake.py | 8 +++++++- 11 files changed, 18 insertions(+), 11 deletions(-) diff --git a/scripts/check_frontmatter.py b/scripts/check_frontmatter.py index 9200df5..ba6a71e 100755 --- a/scripts/check_frontmatter.py +++ b/scripts/check_frontmatter.py @@ -16,7 +16,8 @@ def check(path: str) -> list[str]: - text = open(path, encoding="utf-8").read() + with open(path, encoding="utf-8") as f: + text = f.read() m = FRONTMATTER.match(text) if not m: return [f"{path}: no `---` YAML frontmatter block at the top of the file"] diff --git a/skills/aaif-announcement-post/SKILL.md b/skills/aaif-announcement-post/SKILL.md index e530ae2..ea3a3f1 100644 --- a/skills/aaif-announcement-post/SKILL.md +++ b/skills/aaif-announcement-post/SKILL.md @@ -1,7 +1,7 @@ --- name: aaif-announcement-post description: Write the LinkedIn launch/announcement post for an AAIF meetup when RSVPs open. Use when asked to draft the announcement, launch post, or "RSVPs are open" post for an AAIF event. -argument-hint: [event title / paste tracker entry] +argument-hint: '[event title / paste tracker entry]' --- # AAIF Event Announcement (LinkedIn) diff --git a/skills/aaif-attendee-reminder/SKILL.md b/skills/aaif-attendee-reminder/SKILL.md index 595bdc2..dad6b14 100644 --- a/skills/aaif-attendee-reminder/SKILL.md +++ b/skills/aaif-attendee-reminder/SKILL.md @@ -1,7 +1,7 @@ --- name: aaif-attendee-reminder description: Write the pre-event reminder to people who RSVP'd to an AAIF meetup (sent ~1 week out and the morning of). Use when asked to draft the attendee reminder / "see you tomorrow" note for an AAIF event. -argument-hint: [event title / paste tracker entry] +argument-hint: '[event title / paste tracker entry]' --- # AAIF Attendee Reminder diff --git a/skills/aaif-carousel-copy/SKILL.md b/skills/aaif-carousel-copy/SKILL.md index 51df124..b48c25f 100644 --- a/skills/aaif-carousel-copy/SKILL.md +++ b/skills/aaif-carousel-copy/SKILL.md @@ -1,7 +1,7 @@ --- name: aaif-carousel-copy description: Write copy for a 6-slide LinkedIn carousel announcing an AAIF meetup. Use when asked to draft carousel slides/copy for an AAIF event (built from the LinkedIn Carousel template). -argument-hint: [event title / paste tracker entry] +argument-hint: '[event title / paste tracker entry]' --- # AAIF LinkedIn Carousel Copy diff --git a/skills/aaif-clean-data/scripts/clean.py b/skills/aaif-clean-data/scripts/clean.py index 92f8938..23d6f16 100755 --- a/skills/aaif-clean-data/scripts/clean.py +++ b/skills/aaif-clean-data/scripts/clean.py @@ -249,7 +249,7 @@ def L(name): # (it must not turn the row bright red). It's surfaced by `scan` instead. parts = [] if email: - parts.append(f'IF(ISNUMBER(SEARCH("@",${email}2:${email})),"","missing/bad email; ")') + parts.append(f'IF(REGEXMATCH(${email}2:${email},"^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$"),"","missing/bad email; ")') if link: parts.append(f'IF(REGEXMATCH(LOWER(${link}2:${link}),"linkedin\\.com/"),"","bad LinkedIn; ")') concat = "&".join(parts) if parts else '""' diff --git a/skills/aaif-dayof-slides/SKILL.md b/skills/aaif-dayof-slides/SKILL.md index b83338b..e6d7ba8 100644 --- a/skills/aaif-dayof-slides/SKILL.md +++ b/skills/aaif-dayof-slides/SKILL.md @@ -1,7 +1,7 @@ --- name: aaif-dayof-slides description: Turn an event's tracker entry into the slide text for the AAIF "Day of Event" deck. Use when asked to fill/write the day-of slides or event deck for an AAIF meetup. -argument-hint: [event title / paste tracker entry] +argument-hint: '[event title / paste tracker entry]' --- # AAIF Day-of Slides (from the tracker) diff --git a/skills/aaif-luma-description/SKILL.md b/skills/aaif-luma-description/SKILL.md index 68221d3..9ccfade 100644 --- a/skills/aaif-luma-description/SKILL.md +++ b/skills/aaif-luma-description/SKILL.md @@ -1,7 +1,7 @@ --- name: aaif-luma-description description: Write the Luma event-page description for an AAIF in-person meetup. Use when asked to draft the Luma description / event page copy for an AAIF event. -argument-hint: [event title / paste tracker entry] +argument-hint: '[event title / paste tracker entry]' --- # AAIF Luma Event Description diff --git a/skills/aaif-recap-post/SKILL.md b/skills/aaif-recap-post/SKILL.md index cbb8978..bfbe09a 100644 --- a/skills/aaif-recap-post/SKILL.md +++ b/skills/aaif-recap-post/SKILL.md @@ -1,7 +1,7 @@ --- name: aaif-recap-post description: Write the post-event LinkedIn recap for an AAIF meetup (posted within 48 hours, with photos). Use when asked to draft the recap, thank-you, or wrap-up post after an AAIF event. -argument-hint: [event title / paste tracker entry] +argument-hint: '[event title / paste tracker entry]' --- # AAIF Post-Event Recap (LinkedIn) diff --git a/skills/aaif-speaker-bio/SKILL.md b/skills/aaif-speaker-bio/SKILL.md index a9c0c8c..b36c432 100644 --- a/skills/aaif-speaker-bio/SKILL.md +++ b/skills/aaif-speaker-bio/SKILL.md @@ -1,7 +1,7 @@ --- name: aaif-speaker-bio description: Write a speaker bio (a 60-80 word bio + a one-liner) for an AAIF in-person meetup speaker. Use when asked to draft/write a speaker bio for an AAIF event or chapter. -argument-hint: [speaker name / paste their tracker row] +argument-hint: '[speaker name / paste their tracker row]' --- # AAIF Speaker Bio diff --git a/skills/aaif-speaker-invite/SKILL.md b/skills/aaif-speaker-invite/SKILL.md index 69745f8..1dadfb3 100644 --- a/skills/aaif-speaker-invite/SKILL.md +++ b/skills/aaif-speaker-invite/SKILL.md @@ -1,7 +1,7 @@ --- name: aaif-speaker-invite description: Write a short, warm speaker-invite DM or email for an AAIF in-person meetup. Use when asked to draft a speaker invite, outreach DM, or ask-someone-to-speak message for an AAIF event. -argument-hint: [speaker name + event / paste tracker entry] +argument-hint: '[speaker name + event / paste tracker entry]' --- # AAIF Speaker Outreach / Invite diff --git a/skills/aaif-triage-intake/scripts/intake.py b/skills/aaif-triage-intake/scripts/intake.py index a637adf..f1e2c39 100755 --- a/skills/aaif-triage-intake/scripts/intake.py +++ b/skills/aaif-triage-intake/scripts/intake.py @@ -43,7 +43,13 @@ def fetch(tab): sys.exit(f"gws error reading {tab}: {out.stderr.strip()}") # gws prints a keyring banner line before the JSON; find the JSON start. txt = out.stdout - data = json.loads(txt[txt.index("{"):]) + start = txt.find("{") + if start < 0: + sys.exit(f"gws returned no JSON for {tab} (got: {txt.strip()[:200]!r})") + try: + data = json.loads(txt[start:]) + except json.JSONDecodeError as e: + sys.exit(f"gws returned invalid JSON for {tab}: {e}") vals = data.get("values", []) if not vals: return [], [] From fdf0dbcfcacaee4b58edbc676283bf41574d521c Mon Sep 17 00:00:00 2001 From: Rahul Parundekar Date: Tue, 30 Jun 2026 16:50:45 +0530 Subject: [PATCH 2/2] docs: document required OAuth scopes (Sheets/Drive write, Forms) in README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enabling the APIs is not enough — the ops skills write to Sheets/Drive and read Form responses, so the token needs read-write scopes. Read-only access passes the verify step but fails on the first write. Add Forms API + the forms.body / forms.responses.readonly scopes for intake ops. Co-Authored-By: Claude Opus 4.8 --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 99ead27..f408d20 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,19 @@ Install it, then authenticate with one of: - **Pre‑obtained token:** set `GOOGLE_WORKSPACE_CLI_TOKEN=` - **Client app:** set `GOOGLE_WORKSPACE_CLI_CLIENT_ID` and `GOOGLE_WORKSPACE_CLI_CLIENT_SECRET`, then `gws auth login` -On your Google Cloud project, enable the **Sheets**, **Drive**, and **Docs** APIs. +On your Google Cloud project, enable the **Sheets**, **Drive**, **Docs**, and +**Forms** APIs. + +**OAuth scopes.** The ops skills *read and write* Sheets/Drive (clone, rebrand, +clean) and read Form responses, so grant **read-write** scopes — read-only access +passes the verify step below but then fails on the first write: + +- `https://www.googleapis.com/auth/spreadsheets` — read/write the intake sheet +- `https://www.googleapis.com/auth/drive` — copy, create, and update Drive files + (chapter/series asset clones) +- `https://www.googleapis.com/auth/forms.body` — read/edit the intake form +- `https://www.googleapis.com/auth/forms.responses.readonly` — read form responses + Verify with: ```bash