feat: move and copy messages, files, columns, events, check-ins and forwards to other projects

Merged
RO
4d ago
+5129-606

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 on msgs. A message lands on that project's message board.
  • Docs & Files. files move|copy <id>... --to-project … [--to-folder …], also on docs, 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-folder takes 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 --position still 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 the tools enable command that turns it on.
  • Whole tools. tools move <id> --to-project … moves a dock tool and everything in it. tools move --position still 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 --help says 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 --todolist through: messages copy <id> --todolist <list> copied onto the message's own board and reported success (run 20261027-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 --profile and --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 --position and tools move --position. New behaviour needs --to-project or 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_recording and get_filing tools, without the CLI's destination resolution, guards or copy recovery for these families.

Verification

  • bin/ci passes 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. Runs 20261027-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 (run 20261027-220741-files-show-moved-old-id, with --ref for 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/ci passes 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-patterns and make check-eval-harness pass.

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 on msgs), files move|copy (also on docs, documents, uploads, vaults and their aliases), cards column move|copy, schedule move|copy, checkins question move|copy, forwards move|copy each take --to-project and a family-specific --to-<container> flag (board, folder, card table, schedule, questionnaire, inbox); messages copy takes --include-comments, and a recurring schedule entry files as a whole series, from whichever schedule holds it, a calendar included.
  • files move|copy takes several IDs, filing them one by one, and files move --to <folder> keeps moving within the project, preserving the id.
  • tools move <id> --to-project moves a whole dock tool and everything in it; tools are not copied, doors are refused, and --position keeps reordering the dock. Its help and refusals name projects show <id>, pointing at projects list for 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 --profile and --account.
  • A moved item gets a new id; files show, cards column show, schedule show, checkins question show and forwards show read by account-wide id and follow up on the id their read returns, so a follow-up runs without a project. The schedule entries follow-up names the schedule whenever a project has several, so it runs after a --to-schedule move. A project with several schedules is refused, listing the IDs, and a card table that is turned off is named with a tools enable hint, 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 --all holds only the 250 most recent drafts, so a copy missing from it is checked on the destination's page first; files show reads 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 --help says which flag crosses projects, where the item lands, and what Basecamp refuses for that family.