moflo 4.9.36 → 4.10.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.
- package/.claude/guidance/shipped/moflo-agent-rules.md +12 -0
- package/.claude/guidance/shipped/moflo-memory-protocol.md +34 -0
- package/.claude/guidance/shipped/moflo-memorydb-maintenance.md +22 -11
- package/.claude/guidance/shipped/moflo-root-cause-discipline.md +47 -0
- package/.claude/guidance/shipped/moflo-subagents.md +4 -0
- package/.claude/helpers/gate.cjs +3 -3
- package/.claude/helpers/statusline.cjs +69 -33
- package/.claude/helpers/subagent-bootstrap.json +1 -1
- package/.claude/helpers/subagent-start.cjs +1 -1
- package/.claude/skills/eldar/SKILL.md +8 -0
- package/bin/build-embeddings.mjs +6 -20
- package/bin/cli.js +5 -0
- package/bin/gate.cjs +3 -3
- package/bin/generate-code-map.mjs +4 -24
- package/bin/hooks.mjs +3 -12
- package/bin/index-all.mjs +3 -13
- package/bin/index-guidance.mjs +59 -119
- package/bin/index-patterns.mjs +6 -24
- package/bin/index-tests.mjs +4 -23
- package/bin/lib/db-repair.mjs +4 -25
- package/bin/lib/get-backend.mjs +306 -0
- package/bin/lib/incremental-write.mjs +27 -7
- package/bin/lib/moflo-paths.mjs +64 -4
- package/bin/lib/suppress-sqlite-warning.mjs +57 -0
- package/bin/migrations/knowledge-purge.mjs +7 -8
- package/bin/migrations/knowledge-to-learnings.mjs +7 -9
- package/bin/migrations/purge-doc-entries.mjs +52 -0
- package/bin/migrations/strip-context-preambles.mjs +95 -0
- package/bin/run-migrations.mjs +1 -10
- package/bin/semantic-search.mjs +11 -19
- package/bin/session-start-launcher.mjs +102 -100
- package/bin/simplify-classify.cjs +38 -17
- package/dist/src/cli/commands/daemon.js +38 -11
- package/dist/src/cli/commands/doctor-checks-coverage-truth.js +136 -0
- package/dist/src/cli/commands/doctor-checks-memory-access.js +244 -5
- package/dist/src/cli/commands/doctor-checks-memory.js +13 -18
- package/dist/src/cli/commands/doctor-checks-version-skew.js +94 -0
- package/dist/src/cli/commands/doctor-checks-writers-audit.js +170 -0
- package/dist/src/cli/commands/doctor-embedding-hygiene.js +3 -15
- package/dist/src/cli/commands/doctor-fixes.js +30 -0
- package/dist/src/cli/commands/doctor-registry.js +14 -0
- package/dist/src/cli/commands/doctor.js +1 -1
- package/dist/src/cli/commands/embeddings.js +17 -22
- package/dist/src/cli/commands/memory.js +54 -75
- package/dist/src/cli/embeddings/persistent-cache.js +44 -83
- package/dist/src/cli/init/claudemd-generator.js +4 -0
- package/dist/src/cli/init/moflo-init.js +40 -0
- package/dist/src/cli/mcp-tools/memory-tools.js +177 -32
- package/dist/src/cli/memory/bridge-core.js +256 -30
- package/dist/src/cli/memory/bridge-entries.js +76 -8
- package/dist/src/cli/memory/controller-registry.js +7 -2
- package/dist/src/cli/memory/controllers/batch-operations.js +5 -1
- package/dist/src/cli/memory/controllers/hierarchical-memory.js +7 -2
- package/dist/src/cli/memory/controllers/mutation-guard.js +22 -2
- package/dist/src/cli/memory/daemon-backend.js +400 -0
- package/dist/src/cli/memory/daemon-write-client.js +192 -15
- package/dist/src/cli/memory/database-provider.js +57 -40
- package/dist/src/cli/memory/hnsw-persistence.js +6 -8
- package/dist/src/cli/memory/index.js +0 -1
- package/dist/src/cli/memory/memory-bridge.js +40 -8
- package/dist/src/cli/memory/memory-initializer.js +286 -220
- package/dist/src/cli/memory/rvf-migration.js +25 -11
- package/dist/src/cli/memory/sqlite-backend.js +573 -0
- package/dist/src/cli/memory/suppress-sqlite-warning.js +49 -0
- package/dist/src/cli/services/cherry-pick-learnings.js +32 -21
- package/dist/src/cli/services/daemon-dashboard.js +13 -1
- package/dist/src/cli/services/daemon-lock.js +58 -1
- package/dist/src/cli/services/daemon-memory-rpc.js +245 -10
- package/dist/src/cli/services/embeddings-migration.js +9 -12
- package/dist/src/cli/services/ephemeral-namespace-purge.js +6 -11
- package/dist/src/cli/services/learning-service.js +12 -20
- package/dist/src/cli/services/project-root.js +69 -9
- package/dist/src/cli/services/soft-delete-purge.js +6 -11
- package/dist/src/cli/services/sqljs-migration-store.js +4 -1
- package/dist/src/cli/services/subagent-bootstrap.js +1 -1
- package/dist/src/cli/shared/events/event-store.js +26 -55
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -4
- package/dist/src/cli/memory/sqljs-backend.js +0 -643
|
@@ -24,6 +24,18 @@
|
|
|
24
24
|
|
|
25
25
|
**Search `tests` when looking for test coverage** of a function, module, or behavior — it indexes the test tree separately so you can pinpoint specs without grepping the whole repo.
|
|
26
26
|
|
|
27
|
+
### Traverse Chunks, Don't Bulk-Retrieve
|
|
28
|
+
|
|
29
|
+
Search returns chunked guidance with a compact `navigation` crumb (`parentDoc`, `prevChunk`, `nextChunk`, `chunkTitle`). Use it:
|
|
30
|
+
|
|
31
|
+
| Want | Use |
|
|
32
|
+
|------|-----|
|
|
33
|
+
| Adjacent / sibling / hierarchical context | `mcp__moflo__memory_get_neighbors` |
|
|
34
|
+
| Full content of one chunk | `mcp__moflo__memory_retrieve` (returns full nav for further traversal) |
|
|
35
|
+
| Whole source doc | `Read` `parentPath` from any chunk's nav |
|
|
36
|
+
|
|
37
|
+
Full protocol: `.claude/guidance/moflo-memory-protocol.md`. Don't retrieve every search hit blindly.
|
|
38
|
+
|
|
27
39
|
### Tool Selection (MCP-first)
|
|
28
40
|
|
|
29
41
|
| Tool | Purpose |
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# MoFlo Memory Protocol — Search, Traverse, Retrieve
|
|
2
|
+
|
|
3
|
+
**Purpose:** How to use moflo's chunked memory effectively. Search returns navigable chunks — traverse the chunk graph, do not bulk-retrieve every hit.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Rule (MUST)
|
|
8
|
+
|
|
9
|
+
When a search hit carries a non-null `navigation`, you MUST call `mcp__moflo__memory_get_neighbors` to traverse — not `mcp__moflo__memory_retrieve`. `memory_retrieve` is for fetching one specific chunk in full, or for non-chunk entries where `navigation` is null. Bulk-retrieving search hits defeats the chunking architecture.
|
|
10
|
+
|
|
11
|
+
## Decision Table
|
|
12
|
+
|
|
13
|
+
| You want | Use | Why |
|
|
14
|
+
|----------|-----|-----|
|
|
15
|
+
| Find an entry-point | `mcp__moflo__memory_search` | Returns chunk hits with compact `navigation` (parentDoc, prev/next, chunkTitle) |
|
|
16
|
+
| Adjacent context (1 chunk over) | `mcp__moflo__memory_get_neighbors` `{ key, include: ['prev','next'] }` | One round-trip, returns shaped entries with full nav |
|
|
17
|
+
| Same-section peers (h2/h3 family) | `memory_get_neighbors` `{ include: ['siblings'] }` or `['parent','children']` | Hierarchical traversal — cheaper than re-searching |
|
|
18
|
+
| Full content of one chunk | `mcp__moflo__memory_retrieve` `{ key }` | Returns full nav object for further traversal |
|
|
19
|
+
| Whole source doc when truly needed | `Read` `parentPath` from any chunk's nav | Disk read is cheaper than re-indexed `doc-*` |
|
|
20
|
+
|
|
21
|
+
## Anti-Patterns
|
|
22
|
+
|
|
23
|
+
| Don't | Do instead |
|
|
24
|
+
|-------|-----------|
|
|
25
|
+
| Retrieve every search hit blindly | Traverse via `memory_get_neighbors` when `navigation` is present — bulk `memory_retrieve` per hit is a protocol violation |
|
|
26
|
+
| Open the source file when a chunk would do | Stay in the chunk graph; `Read` `parentPath` only for the rare full-doc case |
|
|
27
|
+
| Search again for a key you already have | `memory_retrieve` or `memory_get_neighbors` directly |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## See Also
|
|
32
|
+
|
|
33
|
+
- `.claude/guidance/moflo-agent-rules.md` § Memory-First Protocol — namespaces, query examples, MCP-first tool selection
|
|
34
|
+
- `.claude/guidance/moflo-memory-strategy.md` — How chunking, embeddings, and the RAG index work
|
|
@@ -7,10 +7,26 @@
|
|
|
7
7
|
## Database Location
|
|
8
8
|
|
|
9
9
|
- **Path:** `.moflo/moflo.db` — canonical location
|
|
10
|
-
- **Engine:** sql.js (WASM
|
|
10
|
+
- **Engine:** `node:sqlite` (built into Node 22+) running in WAL mode — multi-process safe via POSIX/Windows file locks. The sql.js (WASM) backend is no longer used and the npm dep is being removed.
|
|
11
11
|
- **Single table:** `memory_entries` — stores content, embeddings, and metadata in one table
|
|
12
|
+
- **WAL sidecars:** `.moflo/moflo.db-wal` + `.moflo/moflo.db-shm` appear next to `moflo.db` after the first write. Don't commit them; don't manually delete them while moflo is running.
|
|
12
13
|
- **Legacy path:** `.swarm/memory.db` — older consumers may still have this. The launcher migrates it to `.moflo/moflo.db` automatically on session start. After migration the legacy file is renamed `.swarm/memory.db.bak` and is safe to delete once you've verified the canonical DB is healthy (`flo doctor`).
|
|
13
14
|
|
|
15
|
+
## Network filesystem caveat
|
|
16
|
+
|
|
17
|
+
SQLite's multi-process safety relies on POSIX/Windows file locking and on shared-memory WAL sidecars. **Neither works reliably on network-mounted filesystems** — NFS, SMB/CIFS, sshfs, and some FUSE mounts silently break advisory locks and refuse to map the `.db-shm` shared-memory region. If `.moflo/moflo.db` lives on a network mount, two moflo processes can clobber each other's writes and you may see partial-row corruption.
|
|
18
|
+
|
|
19
|
+
On first open against a non-WAL-capable filesystem moflo emits a one-line stderr warning naming the path:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
[moflo] WARNING: SQLite journal_mode=delete on /mnt/network/proj/.moflo/moflo.db (WAL not active).
|
|
23
|
+
If this directory is on NFS/SMB or another network filesystem, POSIX advisory locks are unreliable
|
|
24
|
+
and concurrent moflo processes can corrupt the database. Move the project to a local disk to
|
|
25
|
+
restore multi-process safety.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The warning is deduplicated per path per process. If you see it: move the project (or just the `.moflo/` directory) onto a local disk. There is no in-product workaround — single-machine SQLite cannot be made safe over a remote filesystem.
|
|
29
|
+
|
|
14
30
|
## Schema
|
|
15
31
|
|
|
16
32
|
```sql
|
|
@@ -73,33 +89,28 @@ node bin/build-embeddings.mjs
|
|
|
73
89
|
|
|
74
90
|
## Purging a Namespace
|
|
75
91
|
|
|
76
|
-
Use
|
|
92
|
+
Use the backend factory so you get the same engine + WAL semantics every other writer uses:
|
|
77
93
|
|
|
78
94
|
```js
|
|
79
95
|
node --input-type=module -e "
|
|
80
|
-
import
|
|
81
|
-
import { readFileSync, writeFileSync } from 'fs';
|
|
96
|
+
import { openBackend } from 'moflo/bin/lib/get-backend.mjs';
|
|
82
97
|
|
|
83
|
-
const
|
|
84
|
-
const buf = readFileSync('.moflo/moflo.db');
|
|
85
|
-
const db = new SQL.Database(buf);
|
|
98
|
+
const db = await openBackend(process.cwd(), { create: false });
|
|
86
99
|
|
|
87
|
-
// Check counts before
|
|
88
100
|
const before = db.exec('SELECT namespace, COUNT(*) FROM memory_entries GROUP BY namespace');
|
|
89
101
|
console.log('BEFORE:', JSON.stringify(before[0]?.values));
|
|
90
102
|
|
|
91
|
-
// Purge a namespace (embeddings are inline — no separate table to clean)
|
|
92
103
|
db.run(\"DELETE FROM memory_entries WHERE namespace = 'code-map'\");
|
|
93
104
|
|
|
94
105
|
const after = db.exec('SELECT namespace, COUNT(*) FROM memory_entries GROUP BY namespace');
|
|
95
106
|
console.log('AFTER:', JSON.stringify(after[0]?.values));
|
|
96
107
|
|
|
97
|
-
|
|
108
|
+
db.save();
|
|
98
109
|
db.close();
|
|
99
110
|
"
|
|
100
111
|
```
|
|
101
112
|
|
|
102
|
-
|
|
113
|
+
Under node:sqlite the `db.save()` call is a no-op (WAL persists incrementally), but the factory keeps the API parity so the same snippet works against any backend the factory currently knows about. Concurrent writers serialize through WAL — running this while the daemon is up no longer clobbers anything — but if you want to be defensive, `flo daemon stop` before purging is still the cleanest sequence.
|
|
103
114
|
|
|
104
115
|
After purging, reindex the namespace and rebuild embeddings:
|
|
105
116
|
```bash
|
|
@@ -91,6 +91,35 @@ When you find that the test is the actual problem: change the test, document why
|
|
|
91
91
|
|
|
92
92
|
---
|
|
93
93
|
|
|
94
|
+
## Never Mask a Production Bug with Test-Side Workarounds
|
|
95
|
+
|
|
96
|
+
**When a test catches a real bug, fix the bug — never the test.** A test that passes by avoiding the broken path is a lie about coverage.
|
|
97
|
+
|
|
98
|
+
| Symptom test exposed | Right move | Wrong move (firing-offense pattern) |
|
|
99
|
+
|---------------------|-----------|-------------------------------------|
|
|
100
|
+
| Race between writers | Coordinate the writers | Reorder test so the race window closes before the assertion |
|
|
101
|
+
| Data corruption from a known writer | Find and fix the writer | Pre-seed the corrupted state so the assertion no longer fires |
|
|
102
|
+
| Stale cache served to readers | Invalidate the cache properly | Add `sleep` / `refresh()` in test to mask staleness |
|
|
103
|
+
| Cross-process clobber on a shared file | Single-writer ownership or atomic write | Order the test so only one writer runs |
|
|
104
|
+
| Missing precondition guard in prod | Add the guard | Have test setup satisfy the guard's precondition |
|
|
105
|
+
|
|
106
|
+
**Smell test:** read the test setup as a description of production behavior. If steps appear that production code does not perform, the test is mocking around a real bug.
|
|
107
|
+
|
|
108
|
+
### Case Study: #1053 Memory Traversal
|
|
109
|
+
|
|
110
|
+
> "Healer: probeMemoryGetNeighbors() runs first in checkMemoryAccessFunctional so the bridge instantiates after the seed lands (sql.js whole-DB writeback clobbers external file writes once the in-memory snapshot warms)." — PR #1053 commit
|
|
111
|
+
|
|
112
|
+
Diagnosis correct; fix wrong. The clobber stayed in production. `moflo@4.9.37` shipped two regressions to every consumer (migration-induced embedding loss + `memory_store` silent drop) because the suite was engineered around the failure it had just detected.
|
|
113
|
+
|
|
114
|
+
### Self-Check Before Merging Any Test-Only Change
|
|
115
|
+
|
|
116
|
+
1. **Did the test fail first?** Name the production bug it caught.
|
|
117
|
+
2. **Did my fix change only the test?** If yes, the runtime contract must be the bug — document why in the commit message.
|
|
118
|
+
3. **Would a real user still hit this in production?** If yes, fix the bug, not the test.
|
|
119
|
+
4. **Does test code comment *why* it sleeps / reorders / mocks?** That comment usually names a production bug — move the fix to production.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
94
123
|
## Concrete Example: #1017 Hive-Mind Shutdown
|
|
95
124
|
|
|
96
125
|
This is the canonical case study for this guidance — and it has a second-order lesson that makes it even more useful.
|
|
@@ -141,11 +170,29 @@ Reviewers should reject — not just question — PRs that show patch-on-patch s
|
|
|
141
170
|
| New fix adds a layer without removing one | Ask: "what was wrong with the prior layer? why does it stay?" |
|
|
142
171
|
| Comment in new code says "for safety" or "just in case" | Ask: "what specific failure is this preventing? cite the line that produces it" |
|
|
143
172
|
| The PR description says "this should fix the flake" without a deterministic repro | Ask: "what was the actual root cause? the writeup doesn't name it" |
|
|
173
|
+
| Commit message names a production bug, then describes test setup that avoids it | **Reject.** The bug stays live in production. Demand a fix at the production-code locus, not at the test. |
|
|
144
174
|
|
|
145
175
|
These questions are not pedantic. They are the difference between fixing a bug and growing the surface area of bugs.
|
|
146
176
|
|
|
147
177
|
---
|
|
148
178
|
|
|
179
|
+
## Scoping a Fix Issue — Kill the Class, Not the Instance
|
|
180
|
+
|
|
181
|
+
**When you file an issue for a bug, scope it to eliminate the bug class, not patch the visible instance.** Partial scope guarantees a follow-up epic.
|
|
182
|
+
|
|
183
|
+
| Posture | Right | Wrong |
|
|
184
|
+
|---------|-------|-------|
|
|
185
|
+
| Scope | Name the bug class; list every writer / caller / consumer that can produce it | Name only the surface where the symptom showed |
|
|
186
|
+
| Strategy | Commit to one approach in the body — "we will do X" | "Pick one of: option A or option B" |
|
|
187
|
+
| Audit | Enumerate every member of the class (every writer, every caller, every migration) | Patch the one path that broke today |
|
|
188
|
+
| Acceptance | Every original failure mode reproduces today and is gone after the epic; a new violator triggers a loud test | Symptom no longer reproduces |
|
|
189
|
+
| Tests | Mirror the production failure shape (multi-process, cross-writer, time-coupled) | In-process unit tests only |
|
|
190
|
+
| Follow-ups | None — anything that would be a follow-up is added to scope now | "We'll address this in a separate issue" |
|
|
191
|
+
|
|
192
|
+
If the epic body says "we'll figure out X in a follow-up", the scope is wrong — fold X in or explain why it cannot be in scope (e.g., depends on a release boundary).
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
149
196
|
## How to Apply When You Are Stuck
|
|
150
197
|
|
|
151
198
|
If you genuinely cannot find the root cause after stepping back:
|
|
@@ -18,6 +18,10 @@ CLI fallback when MCP is unavailable: `npx flo memory search --query "..." --nam
|
|
|
18
18
|
|
|
19
19
|
The full namespace reference, query examples by domain, and tool catalog live in `.claude/guidance/moflo-agent-rules.md` § Memory-First Protocol — read that next.
|
|
20
20
|
|
|
21
|
+
### Traverse, don't bulk-retrieve
|
|
22
|
+
|
|
23
|
+
Search hits carry a compact `navigation` crumb. For adjacent/sibling/hierarchical context, call `mcp__moflo__memory_get_neighbors` (one round-trip) instead of retrieving every hit. Full protocol: `.claude/guidance/moflo-memory-protocol.md`.
|
|
24
|
+
|
|
21
25
|
---
|
|
22
26
|
|
|
23
27
|
## Step 2: Apply Universal Agent Rules
|
package/.claude/helpers/gate.cjs
CHANGED
|
@@ -307,7 +307,7 @@ switch (command) {
|
|
|
307
307
|
process.stdout.write('REMINDER: Use TaskCreate before spawning agents. Task tool is blocked until then.\n');
|
|
308
308
|
}
|
|
309
309
|
if (config.memory_first && s.memoryRequired && !s.memorySearched) {
|
|
310
|
-
process.stdout.write('REMINDER: Search memory (mcp__moflo__memory_search) before spawning agents
|
|
310
|
+
process.stdout.write('REMINDER: Search memory (mcp__moflo__memory_search) before spawning agents. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n');
|
|
311
311
|
}
|
|
312
312
|
if (s.lastNamespaceHint) {
|
|
313
313
|
// Per-actor single-shot. Each session_id gets the hint at most once per
|
|
@@ -376,7 +376,7 @@ switch (command) {
|
|
|
376
376
|
if (!s.memoryRequired || isMemorySearchedFor(s)) break;
|
|
377
377
|
var target = (process.env.TOOL_INPUT_pattern || '') + ' ' + (process.env.TOOL_INPUT_path || '');
|
|
378
378
|
if (EXEMPT.some(function(p) { return target.indexOf(p) >= 0; })) break;
|
|
379
|
-
process.stderr.write('BLOCKED: Search memory before exploring files. Use mcp__moflo__memory_search
|
|
379
|
+
process.stderr.write('BLOCKED: Search memory before exploring files. Use mcp__moflo__memory_search. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n');
|
|
380
380
|
process.exit(2);
|
|
381
381
|
}
|
|
382
382
|
case 'check-before-read': {
|
|
@@ -386,7 +386,7 @@ switch (command) {
|
|
|
386
386
|
var fp = process.env.TOOL_INPUT_file_path || '';
|
|
387
387
|
var isGuidance = fp.indexOf('.claude/guidance/') >= 0 || fp.indexOf('.claude\\guidance\\') >= 0;
|
|
388
388
|
if (!isGuidance && EXEMPT.some(function(p) { return fp.indexOf(p) >= 0; })) break;
|
|
389
|
-
process.stderr.write('BLOCKED: Search memory before reading files. Use mcp__moflo__memory_search
|
|
389
|
+
process.stderr.write('BLOCKED: Search memory before reading files. Use mcp__moflo__memory_search. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n');
|
|
390
390
|
process.exit(2);
|
|
391
391
|
}
|
|
392
392
|
case 'record-task-created': {
|
|
@@ -485,58 +485,81 @@ function getHooksStatus() {
|
|
|
485
485
|
}
|
|
486
486
|
|
|
487
487
|
// Embeddings stats — reads from cache file written by embedding/memory ops.
|
|
488
|
-
// No subprocess spawning.
|
|
488
|
+
// No subprocess spawning.
|
|
489
|
+
//
|
|
490
|
+
// Reconciliation (epic #1054.S5 / #1059): the cached vectorCount is only
|
|
491
|
+
// trusted when (a) the daemon owns the lock and (b) the daemon's reported
|
|
492
|
+
// `version` matches the installed package version. Either failing means the
|
|
493
|
+
// cache may have been written by a clobbered or pre-upgrade writer, so the
|
|
494
|
+
// number is flagged `reconciled: false` and the render path shows `?` instead
|
|
495
|
+
// of a value that can't be vouched for. The prior `dbSize / 2` heuristic
|
|
496
|
+
// fallback was exactly the unreconciled fabrication the story prohibits, so
|
|
497
|
+
// it's removed.
|
|
489
498
|
function getEmbeddingsStats() {
|
|
490
499
|
let vectorCount = 0;
|
|
491
500
|
let dbSizeKB = 0;
|
|
492
501
|
let namespaces = 0;
|
|
493
502
|
let hasHnsw = false;
|
|
503
|
+
let reconciled = false;
|
|
504
|
+
let staleReason = null;
|
|
494
505
|
|
|
495
|
-
// Read cached stats (written by memory store/embed/rebuild commands).
|
|
496
|
-
// `.moflo/` is the canonical location post-#699; the `.swarm/` parallel
|
|
497
|
-
// write was retired in #714 because every writer (bridge-core,
|
|
498
|
-
// memory-initializer, build-embeddings) now writes here, so the legacy
|
|
499
|
-
// fallback can only ever be stale — exactly the divergence trap that
|
|
500
|
-
// produced #639.
|
|
501
506
|
const cached = readJSON(path.join(CWD, '.moflo', 'vector-stats.json'));
|
|
502
507
|
if (cached && typeof cached.vectorCount === 'number') {
|
|
503
508
|
vectorCount = cached.vectorCount;
|
|
504
509
|
dbSizeKB = cached.dbSizeKB || 0;
|
|
505
510
|
namespaces = cached.namespaces || 0;
|
|
506
511
|
hasHnsw = cached.hasHnsw || false;
|
|
507
|
-
return { vectorCount, dbSizeKB, namespaces, hasHnsw };
|
|
508
|
-
}
|
|
509
512
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
vectorCount = Math.floor(dbSizeKB / 2);
|
|
523
|
-
namespaces = 1;
|
|
524
|
-
break;
|
|
513
|
+
// Reconciliation against the daemon (single-writer authority post-#1054).
|
|
514
|
+
const daemonLock = readJSON(path.join(CWD, '.moflo', 'daemon.lock'));
|
|
515
|
+
const installedPkg = readJSON(path.join(CWD, 'node_modules', 'moflo', 'package.json'))
|
|
516
|
+
|| readJSON(path.join(CWD, 'package.json'));
|
|
517
|
+
if (!daemonLock || typeof daemonLock.pid !== 'number') {
|
|
518
|
+
staleReason = 'daemon-down';
|
|
519
|
+
} else if (!installedPkg || typeof installedPkg.version !== 'string') {
|
|
520
|
+
staleReason = 'pkg-unknown';
|
|
521
|
+
} else if (daemonLock.version !== installedPkg.version) {
|
|
522
|
+
staleReason = 'version-skew';
|
|
523
|
+
} else {
|
|
524
|
+
reconciled = true;
|
|
525
525
|
}
|
|
526
|
+
} else {
|
|
527
|
+
staleReason = 'cache-missing';
|
|
526
528
|
}
|
|
527
529
|
|
|
528
530
|
const hnswPaths = [
|
|
529
531
|
path.join(CWD, '.moflo', 'hnsw.index'),
|
|
530
532
|
path.join(CWD, '.swarm', 'hnsw.index'), // legacy pre-#727
|
|
531
533
|
];
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
534
|
+
if (!hasHnsw) {
|
|
535
|
+
for (const p of hnswPaths) {
|
|
536
|
+
if (safeStat(p)) {
|
|
537
|
+
hasHnsw = true;
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Fall back to a DB-size probe ONLY for the size display (which has no truth
|
|
544
|
+
// claim); vectorCount remains 0 when cache is missing rather than fabricating
|
|
545
|
+
// a divide-by-2 heuristic.
|
|
546
|
+
if (dbSizeKB === 0) {
|
|
547
|
+
const dbFiles = [
|
|
548
|
+
path.join(CWD, '.moflo', 'moflo.db'),
|
|
549
|
+
path.join(CWD, '.swarm', 'memory.db'),
|
|
550
|
+
path.join(CWD, 'data', 'memory.db'),
|
|
551
|
+
path.join(CWD, '.claude', 'memory.db'),
|
|
552
|
+
];
|
|
553
|
+
for (const f of dbFiles) {
|
|
554
|
+
const stat = safeStat(f);
|
|
555
|
+
if (stat) {
|
|
556
|
+
dbSizeKB = Math.floor(stat.size / 1024);
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
536
559
|
}
|
|
537
560
|
}
|
|
538
561
|
|
|
539
|
-
return { vectorCount, dbSizeKB, namespaces, hasHnsw };
|
|
562
|
+
return { vectorCount, dbSizeKB, namespaces, hasHnsw, reconciled, staleReason };
|
|
540
563
|
}
|
|
541
564
|
|
|
542
565
|
// Test stats (count files only — NO reading file contents)
|
|
@@ -798,13 +821,20 @@ function generateDashboard() {
|
|
|
798
821
|
// Embeddings line \u2014 vector store stats from .moflo/vector-stats.json.
|
|
799
822
|
// Reuses `system.embeddings` (already computed by getSystemMetrics()) instead
|
|
800
823
|
// of re-probing the cache file on every render.
|
|
824
|
+
//
|
|
825
|
+
// Reconciliation (#1054.S5 / #1059): when `vec.reconciled` is false, render
|
|
826
|
+
// the count as `?` and tag the reason \u2014 the underlying cache could have been
|
|
827
|
+
// written by a stale daemon and we refuse to display an unverified number.
|
|
801
828
|
{
|
|
802
829
|
const vec = system.embeddings;
|
|
803
|
-
if (vec.vectorCount > 0) {
|
|
830
|
+
if (vec.vectorCount > 0 || !vec.reconciled) {
|
|
804
831
|
const hnswInd = vec.hasHnsw ? `${c.brightGreen}\u26A1${c.reset}` : '';
|
|
805
832
|
const sizeDisp = vec.dbSizeKB >= 1024 ? `${(vec.dbSizeKB / 1024).toFixed(1)}MB` : `${vec.dbSizeKB}KB`;
|
|
833
|
+
const countDisp = vec.reconciled
|
|
834
|
+
? `${c.brightGreen}\u25CF${vec.vectorCount}${c.reset}${hnswInd}`
|
|
835
|
+
: `${c.brightYellow}?${c.reset} ${c.dim}(${vec.staleReason || 'unreconciled'})${c.reset}`;
|
|
806
836
|
const eParts = [
|
|
807
|
-
`${c.cyan}Vectors${c.reset} ${
|
|
837
|
+
`${c.cyan}Vectors${c.reset} ${countDisp}`,
|
|
808
838
|
`${c.cyan}Size${c.reset} ${c.brightWhite}${sizeDisp}${c.reset}`,
|
|
809
839
|
];
|
|
810
840
|
if (vec.namespaces > 0) {
|
|
@@ -877,13 +907,19 @@ function generateCompactDashboard() {
|
|
|
877
907
|
}
|
|
878
908
|
// Embeddings \u2014 always-on when vectorCount > 0; self-hides on a fresh install.
|
|
879
909
|
// Compact doesn't call getSystemMetrics() so this is the only probe per render.
|
|
910
|
+
// Reconciliation: when the count is unreconciled (#1054.S5 / #1059), render
|
|
911
|
+
// `?` rather than displaying a value that can't be verified across the
|
|
912
|
+
// cache + daemon-reported state.
|
|
880
913
|
{
|
|
881
914
|
const vec = getEmbeddingsStats();
|
|
882
|
-
if (vec.vectorCount > 0) {
|
|
915
|
+
if (vec.vectorCount > 0 || !vec.reconciled) {
|
|
883
916
|
const hnswInd = vec.hasHnsw ? '\u26A1' : '';
|
|
884
917
|
const sizeDisp = vec.dbSizeKB >= 1024 ? `${(vec.dbSizeKB / 1024).toFixed(1)}MB` : `${vec.dbSizeKB}KB`;
|
|
918
|
+
const countDisp = vec.reconciled
|
|
919
|
+
? `${c.brightGreen}${vec.vectorCount}${hnswInd}${c.reset}`
|
|
920
|
+
: `${c.brightYellow}?${c.reset}`;
|
|
885
921
|
segments.push(
|
|
886
|
-
`${c.brightCyan}\uD83D\uDCCA${c.reset} ${
|
|
922
|
+
`${c.brightCyan}\uD83D\uDCCA${c.reset} ${countDisp} ${c.dim}(${sizeDisp})${c.reset}`
|
|
887
923
|
);
|
|
888
924
|
}
|
|
889
925
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
{
|
|
2
|
-
"directive": "MANDATORY FIRST ACTION: Your very first tool call MUST be mcp__moflo__memory_search (any query, any namespace). The memory-first gate WILL BLOCK all Glob, Grep, and Read calls until you do this. After memory search, follow `.claude/guidance/moflo-subagents.md` protocol."
|
|
2
|
+
"directive": "MANDATORY FIRST ACTION: Your very first tool call MUST be mcp__moflo__memory_search (any query, any namespace). The memory-first gate WILL BLOCK all Glob, Grep, and Read calls until you do this. After memory search, follow `.claude/guidance/moflo-subagents.md` protocol. When a search hit carries `navigation`, you MUST call mcp__moflo__memory_get_neighbors to traverse — calling mcp__moflo__memory_retrieve on every hit is a protocol violation. See `.claude/guidance/moflo-memory-protocol.md`."
|
|
3
3
|
}
|
|
@@ -22,7 +22,7 @@ const path = require('path');
|
|
|
22
22
|
// Defense-in-depth copy of the canonical directive in subagent-bootstrap.json.
|
|
23
23
|
// Kept as a single-line literal so the parity test in tests/bin/subagent-start.test.ts
|
|
24
24
|
// can verify it matches the JSON via plain substring containment.
|
|
25
|
-
const FALLBACK_DIRECTIVE = 'MANDATORY FIRST ACTION: Your very first tool call MUST be mcp__moflo__memory_search (any query, any namespace). The memory-first gate WILL BLOCK all Glob, Grep, and Read calls until you do this. After memory search, follow `.claude/guidance/moflo-subagents.md` protocol.';
|
|
25
|
+
const FALLBACK_DIRECTIVE = 'MANDATORY FIRST ACTION: Your very first tool call MUST be mcp__moflo__memory_search (any query, any namespace). The memory-first gate WILL BLOCK all Glob, Grep, and Read calls until you do this. After memory search, follow `.claude/guidance/moflo-subagents.md` protocol. When a search hit carries `navigation`, you MUST call mcp__moflo__memory_get_neighbors to traverse — calling mcp__moflo__memory_retrieve on every hit is a protocol violation. See `.claude/guidance/moflo-memory-protocol.md`.';
|
|
26
26
|
|
|
27
27
|
function loadDirective() {
|
|
28
28
|
const jsonPath = path.join(__dirname, 'subagent-bootstrap.json');
|
|
@@ -121,6 +121,14 @@ mcp__moflo__memory_stats — { namespace: "learnings" }
|
|
|
121
121
|
|
|
122
122
|
Flag empty `learnings` as `info` (project hasn't accumulated decisions yet — fine for new projects). Flag empty `guidance` as `warn` (no indexed guidance means semantic search is degraded).
|
|
123
123
|
|
|
124
|
+
**Legacy `doc-*` residue (#1053 S4)** — moflo retired whole-document indexing in favor of chunk-only RAG. The `purge-doc-entries` migration runs on session-start; if any `doc-*` rows linger, the migration didn't fire (ran with no DB, errored, or the install is below the migration's introduction).
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
mcp__moflo__memory_search — { query: "doc-", namespace: "guidance", threshold: 0, limit: 5 }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
If any returned `key` starts with `doc-`, flag `info`: "legacy doc-* rows present — `purge-doc-entries` migration did not run; fixable via `flo healer --fix` or manual `node node_modules/moflo/bin/run-migrations.mjs`".
|
|
131
|
+
|
|
124
132
|
### 1i. Hooks & MCP Wiring
|
|
125
133
|
|
|
126
134
|
Read `.claude/settings.json`. Check:
|
package/bin/build-embeddings.mjs
CHANGED
|
@@ -16,27 +16,16 @@
|
|
|
16
16
|
* flo-embeddings --namespace guidance # scope to one namespace
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { existsSync, readFileSync
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
|
|
19
|
+
import { existsSync, readFileSync } from 'fs';
|
|
20
|
+
import { mofloInternalURL } from './lib/moflo-resolve.mjs';
|
|
21
|
+
import { memoryDbPath, hnswIndexPath, findProjectRoot } from './lib/moflo-paths.mjs';
|
|
22
|
+
import { openBackend } from './lib/get-backend.mjs';
|
|
24
23
|
const FASTEMBED_INLINE = 'dist/src/cli/embeddings/fastembed-inline/index.js';
|
|
25
24
|
const BRIDGE_CORE = 'dist/src/cli/memory/bridge-core.js';
|
|
26
25
|
const HNSW_PERSISTENCE = 'dist/src/cli/memory/hnsw-persistence.js';
|
|
27
26
|
const { writeVectorStatsJson } = await import(mofloInternalURL(BRIDGE_CORE));
|
|
28
27
|
const { buildAndWriteHnswSidecar } = await import(mofloInternalURL(HNSW_PERSISTENCE));
|
|
29
28
|
|
|
30
|
-
function findProjectRoot() {
|
|
31
|
-
let dir = process.cwd();
|
|
32
|
-
const root = resolve(dir, '/');
|
|
33
|
-
while (dir !== root) {
|
|
34
|
-
if (existsSync(resolve(dir, 'package.json'))) return dir;
|
|
35
|
-
dir = dirname(dir);
|
|
36
|
-
}
|
|
37
|
-
return process.cwd();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
29
|
const projectRoot = findProjectRoot();
|
|
41
30
|
const DB_PATH = memoryDbPath(projectRoot);
|
|
42
31
|
|
|
@@ -100,14 +89,11 @@ async function getDb() {
|
|
|
100
89
|
if (!existsSync(DB_PATH)) {
|
|
101
90
|
throw new Error(`Database not found: ${DB_PATH}`);
|
|
102
91
|
}
|
|
103
|
-
|
|
104
|
-
const buffer = readFileSync(DB_PATH);
|
|
105
|
-
return new SQL.Database(buffer);
|
|
92
|
+
return openBackend(projectRoot, { create: false });
|
|
106
93
|
}
|
|
107
94
|
|
|
108
95
|
function saveDb(db) {
|
|
109
|
-
|
|
110
|
-
writeFileSync(DB_PATH, Buffer.from(data));
|
|
96
|
+
db.save();
|
|
111
97
|
}
|
|
112
98
|
|
|
113
99
|
function getEntriesNeedingEmbeddings(db, namespace, forceAll) {
|
package/bin/cli.js
CHANGED
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
// already ships with correct .js extensions — the patch is unnecessary.
|
|
14
14
|
process.env.SKIP_AGENTDB_PATCH ??= '1';
|
|
15
15
|
|
|
16
|
+
// MUST run before any `node:sqlite` import in the process tree so the
|
|
17
|
+
// once-per-process ExperimentalWarning never fires. See module header for
|
|
18
|
+
// why we filter rather than `--no-warnings`-style broadly suppressing.
|
|
19
|
+
import './lib/suppress-sqlite-warning.mjs';
|
|
20
|
+
|
|
16
21
|
import { randomUUID } from 'crypto';
|
|
17
22
|
|
|
18
23
|
// Check if we should run in MCP server mode
|
package/bin/gate.cjs
CHANGED
|
@@ -307,7 +307,7 @@ switch (command) {
|
|
|
307
307
|
process.stdout.write('REMINDER: Use TaskCreate before spawning agents. Task tool is blocked until then.\n');
|
|
308
308
|
}
|
|
309
309
|
if (config.memory_first && s.memoryRequired && !s.memorySearched) {
|
|
310
|
-
process.stdout.write('REMINDER: Search memory (mcp__moflo__memory_search) before spawning agents
|
|
310
|
+
process.stdout.write('REMINDER: Search memory (mcp__moflo__memory_search) before spawning agents. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n');
|
|
311
311
|
}
|
|
312
312
|
if (s.lastNamespaceHint) {
|
|
313
313
|
// Per-actor single-shot. Each session_id gets the hint at most once per
|
|
@@ -376,7 +376,7 @@ switch (command) {
|
|
|
376
376
|
if (!s.memoryRequired || isMemorySearchedFor(s)) break;
|
|
377
377
|
var target = (process.env.TOOL_INPUT_pattern || '') + ' ' + (process.env.TOOL_INPUT_path || '');
|
|
378
378
|
if (EXEMPT.some(function(p) { return target.indexOf(p) >= 0; })) break;
|
|
379
|
-
process.stderr.write('BLOCKED: Search memory before exploring files. Use mcp__moflo__memory_search
|
|
379
|
+
process.stderr.write('BLOCKED: Search memory before exploring files. Use mcp__moflo__memory_search. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n');
|
|
380
380
|
process.exit(2);
|
|
381
381
|
}
|
|
382
382
|
case 'check-before-read': {
|
|
@@ -386,7 +386,7 @@ switch (command) {
|
|
|
386
386
|
var fp = process.env.TOOL_INPUT_file_path || '';
|
|
387
387
|
var isGuidance = fp.indexOf('.claude/guidance/') >= 0 || fp.indexOf('.claude\\guidance\\') >= 0;
|
|
388
388
|
if (!isGuidance && EXEMPT.some(function(p) { return fp.indexOf(p) >= 0; })) break;
|
|
389
|
-
process.stderr.write('BLOCKED: Search memory before reading files. Use mcp__moflo__memory_search
|
|
389
|
+
process.stderr.write('BLOCKED: Search memory before reading files. Use mcp__moflo__memory_search. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n');
|
|
390
390
|
process.exit(2);
|
|
391
391
|
}
|
|
392
392
|
case 'record-task-created': {
|
|
@@ -29,26 +29,14 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from
|
|
|
29
29
|
import { resolve, dirname, relative, basename, extname } from 'path';
|
|
30
30
|
import { fileURLToPath } from 'url';
|
|
31
31
|
import { execSync, execFileSync, spawn } from 'child_process';
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
32
|
+
import { memoryDbPath, MOFLO_DIR, findProjectRoot } from './lib/moflo-paths.mjs';
|
|
33
|
+
import { openBackend } from './lib/get-backend.mjs';
|
|
34
34
|
import { applyIncrementalChunks, computeContentListHash } from './lib/incremental-write.mjs';
|
|
35
35
|
import { resolveMofloBin } from './lib/resolve-bin.mjs';
|
|
36
|
-
const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
|
|
37
36
|
|
|
38
37
|
|
|
39
38
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
40
39
|
|
|
41
|
-
// Detect project root: walk up from cwd to find a package.json
|
|
42
|
-
function findProjectRoot() {
|
|
43
|
-
let dir = process.cwd();
|
|
44
|
-
const root = resolve(dir, '/');
|
|
45
|
-
while (dir !== root) {
|
|
46
|
-
if (existsSync(resolve(dir, 'package.json'))) return dir;
|
|
47
|
-
dir = dirname(dir);
|
|
48
|
-
}
|
|
49
|
-
return process.cwd();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
40
|
const projectRoot = findProjectRoot();
|
|
53
41
|
const NAMESPACE = 'code-map';
|
|
54
42
|
const DB_PATH = memoryDbPath(projectRoot);
|
|
@@ -110,14 +98,7 @@ function ensureDbDir() {
|
|
|
110
98
|
|
|
111
99
|
async function getDb() {
|
|
112
100
|
ensureDbDir();
|
|
113
|
-
const
|
|
114
|
-
let db;
|
|
115
|
-
if (existsSync(DB_PATH)) {
|
|
116
|
-
const buffer = readFileSync(DB_PATH);
|
|
117
|
-
db = new SQL.Database(buffer);
|
|
118
|
-
} else {
|
|
119
|
-
db = new SQL.Database();
|
|
120
|
-
}
|
|
101
|
+
const db = await openBackend(projectRoot, { create: true });
|
|
121
102
|
|
|
122
103
|
db.run(`
|
|
123
104
|
CREATE TABLE IF NOT EXISTS memory_entries (
|
|
@@ -147,8 +128,7 @@ async function getDb() {
|
|
|
147
128
|
}
|
|
148
129
|
|
|
149
130
|
function saveDb(db) {
|
|
150
|
-
|
|
151
|
-
writeFileSync(DB_PATH, Buffer.from(data));
|
|
131
|
+
db.save();
|
|
152
132
|
}
|
|
153
133
|
|
|
154
134
|
function countNamespace(db) {
|
package/bin/hooks.mjs
CHANGED
|
@@ -26,24 +26,15 @@ import { fileURLToPath, pathToFileURL } from 'url';
|
|
|
26
26
|
import { createProcessManager } from './lib/process-manager.mjs';
|
|
27
27
|
import { shouldDaemonAutoStart } from './lib/daemon-config.mjs';
|
|
28
28
|
import { resolveMofloBin } from './lib/resolve-bin.mjs';
|
|
29
|
+
import { findProjectRoot } from './lib/moflo-paths.mjs';
|
|
29
30
|
|
|
30
31
|
const __filename = fileURLToPath(import.meta.url);
|
|
31
32
|
const __dirname = dirname(__filename);
|
|
32
33
|
|
|
33
|
-
// Detect project root by walking up from cwd to find package.json.
|
|
34
34
|
// IMPORTANT: Do NOT use resolve(__dirname, '..') or '../..' — this script lives
|
|
35
35
|
// in bin/ during development but gets synced to .claude/scripts/ in consumer
|
|
36
|
-
// projects, so __dirname-relative paths break. findProjectRoot() works
|
|
37
|
-
|
|
38
|
-
let dir = process.cwd();
|
|
39
|
-
const root = resolve(dir, '/');
|
|
40
|
-
while (dir !== root) {
|
|
41
|
-
if (existsSync(resolve(dir, 'package.json'))) return dir;
|
|
42
|
-
dir = dirname(dir);
|
|
43
|
-
}
|
|
44
|
-
return process.cwd();
|
|
45
|
-
}
|
|
46
|
-
|
|
36
|
+
// projects, so __dirname-relative paths break. findProjectRoot() works
|
|
37
|
+
// everywhere and resolves identically to the TS bridge (see lib/moflo-paths.mjs).
|
|
47
38
|
const projectRoot = findProjectRoot();
|
|
48
39
|
const logFile = resolve(projectRoot, '.swarm/hooks.log');
|
|
49
40
|
const pm = createProcessManager(projectRoot);
|
package/bin/index-all.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { resolve, dirname } from 'path';
|
|
|
18
18
|
import { fileURLToPath } from 'url';
|
|
19
19
|
import { spawn, spawnSync } from 'child_process';
|
|
20
20
|
import { platform } from 'os';
|
|
21
|
-
import { hnswIndexPath } from './lib/moflo-paths.mjs';
|
|
21
|
+
import { hnswIndexPath, findProjectRoot } from './lib/moflo-paths.mjs';
|
|
22
22
|
import {
|
|
23
23
|
decideStepGate,
|
|
24
24
|
computeStepFingerprint,
|
|
@@ -38,20 +38,10 @@ const ONNX_THREAD_CAP = {
|
|
|
38
38
|
|
|
39
39
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
40
40
|
|
|
41
|
-
// Detect project root by walking up from cwd to find package.json.
|
|
42
41
|
// IMPORTANT: Do NOT use resolve(__dirname, '..') — this script lives in bin/
|
|
43
42
|
// during development but gets synced to .claude/scripts/ in consumer projects,
|
|
44
|
-
// so __dirname-relative paths break. findProjectRoot()
|
|
45
|
-
|
|
46
|
-
let dir = process.cwd();
|
|
47
|
-
const root = resolve(dir, '/');
|
|
48
|
-
while (dir !== root) {
|
|
49
|
-
if (existsSync(resolve(dir, 'package.json'))) return dir;
|
|
50
|
-
dir = dirname(dir);
|
|
51
|
-
}
|
|
52
|
-
return process.cwd();
|
|
53
|
-
}
|
|
54
|
-
|
|
43
|
+
// so __dirname-relative paths break. findProjectRoot() (lib/moflo-paths.mjs)
|
|
44
|
+
// works in both locations and resolves identically to the TS bridge.
|
|
55
45
|
const projectRoot = findProjectRoot();
|
|
56
46
|
const LOG_PATH = resolve(projectRoot, '.swarm/hooks.log');
|
|
57
47
|
|