Skip to content

todos update silently clears completion subscribers (merge misses the field the SDK can't see) #538

Description

@jeremy

🤖 Drafted with agent assistance; filed by @jeremy.

Summary

Every basecamp todos update invocation silently clears the to-do's completion subscribers ("When done, notify…"). Both code paths in newTodosUpdateCmd (internal/commands/todos.go:939) build their PUT body without completion_subscriber_ids, and the BC3 todos update endpoint has replace semantics — omitting a parameter clears it (bc3-api, todos: "Omitting a parameter will clear its value").

The CLI already knows about this server contract — #412/#413 added a read-modify-write merge precisely to stop partial PUTs from wiping untouched fields — but the merge preserves only content, description, due_on, starts_on, and assignee_ids. Completion subscribers were never included, because the SDK's Todo model doesn't map the completion_subscribers field at all (basecamp/basecamp-sdk#355), so the merge has nothing to preserve. git grep -i completion_subscriber across internal/ and cmd/ returns zero hits.

Reproduction

  1. In Basecamp, create a to-do and add a person under "When done, notify…"
  2. basecamp todos update <id> --title "New title" --in <project>
  3. Reload the to-do in Basecamp: the completion subscriber is gone. No warning, exit 0.

Applies equally to the field-clearing path (--no-due, --no-description, --due ""), which hand-builds a raw PUT body (internal/commands/todos.go:1043) that likewise omits completion_subscriber_ids.

Root cause chain

Layer Defect
BC3 API Documented replace semantics: omitted field = cleared (long-established; not changing)
SDK Todo model omits completion_subscribers → clients cannot read them to preserve them (basecamp/basecamp-sdk#355); Todos.Update itself is a partial PUT with the same wipe hazard on every field (basecamp/basecamp-sdk#372)
CLI Read-modify-write merge (both branches of newTodosUpdateCmd) preserves five fields, not six — subscribers fall through

Context: this same field was among those wiped in two production support incidents (June, July 2026) caused by third-party scripts using the Go SDK's partial-PUT Update — see basecamp/basecamp-sdk#372 for the fleet-wide SDK fix (merge-safe Update, new Edit read-modify-write closure, explicit Replace). The CLI is the interim mitigation for its own users until that lands, which makes this gap in its merge worth closing now rather than waiting.

Fix plan

Phase 1 — close the gap now (this issue)

Preserve completion subscribers in both branches of newTodosUpdateCmd:

  1. Fetch the current to-do's completion_subscribers ids. The SDK model doesn't carry them (Stamp plugin version at release time #355), so until it does, read them from the raw GET JSON — the CLI already has app.Account().Get/raw request plumbing (same style the needsClear branch uses for its raw PUT). The BC3 payload field is completion_subscribers, an array of person objects; collect .id.
  2. Include completion_subscriber_ids in the PUT body in both branches, defaulting to the fetched ids (preserve). No new flag is required to fix the wipe.
  3. Optional, natural companion (mirrors --assignee): --notify-on-completion <names-or-ids> and --no-notify-on-completion to set/clear explicitly, reusing resolveAssigneeIDs-style person resolution. If added, follow the existing conflict-detection pattern (--no-X vs --X usage error, internal/commands/todos.go:969).
  4. Test: update a to-do that has completion subscribers via --title only → subscribers unchanged; via --no-due → subscribers unchanged; explicit clear (if flag added) → cleared. The e2e harness's existing todos update coverage is the place to extend.

Phase 2 — after basecamp/basecamp-sdk#372 lands (follow-up, tracked here for the map)

  • Replace the hand-rolled merge (internal/commands/todos.go:1126 branch) and the needsClear raw-PUT workaround (internal/commands/todos.go:1029 branch, ~80 lines) with the SDK's Edit closure — both branches collapse into one: fetch-free flag mapping onto Edit, with --no-X flags becoming plain field-clears on the full representation.
  • Delete the raw-JSON subscriber fetch from Phase 1 once the SDK model round-trips completion_subscribers (Todo model omits completion_subscribers, description_attachments, comments_count basecamp-sdk#355).

Phase 1 is deliberately shaped so Phase 2 deletes it cleanly: the ids ride the same merge that already exists, no new abstractions.

Acceptance criteria

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions