12 Commits
Author SHA1 Message Date
1c703c95f8 feat(templates): start a project from a structure, not an empty folder (#97)
* feat(templates): start a project from a structure, not an empty folder

A new project was an empty folder with a .bdriveignore in it, so every agent
session invented its own layout and the folder rotted into a pile. Both
surfaces now offer the same three starting points — from a template, from
scratch, from an existing folder (which is just a non-empty folder, and is
never restructured).

internal/templates holds the shipped set as literal go:embed'ed files: `docs`
(docs/, decisions/) and `para` (projects/, areas/, resources/, archives/).
cmd/bdrive is one binary for the CLI and the hub, so both read the identical
set — no gallery, no drift. The AGENTS.md in each is the deliverable: where a
new note goes, when something is archived, what a good filename looks like.
Every directory holds a real file, because BearDrive syncs paths and an empty
directory would never reach a teammate.

The hub seeds at creation through the existing Upload+Commit path, journaled
under its own device, and records the choice on the project record — so a user
who picked PARA in a browser sees PARA in the browser, and a later init cannot
seed a second copy. `bdrive init --template <name>` goes through the same
endpoint, with a local-seed fallback for a hub too old to know the field, and
seeds in place when re-run in an already-initialized folder (the agent's
post-init path). Seeding never overwrites an existing path, which is what makes
a double-seed a no-op rather than a divergence.

Refusals cost nothing: an unknown name and --template with --only are both
rejected before any network call or write.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(cli): joining a project that already has a template is refused by name

The one acceptance case with no test behind it: connecting to an existing
project with --template must say what the project was actually created from,
and must not write the other skeleton on the way out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(templates): name the docs template in plain English, not an acronym

"Plain docs + ADRs" was the recommended, first, preselected-adjacent option in
a picker that non-engineers see — and it's the label people accept without
reading further, so half of it not parsing is the worst place for jargon. The
title also disagreed with its own blurb: "ADRs" over "docs/, decisions/", two
words for the same folder one line apart.

Now "Docs + decision records", which says the same thing to everyone and
matches the folder names. The term itself moves into
decisions/0001-record-decisions.md, where the reader is already inside the
structure and the file can teach it in passing.

One line in the registry drives both the web dialog and the CLI menu; the rest
is prose echoing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(templates): add the LLM wiki template

The third starting point from the issue title, unblocked: the spec parked it
because shipping an approximation under someone's name needed a source, and
there is now one — Karpathy's LLM Wiki gist. Worth noting the issue's own
one-line description of it ("few large, append-heavy topic pages") does not
match the source, which is the opposite: many interlinked pages, where a single
ingest touches 10-15 of them.

The pattern is three layers and three operations, not a folder shape. sources/
is yours and immutable; wiki/ is the agent's and it owns every page; AGENTS.md
is the schema layer — which is exactly the file this template system already
treats as the deliverable, so the fit is direct. index.md and log.md ship as
the two navigation files the pattern turns on.

Three of the things the gist tells you to go set up, BearDrive already is:
version history and collaboration (per-file history, bdrive log), an Obsidian-
style reader for [[wikilinks]] (the hub viewer), and a surface for the lint
pass (the dashboard is literally reads x staleness).

Two rules in the AGENTS.md are load-bearing and deliberate. A page write that
has not updated the index is an incomplete write — a stale index is worse than
a missing page, because it is read first and believed. And with no sources yet,
build nothing: the structure grows out of the material rather than ahead of it.

Shipped second, not first: docs stays the recommendation because a default is
the option chosen by people not reading closely, and this pattern degrades
badly when half-followed. Promoting it later is one line in the registry.

The shipped-template test now checks the "what happens when something stops
being true" question through a set of alternatives — PARA archives, a wiki
supersedes and revises — since the vocabulary honestly differs by structure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(web): "I already have a folder" as a starting point

The third way to start from the spec — adopt what you already have — had no
presence in the browser. Templates and "empty" were the only visible answers,
so someone with a folder of notes either hesitated or picked a template and got
four directories merged into their material.

The constraint that shapes it: the browser cannot reach your disk, so this
cannot change what is created. It creates the same empty project "Empty
project" does; what it changes is the next screen. Create therefore stays
enabled — disabling it would leave the dialog a dead end AND produce no project
id, which is the one thing the paste prompt actually needs.

Landing on the project home with the intent, three things differ: the guide
says "in the folder you already have", a note states plainly that connecting
never moves, renames or overwrites anything, and the paste prompt tells the
agent a folder already exists. That last one is the part that isn't cosmetic —
without it an agent reads an empty project and proposes creating shared/, the
one recommendation that is wrong here. It still asks which folder: that is the
runbook's hard gate and nothing here weakens it.

The intent rides in the URL (?connect=existing) rather than onto the project
record, the same way ?v= pins a file version. It belongs to whoever is
connecting right now — a teammate who connects next week has their own answer
and would be told the wrong thing by a persisted flag.

Five rows made the dialog tall enough to push Create off a short viewport, so
.modal scrolls internally. A hairline divider between the seeding and
non-seeding rows was tried and removed: --border is 7% white, which at 1px in a
gap renders as literally nothing. The gap is the cue that reads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(web): with no projects, open the create dialog and give the page a way in

A signed-in account with no projects landed on a page whose only path forward
was pasting a prompt into a coding agent. Now the create dialog opens itself —
with nothing to browse there is nothing else on that page to do — and the page
behind it leads with "Start a project" and a button, so closing the dialog is
not a dead end.

The dialog moves up to HubApp because three things ask for it now: the
sidebar's +, the empty state's button, and the auto-open. ProjectNav keeps only
an onNew callback; one owner beats three copies of the create handler.

Two guards on the auto-open. It fires once per mount, keyed off a ref rather
than the empty state, or closing it would immediately reopen it. And it never
fires on a read-only hub, which refuses creation server-side with a 403 —
opening a dialog that cannot succeed is worse than the page it covers.

The agent paste-prompt stays, demoted to "Or let your agent do it": it is still
the right path for someone who wants the folder connected in the same breath,
and it is the only path on a hub where this account cannot create.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 14:56:58 +09:00
872ba702cb fix(log): bdrive log reads as a timeline — newest first by edit time (#BEA-40) (#90)
`bdrive log` sorted by the lamport clock, so the wall-clock stamps it prints
came out non-monotonic — two 06:09:24 rows above a 06:10:00 row. And every op
of one scan is stamped with that scan's commit time, so a 22-file agent run
collapsed onto a single stamp. Neither is readable as a timeline, which is the
whole job of the command.

Two display-only changes:

- `journal.Op` gains `Mtime` (`omitzero`, so old journals and old binaries are
  unaffected), populated on put ops from the `os.FileInfo` the scan already
  holds. Deletes and conflict copies keep their commit time.
- `syncer.DisplayTime` / `SortForDisplay` order by the timestamp that is
  actually printed, ties broken by reversed `journal.Less`. `bdrive log` sorts
  and *then* truncates, so `-n 25` is the 25 newest by that stamp.

`journal.Less`, `Sort`, and `Replay` are untouched — replay order is the
convergence contract, so the sort lives in `syncer`, not in `journal`.
`LogEntries` also keeps returning causal order because `bdrive restore` walks
it to find a file's previous version.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 21:49:23 +09:00
b927e56fab fix(daemon): let the daemon own its pidfile, so stop can stop it (#91)
* fix(daemon): let the daemon own its pidfile, so stop can stop it

`bdrive stop` could fail with "no such process" and leave sync running.

Liveness became the flock in #88, and Run already announces the child's own
pid only after it holds that lock. But Start still wrote daemon.pid from the
parent, right after fork, with the pid of a child that had not earned
anything yet. Two starts inside that window — `bdrive init` followed by the
login agent's `bdrive resume`, or two resumes close together — race: Running
still reads false, a second child spawns, it loses hold(), and it exits
without ever being the daemon. Its pid is already in the file.

Everything downstream trusts that file. Stop signals the loser and gets
ESRCH, so it reports failure while the winner keeps syncing — the one command
whose job is "stop sending my files" silently does not. status prints the
phantom pid, or "pid 0" when the loser's cleanup removed the file the winner
wrote.

So the parent no longer writes it: the pidfile belongs to whoever holds the
lock. Start now waits for the lock to be taken instead of assuming the spawn
worked, which also means a caller that gets a pid back can trust a daemon
owns it — `bdrive resume` used to print "started (pid N)" for a child that
had already died.

The regression test needs the real binary (Start execs os.Executable), so it
lives with the CLI e2e rather than in internal/daemon, whose tests synthesize
locks. It is deterministic on Linux and roughly one run in five on macOS,
where the window is tighter; `sandbox/run.sh daemon-linux` is the reliable
reproducer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72

* docs(architecture): the daemon owns its pidfile, Start only waits for the lock

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 14:31:44 +09:00
fb6ce347c4 feat(daemon): survive a reboot — login autostart on macOS/Linux/Windows, and a lock instead of a pidfile (#88)
* feat(daemon): bring sync back after a reboot, and stop trusting the pidfile

A reboot killed every daemon and nothing restarted them. Agent hooks still
synced per turn, which is what made it easy to miss: a folder looked fine
while an agent worked in it and went stale the moment one didn't. `bdrive
init` now registers a login item (macOS: a user LaunchAgent) that runs the
new `bdrive resume` — one registration per machine, which starts a daemon for
every enrolled, unpaused mount, so adding a project later needs no
re-registration and `bdrive stop` still means stay stopped. `--no-autostart`
opts out, `bdrive autostart install|uninstall` manages it.

Writing the plist is the whole job: no `launchctl` shell-out. launchd loads
agents at login anyway, the caller has just started the daemon for this
session, and shelling out would let a test or a packaging script register a
real login item as a side effect.

The recovery path was also broken, which is why this is one change. Liveness
came from `kill(pid, 0)` on daemon.pid — but that file lives in
$BDRIVE_HOME and survives the reboot that killed its process, so any
same-user process recycling the pid read as a live daemon. `bdrive status`
said "running", and worse `daemon.Start` returned early, so the one
documented recovery (`bdrive init`) reported success and started nothing.
Liveness is now an flock held for the daemon's lifetime: the kernel drops it
at death or reboot, and it makes two daemons on one mount impossible. The pid
stays for display and for signalling.

internal/autostart is darwin-only today; autostart_other.go returns
ErrUnsupported and every caller already treats that as "nothing to do", so
Linux (systemd user unit) and Windows are one file each.

Tests: internal/daemon gets its first ones — a recycled pid must not read as
running (the exact regression), the lock decides liveness, a second holder is
refused. internal/autostart covers write/idempotency/stale-path-rewrite/
uninstall with HOME redirected, and lints the plist with plutil so launchd
can actually parse it. The CLI e2e asserts init registers the agent, that it
runs `resume`, that resume finds the live daemon instead of starting a
second, and that --no-autostart is silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

* feat(autostart): Linux support — a systemd user unit alongside the launchd agent

Same three functions, same discipline. Linux writes
$XDG_CONFIG_HOME/systemd/user/beardrive.service (Type=oneshot, no Restart= —
`bdrive resume` exits by design) plus the default.target.wants symlink that
`systemctl --user enable` would create, because systemd ignores a unit
nothing wants. No `systemctl` shell-out, for the same reasons as launchctl:
the file is the registration, it only matters at the next login, and a
container or ssh session has no session bus to talk to.

Install declines with ErrUnsupported unless systemd is actually the init
system (/run/systemd/system, i.e. sd_booted) — on Alpine, WSL1 or a slim
container a unit file is inert decoration, and reporting "registered" would
be a lie. Installed() likewise requires the enable symlink, not just the
unit: a unit nothing wants never starts.

os.UserConfigDir honors XDG_CONFIG_HOME, so relocated config dirs work.
Windows is now the only gap; autostart_other.go is !darwin && !linux, and the
shared writeIfDifferent/selfPath moved into the tag-free autostart.go (darwin
now uses them too).

Tests run on Linux, not just compiled for it: cross-compiled test binaries
executed in a container, both with /run/systemd/system present (unit written,
enabled, idempotent, stale ExecStart rewritten, broken symlink repaired,
XDG honored, uninstall removes both) and without it (Install declines and
writes nothing). The daemon flock tests were run there too, since flock
semantics are per-OS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

* feat(autostart): Windows support — a per-user Run entry

Third platform, same three functions. Windows has no user service manager in
the launchd/systemd sense, so the registration is a HKCU\...\Run value via
golang.org/x/sys/windows/registry (already in the module graph; go mod tidy
just promotes it to direct).

Chosen over the alternatives for the same reason the other two write files:
no admin rights, no COM (a Startup-folder .lnk needs it), no schtasks
shell-out. It is also honestly discoverable — the entry appears in Task
Manager's Startup tab, where someone can disable it without knowing bdrive
exists. The executable is quoted because Explorer parses the value as a
command line and Program Files has a space in it.

Two things a reader should not have to discover for themselves:

- The tests here have NEVER RUN. They are written and compile-checked
  (GOOS=windows go test -c) from macOS; there is no Windows host or usable
  container on an arm64 mac. They execute the first time the suite runs on
  Windows. They also cannot use a temp HOME the way the macOS and Linux tests
  do — HKCU is real — so each one snapshots and restores the previous value.
- `GOOS=windows go build ./...` still does not pass, and this package is not
  why: internal/store's Lock uses syscall.Flock and internal/daemon uses
  syscall.Kill and Setsid, all unix-only (true before this branch too). A
  Windows port means LockFileEx plus a stop story for a platform with no
  SIGTERM — a separate change, against the sync invariants, and untestable
  from here. So this code is correct and currently unreachable.

autostart_other.go is now !darwin && !linux && !windows (the BSDs).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 13:32:13 +09:00
31f705e287 chore: drop the Claude plugin and the bundled skill — hooks are the integration (#85)
Two front doors for the same setup, and one of them was a second copy of
everything. The plugin shipped the skill the CLI already installs (to four
platforms, not one), hooks that ran the identical commands `bdrive hooks
install` writes machine-level, and an install flow duplicating
INSTALL_FOR_AGENTS.md. Nothing deduped, so a machine with both pulled twice
per turn — two blocking syncs — and showed two identical `beardrive` skills
in the picker.

What remains is `internal/agenthooks` plus the runbook: init registers a
blocking pull (which also injects the gated-link convention as
additionalContext), an async push on Write/Edit, and read-log for the
heatmap, in each platform's user config, once per machine. That is the whole
integration, and it is the part that was never optional.

Removed: plugin/, .claude-plugin/marketplace.json, internal/agentskills,
`bdrive skill`, `bdrive hook-approve` (its PreToolUse auto-approve only ever
helped when a plugin pre-installed it; the substitute is a `Bash(bdrive:*)`
permission entry, which is user-owned config and needs no code).

The e2e now asserts the absence: no SKILL.md in any platform's skills dir
after init, and no `skill` subcommand. login_test keeps the "no revoke
surface" wording check on logoutNote alone.


Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 12:40:47 +09:00
4ff92c56ab feat(history): group agent runs and restore any version (BEA-6) (#69)
* feat(history): group agent runs and restore any version (BEA-6)

BearDrive recorded everything and could restore nothing. Now every version
of a file has a Restore button — in the hub's History view and as
`bdrive restore` — and the changes one agent run made read as one card
instead of N loose rows.

Restore is a NEW put op pointing at the old blob: journals are never
rewritten, so one-writer-per-journal holds and peers converge on the
restore like any other edit. The hub reuses RemoteSource.Commit (the
upload commit minus the upload); the CLI writes the bytes into the working
folder and lets the ordinary cycle journal them, so the sync engine gains
no new write path.

Grouping is a pure frontend group-by on (note, device) over the existing
/history response — no journal or API change.

Known gap, stated in the UI and the docs: nothing in the hub writes a
delete op yet, so a file a run *created* cannot be un-created.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(history): don't repeat a run's note on every row in its card

UI pass on the real hub: inside a run card the note is the card's header, so
printing it again on each row said the same thing N times. The header now
carries the note (linkified, so an agent's session link still opens) and the
collapse control is its own button rather than the whole header — the link
could not live inside a button.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 17:15:46 +09:00
dcd0517e92 feat(cli): bdrive scope --explain — prove what leaves this machine (BEA-24) (#70)
The local-first claim was asserted, never demonstrated: nothing anywhere
told you what your laptop chose *not* to send. `bdrive scope --explain`
walks the folder and prints two sorted lists — synced and not synced —
with counts and a pointer at what it does not answer.

The decisions come from the same walk the sync cycle uses. scan()'s
WalkDir decision tree moves into walkFolder (internal/syncer/walk.go),
the only copy of the rules; scan and Explain both go through it, so the
output provably cannot drift from real sync behavior.

Pure read: its own Filter, no Session, no volume flock, no network.
Fully-excluded directories collapse to one counted line; nested mounts
are annotated as syncing through their own project rather than called
"not synced", which would be a lie in a trust surface.

Known gap, deliberate: this answers "what leaves from now on", not
"what is already on the hub" — the footer points at `bdrive forget`.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 17:10:51 +09:00
4e34d03e14 feat(hooks): user-scope agent sync hooks, one-command setup, --only scoping (#71)
* feat(hooks): register agent sync hooks per machine, not per project

Agent platforms read hook config only from the directory a session starts
in — never a parent, never a subfolder. Project-level hooks therefore fired
only for sessions that happened to start at the mount, and, living inside a
synced folder, they replicated one machine's agent config to the whole team
(a second writer of a file bdrive already owns). Claude Code additionally
ignores project hooks until the folder is trusted, so in practice they were
often inert without any visible sign.

Hooks now go to each platform's user config, once per machine, covering
every session in every folder; the existing shell guard keeps them a no-op
outside BearDrive projects. Install migrates away blocks older versions
wrote into projects, and `bdrive hooks uninstall` removes ours while leaving
foreign hooks untouched.

Setup is also one command now. init absorbs the skill install, prints the
hub link, and takes --server, so connecting to a named hub no longer needs a
separate login; the runbook forbids preflight and command chaining, since
each distinct command costs the user a permission prompt. For plugin users a
PreToolUse hook auto-approves bdrive's own setup subcommands — narrowly: any
shell operator in the command disqualifies it.

Also drops --shared in favor of `init . --only wiki,docs`, which writes a
managed block of .bdriveignore rules instead of a second scope mechanism.
Because those rules sync, `sync --prune` now refuses on a scoped project
rather than stripping everything outside the scope from the hub for everyone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

* docs: fix stale claims an audit found against the new CLI

An audit of every doc surface against the code turned up claims that the
user-scope hook move and the one-command init made false: project-level
hooks "riding the repo", the Claude trust prompt, Codex's //hooks project
layer, `--no-hooks` skipping the skill (it does not), prune reconciling
against a per-device scope (it now refuses on a scoped project), and
`--scan-interval`/`--remote-interval` documented as init flags when they
only exist on `bdrive daemon run`.

Also documents the surface added today — `--server`, `bdrive hooks
uninstall`, and the plugin's PreToolUse auto-approval — refreshes the two
sample `init` transcripts to the real output, and corrects hook matchers
that had drifted from agenthooks.go.

`bdrive scope` told users to narrow an existing mount with `bdrive init .
--only <dirs>`, which resume then ignored — a dead end. Init now applies
--only on resume, writing the scope block, so the advice works.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 10:08:45 +09:00
9088127176 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>
2026-07-28 07:22:11 +09:00
0236e1b272 feat(cli): multi-folder --shared at init + bdrive scope to edit the sync scope (#53)
* feat(cli): init --shared accepts multiple subfolders (repeatable or comma-separated)

--shared is now a slice flag: `--shared wiki --shared docs` or
`--shared wiki,docs` sync several subfolders into one project
(include list ["wiki/", "docs/"]). The interactive scope prompt takes a
space- or comma-separated list. Entries resolving to ".", "", or ".."
error out — silently dropping them would widen scope to the whole folder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AubcaQscjcQucXwh578vqG

* docs(plugin): skills/commands propose multiple --shared folders at init

The init/install flows now scan for all knowledge folder candidates and
offer them as one --shared list (one project, one permission set), noting
that folders needing different access belong in separate projects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AubcaQscjcQucXwh578vqG

* feat(cli): bdrive scope — add/remove shared subfolders without editing JSON

`bdrive scope` shows the include list; `scope add`/`scope rm` edit it from
the mount root. The daemon re-reads config each tick, so changes apply in
seconds. rm deletes nothing (newly filtered paths drop from the cache with
no delete op); removing the last entry is refused since an empty include
list means whole-folder sync. add onto a whole-folder project is refused
for the same narrowing hazard. Skill/README/docs updated; scope added to
the cli-sync diagram's command list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AubcaQscjcQucXwh578vqG

* docs+cli: scoping guide covers multi-folder --shared and bdrive scope; init hints on ignored --shared at resume

The scoping guide (the dedicated page for this feature) now shows
--shared wiki,docs and a "Change the scope later" section for bdrive
scope; setup-by-hand and project-files point at it. init resume with an
explicit --shared now says the flag is ignored instead of staying silent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AubcaQscjcQucXwh578vqG

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 14:49:58 +09:00
69e7231a70 feat(hub): per-project permissions — none/read/write/admin, invite-only projects, honest degraded sync (#46)
Access was binary and org-wide: any org member got full read+write on every
project. Now each project carries four ordered levels, resolved by one
resolver and enforced at one choke point.

- `projectPerm` (perms.go) replaces `projectAllowed`; `proj(level, h)` in
  server.go gates every per-project route by the level it declares at
  registration, so no handler grows its own check.
- `Project` gains Creator/Default/Perms. `Default == ""` means write, so an
  upgraded hub behaves identically until someone edits permissions.
- Creator becomes the first project admin; org owners are implicitly admin
  everywhere in their org and a grant naming one is refused, not ignored; a
  project always keeps at least one explicit admin.
- Default `none` makes a project invite-only. A `none` member is treated
  exactly like a non-member, including on create-or-join by name.
- Rename/delete move from org-owner-only to project `admin`.
- Both metadata backends persist it: the file store rides along, the SQL
  store gains `project_perms` plus an idempotent ALTER for the two new
  columns (migrate() had only ever created tables).

Client side, a refusal stops looking like an outage: `remote.ErrForbidden`
plus `Result.ReadOnly` (push refused → pull-only) and `Result.NoAccess`
(pull refused → paused, working folder untouched). Neither sets Offline,
neither loses a local op, and re-granting self-heals on the next cycle.
`bdrive status`/`sync` and the daemon (once, on transition) say which.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 10:41:57 +09:00
6628718f1d fix(cli): agent hooks never sync folders this device didn't opt into (#44)
* fix(cli): agent hooks never sync folders this device didn't opt into

The turn hooks decided "this folder is managed" from the mere presence of
.bdrive/config.json — a file designed to travel with the folder. Two holes:

- A config.json arriving via git clone / copied dir made one hook firing
  silently mint a device identity, register the mount, create a volume
  store, journal the whole folder, and inject the hub-link formula — on a
  device that never ran init or login.
- `bdrive stop` only killed the daemon: the next agent turn's
  `bdrive sync --hook` resumed a full sync cycle and kept injecting links,
  and `stop --forget` was undone within one turn by registry self-heal.

Fix: one gate (`syncBlocked`) in the paths all hooks route through —
sync/sync --hook/read-log now require the mount to already be enrolled in
this device's mounts.json (read without ResolveMount's enrolling
self-heal) and not paused. Hook mode exits silently; plain `bdrive sync`
errors with a `bdrive init` pointer. New per-device paused marker in the
volume dir: set by `bdrive stop`, cleared by `bdrive init` (startSync).
Only init enrolls or resumes; folder moves still self-heal since
enrollment is keyed by mount id, not path.

Docs updated (README, SKILL.md, docs cli reference, CHANGELOG). Tests:
hook/read-log no-op + no-enrollment on unenrolled and paused mounts,
plain-sync refusals, stop→pause→forget regression, paused marker contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: architecture-diagram PRs must show before/after excerpts of changed classes

The "Architecture changes" PR section now names exactly what changed and
shows Before and After mermaid excerpts scoped to the affected classes and
their immediate relationships — never the full diagram (Before = merge
base). Convention updated in CLAUDE.md, architecture/README.md, and the
pre-PR hook's reminder text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: add cli-sync architecture diagram; widen diagram convention to the CLI

architecture/cli-sync.md draws the CLI and sync engine (cmd/bdrive +
internal/{syncer,store,journal,config,daemon,agenthooks}): the Session
cycle over Store/journal/remote, and the command layer with the new
syncBlocked opt-in gate, paused marker, and enrollment ownership. The
pre-PR hook and CLAUDE.md now watch these packages too, so CLI-side
structural changes trigger the before/after-excerpt convention the same
way server changes do.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: full-coverage architecture diagrams — overview, frontend, agentskills

Every application package is now drawn somewhere: overview.md (system
diagram — package map, device↔hub↔storage flow, agent surfaces, and the
private cloud/ repo as an external seam consumer), webapp-frontend.md (the
hub SPA's modules: App/HubApp/VolumeApp/Browser, the in-repo nav/router,
api layer, hooks, components), and agentskills added to cli-sync.md. The
pre-PR hook now watches all of cmd/, internal Go code, and frontend/src
(generated static/ excluded); CLAUDE.md and architecture/README.md state
the coverage rule: every code change lands in exactly one detail diagram's
scope, web/docs and cloud/ deliberately excluded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: PR bodies start with a TL;DR — max 5 informal one-liners

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 15:53:23 +09:00