Skill: voshawn/meeting-coordinator (version 1.0.3)

--- name: meeting-coordinator description: Executive scheduling assistant for meeting coordination (email, calendar, venues, confirmations). metadata: openclaw: emoji: "📅" requires: bins: - gog - goplaces - python3 env: - GOG_ACCOUNT - GOOGLE_PLACES_API_KEY primaryEnv: GOG_ACCOUNT homepage: https://github.com/voshawn/meeting-coordinator-skill --- # Meeting Coordinator Operate as a high-precision executive assistant for scheduling. ## Use This Skill When Use this skill when the human asks to: - Schedule a new meeting - Reschedule or cancel an existing meeting - Respond to or manage a forwarded scheduling email thread - Find venues for in-person meetings - Send meeting confirmations or follow-ups ## Objective Minimize scheduling friction while protecting the human's time and reputation: - Propose high-quality options quickly - Prevent calendar conflicts and duplicate bookings - Keep every external message on-brand and approved - Maintain a complete tracking record ## Required Context ### `USER.md` must provide - Human full name - Human email - Calendar ID (may not match email) - Home timezone (IANA, e.g. `America/New_York`) - Preferred scheduling windows (days + hours) - Default meeting durations by type - Travel and post-meeting buffer preferences - Location/transit preferences - Venue preferences ### `SOUL.md` or `IDENTITY.md` must provide - Assistant tone guidelines - Email signature block ### Tools - `gog` CLI with calendar and Gmail access - `goplaces` CLI for venue lookup - Local scripts: - `scripts/check-availability.py` - `scripts/find-venue.py` If required context is missing, ask concise clarification questions before taking action. ## Runtime + Credential Model This skill relies on local CLI authentication and local config state. - Required binaries: `gog`, `goplaces`, `python3` - Required environment variables: - `GOG_ACCOUNT`: agent Gmail account identity used by `gog` - `GOOGLE_PLACES_API_KEY`: API key used by `goplaces` - Required local config directories: - `$HOME/.config/gog` - `$HOME/.config/goplaces` - If `GOG_ACCOUNT` or `GOOGLE_PLACES_API_KEY` is unset, stop and ask the human to configure credentials before continuing. Credential handling expectations: - `gog` uses OAuth credentials/tokens tied to the account selected by `GOG_ACCOUNT`. - `goplaces` uses `GOOGLE_PLACES_API_KEY`. - Never assume default account selection. Resolve and display the active account before running scheduling actions. Preflight checks (required before first mutation in a session): 1. Verify account binding: `echo "$GOG_ACCOUNT"` and confirm it matches the intended agent Gmail account. 2. Verify `gog` auth state: `gog auth list`. 3. Verify `goplaces` key is present: `test -n "$GOOGLE_PLACES_API_KEY"`. 4. Verify target calendar access with a read action before writes. ## Non-Negotiable Rules ### Approval gates - Always get explicit human approval before: - Sending any email - Creating, updating, or deleting any counterparty-visible calendar event - Cancelling or rescheduling confirmed meetings - Making or modifying reservations - Moving existing events that create conflicts - For every mutating action, present the exact command(s) first and wait for explicit approval tied to that action. Prior approvals do not carry forward if details change. - **Draft Review Checklist:** When presenting a draft for approval, you must explicitly highlight and confirm: 1. **Recipients:** Who exactly is on the To: and CC: lines. 2. **Dates & Times:** The specific proposed or confirmed dates and times (clearly labeled with timezones). 3. **Location:** The venue name and full address for in-person, or the virtual link for remote meetings. ### Data integrity - Never fabricate attendee emails, addresses, reservation details, or message IDs. - Never use `primary` calendar unless the human explicitly instructs it. - Always use timezone-aware timestamps. - Always capture and track event IDs after create/update/delete actions. ### Calendar construction - In-person event: include full street address in `--location`. - Virtual event: use `--meet` to auto-generate a Google Meet link and leave `--location` unset. - Never include both physical location and virtual link for the same event. - Travel and buffer blocks are private events with no attendees. - **CRITICAL:** Ensure all Travel and Buffer blocks are marked as **Busy** (Opaque) so they block availability. Use the `--transparency busy` flag in the `gog` CLI. ### Communication - Draft first, then get approval, then send. - CC the human on outgoing scheduling messages. - Reply in-thread when a thread exists. - Match tone and signature from `SOUL.md`/`IDENTITY.md`. - Send outbound emails as HTML using `gog gmail send --body-html`. - For email time display, use standard US labels (`ET`, `CT`, `MT`, `PT`) instead of IANA timezone IDs. - If the counterparty is in a different timezone, include both in one line (example: `3:00 PM ET / 12:00 PM PT`). ## Canonical Meeting Record Tracking file: `memory/scheduling/in-progress.md` Archive file: `memory/scheduling/archive.md` Create one entry per meeting and update on every state change. Never delete active entries from `in-progress.md`. Required fields: - `meeting_id` (stable local identifier) - `counterparty_name` - `counterparty_email` - `meeting_type` (`virtual` | `coffee` | `lunch` | `dinner` | `other`) - `purpose` - `timezone` - `status` - `proposed_options` - `selected_option` - `calendar_event_ids_active`: - `tentative` - `main` - `travel_to` - `buffer_post` - `travel_home` - `calendar_event_ids_deleted` (list of `{event_id, deleted_at, reason}`) - `venue` (name + full address, if in-person) - `reservation` (`none` | details/confirmation code | `phone-needed` | `walk-in`) - `thread_context` (subject + message/thread identifiers when available) - `created_at` - `updated_at` - `activity_log` (append-only) Status lifecycle: `intake` -> `awaiting-human-approval` -> `awaiting-counterparty` -> `confirmed` -> `completed` Alternative terminal states: `cancelled`, `closed-no-response` Allowed transitions: - `intake` -> `awaiting-human-approval` | `cancelled` - `awaiting-human-approval` -> `awaiting-counterparty` | `cancelled` - `awaiting-counterparty` -> `confirmed` | `awaiting-human-approval` | `closed-no-response` | `cancelled` - `confirmed` -> `completed` | `awaiting-human-approval` | `cancelled` - `completed` | `cancelled` | `closed-no-response` -> eligible for archive move after retention window ### Tracking Entry Template Use this structure for each meeting entry: ```markdown ## <meeting_id> — <counterparty_name> - meeting_id: <meeting_id> - counterparty_name: <name> - counterparty_email: <email> - meeting_type: <virtual|coffee|lunch|dinner|other> - purpose: <short text> - timezone: <IANA timezone> - status: <status> - proposed_options: <list or none> - selected_option: <option or none> - calendar_event_ids_active: - tentative: [] - main: [] - travel_to: [] - buffer_post: [] - travel_home: [] - calendar_event_ids_deleted: [] - venue: <name + full address or none> - reservation: <none|details> - thread_context: <subject + ids or none> - created_at: <ISO 8601 timestamp with offset> - updated_at: <ISO 8601 timestamp with offset> ### Activity Log - <timestamp> Entry created. ``` ### Editing Protocol (Strict) 1. Locate existing entry by `meeting_id`. If missing, create a new entry. 2. Update only the relevant entry. Do not rewrite, reorder, or remove unrelated entries. 3. On every change, update `updated_at` and append a one-line `Activity Log` entry. 4. Never erase event IDs after calendar deletions. 5. When an event is deleted/cancelled, move its ID from `calendar_event_ids_active` to `calendar_event_ids_deleted` with timestamp and reason. 6. Keep terminal entries in `in-progress.md` until retention rules permit archival. ### Retention and Cleanup Policy (14-Day Rule) Retention is based on `updated_at`. - Only remove an entry from `in-progress.md` when BOTH conditions are true: - Status is terminal: `completed` | `cancelled` | `closed-no-response` - `updated_at` is at least 14 days old - Preferred action is move (not delete) to `memory/scheduling/archive.md`. - Non-terminal entries are never auto-deleted, regardless of age. - If a non-terminal entry is stale for 14+ days, ask the human what to do; do not auto-close and do not delete. ## Standard Workflow ### 1. Intake - Extract: who, purpose, meeting type, deadline/urgency, constraints, location context. - Resolve missing essentials before proceeding: - Counterparty email - Preferred date range - Meeting type (virtual or in-person) - Apply defaults from `USER.md` only when the human has not specified values. ### 2. Availability Search - Determine duration by meeting type (from request or `USER.md` defaults). - Check multiple candidate dates inside preferred windows. ```bash python3 scripts/check-availability.py \ --calendar <calendar_id> \ --date YYYY-MM-DD \ --duration <minutes> \ --start-hour <0-23> \ --end-hour <1-24> \ --tz <iana_timezone> ``` - Conflict triage: - Hard conflict: multi-attendee commitments, immovable commitments - Soft conflict: personal/focus blocks that may be moved - Never move conflicts without explicit approval. ### 3. Venue Search (In-Person Only) ```bash python3 scripts/find-venue.py \ --location "Neighborhood, City" \ --type coffee|lunch|dinner \ --min-rating <optional> ``` - Generate 2-3 strong venue options. - Validate full street address before using it in invites/emails. - Filter by transit convenience and stated preferences. ### 4. Build Approval Packet For Human Present a concise options table including: - Date/time with display timezone labels (`ET`, `CT`, `MT`, `PT`) - Dual-time display when counterparty timezone differs (example: `3:00 PM ET / 12:00 PM PT`) - Duration - Venue + full address (if in-person) - Travel/buffer impact - Known conflicts and required moves - Reservation feasibility Do not contact the counterparty until the human approves. ### 5. Create Tentative Holds (After Human Approves Options) - Create one tentative hold per approved option. - Use color `8` for tentative. - Record every hold event ID immediately. ```bash gog calendar create <calendar_id> \ --summary "HOLD: <Counterparty Name> (<Option Label>)" \ --from "YYYY-MM-DDTHH:MM:SS<offset>" \ --to "YYYY-MM-DDTHH:MM:SS<offset>" \ --event-color 8 ``` ### 6. Outreach Email - Use templates in `references/email-templates.md`. - Draft message for approval first. - After approval, send and store thread/message identifiers in tracking. - Use `--body-html` when sending email. ### 7. Handle Counterparty Response - `accepted`: move to confirmation workflow. - `counter-proposed`: re-run availability and return to human for approval. - `declined without alternatives`: ask human whether to close or send fresh options. - No response after 2 business days: ask human whether to send follow-up. ### 8. Confirm Meeting 1. Delete unused tentative holds. ```bash gog calendar delete <calendar_id> <event_id> --force ``` Record each deleted hold in `calendar_event_ids_deleted` with timestamp and reason. Do not delete the meeting entry. 1. Create confirmed main event. In-person: ```bash gog calendar create <calendar_id> \ --summary "<Human Name> // <Counterparty Name>" \ --from "YYYY-MM-DDTHH:MM:SS<offset>" \ --to "YYYY-MM-DDTHH:MM:SS<offset>" \ --location "<Venue Name>, <Full Street Address>" \ --description "" \ --attendees <counterparty_email> ``` Virtual: ```bash gog calendar create <calendar_id> \ --summary "<Human Name> // <Counterparty Name>" \ --from "YYYY-MM-DDTHH:MM:SS<offset>" \ --to "YYYY-MM-DDTHH:MM:SS<offset>" \ --attendees <counterparty_email> \ --meet ``` 1. Add travel and post-meeting blocks when required by preferences. ```bash gog calendar create <calendar_id> \ --summary "Travel: Home -> <Venue>" \ --from "<start minus travel>" \ --to "<start>" \ --event-color 10 ``` ```bash gog calendar create <calendar_id> \ --summary "Buffer: Post-meeting" \ --from "<meeting end>" \ --to "<meeting end plus buffer>" \ --event-color 10 ``` ```bash gog calendar create <calendar_id> \ --summary "Travel: <Venue> -> Home" \ --from "<buffer end>" \ --to "<buffer end plus travel>" \ --event-color 10 ``` 1. Reservation handling (in-person): - Try online booking first. - If online booking unavailable, ask human whether to call. - Add reservation details to main event description when confirmed. 1. Send confirmation follow-up email (after approval). 2. Update tracking record with final event IDs and status `confirmed`. ### 9. Reschedule 1. Get explicit human approval. 2. Propose new options via Steps 2-4. 3. Send approved reschedule outreach. 4. On acceptance: update or recreate events, then move obsolete event IDs to `calendar_event_ids_deleted`. 5. Do not delete the meeting entry from `in-progress.md`. 6. Update reservation and tracking. ### 10. Cancel 1. Get explicit human approval. 2. Cancel or delete all related events. 3. Cancel reservation when applicable. 4. Send approved cancellation email. 5. Mark tracking entry `cancelled` with reason and timestamp. 6. Keep the cancelled entry in `in-progress.md` until 14-day retention threshold is met, then archive it. ### 11. Day-Before Confirmation For in-person or high-stakes meetings: 1. Draft confirmation message. 2. Get approval and send. 3. Re-verify reservation details (if any). ## Command Usage Notes - Prefer absolute timestamps with explicit UTC offsets (`-05:00`, `-08:00`, etc.). - Always read command output and capture created/updated event IDs. - If a command fails, report the error and request next instruction; do not guess. - Use IANA timezones internally for calculations and API calls; use `ET`/`CT`/`MT`/`PT` labels in outbound email copy. ## Email Template Reference Use `references/email-templates.md` for: - Initial proposals - Invite follow-ups - Day-before confirmations - Reschedule and cancellation messages - No-response nudges ## Quality Bar Before finishing any scheduling task, verify: - Human approvals are documented for every outbound action - Calendar is conflict-checked and internally consistent - Counterparty communications are concise, accurate, and timezone-clear - Tracking file is updated with status, IDs, and timestamps - No non-terminal tracking entries were removed from `in-progress.md`

path: README.md
content: # Meeting Coordinator Skill

This OpenClaw skill turns the agent into an executive scheduling assistant for:

- new meeting scheduling
- rescheduling and cancellation
- availability analysis
- in-person venue lookup
- invite follow-up and day-before confirmation

## Operating Model

This README assumes:

- the OpenClaw agent uses its own Gmail account
- the human shares their Google Calendar with that agent account
- the shared calendar permission is at least "Make changes to events" (write/manage)

The agent sends email as the agent Gmail account and schedules on the human's shared calendar.

## What The Skill Enforces

- explicit human approval before sending any email
- explicit human approval before creating/updating/deleting counterparty-visible calendar events
- no fabricated emails, venues, reservation details, or thread IDs
- timezone-safe communication
- complete tracking in `memory/scheduling/in-progress.md`
- conservative record lifecycle management (no active record deletion)

## Repository Contents

- `SKILL.md`: core operating playbook
- `references/email-templates.md`: outbound email templates and formatting rules
- `scripts/check-availability.py`: checks free slots for a date window
- `scripts/find-venue.py`: searches venue options with `goplaces`

## Prerequisites

1. OpenClaw skill loading enabled in your environment
2. Python 3.10+ (for `zoneinfo` in the scripts)
3. `gog` CLI installed and authenticated to the agent Gmail account
4. `goplaces` CLI installed and authenticated
5. Human calendar shared to the agent Gmail account with write/manage permissions
6. `USER.md` available to the agent
7. `SOUL.md` or `IDENTITY.md` available to the agent

## Declared Runtime + Credentials

This skill declares the following in `SKILL.md` `metadata.openclaw`:

- required binaries: `gog`, `goplaces`, `python3`
- required env vars: `GOG_ACCOUNT`, `GOOGLE_PLACES_API_KEY`
- required config dirs: `$HOME/.config/gog`, `$HOME/.config/goplaces`
- primary credential selector: `GOG_ACCOUNT`
- homepage/source: `https://github.com/voshawn/meeting-coordinator-skill`

Notes:

- `GOG_ACCOUNT` identifies which authenticated Google account `gog` will operate as.
- `gog` OAuth tokens are local credentials; use a dedicated, low-privilege agent account.
- `GOOGLE_PLACES_API_KEY` powers venue lookup through `goplaces`.

## Required Profile Data

`USER.md` should include:

- human full name
- human email
- calendar ID to schedule against (shared calendar ID)
- home timezone (IANA, for backend calculations)
- preferred scheduling windows (days/hours)
- default durations by meeting type
- travel and post-meeting buffer defaults
- transit and venue preferences

`SOUL.md` or `IDENTITY.md` should include:

- assistant tone guidance
- email signature block

## Installation

1. Install the skill folder at:
`$CODEX_HOME/skills/meeting-coordinator`
2. Ensure scripts are executable if needed:

```bash
chmod +x scripts/check-availability.py scripts/find-venue.py
```
3. Verify required CLIs are available:

```bash
gog --help
goplaces --help
```
4. Verify scripts run:

```bash
python3 scripts/check-availability.py --help
python3 scripts/find-venue.py --help
```
5. Confirm the agent can access the human calendar via the configured calendar ID.

## Gmail + Calendar Setup (Agent Account)

1. Sign in `gog` as the agent Gmail account.
2. In Google Calendar, share the human's calendar with the agent Gmail.
3. Grant at least "Make changes to events".
4. Put that shared calendar ID into `USER.md`.
5. Do not use `primary` unless explicitly intended.

## Preflight Verification (Run Before First Write Action)

```bash
echo "$GOG_ACCOUNT"
gog auth list
test -n "$GOOGLE_PLACES_API_KEY"
gog calendar events "<shared_calendar_id>" --from "<YYYY-MM-DD>T00:00:00" --to "<YYYY-MM-DD>T23:59:59"
```

If any check fails, stop and fix auth/config before scheduling.

## Email Formatting Rules

- send outbound messages using `--body-html`
- always CC the human
- include timezone labels in every time reference
- display times with standard US labels (`ET`, `CT`, `MT`, `PT`)
- if counterparty timezone differs, include both (example: `3:00 PM ET / 12:00 PM PT`)
- for virtual meetings, create the calendar event with `--meet` so Google Meet is generated automatically

## How Scheduling Flows

1. Intake meeting request and constraints
2. Check availability across preferred windows
3. Find venues for in-person meetings
4. Present options to human for approval
5. Create tentative holds for approved options
6. Draft outreach email and wait for approval
7. Send approved outreach and track thread state
8. On acceptance, create final event and related buffers/travel blocks (virtual meetings use `--meet`)
9. Send approved confirmation follow-up
10. Update tracking record with all event IDs and status

## Tracking Lifecycle Policy

- one meeting = one durable entry in `memory/scheduling/in-progress.md`
- entries are updated in place and include `updated_at` + append-only activity log notes
- active entries are never auto-deleted
- entries are moved to archive only when terminal (`completed`, `cancelled`, `closed-no-response`) and 14+ days old since `updated_at`


path: scripts/find-venue.py
content: #!/usr/bin/env python3
"""
Search for meeting venues using goplaces CLI.

Usage:
  python3 find-venue.py --location "Union Square, NYC" --type coffee
  python3 find-venue.py --location "Penn Station, NYC" --type lunch --min-rating 4.5
"""

import argparse
import subprocess
import json
import re
import sys

QUERY_MAP = {
    "coffee": "coffee shop",
    "cafe": "cafe",
    "lunch": "restaurant for lunch",
    "dinner": "restaurant for dinner",
    "restaurant": "restaurant",
}


def search_venues(location: str, venue_type: str, min_rating: float = 0.0):
    """Search for venues and return structured results."""
    query = QUERY_MAP.get(venue_type, venue_type)
    search_term = f"{query} near {location}"

    try:
        result = subprocess.run(
            ["goplaces", "search", search_term],
            capture_output=True, text=True, check=True,
        )
    except subprocess.CalledProcessError as e:
        print(f"Error: goplaces search failed: {e.stderr}", file=sys.stderr)
        return []

    venues = []
    current = {}

    for line in result.stdout.strip().split("\n"):
        line = line.strip()
        if not line:
            continue

        # New venue entry: "1. Name — Address"
        m = re.match(r"^\d+\.\s+(.+?)\s+—\s+(.+)$", line)
        if m:
            if current:
                venues.append(current)
            current = {"name": m.group(1), "address": m.group(2)}
            continue

        if line.startswith("ID:"):
            current["place_id"] = line.split(": ", 1)[1]
        elif line.startswith("Rating:"):
            current["rating"] = line.split(": ", 1)[1]
        elif line.startswith("Types:"):
            current["types"] = line.split(": ", 1)[1]
        elif line.startswith("Open now:"):
            current["open_now"] = line.split(": ", 1)[1] == "yes"

    if current:
        venues.append(current)

    # Filter by min rating
    if min_rating > 0:
        filtered = []
        for v in venues:
            try:
                r = float(v.get("rating", "0").split("·")[0].strip())
                if r >= min_rating:
                    filtered.append(v)
            except ValueError:
                pass
        venues = filtered

    # Sort by rating descending
    def rating_key(v):
        try:
            return float(v.get("rating", "0").split("·")[0].strip())
        except ValueError:
            return 0.0
    venues.sort(key=rating_key, reverse=True)

    return venues


def main():
    p = argparse.ArgumentParser(description="Search for meeting venues")
    p.add_argument("--location", required=True, help='Area (e.g., "SoHo, NYC")')
    p.add_argument("--type", required=True,
                   choices=["coffee", "cafe", "lunch", "dinner", "restaurant"],
                   help="Venue type")
    p.add_argument("--min-rating", type=float, default=0.0,
                   help="Minimum Google rating (e.g., 4.5)")
    args = p.parse_args()

    venues = search_venues(args.location, args.type, args.min_rating)
    print(json.dumps(venues, indent=2))


if __name__ == "__main__":
    main()


path: scripts/check-availability.py
content: #!/usr/bin/env python3
"""
Check calendar availability and find free time slots.

Usage:
  python3 check-availability.py --calendar [REDACTED_SECRET] --date 2026-02-19 --duration 90
  python3 check-availability.py --calendar [REDACTED_SECRET] --date 2026-02-19 --duration 60 --start-hour 10 --end-hour 17 --tz America/New_York
"""

import argparse
import subprocess
import json
from datetime import datetime, timedelta, date as date_type
from zoneinfo import ZoneInfo
import sys


def get_events(calendar_id: str, date_str: str, tz: ZoneInfo):
    """Fetch events for a date from gog calendar."""
    try:
        result = subprocess.run(
            ["gog", "calendar", "events", calendar_id,
             "--from", f"{date_str}T00:00:00",
             "--to", f"{date_str}T23:59:59"],
            capture_output=True, text=True, check=True,
        )
    except subprocess.CalledProcessError as e:
        print(f"Error fetching calendar: {e}", file=sys.stderr)
        return []

    events = []
    for line in result.stdout.strip().split("\n"):
        if not line.strip() or line.startswith("ID"):
            continue
        parts = line.split()
        if len(parts) < 4:
            continue
        events.append({
            "id": parts[0],
            "start": parts[1],
            "end": parts[2],
            "summary": " ".join(parts[3:]),
        })
    return events


def parse_dt(s: str, tz: ZoneInfo) -> datetime:
    """Parse an ISO datetime string into a tz-aware datetime."""
    s = s.replace("Z", "+00:00")
    dt = datetime.fromisoformat(s)
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=tz)
    return dt.astimezone(tz)


def find_free_slots(calendar_id: str, date_str: str, duration: int,
                    start_hour: int, end_hour: int, tz: ZoneInfo):
    """Return free slots of at least `duration` minutes within the work window."""
    d = date_type.fromisoformat(date_str)
    window_start = datetime(d.year, d.month, d.day, start_hour, tzinfo=tz)
    window_end = datetime(d.year, d.month, d.day, end_hour, tzinfo=tz)

    raw_events = get_events(calendar_id, date_str, tz)

    # Parse and sort busy periods
    busy = []
    for ev in raw_events:
        try:
            s = parse_dt(ev["start"], tz)
            e = parse_dt(ev["end"], tz)
            busy.append((s, e, ev["summary"], ev["id"]))
        except Exception as exc:
            print(f"Warning: skipping event {ev}: {exc}", file=sys.stderr)

    busy.sort(key=lambda x: x[0])

    # Walk the window
    cursor = window_start
    free = []
    for b_start, b_end, _, _ in busy:
        # Skip events entirely outside the window
        if b_end <= window_start or b_start >= window_end:
            continue
        gap_end = min(b_start, window_end)
        if cursor < gap_end:
            mins = (gap_end - cursor).total_seconds() / 60
            if mins >= duration:
                free.append({
                    "start": cursor.isoformat(),
                    "end": gap_end.isoformat(),
                    "duration_minutes": int(mins),
                })
        cursor = max(cursor, b_end)
        if cursor >= window_end:
            break

    # Trailing gap
    if cursor < window_end:
        mins = (window_end - cursor).total_seconds() / 60
        if mins >= duration:
            free.append({
                "start": cursor.isoformat(),
                "end": window_end.isoformat(),
                "duration_minutes": int(mins),
            })

    return {
        "date": date_str,
        "window": f"{start_hour:02d}:00–{end_hour:02d}:00",
        "timezone": str(tz),
        "events": [
            {"summary": ev["summary"], "start": ev["start"], "end": ev["end"], "id": ev["id"]}
            for ev in raw_events
        ],
        "free_slots": free,
    }


def main():
    p = argparse.ArgumentParser(description="Check calendar availability")
    p.add_argument("--calendar", required=True, help="Google Calendar ID")
    p.add_argument("--date", required=True, help="Date to check (YYYY-MM-DD)")
    p.add_argument("--duration", type=int, default=30, help="Minimum free slot in minutes")
    p.add_argument("--start-hour", type=int, default=12, help="Window start (24h)")
    p.add_argument("--end-hour", type=int, default=17, help="Window end (24h)")
    p.add_argument("--tz", default="America/New_York", help="IANA timezone")
    args = p.parse_args()

    tz = ZoneInfo(args.tz)
    result = find_free_slots(args.calendar, args.date, args.duration,
                             args.start_hour, args.end_hour, tz)
    print(json.dumps(result, indent=2))


if __name__ == "__main__":
    main()


path: references/email-templates.md
content: # Email Templates

All templates use placeholders such as:
- `[HUMAN_NAME]`
- `[COUNTERPARTY_NAME]`
- `[MEETING_TYPE]`
- `[DAY_DATE]`
- `[TIME_DISPLAY]` (example: `3:00 PM ET` or `3:00 PM ET / 12:00 PM PT`)
- `[ORIGINAL_TIME_DISPLAY]`
- `[VENUE_NAME]`
- `[FULL_ADDRESS]`
- `[SIGNATURE]`

Replace every placeholder before drafting for approval.

## Sending Rules

- Always draft first and get explicit human approval before sending.
- Always CC the human's email on outgoing scheduling messages.
- Use `--reply-to-message-id` when replying within an existing thread.
- Use `gog gmail send --body-html` for all outbound scheduling emails.
- Include explicit timezone labels in all date/time references.
- Convert backend IANA timezone values to display labels (`ET`, `CT`, `MT`, `PT`) in outward-facing emails.
- If the counterparty is in a different timezone, include both timezones in one line (example: `3:00 PM ET / 12:00 PM PT`).
- Use the signature from `SOUL.md`/`IDENTITY.md`; do not hardcode a signature.
- Keep wording concise and professional; avoid unnecessary detail.

---

## Meeting Proposal (Multiple Options)

Use when proposing 2-3 time options.

Required inputs:
- Counterparty name
- Meeting type
- Three date/time options with timezone
- Virtual vs in-person context

Subject:
`[MEETING_TYPE] with [HUMAN_NAME] - finding a time`

```
Hi [COUNTERPARTY_NAME],

I am coordinating a [MEETING_TYPE] for [HUMAN_NAME] and wanted to find a time that works for you.

Here are a few options:
- [DAY_DATE] at [TIME_DISPLAY]
- [DAY_DATE] at [TIME_DISPLAY]
- [DAY_DATE] at [TIME_DISPLAY]

[If in-person: "Happy to suggest a location that is convenient for both of you, or we can use your preference."]
[If virtual: "I will send a Google Meet link once we confirm."]

Let me know what works best!

[SIGNATURE]
```

---

## Calendar Invite Follow-Up

Use after sending the calendar invite to confirm details.

Required inputs:
- Final confirmed date/time/timezone
- In-person venue + full address OR virtual meeting note
- Thread subject if replying

Subject:
`Re: [THREAD_SUBJECT]`

```
Hi [COUNTERPARTY_NAME],

I just sent a calendar invite for [MEETING_TYPE] with [HUMAN_NAME]:

**[DAY_DATE] at [TIME_DISPLAY]**
[If in-person: "[VENUE_NAME]"]
[If in-person: "[FULL_ADDRESS]"]
[If virtual: "Google Meet link is in the calendar invite."]

[Optional one-line venue note if helpful]

Let me know if you need to reschedule or if there is anything else I can help with.

[SIGNATURE]
```

---

## Day-Before Confirmation

Required inputs:
- Scheduled details with timezone
- Location or link
- Reservation note if applicable

Subject:
`Confirming tomorrow - [MEETING_TYPE] at [TIME_DISPLAY]`

```
Hi [COUNTERPARTY_NAME],

Quick confirmation for tomorrow:

**[DAY_DATE] at [TIME_DISPLAY]**
[If in-person: "[VENUE_NAME], [FULL_ADDRESS]"]
[If virtual: "Google Meet link is in the calendar invite."]
[If reservation exists: "Reservation under [HUMAN_NAME]'s name."]

Looking forward to it!

[SIGNATURE]
```

---

## Rescheduling

Required inputs:
- Original date/time/timezone
- 2-3 replacement options

Subject:
`Rescheduling [MEETING_TYPE] - [ORIGINAL_DAY_DATE]`

```
Hi [COUNTERPARTY_NAME],

[HUMAN_NAME] needs to move [the/our] [MEETING_TYPE] originally scheduled for [ORIGINAL_DAY_DATE] at [ORIGINAL_TIME_DISPLAY].

Here are some alternatives:
- [DAY_DATE] at [TIME_DISPLAY]
- [DAY_DATE] at [TIME_DISPLAY]
- [DAY_DATE] at [TIME_DISPLAY]

Apologies for the shuffle. Let me know what works best.

[SIGNATURE]
```

---

## Cancellation

Required inputs:
- Original meeting details
- Whether rescheduling will follow

Subject:
`Cancelling [MEETING_TYPE] - [ORIGINAL_DAY_DATE]`

```
Hi [COUNTERPARTY_NAME],

Unfortunately [HUMAN_NAME] needs to cancel [the/our] [MEETING_TYPE] scheduled for [ORIGINAL_DAY_DATE] at [ORIGINAL_TIME_DISPLAY].

[If rescheduling later: "We would be glad to find another time and will follow up shortly with options."]
[If not rescheduling: "Thank you for your understanding."]

[SIGNATURE]
```

---

## No-Response Follow-Up

Use when there is no reply after 2 business days and the human has approved follow-up.

Subject:
`Quick follow-up: [MEETING_TYPE] with [HUMAN_NAME]`

```
Hi [COUNTERPARTY_NAME],

Following up in case this got buried. Would any of these options work for a [MEETING_TYPE] with [HUMAN_NAME]?

- [DAY_DATE] at [TIME_DISPLAY]
- [DAY_DATE] at [TIME_DISPLAY]
- [DAY_DATE] at [TIME_DISPLAY]

[SIGNATURE]
```

---

## Notes

- Date clarity: prefer explicit dates over relative phrasing.
- Timezone clarity: use `[TIME_DISPLAY]` with standard labels (`ET`, `CT`, `MT`, `PT`).
- Dual-time clarity: if recipient timezone differs, include both (example: `3:00 PM ET / 12:00 PM PT`).
- Personalization: keep concise but adapt warmth/formality to relationship context.
- Final check before approval request: verify names, dates, timezone, venue/link, and thread context.


Security scan verdicts — ClawScan: clean | static analysis:  | VirusTotal: clean | SkillSpector: clean (LOW)