moflo 4.10.29-rc.6 → 4.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  # Cross-Installation Memory Sharing
2
2
 
3
- **Purpose:** Decide how to share moflo's durable learnings across git worktrees, machines, or a team — and how to do it WITHOUT corrupting search. Read this before configuring `memory.durable_path`, running `flo memory sync`, or pointing two checkouts at one database.
3
+ **Purpose:** Decide how to share moflo's durable learnings across git worktrees, machines, or a team — WITHOUT corrupting search. Worktrees on one machine share automatically; read this before customizing that, running `flo memory sync`, or pointing two checkouts at one database.
4
4
 
5
5
  ---
6
6
 
@@ -26,7 +26,7 @@ Choose by who needs the learnings, not by what is easiest to wire.
26
26
 
27
27
  | Topology | Mechanism | How |
28
28
  |----------|-----------|-----|
29
- | Same machine, many worktrees / Conductor workspaces | `memory.durable_path` | Point every checkout at one durable-only store; session-start seeds + writes through automatically |
29
+ | Same machine, many worktrees / Conductor workspaces | **Automatic** (default) | Learnings converge across worktrees at session-start with no config; override the store with `memory.durable_path`, or opt out with `memory.worktree_sharing: false` |
30
30
  | One person, many machines | `flo memory sync` | `--to <file>` then `--from <file>` via a synced folder (Dropbox/iCloud) or a hand-copied file |
31
31
  | A team sharing one repo | Git-tracked team artifact | `flo memory team-export` writes `.moflo/shared/learnings.jsonl`; teammates' session-start import-merges it after `git pull` |
32
32
  | A fresh/empty workspace that must be ready FAST | Whole-DB snapshot (`memory.hydrate_from`) | `flo memory backup --to <snap>` once; a new workspace restores the entire DB so search works on session one — no cold reindex |
@@ -35,16 +35,25 @@ The first three move the SAME durable slice and dedupe on `UNIQUE(namespace, key
35
35
 
36
36
  ---
37
37
 
38
- ## Configure a Same-Machine Shared Store
38
+ ## Automatic Worktree Sharing (Default)
39
39
 
40
- Set `memory.durable_path` in `moflo.yaml` to a dedicated file NOT a full `moflo.db`.
40
+ **Worktrees on one machine need no setup — sharing is automatic.** When moflo starts in a checkout whose repo has git worktrees in play, it derives a durable-only store at `<git-common-dir>/moflo/durable.db` (a path every worktree computes identically, since they share one `.git`) and converges `learnings` + `knowledge` across them at session-start. Conductor builds on git worktrees, so its workspaces are covered too.
41
+
42
+ | Guardrail | Behavior |
43
+ |-----------|----------|
44
+ | Activates only with worktrees | A plain single checkout writes nothing — byte-for-byte unchanged |
45
+ | Never a full DB | The derived store is a dedicated `durable.db`, so it can't trip the index-divergence hazard above |
46
+ | Opt out | `memory.worktree_sharing: false` |
47
+ | Override the location | `memory.durable_path` (also use this to converge separate *clones*, which don't share a `.git`) |
48
+
49
+ To override the store location, set a dedicated file — NOT a full `moflo.db`:
41
50
 
42
51
  ```yaml
43
52
  memory:
44
- durable_path: ~/.moflo-shared/team-learnings.db # a durable-only store, created on first flush
53
+ durable_path: ~/.moflo-shared/team-learnings.db # overrides the auto store; created on first flush
45
54
  ```
46
55
 
47
- The path must resolve to a file that is NOT any project's `.moflo/moflo.db`. Pointing it at a real `moflo.db` re-introduces the whole-DB-sharing hazard. `MOFLO_DURABLE_PATH` overrides the config per process; relative paths resolve against the project root.
56
+ Pointing `durable_path` at a real `moflo.db` re-introduces the whole-DB-sharing hazard. `MOFLO_DURABLE_PATH` overrides per process; relative paths resolve against the project root. The `/memory-worktree` skill verifies, customizes, or opts out for you.
48
57
 
49
58
  ---
50
59
 
@@ -111,6 +120,7 @@ flo doctor -c shared-db
111
120
 
112
121
  | Result | Meaning |
113
122
  |--------|---------|
123
+ | **pass** — "Automatic worktree learning sharing active" | No `durable_path` set, but worktrees are present — the auto-derived durable-only store is on and safe |
114
124
  | **pass** — "durable-only shared store configured" | `durable_path` points at a dedicated file — safe |
115
125
  | **warn** — "points at a full moflo.db" | `durable_path` aliases or is named like a full DB — repoint it at a dedicated store |
116
126
  | **warn** — "is a symlink" | The local `moflo.db` is symlinked into a shared location — replace it with a local DB and use `durable_path` |
@@ -60,7 +60,8 @@ memory:
60
60
  backend: node-sqlite # node-sqlite (default) | rvf (pure-TS fallback) | json (last resort). Passed to createDatabase() as the preferred provider (#1144).
61
61
  embedding_model: Xenova/all-MiniLM-L6-v2 # 384-dim neural embeddings
62
62
  namespace: default # Default namespace for memory operations
63
- # durable_path: ~/.moflo-shared/team-learnings.db # opt-in (#1232): share durable namespaces (learnings, knowledge) across worktrees / Conductor workspaces. Point at a DEDICATED store, never a full moflo.db. Env override: MOFLO_DURABLE_PATH.
63
+ # worktree_sharing: false # opt-OUT (#1231): automatic durable-learning sharing across git worktrees is ON by default (derived at <git-common-dir>/moflo/durable.db, active only when worktrees exist; a single checkout is untouched). Set false to disable.
64
+ # durable_path: ~/.moflo-shared/team-learnings.db # opt-in (#1232): OVERRIDE the auto worktree store — a custom durable-only path for learnings/knowledge, or to converge separate clones. Point at a DEDICATED store, never a full moflo.db. Env override: MOFLO_DURABLE_PATH.
64
65
  # team_artifact: .moflo/shared/learnings.jsonl # opt-in (#1234): git-tracked JSONL the team commits; session-start import-merges it. Env override: MOFLO_TEAM_ARTIFACT. See moflo-cross-install-memory-sharing.md.
65
66
  # hydrate_from: /abs/path/to/moflo-snapshot.db # opt-in (#1244): whole-DB snapshot to seed a fresh/empty workspace on session-start (skips the cold reindex). No-op once the local DB has content. Take one with "flo memory backup --to <path>". Env override: MOFLO_HYDRATE_FROM.
66
67
 
@@ -0,0 +1,164 @@
1
+ ---
2
+ name: memory-team
3
+ description: |
4
+ Guided setup for sharing moflo's durable learnings through a git-tracked JSONL artifact — for a whole
5
+ TEAM on one repo, OR for one person across several MACHINES (a team of one). Use when the user says
6
+ "share learnings with my team", "commit our moflo memory", "sync memory across my laptop and desktop",
7
+ "set up memory.team_artifact", or "auto-export learnings on every PR". Installs a pre-commit hook so the
8
+ artifact rides along in every commit. For many worktrees on ONE machine, use /memory-worktree instead.
9
+ arguments: "[--solo]"
10
+ ---
11
+
12
+ # /memory-team — Share Learnings via a Git-Tracked Artifact
13
+
14
+ Wires up **`memory.team_artifact`**: a diffable JSONL file (`.moflo/shared/learnings.jsonl`) that lives
15
+ **in the repo**, reviewed like any other change. Teammates `git pull` it and their next session
16
+ auto-imports the merged learnings; each entry is stamped with its git author. This skill also installs a
17
+ **pre-commit hook** so the artifact is re-exported and staged on every commit — the learnings ride along
18
+ in each PR with zero effort.
19
+
20
+ > **One mechanism, two audiences.** A *team* and *one person across machines* are the same problem — push
21
+ > the durable slice through git. This skill covers both; the only difference is whether anyone else is on
22
+ > the other end. (Prefer a private, non-git file for your own machines? See "Advanced" at the bottom —
23
+ > `flo memory sync` moves a binary artifact that keeps embeddings verbatim.)
24
+
25
+ **Arguments:** `$ARGUMENTS` — pass `--solo` to skip team-oriented wording (still identical setup).
26
+
27
+ ## When to use
28
+
29
+ - "share learnings / memory with my team"
30
+ - "commit our moflo knowledge to the repo"
31
+ - "sync memory between my laptop and desktop"
32
+ - "auto-export the learnings on every PR / commit"
33
+ - "set up `team_artifact`"
34
+
35
+ For **many worktrees on one machine**, that's a shared local store, not git — use `/memory-worktree`.
36
+
37
+ ## Procedure
38
+
39
+ ### Step 0 — Memory first (gate requirement)
40
+
41
+ ```
42
+ mcp__moflo__memory_search { query: "team_artifact learnings jsonl git share import session-start", namespace: "guidance" }
43
+ ```
44
+
45
+ ### Step 1 — Confirm this is the git path
46
+
47
+ - Team on one repo, or one person across machines who both push to the same repo? → correct skill.
48
+ - Many worktrees on **one** machine, no cross-machine need? → redirect to `/memory-worktree`.
49
+ - A private synced file (Dropbox/USB), no git? → point at the Advanced section (`flo memory sync`).
50
+
51
+ ### Step 2 — Set the config
52
+
53
+ Read the project-root `moflo.yaml` and add/merge under `memory:` — **preserve existing keys**:
54
+
55
+ ```yaml
56
+ memory:
57
+ team_artifact: .moflo/shared/learnings.jsonl
58
+ ```
59
+
60
+ `.moflo/shared/learnings.jsonl` is the default; only override if the user wants a different tracked path.
61
+
62
+ ### Step 3 — First export (creates + tracks the artifact)
63
+
64
+ ```bash
65
+ npx flo memory team-export 2>&1
66
+ ```
67
+
68
+ This writes the JSONL, and **rewrites `.gitignore`** from a blanket `.moflo/` to `.moflo/*` +
69
+ `!/.moflo/shared/` so the shared artifact is tracked while the machine-local DB stays ignored. Surface
70
+ what it reports (entries added, `.gitignore` change). If there are no durable learnings yet, that's fine —
71
+ the artifact is created empty and fills in as learnings accrue.
72
+
73
+ ### Step 4 — Install the pre-commit hook
74
+
75
+ The hook re-exports and stages the artifact on every commit, so it's never stale in a PR. Handle three
76
+ cases; **do not clobber** an existing hook.
77
+
78
+ Define the managed block (POSIX `sh` — git runs hooks via `sh` on Linux, macOS, **and** Windows):
79
+
80
+ ```sh
81
+ # >>> moflo team-export (managed by /memory-team) >>>
82
+ # Refresh the shared learnings artifact so it rides along in this commit.
83
+ npx --no-install flo memory team-export >/dev/null 2>&1 || true
84
+ if [ -n "$(git status --porcelain -- .moflo/shared/learnings.jsonl 2>/dev/null)" ]; then
85
+ git add .moflo/shared/learnings.jsonl
86
+ fi
87
+ # <<< moflo team-export (managed by /memory-team) <<<
88
+ ```
89
+
90
+ Install it:
91
+
92
+ 1. **Husky present** (`.husky/pre-commit` exists) → append the block to that file.
93
+ 2. **Existing `.git/hooks/pre-commit`** → if it already contains the `moflo team-export` markers, leave it
94
+ (idempotent); otherwise append the block after the shebang.
95
+ 3. **No hook** → create `.git/hooks/pre-commit` with `#!/bin/sh` as the first line, then the block.
96
+
97
+ On POSIX, make it executable:
98
+
99
+ ```bash
100
+ node -e "require('fs').chmodSync('.git/hooks/pre-commit', 0o755)"
101
+ ```
102
+
103
+ (Use the Node `fs.chmodSync` primitive, not `chmod` — it's a harmless no-op on Windows. Never shell out
104
+ to `chmod` in a cross-platform step.)
105
+
106
+ - The `|| true` keeps a failed export from ever blocking a commit.
107
+ - The `git status --porcelain` guard means the artifact is only re-staged when it actually changed — no
108
+ empty-diff churn on unrelated commits.
109
+
110
+ ### Step 5 — Commit and share
111
+
112
+ ```bash
113
+ git add .moflo/shared/learnings.jsonl .gitignore
114
+ git commit -m "chore: share moflo learnings"
115
+ git push
116
+ ```
117
+
118
+ Teammates (or your other machine) run `git pull`; their **next session auto-imports** the merged file —
119
+ no manual import command. To pull learnings without waiting for a restart:
120
+
121
+ ```bash
122
+ npx flo memory team-import 2>&1
123
+ ```
124
+
125
+ ## Output
126
+
127
+ End with a single-block summary:
128
+
129
+ ```
130
+ Team Memory Sharing — configured
131
+ ─────────────────────────────────
132
+ Artifact: .moflo/shared/learnings.jsonl (git-tracked, JSONL, author-stamped)
133
+ Config key: memory.team_artifact
134
+ PR hook: .git/hooks/pre-commit → re-exports + stages on every commit
135
+ Import: automatic at session start (after git pull) | npx flo memory team-import
136
+ Scope: learnings + knowledge only (embeddings regenerated on import)
137
+ ```
138
+
139
+ ## Advanced — one person, a private file (no git)
140
+
141
+ If you're solo and would rather not commit learnings, carry a binary artifact instead. It keeps the
142
+ embeddings verbatim (no recompute on the far side) and merges idempotently:
143
+
144
+ ```bash
145
+ npx flo memory sync --to ~/Dropbox/moflo-learnings.db # machine A
146
+ npx flo memory sync --from ~/Dropbox/moflo-learnings.db # machine B
147
+ ```
148
+
149
+ Same durable slice, no repo footprint, no review — the trade-off is it's not diffable and not shared.
150
+
151
+ ## Rules
152
+
153
+ - **Don't** reimplement export/import or the `.gitignore` rewrite — `flo memory team-export/team-import`
154
+ own all of it (`team-artifact-sync.ts`). This skill sets config, runs them, and installs the hook.
155
+ - **Don't** overwrite an existing pre-commit hook or husky config — append the marked block only.
156
+ - **Don't** use `chmod` / `mkdir` / `rm` in shell steps — Windows lacks them on PATH; use Node `fs`.
157
+ - Embeddings are intentionally **omitted** from the JSONL (384 floats/row would wreck diffs) — they're
158
+ regenerated on import. Don't try to add them.
159
+
160
+ ## See Also
161
+
162
+ - `/memory-worktree` — same-machine sharing across worktrees / Conductor (a shared local store, not git)
163
+ - `flo memory team-export --help` / `flo memory sync --help`
164
+ - `docs/blog/memory-sharing.html` — the design rationale (durable slice, why not share the whole DB)
@@ -0,0 +1,127 @@
1
+ ---
2
+ name: memory-worktree
3
+ description: |
4
+ Verify, customize, or opt out of moflo's AUTOMATIC durable-learning sharing across git worktrees /
5
+ Conductor workspaces on one machine. As of the worktree-auto-sharing change this is on by default —
6
+ learnings converge across a repo's worktrees with no setup. Use when the user asks "is memory shared
7
+ across my worktrees?", "where are my worktree learnings stored?", "turn off worktree memory sharing",
8
+ "share learnings across my separate clones", or "point my worktrees at a custom store". For a team or
9
+ cross-machine (git-tracked) sharing, use /memory-team instead.
10
+ arguments: "[--off | --path <store>]"
11
+ ---
12
+
13
+ # /memory-worktree — Durable Sharing Across Worktrees (Automatic)
14
+
15
+ Working across git worktrees is a core moflo competency, so durable-learning sharing across the
16
+ worktrees of a repo is now **automatic and on by default**. When a checkout is part of a repo with
17
+ worktrees in play, moflo derives a shared durable store at **`<git-common-dir>/moflo/durable.db`** and
18
+ converges the `learnings` + `knowledge` namespaces across every worktree at session start — no config.
19
+ A plain single checkout writes nothing and behaves exactly as before.
20
+
21
+ This skill is for the three things you might still want: **verify** it's working, **customize** where it
22
+ stores (or extend it to non-worktree clones), or **opt out**.
23
+
24
+ > Only the durable slice travels. Structural + ephemeral data stays local per checkout and rebuilds
25
+ > itself — the same safety model the auto path is built on (a dedicated `durable.db`, never a full
26
+ > `moflo.db`). See [[memory-team]] for the git-tracked, cross-machine variant.
27
+
28
+ **Arguments:** `$ARGUMENTS` — `--off` to opt out, `--path <store>` to set a custom shared store.
29
+
30
+ ## When to use
31
+
32
+ - "Is memory / are learnings shared across my worktrees?" → **verify** (Step 1).
33
+ - "Where are my worktree learnings stored?" → it's `<git-common-dir>/moflo/durable.db`; verify to confirm.
34
+ - "Turn off / disable worktree memory sharing" → **opt out** (Step 2).
35
+ - "Share learnings across my separate clones / a custom location" → **customize** (Step 3).
36
+
37
+ For a team, or your own laptop+desktop over git, that's `/memory-team`, not this.
38
+
39
+ ## Procedure
40
+
41
+ ### Step 0 — Memory first (gate requirement)
42
+
43
+ ```
44
+ mcp__moflo__memory_search { query: "durable_path worktree sharing auto git common dir learnings", namespace: "guidance" }
45
+ ```
46
+
47
+ ### Step 1 — Verify (default action)
48
+
49
+ Run the safety/status check and surface it verbatim:
50
+
51
+ ```bash
52
+ npx flo doctor -c shared-db 2>&1
53
+ ```
54
+
55
+ - **"Automatic worktree learning sharing active (<path>)"** → it's working; the path is the shared store.
56
+ - **"No shared full moflo.db detected…"** → auto sharing is NOT active here. Most likely this checkout
57
+ has no sibling worktrees yet (auto only turns on once worktrees exist). Confirm with the user; if they
58
+ expected sharing across *separate clones* (not worktrees), go to Step 3 — auto can't converge those.
59
+ - **A `warn`** → a full-DB misconfig is present; follow the check's `fix` line.
60
+
61
+ To see that learnings actually converged, from a second worktree:
62
+
63
+ ```bash
64
+ npx flo memory list --namespace learnings 2>&1 | head
65
+ ```
66
+
67
+ ### Step 2 — Opt out (`--off`)
68
+
69
+ If the user wants it off, set the toggle in the project-root `moflo.yaml` (preserve other `memory:` keys):
70
+
71
+ ```yaml
72
+ memory:
73
+ worktree_sharing: false
74
+ ```
75
+
76
+ Explain: this stops the auto flush/seed at session start. Anything already converged into a worktree's
77
+ local DB stays; nothing new propagates. Takes effect next session.
78
+
79
+ ### Step 3 — Customize the store (`--path`, or non-worktree clones)
80
+
81
+ Setting an explicit `memory.durable_path` **overrides** the auto derivation — use it to:
82
+ - put the shared store somewhere other than `.git/moflo/` (e.g. a synced/backed-up location), or
83
+ - converge checkouts that are **separate clones**, not git worktrees (auto only covers worktrees, since
84
+ only worktrees share a git common dir).
85
+
86
+ ```yaml
87
+ memory:
88
+ durable_path: ~/.moflo-shared/<repo-name>-learnings.db
89
+ ```
90
+
91
+ The path must live outside any single checkout and must **not** be named `moflo.db`. Create the parent
92
+ dir with a Node primitive (cross-platform — never `mkdir -p`):
93
+
94
+ ```bash
95
+ node -e "const p=require('path'),fs=require('fs');fs.mkdirSync(p.dirname(process.argv[1]),{recursive:true})" "<chosen-store-path>"
96
+ ```
97
+
98
+ Then re-run `npx flo doctor -c shared-db` to confirm it reports the durable-only store as safe.
99
+
100
+ ## Output
101
+
102
+ End with a single-block summary of what state you left it in, e.g.:
103
+
104
+ ```
105
+ Worktree Memory Sharing
106
+ ────────────────────────
107
+ Mode: automatic (default) | custom durable_path | disabled
108
+ Store: <git-common-dir>/moflo/durable.db (or the custom/none path)
109
+ Config: memory.worktree_sharing / memory.durable_path
110
+ Verify: npx flo doctor -c shared-db
111
+ Scope: learnings + knowledge only (structural stays local)
112
+ ```
113
+
114
+ ## Rules
115
+
116
+ - **Don't** re-implement the sync or path derivation — it's `resolveDurablePath` / `syncDurableAtSessionStart`
117
+ in `src/cli/services/durable-sync.ts`; this skill only reads config, sets the two keys, and verifies.
118
+ - **Never** set `durable_path` to a file named `moflo.db` or the local `.moflo/moflo.db` — that's the
119
+ HNSW-index-divergence hazard `flo doctor -c shared-db` guards against.
120
+ - **Don't** use `mkdir -p` / `rm` / `cp` in shell steps — Windows lacks them on PATH. Use Node `fs`.
121
+ - **Cross-machine or team? Wrong skill.** Redirect to `/memory-team`.
122
+
123
+ ## See Also
124
+
125
+ - `/memory-team` — git-tracked JSONL sharing for teams **and** one person across machines
126
+ - `flo doctor -c shared-db` — reports auto-sharing status and catches full-DB misconfig
127
+ - `docs/blog/memory-sharing.html` — the design rationale (durable vs structural vs ephemeral)
package/README.md CHANGED
@@ -67,7 +67,7 @@ MoFlo makes deliberate choices so you don't have to:
67
67
  |---------|-------------|
68
68
  | **Semantic Memory** | 384-dim domain-aware embeddings. Store knowledge, search it instantly. |
69
69
  | **Reflection** | Distills durable lessons from your sessions into searchable memory — automatically (auto-meditate), or on demand with `/meditate`. |
70
- | **Cross-Install Sharing** | Share durable `learnings` across git worktrees, your own machines, or a whole team via `memory.durable_path`, `flo memory sync`, or a git-tracked artifact. Structural namespaces stay local. [Full documentation →](.claude/guidance/shipped/moflo-cross-install-memory-sharing.md) |
70
+ | **Cross-Install Sharing** | Durable `learnings` follow you across git worktrees **automatically** (no setup) — and across your own machines or a whole team via `flo memory sync` or a git-tracked artifact. Structural namespaces stay local. [Full documentation →](.claude/guidance/shipped/moflo-cross-install-memory-sharing.md) |
71
71
  | **Code Navigation** | Indexes your codebase structure so Claude can answer "where does X live?" without Glob/Grep. |
72
72
  | **Guidance Indexing** | Chunks your project docs (`.claude/guidance/`, `docs/`) and makes them searchable. |
73
73
  | **Gates** | Enforces memory-first and task-creation patterns via Claude Code hooks. Prevents Claude from skipping steps. |
@@ -271,7 +271,7 @@ Pick the mechanism by topology:
271
271
 
272
272
  | You want to share across… | Use | How |
273
273
  |---------------------------|-----|-----|
274
- | Git worktrees / Conductor workspaces (same machine) | `memory.durable_path` in `moflo.yaml` | Point every checkout at one durable-only store; session-start seeds + writes through automatically |
274
+ | Git worktrees / Conductor workspaces (same machine) | **Automatic** nothing to set | Learnings converge across worktrees at session-start (stored at `<git-common-dir>/moflo/durable.db`). Override with `memory.durable_path`; opt out with `memory.worktree_sharing: false` |
275
275
  | Your own laptop + desktop + CI | `flo memory sync --to/--from <file>` | Export to a synced folder (Dropbox/iCloud) or a copied file, import on the other machine |
276
276
  | A whole team on one repo | `flo memory team-export` → commit `.moflo/shared/learnings.jsonl` | Teammates' session-start import-merges it after `git pull` (first-write-wins; author provenance retained) |
277
277
  | A fresh workspace that must be ready fast | `flo memory backup/restore` or `memory.hydrate_from` | Seed a new workspace from a **whole-DB snapshot** so it's searchable on session one — no cold reindex. Safe because each workspace owns its own copy (a one-time restore, not live sharing) |
@@ -2175,11 +2175,38 @@ try {
2175
2175
  // override is set or moflo.yaml has an UNcommented `durable_path:` line (the
2176
2176
  // generated config ships a commented example, which this regex ignores).
2177
2177
  const durableYamlPath = resolve(projectRoot, 'moflo.yaml');
2178
- let durableEnabled = Boolean(process.env.MOFLO_DURABLE_PATH);
2179
- if (!durableEnabled && existsSync(durableYamlPath)) {
2180
- try {
2181
- durableEnabled = /^[ \t]*durable_path[ \t]*:/m.test(readFileSync(durableYamlPath, 'utf-8'));
2182
- } catch { /* unreadable yaml — treat as not-configured */ }
2178
+ // Read moflo.yaml ONCE, then run every pre-gate regex against the one string —
2179
+ // every consumer with a moflo.yaml pays a single read here, not one per key.
2180
+ let durableYaml = '';
2181
+ try {
2182
+ if (existsSync(durableYamlPath)) durableYaml = readFileSync(durableYamlPath, 'utf-8');
2183
+ } catch { /* unreadable yaml — treat as not-configured */ }
2184
+ let durableEnabled = Boolean(process.env.MOFLO_DURABLE_PATH) || /^[ \t]*durable_path[ \t]*:/m.test(durableYaml);
2185
+ // Auto-enable across git worktrees — moflo's core multi-worktree competency.
2186
+ // When durable_path isn't explicitly configured, still run the sync IF this
2187
+ // repo has worktrees in play (this checkout is a linked worktree, or the
2188
+ // shared .git has a non-empty worktrees/ registry), unless the user opted out
2189
+ // with `worktree_sharing: false`. A plain single checkout short-circuits on a
2190
+ // cheap fs stat with no module import, so solo users pay ~nothing. The
2191
+ // authoritative derivation lives in durable-sync.ts#resolveDurablePath.
2192
+ if (!durableEnabled) {
2193
+ const optedOut = /^[ \t]*(worktree_sharing|worktreeSharing)[ \t]*:[ \t]*false\b/m.test(durableYaml);
2194
+ if (!optedOut) {
2195
+ try {
2196
+ const dotgit = join(projectRoot, '.git');
2197
+ const st = statSync(dotgit);
2198
+ if (st.isFile()) {
2199
+ // Linked worktree — `.git` file's `gitdir:` points into `.git/worktrees/…`.
2200
+ // A submodule's points into `.git/modules/…` (no worktrees segment) and
2201
+ // must NOT auto-share with its superproject, so gate on the segment.
2202
+ if (/[\\/]worktrees[\\/]/.test(readFileSync(dotgit, 'utf-8'))) durableEnabled = true;
2203
+ } else if (st.isDirectory()) {
2204
+ try {
2205
+ if (readdirSync(join(dotgit, 'worktrees')).length > 0) durableEnabled = true;
2206
+ } catch { /* no worktrees registry (ENOENT) — stay off */ }
2207
+ }
2208
+ } catch { /* no/unreadable .git — not a worktree, stay off */ }
2209
+ }
2183
2210
  }
2184
2211
  const durablePaths = durableEnabled
2185
2212
  ? [
@@ -2191,6 +2218,12 @@ try {
2191
2218
  if (durablePath) {
2192
2219
  const { syncDurableAtSessionStart } = await import(pathToFileURL(durablePath).href);
2193
2220
  const result = await syncDurableAtSessionStart({ projectRoot });
2221
+ if (result?.autoWorktree && (result.seededToLocal > 0 || result.flushedToShared > 0)) {
2222
+ emitMutation(
2223
+ 'auto-shared learnings across git worktrees',
2224
+ 'converging durable learnings for this repo — set memory.worktree_sharing: false to opt out',
2225
+ );
2226
+ }
2194
2227
  if (result?.seededToLocal > 0) {
2195
2228
  emitMutation(
2196
2229
  'seeded shared learnings',
@@ -22,6 +22,7 @@ import * as path from 'path';
22
22
  import { memoryDbPath } from '../services/moflo-paths.js';
23
23
  import { findProjectRoot } from '../services/project-root.js';
24
24
  import { loadMofloConfig } from '../config/moflo-config.js';
25
+ import { resolveDurablePath } from '../services/durable-sync.js';
25
26
  import { errorDetail } from '../shared/utils/error-detail.js';
26
27
  const NAME = 'Shared Full DB';
27
28
  /**
@@ -99,6 +100,18 @@ export async function checkSharedFullDb(deps) {
99
100
  message: `Durable-only shared store configured (${durableRaw}) — safe; structural namespaces stay local.`,
100
101
  };
101
102
  }
103
+ // 3. No explicit durable_path — surface automatic worktree sharing when it's
104
+ // active, so the default-on behavior is visible (and reassuringly safe:
105
+ // the derived store is a dedicated durable.db, never a full moflo.db).
106
+ const auto = resolveDurablePath(root, config);
107
+ if (auto.autoWorktree && auto.path) {
108
+ return {
109
+ name: NAME,
110
+ status: 'pass',
111
+ message: `Automatic worktree learning sharing active (${auto.path}) — durable learnings converge across this ` +
112
+ `repo's git worktrees; structural namespaces stay local. Set memory.worktree_sharing: false to disable.`,
113
+ };
114
+ }
102
115
  return {
103
116
  name: NAME,
104
117
  status: 'pass',
@@ -228,6 +228,13 @@ function mergeConfig(raw, root) {
228
228
  const v = raw.memory?.durable_path ?? raw.memory?.durablePath;
229
229
  return typeof v === 'string' && v.trim().length > 0 ? v.trim() : undefined;
230
230
  })(),
231
+ // #1231 follow-up — opt-out toggle for automatic worktree learning
232
+ // sharing. Only an explicit `false` disables it; absent/true leaves the
233
+ // default auto-behavior on. Detection + derivation live in durable-sync.ts.
234
+ worktree_sharing: (() => {
235
+ const v = raw.memory?.worktree_sharing ?? raw.memory?.worktreeSharing;
236
+ return typeof v === 'boolean' ? v : undefined;
237
+ })(),
231
238
  team_artifact: (() => {
232
239
  const v = raw.memory?.team_artifact ?? raw.memory?.teamArtifact;
233
240
  return typeof v === 'string' && v.trim().length > 0 ? v.trim() : undefined;
@@ -340,11 +347,21 @@ function mergeConfig(raw, root) {
340
347
  * Tries moflo.yaml first, then moflo.config.json.
341
348
  * Returns defaults merged with file contents.
342
349
  */
350
+ /**
351
+ * A fresh defaults-only config for `root`. Deep-clones DEFAULT_CONFIG so a caller
352
+ * mutating the returned config's nested objects (e.g. `cfg.memory.durable_path =
353
+ * …`) can't poison the module-global DEFAULT_CONFIG and leak into every later
354
+ * load — a shallow spread shares the nested `memory`/`hooks`/… references, a
355
+ * latent cross-contamination bug.
356
+ */
357
+ function freshDefaultConfig(root) {
358
+ return { ...structuredClone(DEFAULT_CONFIG), project: { name: path.basename(root) } };
359
+ }
343
360
  export function loadMofloConfig(projectRoot) {
344
361
  const root = projectRoot || process.cwd();
345
362
  const configFile = findConfigFile(root);
346
363
  if (!configFile) {
347
- return { ...DEFAULT_CONFIG, project: { name: path.basename(root) } };
364
+ return freshDefaultConfig(root);
348
365
  }
349
366
  try {
350
367
  const content = fs.readFileSync(configFile.path, 'utf-8');
@@ -360,12 +377,12 @@ export function loadMofloConfig(projectRoot) {
360
377
  throw new Error('js-yaml is required to parse moflo.yaml. Install it: npm install js-yaml');
361
378
  }
362
379
  if (!raw || typeof raw !== 'object') {
363
- return { ...DEFAULT_CONFIG, project: { name: path.basename(root) } };
380
+ return freshDefaultConfig(root);
364
381
  }
365
382
  return mergeConfig(raw, root);
366
383
  }
367
384
  catch {
368
- return { ...DEFAULT_CONFIG, project: { name: path.basename(root) } };
385
+ return freshDefaultConfig(root);
369
386
  }
370
387
  }
371
388
  /**
@@ -48,6 +48,8 @@ export const SKILLS_MAP = {
48
48
  'memory-patterns',
49
49
  'memory-optimization',
50
50
  'vector-search',
51
+ 'memory-worktree', // guided memory.durable_path setup (same-machine worktrees)
52
+ 'memory-team', // guided memory.team_artifact setup + PR pre-commit hook
51
53
  ],
52
54
  spells: [
53
55
  'spell-builder',
@@ -33,6 +33,8 @@
33
33
  *
34
34
  * @module cli/services/durable-sync
35
35
  */
36
+ import * as path from 'path';
37
+ import * as fs from 'fs';
36
38
  import { findProjectRoot } from './project-root.js';
37
39
  import { stableAbsolute, pickConfiguredPath } from './configured-path.js';
38
40
  import { memoryDbPath } from './moflo-paths.js';
@@ -40,24 +42,127 @@ import { loadMofloConfig } from '../config/moflo-config.js';
40
42
  import { cherryPickLearningsFromLegacy, DURABLE_NAMESPACES, isDurableNamespace, } from './cherry-pick-learnings.js';
41
43
  export { isDurableNamespace };
42
44
  /**
43
- * Resolve the configured durable-store path to an absolute path, or `null`
44
- * when the feature is off (no env, no `memory.durable_path`) or misconfigured
45
- * (points at this project's own local DB syncing a DB with itself is a
46
- * no-op, so we disable rather than thrash).
45
+ * Read `<projectRoot>/.git` ONCE and classify the checkout for worktree sharing,
46
+ * using filesystem reads only (no `git` subprocess, so it's PATH-independent and
47
+ * cross-platform, Rule #1). Returns the shared `.git` common dir plus whether
48
+ * THIS checkout is a linked worktree — or `null` when it's not a git repo root,
49
+ * `.git` is unreadable, or it's a **submodule**.
47
50
  *
48
- * Precedence: `MOFLO_DURABLE_PATH` env > `memory.durable_path` (moflo.yaml).
49
- * Relative values resolve against the project root. Path-pick + symlink-stable
50
- * identity live in {@link pickConfiguredPath}/{@link stableAbsolute}.
51
+ * - Primary checkout: `<root>/.git` is a directory → that IS the common dir.
52
+ * - Linked worktree: `<root>/.git` is a FILE (`gitdir: <main>/.git/worktrees/<id>`)
53
+ * whose `commondir` file points back to the shared `.git`.
54
+ * - Submodule: `<root>/.git` is also a FILE, but `gitdir: <super>/.git/modules/<name>`
55
+ * with NO `commondir`. It is NOT a worktree and must NEVER share a durable
56
+ * store with its superproject — excluded via the `worktrees/` segment +
57
+ * `commondir` checks (real worktrees have both; submodules have neither).
58
+ *
59
+ * The primary tree and all its worktrees resolve to the SAME common dir, which
60
+ * is what lets an auto-derived shared store converge with zero config.
61
+ */
62
+ function readGitLayout(projectRoot) {
63
+ const dotgit = path.join(projectRoot, '.git');
64
+ let st;
65
+ try {
66
+ st = fs.statSync(dotgit);
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ if (st.isDirectory())
72
+ return { commonDir: dotgit, linkedWorktree: false };
73
+ if (!st.isFile())
74
+ return null;
75
+ try {
76
+ const m = fs.readFileSync(dotgit, 'utf-8').match(/gitdir:\s*(.+)/);
77
+ if (!m)
78
+ return null;
79
+ let gitdir = m[1].trim();
80
+ if (!path.isAbsolute(gitdir))
81
+ gitdir = path.resolve(projectRoot, gitdir);
82
+ // Linked worktree only — exclude submodules (`.../modules/...`, no commondir).
83
+ if (!/[\\/]worktrees[\\/]/.test(gitdir))
84
+ return null;
85
+ let cd = fs.readFileSync(path.join(gitdir, 'commondir'), 'utf-8').trim();
86
+ if (!path.isAbsolute(cd))
87
+ cd = path.resolve(gitdir, cd);
88
+ return { commonDir: path.resolve(cd), linkedWorktree: true };
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ }
94
+ /** The shared git common dir for `projectRoot`, or `null` — see {@link readGitLayout}. */
95
+ export function resolveGitCommonDir(projectRoot) {
96
+ return readGitLayout(projectRoot)?.commonDir ?? null;
97
+ }
98
+ /**
99
+ * Decide whether to AUTO-enable durable sharing for `projectRoot`, returning the
100
+ * shared `.git` common dir to derive the store under — or `null` to stay off.
101
+ *
102
+ * Activates ONLY when git worktrees are actually in play, so a plain single
103
+ * checkout (and any submodule) writes nothing and behaves byte-identically to
104
+ * before. "In play" = this checkout is itself a linked worktree, OR the shared
105
+ * `.git` has a non-empty `worktrees/` registry (a sibling worktree exists).
106
+ */
107
+ export function detectWorktreeCommonDir(projectRoot) {
108
+ const layout = readGitLayout(projectRoot);
109
+ if (!layout)
110
+ return null;
111
+ if (layout.linkedWorktree)
112
+ return layout.commonDir;
113
+ // Primary checkout — activate only if it has registered linked worktrees.
114
+ try {
115
+ if (fs.readdirSync(path.join(layout.commonDir, 'worktrees')).length > 0)
116
+ return layout.commonDir;
117
+ }
118
+ catch {
119
+ /* no worktrees registry (ENOENT) or unreadable — treat as no worktrees */
120
+ }
121
+ return null;
122
+ }
123
+ /**
124
+ * Resolve the durable-store path to an absolute path, or `null` when there's
125
+ * nothing to sync.
126
+ *
127
+ * Precedence:
128
+ * 1. Explicit config — `MOFLO_DURABLE_PATH` env > `memory.durable_path`
129
+ * (moflo.yaml). Relative values resolve against the project root.
130
+ * 2. Auto worktree sharing — when NOTHING is explicitly configured, moflo's
131
+ * multi-worktree competency kicks in: if this checkout is part of a repo
132
+ * with git worktrees in play, derive a shared store at
133
+ * `<git-common-dir>/moflo/durable.db`. Every worktree of the repo resolves
134
+ * to the same path, so their learnings converge with zero config. Opt out
135
+ * with `memory.worktree_sharing: false`. A single checkout (no worktrees)
136
+ * derives nothing and stays byte-identical to before.
137
+ *
138
+ * A store that aliases this project's own local DB is rejected (syncing a DB
139
+ * with itself is a no-op). Path-pick + symlink-stable identity live in
140
+ * {@link pickConfiguredPath}/{@link stableAbsolute}.
51
141
  */
52
142
  export function resolveDurablePath(projectRoot = findProjectRoot(), config) {
53
- const abs = pickConfiguredPath(process.env.MOFLO_DURABLE_PATH, (config ?? loadMofloConfig(projectRoot)).memory.durable_path, projectRoot);
54
- if (!abs)
55
- return { path: null, skipped: 'not-configured' };
56
- // Self-reference guard — never let the shared store alias the local DB.
57
- if (stableAbsolute(abs) === stableAbsolute(memoryDbPath(projectRoot))) {
58
- return { path: null, skipped: 'same-as-local' };
143
+ const cfg = config ?? loadMofloConfig(projectRoot);
144
+ const local = () => stableAbsolute(memoryDbPath(projectRoot));
145
+ const abs = pickConfiguredPath(process.env.MOFLO_DURABLE_PATH, cfg.memory.durable_path, projectRoot);
146
+ if (abs) {
147
+ // Self-reference guard never let the shared store alias the local DB.
148
+ if (stableAbsolute(abs) === local()) {
149
+ return { path: null, skipped: 'same-as-local' };
150
+ }
151
+ return { path: abs };
59
152
  }
60
- return { path: abs };
153
+ // No explicit config → auto-share across git worktrees unless opted out.
154
+ if (cfg.memory.worktree_sharing !== false) {
155
+ const commonDir = detectWorktreeCommonDir(projectRoot);
156
+ if (commonDir) {
157
+ const derived = path.join(commonDir, 'moflo', 'durable.db');
158
+ // Paranoia guard — a derived path should never alias the local DB, but a
159
+ // pathological `.git` layout shouldn't turn into a self-sync.
160
+ if (stableAbsolute(derived) !== local()) {
161
+ return { path: derived, autoWorktree: true };
162
+ }
163
+ }
164
+ }
165
+ return { path: null, skipped: 'not-configured' };
61
166
  }
62
167
  /**
63
168
  * Seed durable rows from the shared store into this project's local DB.
@@ -77,6 +182,14 @@ export async function seedDurableFromShared(projectRoot, durablePath) {
77
182
  * Local is the source, the shared store the target (created on first flush).
78
183
  */
79
184
  export async function flushDurableToShared(projectRoot, durablePath) {
185
+ // Ensure the parent dir exists — auto-derived worktree stores live under
186
+ // `<git-common-dir>/moflo/`, which won't exist on the very first flush.
187
+ try {
188
+ fs.mkdirSync(path.dirname(durablePath), { recursive: true });
189
+ }
190
+ catch {
191
+ /* a real open failure surfaces from cherry-pick below */
192
+ }
80
193
  return cherryPickLearningsFromLegacy({
81
194
  projectRoot,
82
195
  legacyPaths: [memoryDbPath(projectRoot)],
@@ -96,7 +209,7 @@ export async function flushDurableToShared(projectRoot, durablePath) {
96
209
  */
97
210
  export async function syncDurableAtSessionStart(opts = {}) {
98
211
  const projectRoot = opts.projectRoot ?? findProjectRoot();
99
- const { path: durablePath, skipped } = resolveDurablePath(projectRoot, opts.config);
212
+ const { path: durablePath, skipped, autoWorktree } = resolveDurablePath(projectRoot, opts.config);
100
213
  if (!durablePath) {
101
214
  return { durablePath: null, skipped, flushedToShared: 0, seededToLocal: 0 };
102
215
  }
@@ -106,6 +219,7 @@ export async function syncDurableAtSessionStart(opts = {}) {
106
219
  const seed = await seedDurableFromShared(projectRoot, durablePath);
107
220
  return {
108
221
  durablePath,
222
+ autoWorktree: autoWorktree ?? false,
109
223
  flushedToShared: flush.copied,
110
224
  seededToLocal: seed.copied,
111
225
  };
@@ -2,5 +2,5 @@
2
2
  * Auto-generated by build. Do not edit manually.
3
3
  * Source of truth: root package.json → scripts/sync-version.mjs
4
4
  */
5
- export const VERSION = '4.10.29-rc.6';
5
+ export const VERSION = '4.11.0';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.10.29-rc.6",
3
+ "version": "4.11.0",
4
4
  "description": "MoFlo — AI agent orchestration for Claude Code. A standalone, opinionated toolkit with semantic memory, learned routing, gates, spells, and the /flo issue-execution skill.",
5
5
  "main": "dist/src/cli/index.js",
6
6
  "type": "module",
@@ -94,7 +94,7 @@
94
94
  "@typescript-eslint/eslint-plugin": "^7.18.0",
95
95
  "@typescript-eslint/parser": "^7.18.0",
96
96
  "eslint": "^8.0.0",
97
- "moflo": "^4.10.29-rc.5",
97
+ "moflo": "^4.10.29",
98
98
  "tsx": "^4.21.0",
99
99
  "typescript": "^5.9.3",
100
100
  "vitest": "^4.0.0"