Fail when Basecamp drops a requested assignee
basecamp cards create … --to <column id> exited 0 and created an unassigned card. On cards create, --to is the assignee, and Basecamp answers success while silently dropping any assignee_ids entry it can't assign on that recording. Every command that sets assignees had the same hole, and cards update/todos update were worse: they replace the set, so a bad ID cleared the real assignees too.
What bc3 does (measured against local bc3)
| Command, bad assignee | Before | After |
|---|---|---|
cards create --to <column id> | exit 0, card unassigned | exit 1, names card #, dropped ID |
cards create --assignee <account person not on project> | exit 0, unassigned | exit 1 |
cards update --assignee <column id> on a card assigned to DHH | exit 0, DHH removed | exit 1, says it now has no assignees |
cards step create, todos create/update, subtasks create, assign (to-do, card) | exit 0, dropped | exit 1 |
bc3 filters every assignee write through recording.reachable_people(include_admissible_people: true): project members, clients when the recording is visible to them, agents, and any admissible employee on an all-access project. Ids outside that set are ignored, not rejected.
The fix
Each write compares the IDs it sent with the assignees on the record Basecamp returns. Missing ones fail with a usage error that names the record, the dropped IDs and what it was saved with, and a hint that assigns that record (so an agent doesn't create a second one):
card #1069479860 was saved, but Basecamp did not assign 822400739: not a person who can be assigned there; it has no assignees
hint: Assign it with basecamp assign 1069479860 --card --to <person>, naming someone from basecamp people list --project <project>
Why check after the write rather than validate before it: no client-side lookup can reproduce bc3's rule. /people.json passes a person who isn't on the project (measured: dropped), and the project's people list would reject admissible people on an all-access project that bc3 accepts. The response is the authority, and reading it costs no extra request. The record exists by the time we fail; that matches the existing card %d created but assignment failed path in cards create.
Covered: cards create, cards update, cards step create/update, todos create/update, subtasks create/update, assign (to-do, --card, --step). unassign can't drop an ID and is untouched; the TUI picks from a people list and is out of scope. Name and email resolution is unchanged.
The second commit covers a mixed assign A B batch, where the person is kept on A and dropped on B. Partial batches still exit 0, which is the tested contract, but --agent prints only data, so B's failure was invisible. The batch now carries a diagnostic naming each failed item and why; quiet/agent mode writes it to stderr.
cards create --to stays as is. Its help now reads "Person to assign (alias for --assignee); the column is --column", since cards move --to is a column.
Verification
TestAssigneeWritesFailWhenBasecampDropsTheAssigneedrives all 11 write paths against a mock that drops the ID; all 11 failed before the fix.TestAssigneeWritesSucceedWhenBasecampKeepsTheAssigneeis the paired control. Three existing fixtures answered writes without the assignees they were sent; they now echo them, as bc3 does.- Live against local bc3 (verify-cli runs
20261027-235034-assignee-droppedbefore/after,20261027-235248-assignee-keptfor all 11 paths with valid people,20261027-235440-assign-batch-mixed). Created records trashed. bin/cigreen.
Summary by cubic
Commands that assign people now fail when Basecamp silently drops a requested assignee, instead of exiting 0 with an unassigned or de-assigned record.
- Bad IDs such as a column ID passed to
cards create --to, or a person not on the project, now produce a usage error naming the record, the dropped IDs, and what the record was saved with. cards update/todos updatepreviously replaced the assignee set, so a bad ID also cleared the real assignees; that is now caught too.- The check compares the requested IDs against the assignees on the record Basecamp returns, so no extra request and no client-side lookup that could diverge from bc3's assignment rules.
- Covered across
cards create/update,cards step create/update,todos create/update,subtasks create/update, andassign(to-do, card, step). - Partial
assignbatches still exit 0, but under--agent/--quietthey now write a diagnostic to stderr naming each item that dropped the assignee. - The recovery hint keeps the assignees the record saved (subtasks update replaces the set) and names the
--projectflag, whichassignrequires.