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
Read-only audit of the CLI against basecamp-sdk main (1d8ddb1c0), ahead of the v0.13.0 tag. No bump has happened and none should until the tag exists — there is nothing to pin to. This is the worklist the bump executes against.
Source: the SDK's MIGRATING.md at that SHA, which reports 58 merged PRs, 15 breaking, and enumerates 12 class A and 4 class B breaks for Go. basecamp-sdk is public, so quoting it here is fine.
Every entry below was checked against the tree at 3d4e235d, not inferred. Confirmed non-issues are recorded too, so nobody re-derives them.
Go auto-dereferences a pointer for a value-receiver method call, so this compiles untouched and panics when the server omits the field. It is the only class-B site in the CLI, and it is in a display path that runs on every basecamp timeline.
Nine other wrapper timestamps pointerize in the same sweep (#615 + #658). I checked all ten:
Field
CLI site
Verdict
TimelineEvent.CreatedAt
timeline.go:393
panics — method call
SearchResult.CreatedAt
search.go:379relativeTime(r.CreatedAt)
compile error — a function argument gets no auto-deref, so this one is caught
QuestionReminder.RemindAt
checkins.go:131
output shape only — see below
HillChart.UpdatedAt
—
not read
Notification.ReadAt / .UnreadAt
—
not read
SearchResult.UpdatedAt
—
not read
ClientApprovalResponse.CreatedAt / .UpdatedAt
—
type unused in the CLI
WebhookDelivery.CreatedAt
—
type unused in the CLI
The compile/panic split is the whole point: search.go will stop the build, timeline.go will not.
Compile breaks — the bump will not build until these are handled
Recordings().Get is dead code calling a route that never existed
The plan going in assumed this needed a redesign. It does not. FetchCommentThread — the only caller of Recordings().Get — has no callers of its own, in internal/, in tests, or in e2e/. Its only occurrence in the repo is its own definition.
It also never worked. Per the SDK's route corrections, bc3 draws resources :recordings, only: [], so the flat show does not exist, and app/views/api/recordings/ holds only partials, so the bucket-scoped show cannot render on the API host either. GetRecording was a 404 in every shape. Anyone who had reached this function would have got an error.
Action: delete FetchCommentThread. No replacement, no redesign, nothing to preserve. Check whether NewCommentThread and the rest of comment_thread.go still have live callers while you are in there.
ParticipantIDs is the sharp edge in schedule.go
*[]int64, where nil and empty are different instructions:
A slice built by filtering is basecamp.Ptr(ids) either way, so a filter matching nothing clears the list instead of leaving it. Guard on len(ids) > 0 when porting schedule.go:642.
Silent behaviour changes
files update on a document: three requests, and the explicit clear may inverse
The highest-risk item here, because it is the one place where the CLI and the SDK now both try to solve the same problem in opposite directions.
internal/commands/files.go:1770buildDocumentUpdateRequest already does its own read-modify-write, and its comment states the mechanism it depends on:
BC3 rebuilds documents from permitted params on PUT, so omitted title/content fields are replaced with empty values. […] Explicit clears via --title "" or --content "" work by composition: the SDK strips empty strings to absent JSON fields, and the controller then nulls those absent fields during rebuild.
#601 makes Documents().Update a read-modify-write with preserve-on-omission. That inverts the second half: an absent field is now preserved rather than nulled. So --title "" — which the CLI implements by producing an absent field — plausibly stops clearing and starts preserving.
Two consequences at files.go:1652 and files.go:1690:
Three HTTP requests where there was one — the CLI's Documents().Get, then the SDK's internal GET and PUT.
--title "" / --content "" may silently stop clearing.
TestFilesUpdateDocumentEmptyTitleClearsWhilePreservingContent pins the wire shape and should catch it — confirm that it does rather than assuming, since it asserts on a request the SDK now wraps. Likely resolution: delete the CLI's copy and let the SDK own preservation, but that is a behaviour decision about what --title "" means, not a mechanical deletion.
Other read-modify-write sites
Todolists().Update (#574) at todolists.go:435 and tui/workspace/data/hub.go:1136; Schedules().UpdateEntry (#632) at schedule.go:724. Same shape: two round-trips, non-atomic last-write-wins, omission no longer clears.
Cards().Update dropped its preservation GetCard (#647)
cards.go:996, cards.go:1140, assign.go:601, assign.go:714, hub.go:1050. Signature and request type unchanged, so every call site compiles. What moves is the request count, the hook sequence, and the encoding of a due-date clear. Cross-check against the #604/#620 work, which was specifically about not echoing back fields the caller never set.
schedule.go:560 and hub.go:1155. The SDK's local ErrUsage guard on RFC3339 StartsAt/EndsAt is gone; the value now goes on the wire verbatim. A bare date creates an all-day entry where v0.12.0 refused it locally, and a malformed value reaches bc3 instead of failing with CodeUsage. If the CLI was relying on that error as its input validation, it now has none — check whether schedule.go validates these itself before deciding whether to add a guard.
checkins.go:131 puts it straight into a map[string]any, so it compiles and cannot panic. The JSON output changes: a nil now marshals as null (or is omitted) where it previously emitted 0001-01-01T00:00:00Z. Agent-facing output shape — worth a deliberate choice, not a shrug.
--page is not aligned — the CLI is now over-restrictive
The plan expected the CLI's --page handling to already match. It does not, in an interesting direction.
Five commands hard-reject anything past the first page:
internal/commands/schedule.go:166 only --page 1 is supported; use --all to fetch everything
internal/commands/forwards.go:103 (same)
internal/commands/forwards.go:459 (same)
internal/commands/events.go:51 (same)
internal/commands/projects.go:85 (same)
That guard is correct today, because v0.12.0's Page disabled pagination and returned page 1 regardless of the number. #561/#617 make Page an actual page selector: Page: 3 now issues ?page=3 and populates Meta.Truncated.
So after the bump the restriction blocks a capability that works. The bump should lift these five guards, not preserve them. cards.go:288-308 and accountwide.go already do range and mutual-exclusion checking that stays valid — and --page > MaxInt32 now matches the SDK's own ErrUsage("page is out of range").
skills/basecamp/SKILL.md:156 (--page 1 # First page only, no auto-pagination) becomes incomplete rather than wrong; update it in the same change.
#586 repoints this to /inbox_forwards.json. The old path 404'd against bc3, so no working call is lost — but the test double registered on the old path stops matching. The other #586 change (todolists/{groupId}/position.json → todolists/groups/{groupId}/position.json) has no CLI call site; todolists_test.go:330 refers to the todoset position route, which is untouched.
The nine #619 bucket-scoping rewrites also change URLs, but they change signatures too, so the compiler surfaces them. None of the nine are used here — see below.
Reporting only — not gating
internal/observability/collector.go:118 (RecordOperationFromSDK) and hooks.go:74,101 key metrics and trace logs on op.Service / op.Operation straight from the SDK's OperationInfo. Where an update became read-modify-write, hooks now observe {Documents, Get} + {Documents, Replace} instead of {Documents, Update} — likewise for Todolists and schedule entries. Metric and trace names shift; dashboards and any operation allowlist need repointing. Nothing breaks.
Also expect error text to change (#541/#549): 400/422 now fold field detail into the message, and a top-level message key is honoured at every status. Error.FieldErrors is the structured replacement. Any string match on a message is dead — worth a grep during the bump.
Confirmed non-issues — do not re-check these
WithMaxRetries appears nowhere in non-test code, so the Disable keyring in installer canary #571max_retries 0-or-1 token-refresh trap does not apply. The tests that pass WithMaxRetries(1) use static token providers.
No file imports basecamp-sdk/go/pkg/generated. The andon cord holds. That takes out the two largest class-B entries wholesale: the ~107 pointerized generated fields (Render GFM tables in message and comment bodies (#405) #560) and Question.Schedule.Hour/.Minute.
Todos().Trash is not called anywhere. It was the one removal that took away a working call, and it archived despite its name. Nothing to decide, and nothing to sed.
Read-only audit of the CLI against basecamp-sdk
main(1d8ddb1c0), ahead of the v0.13.0 tag. No bump has happened and none should until the tag exists — there is nothing to pin to. This is the worklist the bump executes against.Source: the SDK's
MIGRATING.mdat that SHA, which reports 58 merged PRs, 15 breaking, and enumerates 12 class A and 4 class B breaks for Go. basecamp-sdk is public, so quoting it here is fine.Every entry below was checked against the tree at
3d4e235d, not inferred. Confirmed non-issues are recorded too, so nobody re-derives them.The one that bites silently
TimelineEvent.CreatedAtbecame*time.Time(#658)internal/commands/timeline.go:393Go auto-dereferences a pointer for a value-receiver method call, so this compiles untouched and panics when the server omits the field. It is the only class-B site in the CLI, and it is in a display path that runs on every
basecamp timeline.Nine other wrapper timestamps pointerize in the same sweep (#615 + #658). I checked all ten:
TimelineEvent.CreatedAttimeline.go:393SearchResult.CreatedAtsearch.go:379relativeTime(r.CreatedAt)QuestionReminder.RemindAtcheckins.go:131HillChart.UpdatedAtNotification.ReadAt/.UnreadAtSearchResult.UpdatedAtClientApprovalResponse.CreatedAt/.UpdatedAtWebhookDelivery.CreatedAtThe compile/panic split is the whole point:
search.gowill stop the build,timeline.gowill not.Compile breaks — the bump will not build until these are handled
internal/tui/resolve/comment_thread.go:141Recordings().Getremoved (#619)internal/commands/schedule.go:642UpdateScheduleEntryRequestfields → pointers (#632)ParticipantIDstrap belowinternal/commands/gauges.go:61Gauges().Listgained options + result struct (#617)internal/commands/gauges.go:104Gauges().ListNeedlessame (#617)internal/commands/gauges.go:274UpdateGaugeNeedleRequest.Description→*string(#560)internal/commands/cards.go:2938,assign.go:647,assign.go:749UpdateStepRequest.DueOn→*string(#647)internal/commands/reports.go:298,reports.go:306UpcomingSchedulereturns reduced types,Assignablesgone (#648)Recordings().Getis dead code calling a route that never existedThe plan going in assumed this needed a redesign. It does not.
FetchCommentThread— the only caller ofRecordings().Get— has no callers of its own, ininternal/, in tests, or ine2e/. Its only occurrence in the repo is its own definition.It also never worked. Per the SDK's route corrections, bc3 draws
resources :recordings, only: [], so the flat show does not exist, andapp/views/api/recordings/holds only partials, so the bucket-scoped show cannot render on the API host either.GetRecordingwas a 404 in every shape. Anyone who had reached this function would have got an error.Action: delete
FetchCommentThread. No replacement, no redesign, nothing to preserve. Check whetherNewCommentThreadand the rest ofcomment_thread.gostill have live callers while you are in there.ParticipantIDsis the sharp edge inschedule.go*[]int64, where nil and empty are different instructions:A slice built by filtering is
basecamp.Ptr(ids)either way, so a filter matching nothing clears the list instead of leaving it. Guard onlen(ids) > 0when portingschedule.go:642.Silent behaviour changes
files updateon a document: three requests, and the explicit clear may inverseThe highest-risk item here, because it is the one place where the CLI and the SDK now both try to solve the same problem in opposite directions.
internal/commands/files.go:1770buildDocumentUpdateRequestalready does its own read-modify-write, and its comment states the mechanism it depends on:#601 makes
Documents().Updatea read-modify-write with preserve-on-omission. That inverts the second half: an absent field is now preserved rather than nulled. So--title ""— which the CLI implements by producing an absent field — plausibly stops clearing and starts preserving.Two consequences at
files.go:1652andfiles.go:1690:Documents().Get, then the SDK's internal GET and PUT.--title ""/--content ""may silently stop clearing.TestFilesUpdateDocumentEmptyTitleClearsWhilePreservingContentpins the wire shape and should catch it — confirm that it does rather than assuming, since it asserts on a request the SDK now wraps. Likely resolution: delete the CLI's copy and let the SDK own preservation, but that is a behaviour decision about what--title ""means, not a mechanical deletion.Other read-modify-write sites
Todolists().Update(#574) attodolists.go:435andtui/workspace/data/hub.go:1136;Schedules().UpdateEntry(#632) atschedule.go:724. Same shape: two round-trips, non-atomic last-write-wins, omission no longer clears.Cards().Updatedropped its preservationGetCard(#647)cards.go:996,cards.go:1140,assign.go:601,assign.go:714,hub.go:1050. Signature and request type unchanged, so every call site compiles. What moves is the request count, the hook sequence, and the encoding of a due-date clear. Cross-check against the #604/#620 work, which was specifically about not echoing back fields the caller never set.Schedules().CreateEntrystopped validating timestamps (#664)schedule.go:560andhub.go:1155. The SDK's localErrUsageguard on RFC3339StartsAt/EndsAtis gone; the value now goes on the wire verbatim. A bare date creates an all-day entry where v0.12.0 refused it locally, and a malformed value reaches bc3 instead of failing withCodeUsage. If the CLI was relying on that error as its input validation, it now has none — check whetherschedule.govalidates these itself before deciding whether to add a guard.QuestionReminder.RemindAt→*time.Time+omitemptycheckins.go:131puts it straight into amap[string]any, so it compiles and cannot panic. The JSON output changes: a nil now marshals asnull(or is omitted) where it previously emitted0001-01-01T00:00:00Z. Agent-facing output shape — worth a deliberate choice, not a shrug.--pageis not aligned — the CLI is now over-restrictiveThe plan expected the CLI's
--pagehandling to already match. It does not, in an interesting direction.Five commands hard-reject anything past the first page:
That guard is correct today, because v0.12.0's
Pagedisabled pagination and returned page 1 regardless of the number. #561/#617 makePagean actual page selector:Page: 3now issues?page=3and populatesMeta.Truncated.So after the bump the restriction blocks a capability that works. The bump should lift these five guards, not preserve them.
cards.go:288-308andaccountwide.goalready do range and mutual-exclusion checking that stays valid — and--page > MaxInt32now matches the SDK's ownErrUsage("page is out of range").skills/basecamp/SKILL.md:156(--page 1 # First page only, no auto-pagination) becomes incomplete rather than wrong; update it in the same change.Test fixtures keyed on paths that moved
internal/commands/forwards_test.go:27#586 repoints this to
/inbox_forwards.json. The old path 404'd against bc3, so no working call is lost — but the test double registered on the old path stops matching. The other #586 change (todolists/{groupId}/position.json→todolists/groups/{groupId}/position.json) has no CLI call site;todolists_test.go:330refers to the todoset position route, which is untouched.The nine #619 bucket-scoping rewrites also change URLs, but they change signatures too, so the compiler surfaces them. None of the nine are used here — see below.
Reporting only — not gating
internal/observability/collector.go:118(RecordOperationFromSDK) andhooks.go:74,101key metrics and trace logs onop.Service/op.Operationstraight from the SDK'sOperationInfo. Where an update became read-modify-write, hooks now observe{Documents, Get}+{Documents, Replace}instead of{Documents, Update}— likewise for Todolists and schedule entries. Metric and trace names shift; dashboards and any operation allowlist need repointing. Nothing breaks.Also expect error text to change (#541/#549): 400/422 now fold field detail into the message, and a top-level
messagekey is honoured at every status.Error.FieldErrorsis the structured replacement. Any string match on a message is dead — worth a grep during the bump.Confirmed non-issues — do not re-check these
WithMaxRetriesappears nowhere in non-test code, so the Disable keyring in installer canary #571max_retries0-or-1 token-refresh trap does not apply. The tests that passWithMaxRetries(1)use static token providers.basecamp-sdk/go/pkg/generated. The andon cord holds. That takes out the two largest class-B entries wholesale: the ~107 pointerized generated fields (Render GFM tables in message and comment bodies (#405) #560) andQuestion.Schedule.Hour/.Minute.Todos().Trashis not called anywhere. It was the one removal that took away a working call, and it archived despite its name. Nothing to decide, and nothing tosed.ClientApprovals().List,ClientCorrespondences().List,ClientReplies().List,ClientReplies().Get.Forwards().CreateReplyis not called.Forwards().List,Get,GetInbox,ListReplies,GetReplyare, and all survive.TodolistGroups().Updateis not called, so its removal in favour ofReplace(Installer canary failure #574) costs nothing.Sequence when the tag lands
make bump-sdk— never editgo.modby hand, and never a localreplace.go build ./...— the seven compile-break rows above, plussearch.go:379.timeline.go:393before trusting any manual testing. It is the only site that will not announce itself.files updatedocument question deliberately; it is a behaviour change, not a port.--pageguards and updateSKILL.md.forwards_test.go:27.make test, thenbin/ci.API-COVERAGE.md— and note that Add basecamp files versions — HELD, blocked on the SDK #622 stays blocked regardless: sdk#649 is open with no fix PR and is not in v0.13.0, sobasecamp files versionsdoes not unblock at this tag.Tracking the tag itself: sdk#667.