You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Refuse occurrence URLs instead of acting on the whole series - #950
The shared ID extractors in urlarg now refuse an occurrence URL (schedule_entries/{id}/occurrences/{date}, with or without /edit). They return an *urlarg.OccurrenceError instead of the entry's ID. The commands package turns it into a usage error:
That URL names one occurrence of a recurring event
Hint: To comment on, list the comments of, or subscribe to just this occurrence, pass its own ID, which basecamp show https://3.basecamp.com/99999/buckets/123/schedule_entries/789/occurrences/20261022 prints. To update, trash or boost the event, pass its ID, 789, which acts on the whole series. Basecamp's API cannot change a single occurrence
The extractors now return an error, so every caller has to take it. A codemod rewrote 73 of the 79 call sites in internal/commands. I edited the other 6, and one call through a function value, by hand. The compiler rejects a caller that does not take the error, so no call site can drop it without an explicit _. A comment link on an occurrence (…/occurrences/20261022#__recording_555) still names comment 555, in attachments list and download too.
Reads of an occurrence URL still work, and they read the occurrence:
show and schedule show read the URL with Parse, not the extractors, and show the occurrence. They now read the occurrence's own comments, and their comment breadcrumbs name the occurrence's recording ID, not the series. schedule show drops its schedule update breadcrumb for an occurrence, since the API cannot update one.
attachments list and download read the occurrence through GET /schedule_entries/{id}/occurrences/{date}. An occurrence has no description of its own; bc3 serves its event's, so these are the event's attachments. The comment breadcrumb names the occurrence's recording, and the show and download breadcrumbs name the URL. With a --type other than a schedule entry or a comment, the URL is refused.
spotlight keeps its own occurrence message, which names the occurrence's own ID.
url parse still reports the series ID as recording_id, since it parses offline. For an occurrence URL it adds occurrence_date, a notice that recording_id is the whole series, and suggests basecamp show <url> in place of comments create <series id>.
The refusal hint, url parse and spotlight all print basecamp show with the caller's own URL, without any comment fragment. That command returns the occurrence from every form of its URL: the web URL, /edit, a comment link, and an API URL with no project. schedule show needs a project, and show on a comment link reads the comment, which is why the fragment is dropped. Neither message offers a boost, since bc3 answers a boost on an occurrence with 403.
Why
An occurrence is its own recording in bc3, but its URL carries the entry's ID. The extractors returned that ID and dropped the date, so commands acted on the whole series. On main, recordings trash <occurrence URL> trashes the series, and comments create <occurrence URL> posts on the series. The API has no endpoint to change one occurrence, so the CLI refuses the URL.
bc3 gives a recurring event an occurrence URL as its app_url, even in API responses. The hint therefore names the entry's ID, because a user who pastes a link has no other way to name the series.
The occurrence has comments of its own (comments_url is /recordings/{occurrence id}/comments.json). On main, show <occurrence URL> reported the occurrence's comment count but listed the series' comments, and suggested comments create <series id>.
Not included
schedule update on a recurring entry by its ID still exits 0 and changes nothing, as on main. bc3 answers PUT /schedule_entries/{id} for a recurring entry with a 302 to its first occurrence. The client follows the redirect as a GET, and the CLI reports "Updated". This is a separate bug, so for schedule update the hint's "to act on the whole series" leads to that no-op.
The TUI opens an occurrence URL as the entry, as on main. It only shows the entry.
show <occurrence URL> still suggests attachments download <series id>. That reads the same description, so it downloads the same files.
Testing
internal/commands/occurrence_url_test.go runs 8 commands with an occurrence URL and its /edit form: schedule update, recordings trash and archive, comments create and list, boost create, subscriptions subscribe and attachments list --type todo. Between them they reach every extractor. Each case expects a usage error whose hint names entry 789, and no HTTP request. All 16 cases fail on main.
The same 8 commands, given the entry URL or the bare ID, must succeed and send exactly the expected method and path. The mock answers each endpoint in the shape the SDK expects and 404s anything else. The old mock answered every request with {"id": 789}, so 4 of the 8 commands (schedule update, comments create, comments list, boost create) failed and the test still passed. With a mutation that makes comments list error after its request, the new test fails and the old one passes.
The mock gives the series ID 789 and the occurrence recording ID 1001. show and schedule show --comments on an occurrence URL must read /recordings/1001/comments.json and suggest comments create 1001. attachments list and download on an occurrence URL must read the occurrence endpoint, and with #__recording_555 (type inferred, and --type comment) must read /comments/555. These 10 cases fail on 8fd027f, the head before this round.
internal/urlarg/urlarg_occurrence_test.go checks each extractor. If the occurrence check in urlarg is disabled, that test and the 16 command cases fail.
bin/ci passes
Verified against local bc3 (verify-basecamp-cli run 20261022-215052-occurrence-urls), on a recurring entry made for the run:
Before, on main, recordings trash <occurrence edit URL> trashed the whole series. The API listed it as trashed.
Before, on main, comments create <occurrence URL> posted the comment on the series, not on the occurrence.
After, schedule update, recordings trash, comments create and boost create with an occurrence URL exit 1 with the usage error. bc3's log shows no write, and the API shows the series active, its summary unchanged, and no new comment.
After, show <occurrence edit URL> and schedule show <occurrence URL> return the occurrence's own recording.
After, schedule update by bare ID and by entry URL changes a one-off entry, and recordings trash by bare ID trashes the series.
Verified again for this round (run 20261022-221805-occurrence-reads), on a new daily series 1069478916 whose 2026-10-22 occurrence is recording 1069478918, with comment 1069478919 and a file on that occurrence. "Before" is 8fd027f, "after" is this branch:
attachments list <occurrence URL>#__recording_1069478919, inferred and with --type comment, refused before. After, it lists notes.txt, and attachments download saves it.
attachments list <occurrence URL> refused before. After, it reads the occurrence ("0 attachment(s) on #1069478918") and suggests comments create 1069478918.
schedule show <occurrence URL> --comments listed no comments and suggested schedule update 1069478916 before. After, it lists comment 1069478919 and suggests comments create 1069478918.
show <occurrence URL> said "(1 comment)" but listed none, and suggested comments create 1069478916, before. After, it lists comment 1069478919 and suggests comments create 1069478918.
Running that suggested command posted comment 1069478922. The API lists it under the occurrence (parent 1069478918). The series' comments stay empty.
comments create and recordings trash with the occurrence URL still exit 1 with the usage error. bc3's log shows one write to the series in the run, the cleanup trash.
Agent check (Claude Code, Sonnet, with the basecamp plugin, against local bc3). Task: Comment "Running 10 minutes late" on <occurrence URL>, 6 runs per build, comment location read back from the API:
Commands acting on a whole series now refuse a URL for one occurrence of a recurring schedule entry, instead of treating it as the entry's ID and acting on the series.
The ID extractors in urlarg return an *urlarg.OccurrenceError for occurrence URLs (schedule_entries/{id}/occurrences/{date}), and the commands package converts that to a usage error. The hint names the entry's ID for updates, trash, and boosts, and the occurrence's own ID — which basecamp show <url> prints — for comments, comment lists, and subscriptions. Previously recordings trash, comments create, and similar commands affected the entire series.
Entry URLs and bare IDs still work as before.
Reads (show, schedule show, attachments list, attachments download) go through the occurrence, list its own comments and attachments, and suggest commands on its recording.
show, attachments list and download read the occurrence with no --type or with --type recording; a type that names something else, like todo or line, is still refused.
A comment fragment on an occurrence URL still names the comment.
When attachments list and download read an occurrence by type (--type schedule-entry), their suggested show and download commands drop the comment fragment so re-running them reads the occurrence, not the comment.
url parse reports occurrence_date, warns that recording_id is the whole series, and suggests basecamp show <url> — which works from any form of the URL — dropping any comment fragment from the suggested URL.
Not included
schedule update with an entry ID still no-ops on recurring entries (a separate bug).
The TUI still opens an occurrence URL as the entry.
Written for commit 95c1d1a. Summary will update on new commits.
Each command reaches the entry through a different shared extractor. On main every one of them acts on the recurring entry the occurrence belongs to, so the refusal cases fail; the entry URL, bare ID and show cases pass and guard what the fix must keep.
An occurrence URL's recording ID is the recurring entry's. The extractors
returned it and dropped the occurrence date, so every command that took
one acted on the whole series: recordings trash trashed it, comments and
boosts landed on it. The API cannot change a single occurrence, so the
extractors now return an *urlarg.OccurrenceError instead, and the
commands package turns it into a usage error whose hint names the
entry's ID.
The extractors' callers now take the error. A codemod rewrote the 73
call sites it could, the rest were edited by hand, and the compiler holds
any caller that ignores it. show and schedule show read the URL with
Parse and keep showing the occurrence.
Addresses #947 by rejecting occurrence URLs in shared ID extractors, preventing commands from accidentally acting on an entire recurring series.
Changes:
Adds a typed occurrence error and a usage hint identifying the series ID.
Updates command callers to handle extraction errors.
Adds regression tests for occurrence refusal, ordinary IDs, comment fragments, and occurrence viewing.
[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
File
Description
internal/urlarg/urlarg.go
Rejects occurrence URLs during ID extraction.
internal/urlarg/urlarg_trailing_test.go
Adapts trailing-path tests to error returns.
internal/urlarg/urlarg_test.go
Updates extractor tests for error returns.
internal/urlarg/urlarg_occurrence_test.go
Tests occurrence refusal and comment-fragment handling.
internal/commands/todos.go
Propagates extraction errors in todo operations.
internal/commands/todolists.go
Handles extraction errors in todolist operations.
internal/commands/subtasks.go
Rejects occurrence URLs as subtask parents.
internal/commands/subscriptions.go
Propagates subscription target errors.
internal/commands/schedule.go
Rejects occurrence URLs before updating entries.
internal/commands/recordings.go
Handles extraction errors in recording operations.
internal/commands/occurrence_url_test.go
Adds command-level occurrence regression tests.
internal/commands/messages.go
Propagates message target errors.
internal/commands/lineup.go
Handles marker ID extraction errors.
internal/commands/helpers.go
Converts occurrence errors into usage hints.
internal/commands/forwards.go
Handles forward and reply extraction errors.
internal/commands/files.go
Propagates file target extraction errors.
internal/commands/events.go
Rejects occurrence URLs for event lookup.
internal/commands/comment.go
Handles comment and batch target errors.
internal/commands/checkins.go
Propagates question and answer extraction errors.
internal/commands/chat.go
Returns non-URL arguments directly.
internal/commands/cards.go
Handles card, column, step, and wormhole extraction errors.
attachments list and download refused an occurrence URL with a comment
fragment before the fragment could name the comment. The comment now
wins, as everywhere else. Without a fragment they read the occurrence
itself, a read like show, whose description is its event's.
show and schedule show read the comments of the event's series and
suggested commenting on it, though the occurrence is a recording with
comments of its own. They now use the occurrence's recording ID, and
schedule show drops its update breadcrumb there, since the API cannot
update one occurrence.
The tests that guard the entry URL and bare ID now answer each request
in the shape the SDK expects and require success and the exact request.
Four of the eight commands failed under the old mock and still passed.
Codex review of 8fd027f (gpt-6.1-sol, run locally; it has no GitHub threads). Three findings, all fixed in 91064ca.
Attachments refused a comment link on an occurrence: the same finding as Copilot's thread. Fixed, and a bare occurrence URL now reads the occurrence through bc3's occurrence endpoint.
show and schedule show read the series' comments and suggested comments create <series>: this predates the PR, but I took it here because it steers a later write to the series, which is what Occurrence URLs make commands act on the whole recurring series #947 is about. Both now use the occurrence's own recording for comments and the comment suggestion. On local bc3, a comment posted through the suggested command landed on the occurrence, and the series has no comments.
The compatibility tests accepted failed commands: fixed. The mock answers each endpoint in the shape the SDK expects. Each "still works" case must succeed and send exactly the expected method and path. Under the old mock, 4 of the 8 commands errored and still passed.
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Attachment breadcrumbs can follow a comment fragment instead of the explicitly selected occurrence, causing follow-up commands to target the wrong recording.
With an occurrence URL carrying #__recording_555 and --type
schedule-entry, attachments list and download read the occurrence, but
their show and download breadcrumbs repeated the URL with its fragment.
Run as suggested, those commands read comment 555 instead. The
breadcrumbs now name the occurrence URL without the fragment.
attachments list and download read an occurrence URL with no --type,
but --type recording or recordings sent it to the refusal instead,
though neither names a type. Both now read the occurrence, as with no
--type. A type that names something else, such as todo or line, is
still refused.
basecamp show <occurrence URL> --type recording (or recordings) read the
series: it fetched recording 789 and the series' comments, and its
breadcrumbs wrote to the series. Neither type names what the item is, so
an occurrence URL now takes its type from the URL, as with no --type,
and show reads the occurrence and its own comments. A type that names
something else is still used as given.
Codex review of c9ca892 (gpt-6.1-sol, run locally). One minor finding, fixed in 8a751a0.
attachments list|download <occurrence-url> --type recording was refused: the same read succeeds with no --type. Fixed. A generic type (recording, recordings) now reads the occurrence too. A type naming something else, such as todo or line, is still refused.
The same gap was in show. show <occurrence-url> --type recording read the series and its comments, and suggested comments create <series>. That predates this PR, but this PR is what makes show read occurrences, so it is fixed here too, in 82e471c, using the same rule. No other read command has the gap; schedule show takes no --type.
The reason will be displayed to describe this comment to others. Learn more.
🟢 Approved
Static review found no blocking issues, and regression coverage addresses occurrence refusal, supported reads, and comment targeting; tests were not run during this review.
Labeled breaking: occurrence URLs that commands used to accept (and act on as the whole series) are now a usage error.
Live sweep at head 82e471c6 vs main (d0c313d8), local bc3 (run 20261023-031731-pr950-head-sweep):
Command
main
head
todos update, todos complete, comments create, boost create on a to-do URL
0
0
messages update, comments create on a message URL
0
0
cards update, comments create on a card URL
0
0
comments create on a recurring series URL
0
0
recordings trash on to-do, message, card and series URLs
0
0
comments create on an occurrence URL
0 (lands on the series)
1, nothing sent
API read-backs confirm the edits, comments and trashes landed on the same records under both builds, and the series holds only main's stray occurrence comment. show recording and attachments list --type recording on an occurrence now report the occurrence (#…945) rather than the series (#…942) and suggest commenting on the occurrence.
The refusal said Basecamp's API cannot change a single occurrence and
offered the series ID, so agents asked to comment on an occurrence either
gave up or commented on the whole series. An occurrence takes comments and
boosts under its own ID, which basecamp show <url> prints; the hint now
says so, and keeps the series ID for updates and trash.
url parse reported an occurrence URL's series ID as recording_id and
suggested commenting on it. It now adds occurrence_date, a notice that
recording_id is the whole series, and suggests basecamp show <url>.
bc3 refuses a boost on an occurrence with a 403 (an occurrence has no
content to boost), so the hint and url parse notice no longer offer it.
basecamp show on an occurrence comment link resolves the comment first and
prints the comment's ID. schedule show reads the occurrence from any form
of its URL, so the hint, the notice and url parse's breadcrumb name it.
The test that non-occurrence URLs omit occurrence_date now checks the
decoded keys: an absent field and a null one unmarshal to the same nil.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
The broad extractor migration still needs human validation, and an unresolved breadcrumb error affects occurrence URLs without a bucket ID.
No open issues 🎉
1 previously missed issue
In code that hasn't changed since last review
Avoid empty project flag for account-scoped occurrence URLs
internal/commands/url.go:198
For an account-scoped occurrence URL such as https://3.basecampapi.com/99999/schedule_entries/789/occurrences/20261022.json, bucketID is empty. This emits basecamp show schedule_entry 789 --in , which fails because --in requires a value. Omit the project flag here: typed show reads /schedule_entries/{id}.json without needing a project. Add a regression case for an occurrence URL without /buckets/<id>.
…rumb
An occurrence has its own comments and subscribers: unsubscribing from one
on bc3 leaves the series subscription in place. The hint said anything but
a comment goes through the event's ID, which subscribed to and listed the
series. It now names comments, comment lists and subscriptions for the
occurrence's ID, and updates, trash and boosts for the event's.
url parse's series breadcrumb, show schedule_entry <id>, shows the first
occurrence, since bc3 redirects a recurring entry's GET there, and printed
an empty --in for a URL with no project. It is gone. The notice now names
the exact command: schedule show <url>, or show <url> when the URL names no
project, since schedule show needs one.
Review rounds on the occurrence-hint commits (92ea0e6 → cb151f3).
All fixed, none declined:
Boosts on an occurrence (Codex, 92ea0e6). bc3 answers boost create <occurrence id> with 403, confirmed on local bc3. The hint and notice no longer offer it. Fixed in c41746a.
Comment links (Codex, and Copilot in this thread). show on …/occurrences/{date}#__recording_N returns the comment. Everything now names schedule show <url>, which returns the occurrence from that link, confirmed on bc3. Fixed in c41746a, with regression cases.
occurrence_date omitempty test (Codex). It could not tell an absent field from a null one. It now checks the decoded keys, and dropping omitempty fails it. Fixed in c41746a.
Subscriptions and comment lists (Codex, c41746a). "For anything else, pass the event's ID" sent subscriptions subscribe and comments list to the series. On bc3, unsubscribing from an occurrence by its ID left the series subscription in place, so the two are separate. The hint now names comments, comment lists and subscriptions for the occurrence's ID. Fixed in cb151f3.
Empty --in for a URL without a project (Copilot review body, c41746a). The series breadcrumb is removed. show schedule_entry <id> shows the first occurrence anyway, because bc3 redirects a recurring entry's GET there (schedule update on a recurring event reports success but changes nothing #951). The notice and the one remaining breadcrumb use show <url> when the URL names no project, since schedule show needs one. All three URL forms were run on bc3, and each returns the occurrence. Fixed in cb151f3.
Agent check on cb151f3's code: 6 of 6 runs commented on the occurrence. Each went url parse, then schedule show, then comments create <occurrence id>.
Found along the way, filed separately: #954. --hints is ignored on subcommands in release builds, so the breadcrumbs above reach release users only with BASECAMP_HINTS=1 or the config setting. The notice carries the command, so it works either way.
For a URL with no project the suggestion falls back to show, which reads a
comment fragment before the occurrence, so show <url>#__recording_N printed
the comment's ID. The suggested URL now drops the fragment in both forms;
comment_id still reports it.
The refusal hint named schedule show <url>, which needs a project, so for
an occurrence URL without /buckets/ it failed before printing the ID. Each
new URL form had needed its own branch. show reads the occurrence from every
form once the comment fragment is dropped, and needs no project, so
OccurrenceError now carries the URL and one helper prints show <url> with
the fragment removed. The refusal hint, url parse and spotlight's occurrence
message all use it, and each prints the command with the caller's own URL.
Codex on 75a80bc. Following the refusal hint's schedule show <url> failed with "Project ID required" for an occurrence URL without /buckets/. This was the second finding in a row on the no-project URL form, after the comment-fragment one. So rather than add a branch, 95c1d1a removes the axis. OccurrenceError carries the URL, and one helper prints basecamp show <url> with the fragment dropped. The refusal hint, url parse and spotlight share it.
I ran it on local bc3 with subscriptions subscribe and spotlight against 3 URL forms: an API URL with no project and a comment fragment, an /edit URL, and a web URL with a comment fragment. In all 6 cases, the printed command returned the 22 Oct occurrence. In the agent check on 95c1d1a, 6 of 6 runs commented on the occurrence.
The reason will be displayed to describe this comment to others. Learn more.
🟢 Approved
Static review identified no blocking issues, and the supplied regression coverage addresses occurrence refusal, supported reads, and correct comment targeting.
No open issues 🎉
Review effort: Balanced
This branch has not been deployed
No deployments
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
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.
Fixes #947.
What
The shared ID extractors in
urlargnow refuse an occurrence URL (schedule_entries/{id}/occurrences/{date}, with or without/edit). They return an*urlarg.OccurrenceErrorinstead of the entry's ID. The commands package turns it into a usage error:The extractors now return an error, so every caller has to take it. A codemod rewrote 73 of the 79 call sites in
internal/commands. I edited the other 6, and one call through a function value, by hand. The compiler rejects a caller that does not take the error, so no call site can drop it without an explicit_. A comment link on an occurrence (…/occurrences/20261022#__recording_555) still names comment 555, inattachments listanddownloadtoo.Reads of an occurrence URL still work, and they read the occurrence:
showandschedule showread the URL withParse, not the extractors, and show the occurrence. They now read the occurrence's own comments, and their comment breadcrumbs name the occurrence's recording ID, not the series.schedule showdrops itsschedule updatebreadcrumb for an occurrence, since the API cannot update one.attachments listanddownloadread the occurrence throughGET /schedule_entries/{id}/occurrences/{date}. An occurrence has no description of its own; bc3 serves its event's, so these are the event's attachments. The comment breadcrumb names the occurrence's recording, and the show and download breadcrumbs name the URL. With a--typeother than a schedule entry or a comment, the URL is refused.spotlightkeeps its own occurrence message, which names the occurrence's own ID.url parsestill reports the series ID asrecording_id, since it parses offline. For an occurrence URL it addsoccurrence_date, a notice thatrecording_idis the whole series, and suggestsbasecamp show <url>in place ofcomments create <series id>.url parseandspotlightall printbasecamp showwith the caller's own URL, without any comment fragment. That command returns the occurrence from every form of its URL: the web URL,/edit, a comment link, and an API URL with no project.schedule showneeds a project, andshowon a comment link reads the comment, which is why the fragment is dropped. Neither message offers a boost, since bc3 answers a boost on an occurrence with 403.Why
An occurrence is its own recording in bc3, but its URL carries the entry's ID. The extractors returned that ID and dropped the date, so commands acted on the whole series. On
main,recordings trash <occurrence URL>trashes the series, andcomments create <occurrence URL>posts on the series. The API has no endpoint to change one occurrence, so the CLI refuses the URL.bc3 gives a recurring event an occurrence URL as its
app_url, even in API responses. The hint therefore names the entry's ID, because a user who pastes a link has no other way to name the series.The occurrence has comments of its own (
comments_urlis/recordings/{occurrence id}/comments.json). Onmain,show <occurrence URL>reported the occurrence's comment count but listed the series' comments, and suggestedcomments create <series id>.Not included
schedule updateon a recurring entry by its ID still exits 0 and changes nothing, as onmain. bc3 answersPUT /schedule_entries/{id}for a recurring entry with a 302 to its first occurrence. The client follows the redirect as a GET, and the CLI reports "Updated". This is a separate bug, so forschedule updatethe hint's "to act on the whole series" leads to that no-op.main. It only shows the entry.show <occurrence URL>still suggestsattachments download <series id>. That reads the same description, so it downloads the same files.Testing
internal/commands/occurrence_url_test.goruns 8 commands with an occurrence URL and its/editform:schedule update,recordings trashandarchive,comments createandlist,boost create,subscriptions subscribeandattachments list --type todo. Between them they reach every extractor. Each case expects a usage error whose hint names entry 789, and no HTTP request. All 16 cases fail onmain.{"id": 789}, so 4 of the 8 commands (schedule update,comments create,comments list,boost create) failed and the test still passed. With a mutation that makescomments listerror after its request, the new test fails and the old one passes.showandschedule show --commentson an occurrence URL must read/recordings/1001/comments.jsonand suggestcomments create 1001.attachments listanddownloadon an occurrence URL must read the occurrence endpoint, and with#__recording_555(type inferred, and--type comment) must read/comments/555. These 10 cases fail on 8fd027f, the head before this round.internal/urlarg/urlarg_occurrence_test.gochecks each extractor. If the occurrence check inurlargis disabled, that test and the 16 command cases fail.bin/cipassesVerified against local bc3 (verify-basecamp-cli run
20261022-215052-occurrence-urls), on a recurring entry made for the run:main,recordings trash <occurrence edit URL>trashed the whole series. The API listed it astrashed.main,comments create <occurrence URL>posted the comment on the series, not on the occurrence.schedule update,recordings trash,comments createandboost createwith an occurrence URL exit 1 with the usage error. bc3's log shows no write, and the API shows the series active, its summary unchanged, and no new comment.show <occurrence edit URL>andschedule show <occurrence URL>return the occurrence's own recording.schedule updateby bare ID and by entry URL changes a one-off entry, andrecordings trashby bare ID trashes the series.Verified again for this round (run
20261022-221805-occurrence-reads), on a new daily series 1069478916 whose 2026-10-22 occurrence is recording 1069478918, with comment 1069478919 and a file on that occurrence. "Before" is 8fd027f, "after" is this branch:attachments list <occurrence URL>#__recording_1069478919, inferred and with--type comment, refused before. After, it listsnotes.txt, andattachments downloadsaves it.attachments list <occurrence URL>refused before. After, it reads the occurrence ("0 attachment(s) on #1069478918") and suggestscomments create 1069478918.schedule show <occurrence URL> --commentslisted no comments and suggestedschedule update 1069478916before. After, it lists comment 1069478919 and suggestscomments create 1069478918.show <occurrence URL>said "(1 comment)" but listed none, and suggestedcomments create 1069478916, before. After, it lists comment 1069478919 and suggestscomments create 1069478918.comments createandrecordings trashwith the occurrence URL still exit 1 with the usage error. bc3's log shows one write to the series in the run, the cleanup trash.Agent check (Claude Code, Sonnet, with the basecamp plugin, against local bc3). Task:
Comment "Running 10 minutes late" on <occurrence URL>, 6 runs per build, comment location read back from the API:url parse'srecording_id)url parsenotice (92ea0e6)schedule showwording (c41746a)showcommand (95c1d1a)Summary by cubic
Commands acting on a whole series now refuse a URL for one occurrence of a recurring schedule entry, instead of treating it as the entry's ID and acting on the series.
The ID extractors in
urlargreturn an*urlarg.OccurrenceErrorfor occurrence URLs (schedule_entries/{id}/occurrences/{date}), and the commands package converts that to a usage error. The hint names the entry's ID for updates, trash, and boosts, and the occurrence's own ID — whichbasecamp show <url>prints — for comments, comment lists, and subscriptions. Previouslyrecordings trash,comments create, and similar commands affected the entire series.show,schedule show,attachments list,attachments download) go through the occurrence, list its own comments and attachments, and suggest commands on its recording.show,attachments listanddownloadread the occurrence with no--typeor with--type recording; a type that names something else, liketodoorline, is still refused.attachments listanddownloadread an occurrence by type (--type schedule-entry), their suggested show and download commands drop the comment fragment so re-running them reads the occurrence, not the comment.url parsereportsoccurrence_date, warns thatrecording_idis the whole series, and suggestsbasecamp show <url>— which works from any form of the URL — dropping any comment fragment from the suggested URL.Not included
schedule updatewith an entry ID still no-ops on recurring entries (a separate bug).Written for commit 95c1d1a. Summary will update on new commits.