feat: move and copy messages, files, columns, events, check-ins and forwards to other projects
Why
The web can move or copy a message, a Docs & Files item, a card table column, a schedule entry, a check-in question or an email forward to another project, and move a whole tool to another project's dock. The CLI could do this only for cards (#973) and to-dos (#976). bc3's generic POST /recordings/:id/moves|copies, now deployed, takes all of these, and basecamp-sdk wraps it as of v0.30.0.
What changed
Every family gets move and copy with --to-project <project> (ID or name) and its own --to-<container> (ID, URL, or a name with --to-project). The same commands sit on every alias group an agent might reach for.
- Messages.
messages move|copy <id> --to-project … [--to-board …], also onmsgs. A message lands on that project's message board. - Docs & Files.
files move|copy <id>... --to-project … [--to-folder …], also ondocs,documents,folders,vaults,uploads,file,vault. It covers documents, uploads, cloud files, Google documents and folders. Several IDs file one by one, as the web's bulk Move and Copy do.--to-foldertakes any folder by ID or URL, nested ones included, or a top-level folder's title with--to-project.files move --to <folder>still moves within the project, keeping the id. - Card table columns.
cards column move|copy <id> --to-project … [--to-card-table …]takes the column with its cards.cards column move --positionstill repositions. Triage, Not now, Done and On hold are refused, as Basecamp refuses them. - Schedule entries.
schedule move|copy <id> --to-project … [--to-schedule …]. A recurring event is filed from the schedule it is actually on, a calendar included. - Check-in questions.
checkins question move|copy <id> --to-project … [--to-questionnaire …]. - Email forwards.
forwards move|copy <id> --to-project … [--to-inbox …]. A project whose Email Forwards is off is named, with thetools enablecommand that turns it on. - Whole tools.
tools move <id> --to-project …moves a dock tool and everything in it.tools move --positionstill reorders the dock. Tools are not copied (the web has no Copy for them) and doors are not moved (Basecamp refuses). filings show <id>. It reads a move or copy that was still running when the command stopped waiting. It reports the status and, once landed, the recording's new id.
Every command waits for Basecamp to finish. It prints the record at its new id, with source_id for the original, and a summary naming where it landed. Follow-up commands name the new id, and the old id stops taking writes. files show, cards column show, schedule show, checkins question show and forwards show now read by account-wide id, so a follow-up runs without a project.
Agent-friendliness
--agent --helpsays which flag crosses projects and where the item lands. It says the item gets a new id, that a copy is never retried, and what Basecamp refuses for that family. Each family's Long help has an example per mode.- Guards refuse rather than guess:
- an ambiguous or duplicate container name, listing the IDs;
- a project with no suitable destination, by name;
- a destination URL from another host, account or kind, or one that names nothing (it never falls back to a title);
- an item named twice, even by its old and new id;
- a same-project flag (
--message-board,--vault,--folder,--card-table,--schedule,--questionnaire,--inbox,--position) combined with a cross-project move or copy; - a move onto the item's own container, with the same-project command as the hint.
- Every move and copy command here goes through #976's
guardFilingFlags, which refuses before anything is sent any container flag the command inherits but would ignore, naming its--to-<container>flags. It replaces each family's own list of refused group flags, which let the root's--todolistthrough:messages copy <id> --todolist <list>copied onto the message's own board and reported success (run20261027-230923-messages-copy-todolist-guard, before and after). The table test covers all 28 move and copy commands. - Bulk reports each item and exits non-zero if any did not land. It asks to re-run only the items Basecamp refused before filing, never a copy that was made or may have been.
- Every command a filing prints carries the run's
--profileand--account, so it reaches the same account from a fresh shell. - Skill eval cases cover natural requests for each family (
skill-evals/cases/*-move-copy.yml,tools-move.yml).
Finding a copy whose start got no answer
A copy request that fails without a clear refusal may still have made a copy, so the CLI never retries one. Instead it says where to look. One table in filing.go (copyListings) maps each recording type, status and calendar-or-project to the listing that is known to show a copy there. Examples are recordings list, drafts list, schedule entries --status, timeline and the family's own listing. Anything the table doesn't name points at the destination's page in Basecamp instead. drafts list holds only the 250 most recently updated drafts, so its hint also says to check the destination page when the copy is not there. A copy keeps its original's title, created_at and updated_at (bc3 Recording::Copier), so the hint says to look for the original's title in the destination container rather than sorting by date. TestEveryCopyRecoveryCanShowTheCopy checks every type, status and destination kind against the listings known to show it. A move or copy that is still running when the wait ends names its filing instead, and filings show follows it.
Scope
- Stacked on #976 (to-dos, lists and groups), which is stacked on #975 (the shared filing helper).
- Existing invocations behave as before. That covers
files move --to,cards column move --positionandtools move --position. New behaviour needs--to-projector a--to-<container>flag. - Not in this PR:
- copying whole tools, since the web offers only Move for tools;
- MCP parity. The MCP server keeps its generic
move_recording,copy_recordingandget_filingtools, without the CLI's destination resolution, guards or copy recovery for these families.
Verification
bin/cipasses on the head.- Against a local bc3 on the head (
c1d979fb), one move and one copy per family between Ten-year anniversary and Nike Promotion, each read back from the API in the destination project. Each moved item's old id answered 302. The dock tool showed up in Nike Promotion's dock and was gone from the source dock. Two refusals came back non-zero. Everything created was trashed afterwards. Runs20261027-220602-web-parity-final-r5,20261027-220710-web-parity-final-r5b(messages and schedule with explicit containers, since the fixture projects now hold two boards and two schedules). files show <old id>of a moved document reported a folder before the last fix and reports the document after it (run20261027-220741-files-show-moved-old-id, with--reffor the before).- An independent GPT-6.1-sol review ran seven rounds on this branch and ended with no findings. Each finding it raised has a test that fails without its fix.
- After rebasing onto #976's guard,
bin/cipasses on the head (3bb6e10e). Two GPT-6.1-sol rounds on the guard and on the calendar-breadcrumb fix found nothing. - The live runs were on
c1d979fb. The one commit after it (9be530c1, from Copilot's review) changes only the wording of a drafted copy's recovery hint, which needs a lost response to reach. - Model-graded skill evals need an API key that isn't set here.
make check-eval-patternsandmake check-eval-harnesspass.
Summary by cubic
Adds move and copy to messages, Docs & Files items, card table columns, schedule entries, check-in questions and email forwards so each can be filed into another project with --to-project, matching the web's Move… and Copy… dialogs. The CLI previously could only cross-file cards and to-dos. tools move now also moves a whole dock tool to another project, backed by bc3's generic recordings move/copy endpoints.
New commands
messages move|copy(also onmsgs),files move|copy(also ondocs,documents,uploads,vaultsand their aliases),cards column move|copy,schedule move|copy,checkins question move|copy,forwards move|copyeach take--to-projectand a family-specific--to-<container>flag (board, folder, card table, schedule, questionnaire, inbox);messages copytakes--include-comments, and a recurring schedule entry files as a whole series, from whichever schedule holds it, a calendar included.files move|copytakes several IDs, filing them one by one, andfiles move --to <folder>keeps moving within the project, preserving the id.tools move <id> --to-projectmoves a whole dock tool and everything in it; tools are not copied, doors are refused, and--positionkeeps reordering the dock. Its help and refusals nameprojects show <id>, pointing atprojects listfor the ID.filings show <id>reads a move or copy still running and, once landed, prints the recording's new id; its follow-ups carry the run's--profileand--account.- A moved item gets a new id;
files show,cards column show,schedule show,checkins question showandforwards showread by account-wide id and follow up on the id their read returns, so a follow-up runs without a project. Theschedule entriesfollow-up names the schedule whenever a project has several, so it runs after a--to-schedulemove. A project with several schedules is refused, listing the IDs, and a card table that is turned off is named with atools enablehint, not treated as missing; To-dos tool plural is spelled "To-dos tools" in ambiguity messages.
Guards and copy recovery
- A copy that may have started is never retried: the command says where to look, a listing known to show it or the destination's page in Basecamp, matching on the copy's title since a copy keeps its original's title and dates.
drafts list --allholds only the 250 most recent drafts, so a copy missing from it is checked on the destination's page first;files showreads a moved item's kind from what its old id returns, so a moved document reads as a document, not a folder.- Conflicting same-project flags, any inherited container flag a command does not read, ambiguous container names, a destination URL that names nothing (URLs never read as titles), an item named twice, a move onto the item's own container, and system columns like Triage and Done are refused before anything is filed.
- Bulk runs report each item and exit non-zero if any did not land; only items Basecamp refused may be re-run.
--agent --helpsays which flag crosses projects, where the item lands, and what Basecamp refuses for that family.