mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
Merge pull request #11 from runbear-io/feat/knowledge-init-flows
feat(sync): fence nested mounts; knowledge-init flows in plugin docs
This commit is contained in:
@@ -460,9 +460,11 @@ working folder ←materialize/scan→ local volume store ←push/pull→ obj
|
||||
### What beardrive does not sync
|
||||
|
||||
`.git` directories (per-file LWW would corrupt repositories), `.DS_Store`,
|
||||
the `.bdrive` settings file, its own temp files, and anything excluded by
|
||||
`.bdriveignore` or omitted from an `include` list. Empty directories are not
|
||||
tracked (like git).
|
||||
the `.bdrive` settings file, its own temp files, nested mounts (a
|
||||
subdirectory with its own `.bdrive/config.json` syncs only through its own
|
||||
project — the parent never scans into it, writes over it, or propagates
|
||||
deletes for it), and anything excluded by `.bdriveignore` or omitted from an
|
||||
`include` list. Empty directories are not tracked (like git).
|
||||
|
||||
## Roadmap
|
||||
|
||||
|
||||
@@ -41,6 +41,14 @@ func projectConfigPath(folder string) string {
|
||||
return filepath.Join(folder, ProjectDir, "config.json")
|
||||
}
|
||||
|
||||
// IsMount reports whether folder is a BearDrive mount root, i.e. has a
|
||||
// .bdrive/config.json — even an unparseable one, so callers that must not
|
||||
// treat a mount as plain files (e.g. a parent mount's scanner) stay safe.
|
||||
func IsMount(folder string) bool {
|
||||
_, err := os.Stat(projectConfigPath(folder))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// LoadProject reads <folder>/.bdrive/config.json; ok is false if it does not
|
||||
// exist.
|
||||
func LoadProject(folder string) (Project, bool, error) {
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
package syncer
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/runbear-io/beardrive/internal/config"
|
||||
"github.com/runbear-io/beardrive/internal/remote"
|
||||
"github.com/runbear-io/beardrive/internal/store"
|
||||
)
|
||||
|
||||
// End-to-end scenarios for the init knowledge flows documented in
|
||||
// plugin/commands/init.md and the beardrive skill ("Connecting knowledge
|
||||
// tooling"): a shared subfolder carved out of a repo, a teammate connecting
|
||||
// over pre-existing local content, and a nested mount (e.g. a team knowledge
|
||||
// folder inside a personal brain that is itself a mount) syncing through its
|
||||
// own project.
|
||||
|
||||
// deviceAt is newDevice with an explicit working folder, for topologies where
|
||||
// the folder's location matters (nested mounts).
|
||||
func deviceAt(t *testing.T, name, folder string, backend remote.Backend) *Session {
|
||||
t.Helper()
|
||||
st, err := store.Open(filepath.Join(t.TempDir(), "volume"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &Session{
|
||||
Folder: folder,
|
||||
Store: st,
|
||||
Device: config.Device{ID: name, Name: name, Author: name + "@test"},
|
||||
Backend: backend,
|
||||
}
|
||||
}
|
||||
|
||||
func conflictFiles(t *testing.T, folder string) []string {
|
||||
t.Helper()
|
||||
var out []string
|
||||
err := filepath.WalkDir(folder, func(p string, d os.DirEntry, err error) error {
|
||||
if err == nil && !d.IsDir() && strings.Contains(d.Name(), ".bdrive-conflict-") {
|
||||
out = append(out, p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// A repo shares only its knowledge subfolder (`bdrive init --shared Wiki`);
|
||||
// a teammate connects from their own checkout with the same scope. Wiki
|
||||
// content flows both ways; each side's code never leaves its machine.
|
||||
func TestSharedSubfolderScopeBothDevices(t *testing.T) {
|
||||
be := sharedRemote(t)
|
||||
a := newDevice(t, "deva", be)
|
||||
b := newDevice(t, "devb", be)
|
||||
|
||||
write(t, a.Folder, ".bdrive/config.json", `{"include": ["Wiki/"]}`)
|
||||
write(t, a.Folder, "Wiki/home.md", "welcome")
|
||||
write(t, a.Folder, "src/code.go", "package main")
|
||||
cycle(t, a)
|
||||
|
||||
write(t, b.Folder, ".bdrive/config.json", `{"include": ["Wiki/"]}`)
|
||||
write(t, b.Folder, "main.py", "print('local only')")
|
||||
res := cycle(t, b)
|
||||
if res.LocalOps != 0 {
|
||||
t.Fatalf("b journaled %d ops for out-of-scope files, want 0", res.LocalOps)
|
||||
}
|
||||
if got := read(t, b.Folder, "Wiki/home.md"); got != "welcome" {
|
||||
t.Fatalf("Wiki/home.md = %q, want welcome", got)
|
||||
}
|
||||
|
||||
// Wiki edits flow back; code never crosses in either direction.
|
||||
write(t, b.Folder, "Wiki/home.md", "welcome v2")
|
||||
cycle(t, b)
|
||||
cycle(t, a)
|
||||
if got := read(t, a.Folder, "Wiki/home.md"); got != "welcome v2" {
|
||||
t.Fatalf("a Wiki/home.md = %q, want welcome v2", got)
|
||||
}
|
||||
for folder, absent := range map[string]string{a.Folder: "main.py", b.Folder: "src/code.go"} {
|
||||
if _, err := os.Stat(filepath.Join(folder, absent)); !os.IsNotExist(err) {
|
||||
t.Fatalf("%s leaked across devices", absent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The git-handoff teammate story: after the first user connects docs/, a
|
||||
// teammate's checkout already holds the same files. Connecting must converge
|
||||
// with zero conflict copies (identical content is adopted, not duplicated).
|
||||
func TestConnectWithIdenticalLocalContent(t *testing.T) {
|
||||
be := sharedRemote(t)
|
||||
a := newDevice(t, "deva", be)
|
||||
write(t, a.Folder, "docs/guide.md", "v1")
|
||||
write(t, a.Folder, "docs/setup.md", "steps")
|
||||
cycle(t, a)
|
||||
|
||||
b := newDevice(t, "devb", be)
|
||||
write(t, b.Folder, "docs/guide.md", "v1")
|
||||
write(t, b.Folder, "docs/setup.md", "steps")
|
||||
cycle(t, b)
|
||||
cycle(t, a)
|
||||
cycle(t, b)
|
||||
|
||||
for _, s := range []*Session{a, b} {
|
||||
if got := read(t, s.Folder, "docs/guide.md"); got != "v1" {
|
||||
t.Fatalf("guide.md = %q, want v1", got)
|
||||
}
|
||||
if c := conflictFiles(t, s.Folder); len(c) != 0 {
|
||||
t.Fatalf("identical content produced conflict copies: %v", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Same story with a stale divergent copy: nothing is silently lost — the
|
||||
// devices converge on one version and the other survives as a conflict copy.
|
||||
func TestConnectWithDivergentLocalContent(t *testing.T) {
|
||||
be := sharedRemote(t)
|
||||
a := newDevice(t, "deva", be)
|
||||
write(t, a.Folder, "docs/guide.md", "hub version")
|
||||
cycle(t, a)
|
||||
|
||||
b := newDevice(t, "devb", be)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
write(t, b.Folder, "docs/guide.md", "stale local version")
|
||||
cycle(t, b)
|
||||
cycle(t, a)
|
||||
cycle(t, b)
|
||||
|
||||
av, bv := read(t, a.Folder, "docs/guide.md"), read(t, b.Folder, "docs/guide.md")
|
||||
if av != bv {
|
||||
t.Fatalf("devices diverged: %q vs %q", av, bv)
|
||||
}
|
||||
survived := map[string]bool{av: true}
|
||||
for _, s := range []*Session{a, b} {
|
||||
for _, p := range conflictFiles(t, s.Folder) {
|
||||
rel, _ := filepath.Rel(s.Folder, p)
|
||||
survived[read(t, s.Folder, rel)] = true
|
||||
}
|
||||
}
|
||||
if !survived["hub version"] || !survived["stale local version"] {
|
||||
t.Fatalf("a version was silently lost; surviving: %v", survived)
|
||||
}
|
||||
}
|
||||
|
||||
// The follower-brain topology: a personal folder is a mount on one project
|
||||
// while a team knowledge folder nested inside it is a mount on another.
|
||||
// Each project sees only its own files, in both directions, even as both
|
||||
// actively sync.
|
||||
func TestNestedMountSyncsIndependently(t *testing.T) {
|
||||
personal := sharedRemote(t)
|
||||
team := sharedRemote(t)
|
||||
|
||||
// Alice: personal brain mount with a nested team mount inside it.
|
||||
aliceRoot := t.TempDir()
|
||||
alice := deviceAt(t, "alice", aliceRoot, personal)
|
||||
aliceTeam := deviceAt(t, "alice-team", filepath.Join(aliceRoot, "team"), team)
|
||||
write(t, aliceRoot, "private.md", "my captures")
|
||||
write(t, aliceRoot, "team/.bdrive/config.json", `{"mount_id":"m-team"}`)
|
||||
write(t, aliceRoot, "team/plan.md", "roadmap v1")
|
||||
cycle(t, alice)
|
||||
cycle(t, aliceTeam)
|
||||
|
||||
// Bob syncs only the team project; Alice's laptop syncs only personal.
|
||||
bobTeam := newDevice(t, "bob-team", team)
|
||||
cycle(t, bobTeam)
|
||||
if got := read(t, bobTeam.Folder, "plan.md"); got != "roadmap v1" {
|
||||
t.Fatalf("team plan.md = %q, want roadmap v1", got)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(bobTeam.Folder, "private.md")); !os.IsNotExist(err) {
|
||||
t.Fatal("personal file leaked into the team project")
|
||||
}
|
||||
laptop := newDevice(t, "alice-laptop", personal)
|
||||
cycle(t, laptop)
|
||||
if got := read(t, laptop.Folder, "private.md"); got != "my captures" {
|
||||
t.Fatalf("private.md = %q, want my captures", got)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(laptop.Folder, "team")); !os.IsNotExist(err) {
|
||||
t.Fatal("team folder leaked into the personal project")
|
||||
}
|
||||
|
||||
// Bob's team edit reaches Alice's nested mount; her personal project
|
||||
// must not journal the change it can see on disk.
|
||||
write(t, bobTeam.Folder, "plan.md", "roadmap v2")
|
||||
cycle(t, bobTeam)
|
||||
cycle(t, aliceTeam)
|
||||
if got := read(t, aliceRoot, "team/plan.md"); got != "roadmap v2" {
|
||||
t.Fatalf("alice team/plan.md = %q, want roadmap v2", got)
|
||||
}
|
||||
if res := cycle(t, alice); res.LocalOps != 0 {
|
||||
t.Fatalf("personal mount journaled %d ops for nested team content, want 0", res.LocalOps)
|
||||
}
|
||||
}
|
||||
|
||||
// The conflict-copy filename must keep matching the glob documented for the
|
||||
// OKF validation hook (SKILL.md): validate alone can't see conflict copies,
|
||||
// so agents check `*.bdrive-conflict-*` — renaming the pattern breaks them.
|
||||
func TestConflictCopyNameMatchesDocumentedGlob(t *testing.T) {
|
||||
name := conflictName("Wiki/page.md", "Snow's MacBook", time.Now())
|
||||
ok, err := filepath.Match("*.bdrive-conflict-*", filepath.Base(name))
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("conflict copy %q no longer matches the documented glob *.bdrive-conflict-*", name)
|
||||
}
|
||||
if strings.ContainsAny(filepath.Base(name), " '") {
|
||||
t.Fatalf("conflict copy name %q should sanitize device names", name)
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,28 @@ type Filter struct {
|
||||
ignore []pattern
|
||||
include []pattern
|
||||
negated bool // any `!` rules → directory pruning is unsafe
|
||||
|
||||
// nested holds subdirectories that are BearDrive mounts of their own
|
||||
// (they contain .bdrive/config.json), discovered during the scan walk.
|
||||
// A nested mount syncs through its own project: the parent never scans
|
||||
// into it, never materializes over it, and drops cached paths under it
|
||||
// without a delete op (same posture as newly ignored paths).
|
||||
nested []string
|
||||
}
|
||||
|
||||
// addNestedMount records a nested mount root (slash-relative to the parent
|
||||
// mount) so Skip excludes everything under it for the rest of the cycle.
|
||||
func (f *Filter) addNestedMount(rel string) {
|
||||
f.nested = append(f.nested, rel+"/")
|
||||
}
|
||||
|
||||
func (f *Filter) underNestedMount(rel string) bool {
|
||||
for _, root := range f.nested {
|
||||
if strings.HasPrefix(rel, root) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type pattern struct {
|
||||
@@ -117,7 +139,7 @@ func compile(line string) (pattern, bool) {
|
||||
|
||||
// Skip reports whether a file path should not sync.
|
||||
func (f *Filter) Skip(rel string) bool {
|
||||
if f.ignoredFile(rel) {
|
||||
if f.underNestedMount(rel) || f.ignoredFile(rel) {
|
||||
return true
|
||||
}
|
||||
if len(f.include) == 0 {
|
||||
|
||||
@@ -234,6 +234,11 @@ func (s *Session) scan(cache map[string]store.CachedFile, st *store.SyncState, s
|
||||
if ignoreDirs[d.Name()] || filter.PruneDir(rel) {
|
||||
return fs.SkipDir
|
||||
}
|
||||
if config.IsMount(p) {
|
||||
// A mount of its own: it syncs through its own project.
|
||||
filter.addNestedMount(rel)
|
||||
return fs.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !d.Type().IsRegular() || ignoredFile(d.Name()) || filter.Skip(rel) {
|
||||
|
||||
@@ -330,3 +330,55 @@ func TestExecutableBitPreserved(t *testing.T) {
|
||||
t.Fatalf("exec bit lost: %v", fi.Mode())
|
||||
}
|
||||
}
|
||||
|
||||
// TestNestedMountExcluded verifies that a subdirectory which is a BearDrive
|
||||
// mount of its own (has .bdrive/config.json) is fenced off from the parent
|
||||
// mount: the parent scanner never journals its files, dropping it emits no
|
||||
// delete ops toward peers, and remote state is never materialized into it.
|
||||
func TestNestedMountExcluded(t *testing.T) {
|
||||
be := sharedRemote(t)
|
||||
a := newDevice(t, "deva", be)
|
||||
b := newDevice(t, "devb", be)
|
||||
|
||||
// Both devices converge on a folder that includes team/.
|
||||
write(t, a.Folder, "readme.md", "root")
|
||||
write(t, a.Folder, "team/notes.md", "v1")
|
||||
cycle(t, a)
|
||||
cycle(t, b)
|
||||
if got := read(t, b.Folder, "team/notes.md"); got != "v1" {
|
||||
t.Fatalf("b team/notes.md = %q, want v1", got)
|
||||
}
|
||||
|
||||
// team/ becomes a nested mount on A (its own project).
|
||||
write(t, a.Folder, "team/.bdrive/config.json", `{"mount_id":"m-nested"}`)
|
||||
write(t, a.Folder, "team/local.md", "only for the nested project")
|
||||
res := cycle(t, a)
|
||||
if res.LocalOps != 0 {
|
||||
t.Fatalf("a journaled %d ops for nested-mount content, want 0", res.LocalOps)
|
||||
}
|
||||
|
||||
// B must keep its copy (no delete propagated) and never see new files.
|
||||
res = cycle(t, b)
|
||||
if got := read(t, b.Folder, "team/notes.md"); got != "v1" {
|
||||
t.Fatalf("b team/notes.md = %q after a's cycle, want v1 (no deletes)", got)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(b.Folder, "team/local.md")); !os.IsNotExist(err) {
|
||||
t.Fatal("nested-mount file leaked to peer")
|
||||
}
|
||||
|
||||
// B edits inside team/; A must not materialize over its nested mount.
|
||||
write(t, b.Folder, "team/notes.md", "v2")
|
||||
cycle(t, b)
|
||||
cycle(t, a)
|
||||
if got := read(t, a.Folder, "team/notes.md"); got != "v1" {
|
||||
t.Fatalf("a team/notes.md = %q, want v1 (nested mount not written)", got)
|
||||
}
|
||||
|
||||
// Paths outside the nested mount keep syncing both ways.
|
||||
write(t, a.Folder, "readme.md", "root v2")
|
||||
cycle(t, a)
|
||||
cycle(t, b)
|
||||
if got := read(t, b.Folder, "readme.md"); got != "root v2" {
|
||||
t.Fatalf("b readme.md = %q, want root v2", got)
|
||||
}
|
||||
}
|
||||
|
||||
+35
-9
@@ -18,20 +18,46 @@ Follow these steps:
|
||||
coming; it completes by itself). Default server is beardrive.ai; pass a
|
||||
self-hosted URL if the user mentioned one.
|
||||
|
||||
3. **Initialize**: if the folder already contains `.bdrive/`, just run
|
||||
`bdrive init --yes` there — it resumes syncing (including after a
|
||||
rename/move). Otherwise decide the project name (argument, or ask, or
|
||||
default to the folder name) and scope (whole folder, or only a shared
|
||||
subfolder like `./wiki` via `--shared`), then run it non-interactively:
|
||||
3. **Detect knowledge tooling** (skip if the folder already contains
|
||||
`.bdrive/` — then just run `bdrive init --yes`; it resumes syncing,
|
||||
including after a rename/move). Check, in order — first match wins,
|
||||
ask if two match (full playbook: the beardrive skill's "Connecting
|
||||
knowledge tooling" section):
|
||||
|
||||
- **gbrain** (`gbrain.yml`, or a gbrain MCP server / brain-first
|
||||
CLAUDE.md block) → offer to sync the brain's shared subfolder as its
|
||||
own project; never a brain root.
|
||||
- **OKF wiki** (markdown with OKF frontmatter) → offer: connect the
|
||||
wiki dir via `--shared`, or keep it PR-gated and create a new shared
|
||||
folder.
|
||||
- **Wiki-ish folder** (`docs`/`wiki`/`notes` full of markdown) → check
|
||||
`git log -- <dir>`; dormant → recommend connecting it, active PR
|
||||
traffic → recommend a new shared folder. Offer an OKF upgrade
|
||||
(`openknowledge from`) after connecting, as a separate consent.
|
||||
- **Nothing / empty** → offer a starting point in this order:
|
||||
OKF (recommended), gbrain, blank, describe-it.
|
||||
|
||||
4. **Initialize** — two hard rules:
|
||||
|
||||
- **Never sync a repo root**: inside a repo, knowledge syncs as a
|
||||
scoped subfolder via `--shared`. A dedicated knowledge folder
|
||||
(empty dir, standalone vault) may be the mount itself.
|
||||
- **One transport per folder**: a git-tracked dir must leave git
|
||||
tracking before it syncs (`git rm -r --cached <dir>` + gitignore;
|
||||
stage it, let the user commit). Offer one-way git snapshots if they
|
||||
want a git record; `bdrive log -p <path>` covers history for most.
|
||||
|
||||
```sh
|
||||
bdrive init --name <project> --yes # whole folder
|
||||
bdrive init --name <project> --shared wiki # only ./wiki syncs
|
||||
bdrive init --name <project> --yes # dedicated knowledge folder
|
||||
bdrive init --name <project> --shared wiki # in a repo: only ./wiki syncs
|
||||
```
|
||||
|
||||
4. **Verify**: run `bdrive status <folder>` and confirm the daemon is
|
||||
5. **Verify**: run `bdrive status <folder>` and confirm the daemon is
|
||||
running and pending is 0. Summarize: project name/id, what syncs, and
|
||||
that edits propagate to every team member within seconds.
|
||||
that edits propagate to every team member within seconds. Offer a
|
||||
consent-gated CLAUDE.md note and tell the user how teammates connect
|
||||
(invite link → `bdrive init` → same `--shared` scope, which is
|
||||
per-device).
|
||||
|
||||
For the full team setup (CLAUDE.md guidance + per-project sync hooks in
|
||||
`.claude/settings.json`), suggest `/beardrive:install` instead.
|
||||
|
||||
@@ -133,9 +133,32 @@ bdrive stop ~/agent-workspace
|
||||
bdrive stop ./notes --forget
|
||||
```
|
||||
|
||||
### Connecting knowledge tooling (gbrain, OKF, docs folders)
|
||||
|
||||
When guiding `bdrive init`, detect existing knowledge tooling and connect it instead of blind-syncing the folder. Two rules govern every case:
|
||||
|
||||
- **One transport per folder.** Never sync a folder that has another writer. Git-tracked paths: a teammate's `git pull` or branch switch rewrites files with older content, and sync broadcasts that as a fresh edit — silently reverting the team's latest pages. A gbrain brain root: every private capture and overnight enrichment would become team-visible, and each member's cron rewriting the same pages fills the project with conflict copies. Moving a folder from git to beardrive is a **handoff**: `git rm -r --cached <dir>` + add `<dir>/` to `.gitignore`, stage the change but let the user commit it (teammates then pull and re-init; identical content converges with no conflicts). If they want a git record anyway, offer **one-way snapshots** (a scheduled job commits the synced folder's state to an archive branch — git only ever reads the folder) and note that hub history (`bdrive log -p <path>`) usually covers the need.
|
||||
- **Knowledge syncs as a scoped folder.** Inside a repo, always `--shared <dir>` — never the repo root. A dedicated knowledge folder (an empty dir, a standalone vault) may be the mount itself. The sync scope is per-device (`.bdrive/` never syncs), so recommend the same `--shared <dir>` when each teammate connects.
|
||||
|
||||
Detection ladder — first match wins; if two rungs match, ask which to connect:
|
||||
|
||||
1. **gbrain** — `gbrain.yml` in the folder, or a gbrain MCP server in `.mcp.json` / a brain-first protocol block in CLAUDE.md (brain root is then found via `~/.gbrain/config.json` or the MCP config). gbrain's own architecture states the markdown repo is the system of record and the DB a derived cache — beardrive replaces the git hop as the brain's sync transport:
|
||||
- The shared team folder is **never anyone's brain root**. First user: share a subfolder of their brain (e.g. `~/brain/team/`) as its own project. Followers: mount that project as a subfolder of their own brain (or a sibling dir) — never merge roots; "merge my existing brain into it" is a manual migration, not an init option.
|
||||
- Register the shared folder as its **own gbrain source** (`gbrain sources add`; `.gbrain-source` dotfile routes writes) — slugs are per-source, the `federated` flag controls blending into personal search, and the schema pack is settable per source so team pages keep their types (`person`, `company`, …) instead of degrading to untyped notes.
|
||||
- Recommend `gbrain config set link_resolution.global_basename true` so the team's `[[wikilinks]]` resolve from any mount path (`gbrain doctor` reports the edge gain first).
|
||||
- Mirror the brain's `db_only` dirs from `gbrain.yml` into `.bdriveignore` (machine-generated, restorable from the DB — and often a privacy fence; ask before syncing them).
|
||||
- gbrain's sync cron keeps re-indexing what beardrive pulls in; suggest switching it to `gbrain sync --no-pull` (ask before editing a crontab). On PGLite, remind: stop `gbrain serve` before large syncs (single-writer contention).
|
||||
2. **OKF** — markdown with OKF v0.1 frontmatter (confirm with `openknowledge validate` if the CLI is present; don't install just to detect). Offer: (a) connect the wiki dir via `--shared` — with the git handoff if tracked — or (b) keep the wiki PR-gated in git and create a new shared folder (starting-point menu below). Recommend (a) when the wiki is the team's knowledge, (b) when it's review-gated repo documentation.
|
||||
3. **Wiki-ish folder** — a markdown-dense dir named `docs`/`wiki`/`notes`/`kb` with no knowledge tooling. Check `git log -- <dir>`: dormant → recommend connecting it as the live team space (handoff included); active PR traffic → recommend a new shared folder instead, and say why. After connecting, offer — as a separate consent, it rewrites their files — an in-place upgrade to OKF (`openknowledge from <dir>`) for validation and agent-readability.
|
||||
4. **Nothing** — empty or unstructured folder: offer a starting point, in this order: **(a) OKF (recommended** — open spec, plain files, zero runtime, upgradeable to gbrain later**)**, (b) gbrain (full agent brain; heavier — per-member local DB), (c) blank, (d) describe-it (user describes the purpose; scaffold a custom OKF shape, redirecting to gbrain if the description is graph-shaped: entities, relationships, "who/what" queries).
|
||||
|
||||
Conflict copies are named `<file>.bdrive-conflict-<device>-<timestamp>` and sync like normal files. `openknowledge validate` does **not** flag them (they aren't `.md`) — pair validate with a `*.bdrive-conflict-*` glob check when offering a post-edit validation hook.
|
||||
|
||||
Every branch ends the same way: verify (`bdrive status`, pending 0), a consent-gated CLAUDE.md note describing what syncs and how teammates connect, and the teammate onboarding sentence (invite link → `bdrive init` → same `--shared` scope).
|
||||
|
||||
### What beardrive does not sync
|
||||
|
||||
`.git` directories, `.DS_Store`, the `.bdrive` settings file, beardrive's own temp files, empty directories, and anything excluded by `.bdriveignore` or left out of an `include` list. Don't suggest mounting a folder where `.git` is the content the user expects synced — they want git, not beardrive.
|
||||
`.git` directories, `.DS_Store`, the `.bdrive` settings file, beardrive's own temp files, empty directories, nested mounts (a subdirectory with its own `.bdrive/config.json` syncs only through its own project — the parent never scans into it, materializes over it, or propagates deletes for it), and anything excluded by `.bdriveignore` or left out of an `include` list. Don't suggest mounting a folder where `.git` is the content the user expects synced — they want git, not beardrive.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user