Maestro task 5885d2b1-cf06-44ff-abf3-242d7763c3c9 — "Push plan to Google Calendar" (Feature 16, task 3 of 5).
Done-condition
POST /api/v1/plans/{weekStart}/push-calendar writes one event per plan day to the selected calendar: summary "Dinner: {name}", start at household dinner_time and end 60 min later in the household timezone, description with the dinner name, "Start by {time}" for crock-pot dinners and a link to {MEALQUEST_PUBLIC_URL}/dinner/{id}. Event ids are deterministic (lowercase base32hex of SHA-256 of "mealquest|{weekStart}|{date}", within Google's id rules); insert, and on 409 update the existing event. Afterwards the plan is synced with calendar_synced_at and plan_days.calendar_event_id is stored. Feature 17 owns generating the week's shopping list from a pushed plan — this task exposes what it needs and does not stub it.
Scope
The push endpoint, the event id/times/description, the synced bookkeeping, the Feature 17 seam and tests. Task b8d8da5a's invalid_grant disconnect bookkeeping, the settings screen (677261bc) and any shopping-list generation are untouched. No migration: the schema already has calendar_synced_at and calendar_event_id.
Points for review
The deterministic event id satisfies Google's charset and length rules, insert-versus-409-update behaves as specified, and a second push truly creates no new events.
The times are the household dinner_time in the household timezone with the UTC instant shifting correctly across a DST transition in the week (the developer tested both fall-back 2026-10-26 and spring-forward 2027-03-08).
A partial failure must not leave the plan marked synced; the developer states what it chose.
The stale-selection hand-off from task 0fe3f1f2: a selected calendar since deleted or no longer writable is reported clearly rather than failing obscurely, and a 403 requiredAccessLevel is mapped rather than swallowed.
The Feature 17 seam is exposed and nothing more — no shopping-list code, no stub.
internal/gcal/calendars.go was refactored (the connection read split out of accessToken for reuse) and internal/suggest/suggest_test.go was touched: check both for regressions.
migrations/queries/plans.sql gained queries and sqlc was regenerated (committed, no drift); applied migrations untouched.
go test -race -count=1 ./... — 26 packages ok, 0 failures
CGO_ENABLED=0 go build ./... — ok
make sqlc-check SQLC=$(go env GOPATH)/bin/sqlc — no drift
cd web && npm run typecheck && npm test && npm run build — PASS, 45 files / 764 tests (no web change)
The developer ran 13 deliberate mutation checks (no update on 409, always push all days, swap keeps the event id, no revision check, fixed-offset times, marked synced despite a failure, no-calendar check removed, 403 unmapped, Google's body leaked, event-id prefix changed, no "Start by" line, account lock removed, no no_calendar mapping in the server) and reports all 13 were caught. The endpoint was also exercised through the real router against the fake Google; it was not run against real Google (no credentials or network).
## Task
Maestro task `5885d2b1-cf06-44ff-abf3-242d7763c3c9` — "Push plan to Google Calendar" (Feature 16, task 3 of 5).
## Done-condition
`POST /api/v1/plans/{weekStart}/push-calendar` writes one event per plan day to the selected calendar: summary "Dinner: {name}", start at household dinner_time and end 60 min later in the household timezone, description with the dinner name, "Start by {time}" for crock-pot dinners and a link to `{MEALQUEST_PUBLIC_URL}/dinner/{id}`. Event ids are deterministic (lowercase base32hex of SHA-256 of "mealquest|{weekStart}|{date}", within Google's id rules); insert, and on 409 update the existing event. Afterwards the plan is synced with `calendar_synced_at` and `plan_days.calendar_event_id` is stored. Feature 17 owns generating the week's shopping list from a pushed plan — this task exposes what it needs and does not stub it.
## Scope
The push endpoint, the event id/times/description, the synced bookkeeping, the Feature 17 seam and tests. Task `b8d8da5a`'s invalid_grant disconnect bookkeeping, the settings screen (`677261bc`) and any shopping-list generation are untouched. No migration: the schema already has `calendar_synced_at` and `calendar_event_id`.
## Points for review
- The deterministic event id satisfies Google's charset and length rules, insert-versus-409-update behaves as specified, and a second push truly creates no new events.
- The times are the household dinner_time in the household timezone with the UTC instant shifting correctly across a DST transition in the week (the developer tested both fall-back 2026-10-26 and spring-forward 2027-03-08).
- A partial failure must not leave the plan marked synced; the developer states what it chose.
- The stale-selection hand-off from task `0fe3f1f2`: a selected calendar since deleted or no longer writable is reported clearly rather than failing obscurely, and a 403 `requiredAccessLevel` is mapped rather than swallowed.
- The Feature 17 seam is exposed and nothing more — no shopping-list code, no stub.
- `internal/gcal/calendars.go` was refactored (the connection read split out of `accessToken` for reuse) and `internal/suggest/suggest_test.go` was touched: check both for regressions.
- `migrations/queries/plans.sql` gained queries and sqlc was regenerated (committed, no drift); applied migrations untouched.
## Evidence (orchestrator-run gate at 691c1ff34abfd1ee182053ad0067077a690ac148)
- `gofmt -l .` — empty; `go vet ./...` — clean
- `go test -race -count=1 ./...` — 26 packages ok, 0 failures
- `CGO_ENABLED=0 go build ./...` — ok
- `make sqlc-check SQLC=$(go env GOPATH)/bin/sqlc` — no drift
- `cd web && npm run typecheck && npm test && npm run build` — PASS, 45 files / 764 tests (no web change)
The developer ran 13 deliberate mutation checks (no update on 409, always push all days, swap keeps the event id, no revision check, fixed-offset times, marked synced despite a failure, no-calendar check removed, 403 unmapped, Google's body leaked, event-id prefix changed, no "Start by" line, account lock removed, no `no_calendar` mapping in the server) and reports all 13 were caught. The endpoint was also exercised through the real router against the fake Google; it was not run against real Google (no credentials or network).
Implementation commit: `691c1ff34abfd1ee182053ad0067077a690ac148`
POST /api/v1/plans/{weekStart}/push-calendar writes one event per plan
day to the chosen calendar: "Dinner: {name}" from the household dinner
time for 60 minutes in the household zone, with the dinner name,
"Start by {time}" for crock-pot dinners and the
{MEALQUEST_PUBLIC_URL}/dinner/{id} link. Event ids are the lowercase
base32hex SHA-256 of "mealquest|{weekStart}|{date}"; each event is
inserted, and a 409 updates it instead.
gcal.Service.PushEvents does the Calendar writes under the account lock,
with the same redaction as listing. plan.Service.Push records a push only
when every event was written: the plan becomes synced with
calendar_synced_at and each day stores its event id. A partial failure
records nothing; a plan edited mid-push is 409 plan_changed.
A swap, or a redraft to another dinner, clears the day's
calendar_event_id, so the next push writes just those days; a push with
none pending writes every day again.
No calendar chosen is 409 no_calendar; a deleted or read-only chosen
calendar is 409 calendar_unavailable; a refused grant is 502
reconnect_required, leaving the connection as it is.
Maestro task 5885d2b1-cf06-44ff-abf3-242d7763c3c9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Choosing a calendar other than the stored one now clears every plan
day's calendar_event_id and edits each plan that had one (revision
raised, synced -> changed) in the same transaction as the choice, so a
stored event id is always on the chosen calendar and the next push
writes the whole week there.
PushEvents now runs the push's record under the same account-lock hold
as its event writes, so the chosen calendar cannot change between the
writes and the sync bookkeeping; a push that read a plan before the
choice is refused by its revision check (ErrPlanChanged).
Fixes review finding F-01 on task 5885d2b1-cf06-44ff-abf3-242d7763c3c9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Disconnect deleted the connection but left every pushed plan synced with
its stored event ids, though no calendar was chosen any more. It now
deletes the connection and runs the same invalidation as choosing another
calendar (forgetEvents: MarkCalendarPlansEdited, ClearCalendarEvents) in
one transaction, under the account lock. A refused revoke still changes
nothing.
Tests push three weeks, disconnect, and check each is changed with no
event id; then connect again, choose a calendar and push the whole week
to it. Also: a refused revoke keeps every week synced, and a disconnect
during a push waits for its record and then forgets it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Task
Maestro task
5885d2b1-cf06-44ff-abf3-242d7763c3c9— "Push plan to Google Calendar" (Feature 16, task 3 of 5).Done-condition
POST /api/v1/plans/{weekStart}/push-calendarwrites one event per plan day to the selected calendar: summary "Dinner: {name}", start at household dinner_time and end 60 min later in the household timezone, description with the dinner name, "Start by {time}" for crock-pot dinners and a link to{MEALQUEST_PUBLIC_URL}/dinner/{id}. Event ids are deterministic (lowercase base32hex of SHA-256 of "mealquest|{weekStart}|{date}", within Google's id rules); insert, and on 409 update the existing event. Afterwards the plan is synced withcalendar_synced_atandplan_days.calendar_event_idis stored. Feature 17 owns generating the week's shopping list from a pushed plan — this task exposes what it needs and does not stub it.Scope
The push endpoint, the event id/times/description, the synced bookkeeping, the Feature 17 seam and tests. Task
b8d8da5a's invalid_grant disconnect bookkeeping, the settings screen (677261bc) and any shopping-list generation are untouched. No migration: the schema already hascalendar_synced_atandcalendar_event_id.Points for review
0fe3f1f2: a selected calendar since deleted or no longer writable is reported clearly rather than failing obscurely, and a 403requiredAccessLevelis mapped rather than swallowed.internal/gcal/calendars.gowas refactored (the connection read split out ofaccessTokenfor reuse) andinternal/suggest/suggest_test.gowas touched: check both for regressions.migrations/queries/plans.sqlgained queries and sqlc was regenerated (committed, no drift); applied migrations untouched.Evidence (orchestrator-run gate at
691c1ff34a)gofmt -l .— empty;go vet ./...— cleango test -race -count=1 ./...— 26 packages ok, 0 failuresCGO_ENABLED=0 go build ./...— okmake sqlc-check SQLC=$(go env GOPATH)/bin/sqlc— no driftcd web && npm run typecheck && npm test && npm run build— PASS, 45 files / 764 tests (no web change)The developer ran 13 deliberate mutation checks (no update on 409, always push all days, swap keeps the event id, no revision check, fixed-offset times, marked synced despite a failure, no-calendar check removed, 403 unmapped, Google's body leaked, event-id prefix changed, no "Start by" line, account lock removed, no
no_calendarmapping in the server) and reports all 13 were caught. The endpoint was also exercised through the real router against the fake Google; it was not run against real Google (no credentials or network).Implementation commit:
691c1ff34abfd1ee182053ad0067077a690ac148POST /api/v1/plans/{weekStart}/push-calendar writes one event per plan day to the chosen calendar: "Dinner: {name}" from the household dinner time for 60 minutes in the household zone, with the dinner name, "Start by {time}" for crock-pot dinners and the {MEALQUEST_PUBLIC_URL}/dinner/{id} link. Event ids are the lowercase base32hex SHA-256 of "mealquest|{weekStart}|{date}"; each event is inserted, and a 409 updates it instead. gcal.Service.PushEvents does the Calendar writes under the account lock, with the same redaction as listing. plan.Service.Push records a push only when every event was written: the plan becomes synced with calendar_synced_at and each day stores its event id. A partial failure records nothing; a plan edited mid-push is 409 plan_changed. A swap, or a redraft to another dinner, clears the day's calendar_event_id, so the next push writes just those days; a push with none pending writes every day again. No calendar chosen is 409 no_calendar; a deleted or read-only chosen calendar is 409 calendar_unavailable; a refused grant is 502 reconnect_required, leaving the connection as it is. Maestro task 5885d2b1-cf06-44ff-abf3-242d7763c3c9. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Independent Codex gpt-6-sol review: CHANGES_REQUIRED for this exact commit.
Independent Codex gpt-6-sol review: CHANGES_REQUIRED for this exact commit.
Independent Codex gpt-6-sol review: APPROVED for this exact commit.