docs(agents): add attendee-support work pack + autonomous delivery loop - #2870
Open
tyler-dane wants to merge 22 commits into
Open
docs(agents): add attendee-support work pack + autonomous delivery loop#2870tyler-dane wants to merge 22 commits into
tyler-dane wants to merge 22 commits into
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
wip/attendee-support/— a temporary work pack (same format thewip/restructure/pack used) that specs attendee support into nine workpackages, 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 invitationemails?" 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.
mainhas been merged back in (conflict inAGENTS.mdresolved; restructure-pack removal + our pack pointer both kept).Work package status (mirrors
wip/attendee-support/TRACKING.md)ebea75c— 605 core tests, web/sync regression green)8ae944e— test:sync 1017 pass / 0 fail incl. safety-canary; merge-by-email vs freshly fetched provider state; organizer guard; preserve byte-identical)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)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)38a5e4b— contacts.readonly + contacts.other.readonly as OPTIONAL scopes only; every REQUIRED scope list untouched and literal-pinned; beginfeatures:["contacts"]widens consent, plain begin byte-identical;suggestContactscapability from either granted scope; ContactsPort + google-people adapter; principal-scoped/internal/contacts/suggestionsreturning{email, displayName}[]only; safety-canary extended with People patterns, green; test:sync 1052 pass / 0 fail)3adc814— backendGET /api/contacts/suggestionsproxy (every sync failure degrades to a typed empty 200 — no toast storms);canSuggestContactsthreaded 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 withfeatures:["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)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)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)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-connectiongrantedScopes→ capabilities (already derivesinviteAttendees), theoccurrence-id decode, and the read path that already syncs attendees.
Automated validation
bun test:core605 pass / 0 fail;bun test:web2331 pass / 0 fail;bun test:sync:fast361 pass / 0 fail; type-check, lint, knip exit 0.(
test:backend:fasthas 20 pre-existing sandbox failures that reproduceidentically on the base commit with the WP-01 work stashed — evidence in the
WP-01 file.)
bun test:sync(full) 1017 pass / 0 fail across 78 files, includingthe 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.
bun test:backend(full) 390 pass / 1 skip / 8 fail — the identical8 failures (config x3, UserController x5) reproduce on the base tree with
the work stashed (pre-existing sandbox env; +16 new passing tests);
bun test:core605 pass / 0 fail; type-check, lint, knip exit 0. Legacyidempotency 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.
bun test:web2362 pass / 0 fail (312 files, +30 new tests: fieldbehavior, editor gating, invitation prompt, MSW wire payloads, optimistic
merge/rollback, scope-dialog narrowing); type-check, lint, knip exit 0.
bun test:a11yat its default 30s timeout fails 6/7 in the sandbox with axeframe.evaluate timeouts that reproduce identically on the base tree; the
same suite passes 7/7 at
--timeout=180000on the final tree. Full evidencebun test:sync(full) 1052 pass / 0 fail across 81 files includingthe safety-canary suite (extended with People-shaped patterns);
bun test:core615 pass / 0 fail; type-check, lint, knip exit 0.Sign-in scope-split regression:
bun test:web2364 pass / 0 fail andbun test:backend:fastfailing only the identical 20 pre-existing sandboxtests from the WP-01 baseline. Required scope lists (web
GOOGLE_AUTH_SCOPES_REQUIRED, backendGOOGLE_AUTH_SCOPES, syncGOOGLE_SCOPES, e2eREQUIRED_SCOPES) untouched and pinned by literalregression tests, with explicit sign-in-succeeds-without-contacts tests in
backend and web. Full evidence + spec deltas in the WP-05 file.
bun test:web2388 pass / 0 fail (316 files, +24 new tests: nudgefrequency gate, debounce/min-length/rank/cancel-on-unmount, combobox footer,
full-form suggestion flow,
featureson the begin body, post-connectmetadata refresh);
bun test:core618 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 testspass (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.
bun test:sync(full) 1072 pass / 0 fail across 81 files includingthe 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.bun test:web2407 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 — theidentical 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:core618 pass / 0 fail; a11y 7/7 at--timeout=180000(the anonymous local-mode axe harness cannot mount thecontrol — 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/rsvppath, 204 response, dialog only foroccurrences) 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).
main: type-check, lint,bun test:corere-run green on themerged 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
bun run type-check+bun lint+bun knip(sync WPs additionally: safety-canary suite), recorded verbatim in each WP's
Evidence section
bun lintexit 0; relative-link sweep resolves