Skip to content

docs(agents): add attendee-support work pack + autonomous delivery loop - #2870

Open
tyler-dane wants to merge 22 commits into
mainfrom
claude/attendee-support-planning-nljgeg
Open

docs(agents): add attendee-support work pack + autonomous delivery loop#2870
tyler-dane wants to merge 22 commits into
mainfrom
claude/attendee-support-planning-nljgeg

Conversation

@tyler-dane

@tyler-dane tyler-dane commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds wip/attendee-support/ — a temporary work pack (same format the
wip/restructure/ pack used) that specs attendee support into nine work
packages, plus a pointer in AGENTS.md. Scope: editing an event's guest list,
Google-contact attendee suggestions behind an optional incremental contacts
grant, invitation emails via Google sendUpdates (save-time "Send invitation
emails?" prompt), and full RSVP including per-occurrence responses. Product
decisions were approved by the product owner on 2026-08-25 and are recorded in
00-context-and-invariants.md.

This branch is also the integration branch: an hourly manager loop picks up
the next queued WP from wip/attendee-support/TRACKING.md, implements it,
validates, and pushes here. Subsequent commits on this PR are the feature
itself, landing WP by WP. main has been merged back in (conflict in
AGENTS.md resolved; restructure-pack removal + our pack pointer both kept).

Work package status (mirrors wip/attendee-support/TRACKING.md)

WP Title Status
01 Core write contracts done (ebea75c — 605 core tests, web/sync regression green)
02 Sync attendee writes done (8ae944e — test:sync 1017 pass / 0 fail incl. safety-canary; merge-by-email vs freshly fetched provider state; organizer guard; preserve byte-identical)
03 Backend write path done (880715a — attendees + invitation threaded into sync commands; legacy submit requests byte-identical with pinned-stable idempotency keys; ATTENDEES_UNSUPPORTED 403 gate; backend 390 pass / 8 pre-existing env fails identical on base)
04 Web attendee editor done (345ca82 — LAUNCH GATE: AttendeeField email-chip combobox on writable Google calendars the user organizes; save-time "Send invitation emails?" prompt; recurring guest edits series-wide only; optimistic needsAction merge with rollback; test:web 2362 pass / 0 fail; a11y 7/7 pass at extended timeout, default-timeout failures reproduce identically on base — container env)
05 Contacts scope + suggestions endpoint done (38a5e4b — contacts.readonly + contacts.other.readonly as OPTIONAL scopes only; every REQUIRED scope list untouched and literal-pinned; begin features:["contacts"] widens consent, plain begin byte-identical; suggestContacts capability from either granted scope; ContactsPort + google-people adapter; principal-scoped /internal/contacts/suggestions returning {email, displayName}[] only; safety-canary extended with People patterns, green; test:sync 1052 pass / 0 fail)
06 Contacts surface done (3adc814 — backend GET /api/contacts/suggestions proxy (every sync failure degrades to a typed empty 200 — no toast storms); canSuggestContacts threaded sync capability → connection summary → user metadata → web; 250ms-debounced, 2-char-minimum, command-palette-ranked suggestions plugged into AttendeeField; dismissible once-per-session enable-contacts nudge (frequency rule pinned by tests) starting connect with features:["contacts"] + metadata refresh on completion; e2e contacts granted/denied both green, denied leaves the connection healthy with capability false; REQUIRED scope lists untouched; test:web 2388 pass / 0 fail)
07 RSVP sync done (aeaa822 — rsvp command execution: fetch current provider state, rewrite ONLY the self attendee entry (case-insensitive account-email match), patch the full merged list with sendUpdates none, unconditionally — RSVP drift never blocks; scope "this" patches the fetchInstanceAt-resolved Google instance with the master proven byte-untouched, scope "all" the master; replay confirms without a second write; typed unsupportedCapability guards incl. cloud-only + thisAndFollowing; organizer self-RSVP allowed; confirmed answers land on the stored record/instance exception and ride the invalidation outbox to SSE eventsChanged; test:sync 1072 pass / 0 fail incl. safety-canary)
08 RSVP surface done (bb26c1c — POST /api/event/:id/rsvp translating status + scope into the WP-07 rsvp command: occurrence targeting via the composite-id decode, nonce-free idempotency key from event + status + scope, auth/billing parity with writes but deliberately NO writable-calendar gate (RSVP works on viewer-access calendars), needsAction → 400; Going / Maybe / Decline radiogroup shown when the calendar's account email is an attendee (organizer included), hidden when self is not an attendee or the event is local; occurrence answers open a This Event / All Events choice — never this-and-following — while single events skip the dialog and a series base answers the whole series; optimistic self-entry rewrite with 503 rollback, settling via the SSE-backed invalidation; test:web 2407 pass / 0 fail; backend 415 pass / 8 pre-existing env fails identical on base; a11y 7/7 at extended timeout)
09 E2E, docs, closeout queued

Simplicity

The pack reuses the existing restructure pack format (ledger, WP skeleton,
session prompts) rather than inventing a new coordination scheme, and the
feature design reuses existing machinery everywhere it exists:
InvitationIntentSchema/sendUpdates (already plumbed), per-connection
grantedScopes → capabilities (already derives inviteAttendees), the
occurrence-id decode, and the read path that already syncs attendees.

Automated validation

  • WP-01: bun test:core 605 pass / 0 fail; bun test:web 2331 pass / 0 fail;
    bun test:sync:fast 361 pass / 0 fail; type-check, lint, knip exit 0.
    (test:backend:fast has 20 pre-existing sandbox failures that reproduce
    identically on the base commit with the WP-01 work stashed — evidence in the
    WP-01 file.)
  • WP-02: bun test:sync (full) 1017 pass / 0 fail across 78 files, including
    the safety-canary suite; type-check, lint, knip exit 0. Preserve/legacy
    regression proven by exact-body adapter tests and executor tests; full
    evidence + spec deltas in the WP-02 file.
  • WP-03: bun test:backend (full) 390 pass / 1 skip / 8 fail — the identical
    8 failures (config x3, UserController x5) reproduce on the base tree with
    the work stashed (pre-existing sandbox env; +16 new passing tests);
    bun test:core 605 pass / 0 fail; type-check, lint, knip exit 0. Legacy
    idempotency keys captured from the pre-change translator and pinned as
    literals in the key-stability tests; full evidence + spec deltas in the
    WP-03 file.
  • WP-04: bun test:web 2362 pass / 0 fail (312 files, +30 new tests: field
    behavior, editor gating, invitation prompt, MSW wire payloads, optimistic
    merge/rollback, scope-dialog narrowing); type-check, lint, knip exit 0.
    bun test:a11y at its default 30s timeout fails 6/7 in the sandbox with axe
    frame.evaluate timeouts that reproduce identically on the base tree; the
    same suite passes 7/7 at --timeout=180000 on the final tree. Full evidence
    • the recurring-event UX choice in the WP-04 file.
  • WP-05: bun test:sync (full) 1052 pass / 0 fail across 81 files including
    the safety-canary suite (extended with People-shaped patterns);
    bun test:core 615 pass / 0 fail; type-check, lint, knip exit 0.
    Sign-in scope-split regression: bun test:web 2364 pass / 0 fail and
    bun test:backend:fast failing only the identical 20 pre-existing sandbox
    tests from the WP-01 baseline. Required scope lists (web
    GOOGLE_AUTH_SCOPES_REQUIRED, backend GOOGLE_AUTH_SCOPES, sync
    GOOGLE_SCOPES, e2e REQUIRED_SCOPES) untouched and pinned by literal
    regression tests, with explicit sign-in-succeeds-without-contacts tests in
    backend and web. Full evidence + spec deltas in the WP-05 file.
  • WP-06: bun test:web 2388 pass / 0 fail (316 files, +24 new tests: nudge
    frequency gate, debounce/min-length/rank/cancel-on-unmount, combobox footer,
    full-form suggestion flow, features on the begin body, post-connect
    metadata refresh); bun test:core 618 pass / 0 fail; bun test:backend
    (full) 406 pass / 1 skip / 8 fail — the identical pre-existing WP-03
    baseline set (+16 new passing tests); type-check, lint, knip exit 0.
    bunx playwright test e2e/oauth --timeout=180000: both NEW contacts tests
    pass (granted → capability true; denied → sign-in completes, connection
    HEALTHY, capability false — no insufficientScopes regression); the
    pre-existing sign-in spinner test fails identically on the base tree
    (container timing, not WP-06). Required scope lists diff-empty. Full
    evidence + spec deltas in the WP-06 file.
  • WP-07: bun test:sync (full) 1072 pass / 0 fail across 81 files including
    the safety-canary suite (new assertions: a failed rsvp's outcome and the
    route's log-line template carry no attendee JSON); type-check, lint, knip
    exit 0 — all re-run on the final tree. Instance-vs-master targeting proven
    by paired executor tests (scope "this": fetchInstanceAt resolves the
    instance, the single patch targets the instance id with the master never
    fetched, never patched, stored list and version unchanged, sibling
    occurrences intact; scope "all": no instance resolution, the single patch
    targets the master). End-to-end route test pins a confirmed rsvp's
    invalidation-outbox rows (SSE eventsChanged source) and the route-level 400
    for needsAction. Full evidence + spec deltas in the WP-07 file.
  • WP-08: bun test:web 2407 pass / 0 fail (319 files, +19 new tests:
    RsvpControl radiogroup semantics + wire payloads, EventForm gating incl.
    viewer-access and organizer cases, optimistic self-only rewrite with 503
    rollback and series-wide paint, settle-by-invalidation regression for the
    SSE path); bun test:backend (full) 415 pass / 1 skip / 8 fail — the
    identical pre-existing WP-03/06 baseline set (+9 new passing tests:
    toRsvpSubmitRequest occurrence/series/coercion/idempotency/malformed-id,
    controller 204-without-calendar-lookup, decoded occurrence pass-through,
    needsAction 400, typed 403); bun test:core 618 pass / 0 fail; a11y 7/7 at
    --timeout=180000 (the anonymous local-mode axe harness cannot mount the
    control — no Google invitations exist there — so its accessibility contract
    is pinned via RTL role/name tests instead, noted loudly in the WP file);
    type-check, lint, knip exit 0. Occurrence-scope payload proof + spec deltas
    (singular /api/event/:id/rsvp path, 204 response, dialog only for
    occurrences) in the WP-08 file. Delivered as a takeover: the prior WP-08
    session hit a usage limit mid-way; its uncommitted tree was reviewed, kept,
    and validated (formatter-only fixes).
  • Post-merge of main: type-check, lint, bun test:core re-run green on the
    merged tree.

Independent review

Not yet run — this PR accumulates the implementation WPs; review is requested
when the pack finish line is met (WP-09 posts the closing comment).

Test plan

  • Per-WP: package suite + bun run type-check + bun lint + bun knip
    (sync WPs additionally: safety-canary suite), recorded verbatim in each WP's
    Evidence section
  • Pack docs round: bun lint exit 0; relative-link sweep resolves

claude added 22 commits August 25, 2026 21:32
Spec for attendee support (guest editing, google contact suggestions,
invitation emails via sendUpdates, full rsvp incl. per-occurrence) as
nine work packages coordinated by an autonomous manager loop. Follows
the wip/restructure pack format; product decisions approved 2026-08-25.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3iZuGCEhpP1TZS7fQwjw3
AttendeeInputSchema (no responseStatus, unique emails case-insensitive)
and RsvpResponseStatusSchema in event-attendance.contracts; optional
attendees on EditableContentSchema plus optional invitation on the
create/replace/delete browser inputs and a new RsvpEventInputSchema
(accepted|declined|tentative, scope single|all); attendeesEdit
enum(replace|preserve).default(preserve) on sync create/update command
inputs and a new rsvp command union member wired into the
recurrence-coherence refines. Zero runtime behavior change: legacy
payloads parse unchanged (defaults apply), and the web-side edits are
type-level companions that preserve replay/optimistic behavior
byte-for-byte. WP-01 of wip/attendee-support marked done with evidence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3iZuGCEhpP1TZS7fQwjw3
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3iZuGCEhpP1TZS7fQwjw3

# Conflicts:
#	AGENTS.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3iZuGCEhpP1TZS7fQwjw3
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3iZuGCEhpP1TZS7fQwjw3
attendeesEdit "replace" now reaches Google: mergeAttendees (pure,
table-tested) merges the intended membership by email, case-insensitively,
against the FRESHLY FETCHED provider list — retained guests keep the
provider's responseStatus/displayName, new guests enter as needsAction,
dropped guests are removed. Threaded through executeProviderUpdate, the
series edit-all, and the create path (create merges against an empty list);
the merged list lands on the sync record at confirm.

A non-organizer replace fails typed (unsupportedCapability) before any
provider call, via a new narrow ProviderConnectionLookup dep; unverifiable
states fail closed. Replace on this/thisAndFollowing scopes is refused typed
(no v1 per-occurrence guest semantics) instead of silently preserving.
toGoogleBody emits attendees only when the write intends them (present incl.
[] replaces the whole array; absent leaves Google's list via merge-by-key).
matchesIntendedEdit compares attendee email sets only on replace — order-
insensitive, responseStatus-ignored — so RSVP drift never blocks replay.
Cloud-only records merge against the stored list on replace; creates
normalize every guest to needsAction. "preserve"/legacy commands stay
byte-identical (regression-tested); safety-canary untouched and green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3iZuGCEhpP1TZS7fQwjw3
toSyncContent maps an intended guest list to needsAction placeholders
(attendeesEdit replace; absent keeps the [] pad + preserve); the three
hardcoded invitation "none" call sites now honor the input's save-time
choice, including guest cancellation emails on delete (query param).
Legacy payloads stay byte-identical with pinned-stable idempotency keys.
Guest edits are gated to writable Google calendars with a typed 403
ATTENDEES_UNSUPPORTED before any sync submission, and synthesized
optimistic response events carry the intended attendees.

WP-03 of wip/attendee-support (evidence + ledger updated).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3iZuGCEhpP1TZS7fQwjw3
WP-04 (launch gate): the event form gains an AttendeeField email-chip
combobox (react-select CreatableSelect, semantic tokens, pluggable
suggestion source for WP-06) on writable Google calendars for events the
user organizes; non-organizer, read-only, local-calendar, and
series-occurrence events keep the read-only guest list. Drafts carry
attendees only when the guest list was touched; a membership-changing
save asks "Send invitation emails?" (Send -> invitation "all", default;
Don't send -> "none") before mutating. Recurring guest edits are
series-wide only: occurrences never render the editor and the
recurrence-scope dialog narrows guest-changed saves to "All Events",
with a save-time belt dropping any non-"all" recurring guest edit. The
wire boundary (editableContent) forwards genuine guest edits and keeps
stripping replayed read-shaped lists so undo/redo stays byte-identical;
optimistic cache paints retained guests with their current RSVP and new
guests as needsAction, rolling back on failure. Enter creates chips
without submitting (isComboboxInteraction) and Escape closes the listbox
before the form (useFloatingLayer).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3iZuGCEhpP1TZS7fQwjw3
WP-05: contacts.readonly + contacts.other.readonly as OPTIONAL scopes only —
connect begin accepts features:["contacts"] (plain begin byte-identical), the
sign-in flow REQUESTS them while every REQUIRED verification list stays
untouched (literal-pinned by regression tests, explicit
sign-in-without-contacts tests in backend and web). suggestContacts capability
derives from either granted contacts scope; new ContactsPort +
google-people.adapter (injectable client factory, queries only the surfaces
the granted scopes allow, merge + rank + dedupe); principal-scoped
GET /internal/contacts/suggestions returns {email, displayName}[] per new
core contact.contracts.ts, refuses typed without the capability, and returns
an empty 200 without a Google call for queries under 2 chars. Safety-canary
patterns extended with People shapes; suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3iZuGCEhpP1TZS7fQwjw3
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3iZuGCEhpP1TZS7fQwjw3
WP-06: backend GET /api/contacts/suggestions proxy over sync (session
parity with event reads; every sync-side failure degrades to a typed
empty 200 so the type-ahead never toasts), canSuggestContacts threaded
sync capability -> connection summary -> user metadata -> web,
useContactSuggestions (TanStack Query, 250ms debounce, 2-char minimum,
abort on unmount) ranked with the command-palette scorer and plugged
into AttendeeField's suggestion source, and the dismissible once-per-
session enable-contacts nudge in the combobox footer that starts the
connect flow with features:["contacts"] and refreshes metadata on
completion. e2e oauth covers contacts granted and denied; required
scope lists untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3iZuGCEhpP1TZS7fQwjw3
WP-07: an rsvp command fetches current provider state, rewrites only the
connection account's own attendee entry (case-insensitive email match via
the ProviderConnectionLookup dep), and patches the full merged list with
sendUpdates none, unconditionally — RSVP drift never blocks an RSVP.
Scope "all" targets the series master (or single event); scope "this"
patches the Google instance resolved by fetchInstanceAt, leaving the
master and sibling occurrences untouched. A fetched self entry already
holding the intended status confirms as a replay without a second write.
Guards fail typed unsupportedCapability: self not an attendee (stored
list pre-fetch, fetched list post-fetch), unverifiable connection,
cloud-only/unlinked events, and thisAndFollowing scope; the organizer
RSVPing their own event is allowed. Confirmed answers land on the stored
record (whole event) or an instance-identity exception (per occurrence),
reproject through reprojectMaster, and ride the existing invalidation
outbox to SSE eventsChanged. rsvp joins the stale-command sweep's
retryable kinds; needsAction stays a route-level 400 by contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3iZuGCEhpP1TZS7fQwjw3
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3iZuGCEhpP1TZS7fQwjw3
POST /api/event/:id/rsvp translates the browser's status+scope into the
WP-07 rsvp sync command (occurrence targeting via the existing composite-id
decode, nonce-free idempotency key from event+status+scope, auth/billing
parity with writes but deliberately no writable-calendar gate). The web
gains a Going/Maybe/Decline radiogroup shown when the calendar's account
email is an attendee (organizer included), with a This Event / All Events
scope dialog for occurrences (never this-and-following), optimistic
self-entry rewrite with rollback, settling via the SSE-backed invalidation.

Takeover: continues and validates the prior WP-08 worker's uncommitted
tree after it hit a usage limit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3iZuGCEhpP1TZS7fQwjw3
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3iZuGCEhpP1TZS7fQwjw3
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3iZuGCEhpP1TZS7fQwjw3
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