Skip to content

Edit one day of a repeating event with --occurrence and --apply-to - #435

Open
robzolkos wants to merge 4 commits into
mainfrom
event-edit-occurrence-scopes
Open

Edit one day of a repeating event with --occurrence and --apply-to#435
robzolkos wants to merge 4 commits into
mainfrom
event-edit-occurrence-scopes

Conversation

@robzolkos

@robzolkos robzolkos commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

hey event edit <id> changed a whole series and nothing else, while hey event day and week served an occurrence_id nothing on the CLI could take. HEY's own form asks anyone editing one day of a series whether to update just this event or this and all future events, and the SDK already speaks both through UpdateOccurrence. The Omarchy HEY Calendar plugin wants that choice in a machine-safe shape.

hey event edit SERIES_ID [DATE] --occurrence SERIES_ID_YYYY-MM-DD --apply-to current|future [edit flags] --json

What changes

  • --occurrence takes the occurrence_id byte for byte as day/week serve it, and its series must be the positional id. --apply-to is required with it and takes only current (apply_to_future=0, OccurrenceScopeThisEvent) or future (apply_to_future=1, OccurrenceScopeThisAndFollowing); it is a usage error without --occurrence. current refuses --repeat, --repeat-until and --repeat-times, matching HEY's rule that a schedule change cannot apply to one occurrence.
  • The day is read on its own date as [date, date+1), over --calendar or every calendar. HEY answers that window with every series still recurring through it and with the day itself where an earlier edit already wrote it out, so no wider scan is needed, and a day HEY has made its own keeps its own title, times, notes, reminders and countdown rather than the series'. An id that names one day of another series, or an event that does not repeat, is refused with the reason before anything is written.
  • Every unmentioned field is sent back: the day's schedule and zones (the series' wall-clock time on the occurrence's date, with the UTC-date naming HEY uses accounted for), all-day state, notes, location, link, attached entry, reminders, circle and countdown. Attendees stay off the wire unless --invite is given, exactly as before.
  • The two documented lossy edits are handled rather than broadened. A countdown is a recording of its own under the event, so it is read back from the day, or from the series' first day in one more single-day read, and re-sent; --countdown 0 is the explicit removal and an unreadable label stops the edit. Notes are served only as plain text and nothing can tell formatted from plain, so an occurrence edit that would send notes back as text refuses unless --allow-plain-notes accepts the loss or --notes replaces them.
  • HEY's 404 on the occurrence route comes out of the SDK as a bare form error; the edit reports it as the not-found it is, naming both things it can mean.
  • The whole-event edit by numeric id is unchanged on the wire, summary and envelope included.

Output

Same envelope as every mutation: summary is Occurrence updated or Occurrence and the following updated, data is the recording HEY answered. After future HEY splits the series and the answer is still the edited day, so callers should read day/week again for the new series id.

Also

Help text, docs/cli.md, the skill, API-COVERAGE.md and the .surface snapshot are updated. MCP tool metadata is derived from the SDK model and needs no change.


Summary by cubic

Adds editing one day of a repeating event with hey event edit <series-id> --occurrence <occurrence_id> --apply-to current|future, bringing HEY's own form choice to the CLI; an id alone still edits the whole series unchanged.

New Features

  • --occurrence takes the occurrence_id from hey event day/week exactly as served, and the series it names must match the positional id; an empty value is a usage error.
  • --apply-to is required with --occurrence and accepts only current (that day alone) or future (that day and all following); current refuses --repeat, --repeat-until, and --repeat-times.
  • The day is read on its own date over every calendar, preferring a day HEY already wrote out as its own recording; that day keeps its own id, title, and times, and parent_id names the series --occurrence takes.
  • A future edit makes HEY split the series, so callers should re-read day/week for the new series id before editing it further; it keeps a moved day on its current calendar and refuses a day whose guest list differs from the series' until --invite names the new list.
  • With --occurrence, --calendar only moves the day.

Safeguards

  • The countdown's length is read off its recording's span rather than HEY's rounded label, and it is read back from wherever it now lives — the day, the day a moved day starts on, or the series' first day — so it survives unless --countdown 0 removes it, which is refused for one day of a series that has one.
  • The edit keeps the series' wall-clock time on the occurrence's date across DST changes as HEY resolves them, so a title-only edit doesn't move the day.
  • An edit that would send notes back as plain text is refused unless --allow-plain-notes is accepted or --notes replaces them.
  • HEY's 404 on the occurrence route is reported as not-found, naming both a date that is not a day of the series and a series you cannot edit; an attached email the editor cannot read is detached, per the docs.
  • Whole-event edits by numeric id are unchanged on the wire, except that --repeat-times under one — which used to mean forever — is now refused there, on a create, and on a future occurrence edit.

Written for commit b2c8504. Summary will update on new commits.

Review in cubic

hey event edit <id> changed a whole series and nothing else; the day view served an
occurrence_id nothing on the CLI could take. HEY's own form puts a choice to anyone
editing one day of a series — update just this event, or this and all future events —
and the SDK already spoke both through UpdateOccurrence. The Omarchy calendar plugin
wants that choice in a machine-safe shape.

--occurrence takes the occurrence_id exactly as day and week serve it, naming the series
the positional id names; --apply-to is required with it and takes current or future,
which are apply_to_future=0 and =1 on the wire. The day is read on its own date, as the
window HEY answers with every series still recurring through it and with the day itself
where an earlier edit already wrote it out, so no wider scan is needed and a day HEY has
made its own keeps its own title, times and countdown. A schedule change cannot apply to
one day, so current refuses the repeat flags.

An occurrence edit keeps what a whole-event edit loses, or refuses. The countdown is a
recording of its own under the event, so it is read back — from the day, or from the
series' first day in one more bounded read — and sent again; only --countdown 0 removes
it, and a label that cannot be read back stops the edit. Notes are served only as plain
text and nothing can tell formatted ones from plain, so an edit that would send notes
back as text refuses unless --allow-plain-notes accepts the loss or --notes replaces
them. The circle is sent back too, because a future edit records a new series that HEY
circles only when told. A whole-event edit is unchanged on the wire.

HEY's 404 on the occurrence route arrives from the SDK as a bare form error, so the edit
names it as the not-found it is, with both things it can mean.
@robzolkos
robzolkos requested a review from a team as a code owner September 13, 2026 19:40
Copilot AI balanced review requested due to automatic review settings September 13, 2026 19:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Validate countdown labels and their 1–30 range before constructing update parameters.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds CLI support for editing individual occurrences of repeating events with current/future scope and field preservation.

Changes:

  • Adds occurrence editing flags, validation, and routing.
  • Preserves event metadata, countdowns, and notes safely.
  • Updates tests, documentation, skills, API coverage, and CLI surface.
  • Requires countdown label range validation before approval.
File summaries
File Reviewed changes
skills/hey/SKILL.md Updated agent guidance.
internal/cmd/recording_filter.go Supports multi-type recording reads.
internal/cmd/events.go Adds flags, help text, and routing.
internal/cmd/events_occurrence.go Implements occurrence editing and preservation.
internal/cmd/events_occurrence_test.go Adds occurrence edit coverage.
docs/cli.md Documents occurrence editing.
API-COVERAGE.md Records endpoint coverage.
.surface Updates the CLI surface snapshot.
Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 1
  • Review effort level: Lite (auto)

Note

Copilot is running an experiment and ran this review at Lite.


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/cmd/events_occurrence.go Outdated
Comment on lines +389 to +395
value, _ := strconv.Atoi(match[1])
units := map[string]hey.CountdownUnit{
"day": hey.CountdownUnitDays,
"week": hey.CountdownUnitWeeks,
"month": hey.CountdownUnitMonths,
}
return hey.CountdownParams{Value: value, Unit: units[match[2]]}, nil
Review of the occurrence edit found six ways a day could be written with something other
than what it held, and one it cannot keep at all.

A countdown exactly one day long is labelled "0 months before": HEY tries months before
weeks before days and takes a remainder of up to a day as a match, which is every all-day
event's one-day countdown. The parser took the zero and the SDK then sent nothing, which
HEY reads as removal. The recording's own span says what a zero is, and any other zero
stops the edit.

A day that was moved is still listed under the date it stands for, but its own countdown
ends where it now starts, so the identifier day never held it and the series' countdown
was sent in its place. The moved-to day is read for it first.

A clock time the zone springs over — 02:30 in New York on the second Sunday of March —
is moved an hour on by ActiveSupport and an hour back by Go's time.Date, so a title-only
edit would have moved the day. wallClockOn resolves it as HEY does.

--calendar was both where the series was looked for and where the day was moved to, so
a move to another calendar could never find its series. The day is read over every
calendar now, and with --occurrence the flag is only the destination.

A day HEY has written out carried its own id in `day` and `week` while its occurrence_id
named the series, so the pair as served could not feed the edit. The row publishes the
series id, as the docs already promised.

--occurrence given empty, a script's unset variable, fell through to editing the whole
series. It is a usage error now.

An attached email the editor cannot read is left out of what HEY serves, and HEY clears
the attachment whether the write sends an empty entry id or no key at all, so nothing
client-side can keep it. The help and the docs say so instead.
…ld undo

A second review of the occurrence edit found four more ways a write could reach
something other than what was asked.

A day HEY has written out has an id of its own, and HEY's event routes act on that id
for that day alone. The last commit rewrote it to the series id in `day` and `week`, so
the same `edit <id>` or `delete <id>` that had changed one day would have reached the
whole series. It keeps its id again; parent_id names the series, and the docs say that
is what --occurrence takes beside the occurrence_id.

A future edit records the new series on the series' calendar unless told otherwise, so
a day that had been moved to another calendar would have moved back with everything
after it. The write names the day's calendar when --calendar does not.

A future edit records the new series from the series' guest list and invites it, so a
day that had come to have guests of its own would have lost them, and the series' guests
would have been invited to it. That is refused until --invite says whose list the new
series gets; the day alone keeps its own list as before.

HEY shows one day of a series the series' countdown whenever the day has none of its
own, and a write that names no countdown removes only the day's own, so --countdown 0 on
one day of a series with a countdown was reported done and changed nothing. It is refused
with where the countdown can come off: a future edit, or the series itself.
…nothing

HEY labels a countdown by trying months, then weeks, then days, and taking a remainder
of up to a whole day as a match, so eight days at midnight is "1 weeks before" and 29
days is "4 weeks before" — and sending the label back shortened every one of them by a
day. The recording's span is the countdown plus however far into its day the event
starts, never a day more, so the length is read off the span now; the label settles the
one thing the span cannot, days against HEY's month, which is not a whole number of them.
The one-day case that used to be special is just a span of a day.

--repeat-times 0 and a negative count meant "forever", since the parser only knew a
positive count from the zero value. It knows the flag was given now, and refuses a count
under one — on a create and a whole-event edit as well as on the future edit that would
have split the series on it.

The day and the week help said an occurrence's id is its series, which is true of a day
HEY draws from the series and not of one it has written out, whose id is its own and
whose series is parent_id. Both say so.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants