feat(sync): bdrive forget + sync --prune to take ignored paths off the hub (BEA-20) (#68)

Adding a path to .bdriveignore only stopped future uploads: anything that
synced before the rule existed stayed on the hub forever, with no command
that removed it without deleting it from local disk on every device.

Two engine changes make an explicit removal safe:

- materialize's delete loop now consults the filter. A cached path absent
  from the replayed target that the rules exclude is dropped from tracking
  instead of unlinked — without this, any delete op for a now-filtered path
  wipes every peer's local copy, which is the data loss this issue is about.
- the filter is reloaded mid-cycle from the pulled .bdriveignore, before
  materialize. A peer receiving the new rules and the deletes they justify
  in one batch would otherwise materialize with stale rules and the guard
  would never fire. materialize's write side is split into materializeFile
  so the ignore file can land on its own.

On top of that, Session.Prune journals a delete for every path the replayed
state still holds that the SHARED rules exclude — reconciling against the
replay, not the local cache, because a path filtered out in an earlier cycle
was dropped from the cache back then and is invisible locally today. The
rules are deliberately ignore-only: the include scope lives in each device's
own .bdrive/config.json and does not sync, so pruning against it would let a
narrow-scope device delete a whole-folder teammate's files.

Plain `bdrive sync` and the daemon are unchanged — pruning is never a side
effect of editing .bdriveignore.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Snow W. Lee (Sungwon)
2026-07-28 07:22:11 +09:00
committed by GitHub
co-authored by Claude Opus 5
parent 411809a785
commit 9088127176
12 changed files with 712 additions and 39 deletions
+38 -1
View File
@@ -14,9 +14,10 @@ One binary, `bdrive` — the CLI, the sync daemon, and the web server.
| `bdrive init [folder]` | Create or connect a project and start syncing. Interactive on a TTY; flags (`--name`, `--project`, `--shared`, `--yes`) for scripts. Re-run to resume |
| `bdrive stop [folder]` | Stop syncing — daemon and agent sync hooks both pause. Files stay on disk; `bdrive init` resumes |
| `bdrive scope [add\|rm <dirs...>]` | Show or change which subfolders sync — the include list set by `init --shared`. Run from the mount root; the daemon picks changes up in seconds. `rm` stops syncing a folder but deletes nothing, locally or on the hub |
| `bdrive forget <path>...` | Stop syncing a path and remove it from the hub. Adds the rule to `.bdriveignore` (which syncs) and prunes in one step. Local files are never touched, here or on teammates' devices |
| `bdrive url [path]` | Internal hub link for a file or folder — sign-in and membership required. `--sync` pushes first; no argument gives the project home. Computed locally |
| `bdrive share <file>` | Public URL for a synced file. `--list`, `--revoke`, `--expires` |
| `bdrive sync [folder]` | Run one sync cycle now. Refuses folders this device never `init`ed and folders paused by `bdrive stop`. `--note <text>` stamps session context onto changes; `--note-ttl` (default 30m) bounds it. `--hook <label>` is agent-hook plumbing |
| `bdrive sync [folder]` | Run one sync cycle now. Refuses folders this device never `init`ed and folders paused by `bdrive stop`. `--note <text>` stamps session context onto changes; `--note-ttl` (default 30m) bounds it. `--prune` also removes from the hub what `.bdriveignore` now excludes (files stay on disk everywhere). `--hook <label>` is agent-hook plumbing |
| `bdrive hooks [install]` | Register turn-boundary sync hooks with detected agent platforms. Idempotent; `--agent` overrides detection |
| `bdrive skill [install]` | Install the `beardrive` skill into detected agent platforms so the agent can do setup itself. Idempotent; `--agent` overrides detection |
| `bdrive read-log [folder]` | Hook plumbing: queue agent file reads for the hub's read heatmap. Registered by `bdrive hooks install` |
@@ -51,6 +52,42 @@ Stamps session context — an agent session id, say — onto changes. It shows u
`bdrive log` and hub history, and keeps applying to daemon-committed changes
until `--note-ttl` expires.
### `bdrive forget` and `bdrive sync --prune` — cleaning up the hub
Adding a rule to `.bdriveignore` only stops *future* uploads. Anything that
synced before the rule existed stays on the hub. These two commands are how it
comes off:
```
$ bdrive forget .omc
added `.omc/` to .bdriveignore
synced /Users/you/notes (project "notes")
...
pruned: 72 path(s) removed from the hub (kept on disk)
$ bdrive sync --prune # same cleanup for rules you added by hand
```
`forget` writes the rule (a trailing `/` for a directory) and prunes in the
same run; it is idempotent, so re-running it just prunes. A path outside the
project is an error and writes nothing.
**No device loses a file.** The removal is journaled as an ordinary delete, and
because `.bdriveignore` syncs, every device receives the rule alongside the
delete and simply stops tracking the path — the file itself stays on disk here
and on every teammate's machine. Nothing is destroyed either: blobs are
retained forever, so the removal shows in `bdrive log` and every past version
stays in the hub's history.
Prune reconciles against `.bdriveignore` only, never against this device's own
sync scope (`bdrive scope` / `init --shared`). Ignore rules are shared; the
scope is per-device, and a narrow scope means "not on my disk", not "not on the
hub". To clean up something your scope excludes, `bdrive forget` it — that
writes the exclusion into the shared rules first, which is what makes it safe.
If a teammate edits the file between your prune and their next sync, their
version wins and the path comes back. Run `--prune` again once they have synced.
### `bdrive status` — and the two degraded access states
Alongside `pending`, `status` prints an `access:` line whenever the hub is