moflo 4.9.35 → 4.9.37
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 +30 -0
- package/.claude/guidance/shipped/moflo-subagents.md +4 -0
- package/.claude/helpers/gate.cjs +3 -3
- 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/gate.cjs +3 -3
- package/bin/index-guidance.mjs +41 -52
- package/bin/migrations/purge-doc-entries.mjs +53 -0
- package/bin/migrations/strip-context-preambles.mjs +97 -0
- package/bin/semantic-search.mjs +4 -1
- package/bin/session-start-launcher.mjs +2 -0
- package/dist/src/cli/commands/doctor-checks-memory-access.js +179 -0
- package/dist/src/cli/commands/memory.js +41 -52
- package/dist/src/cli/init/claudemd-generator.js +4 -0
- package/dist/src/cli/init/moflo-init.js +13 -5
- package/dist/src/cli/mcp-tools/memory-tools.js +169 -31
- package/dist/src/cli/memory/auto-memory-bridge.js +8 -11
- package/dist/src/cli/memory/bridge-entries.js +6 -2
- package/dist/src/cli/memory/memory-initializer.js +17 -11
- package/dist/src/cli/services/claude-stats.js +2 -16
- package/dist/src/cli/services/subagent-bootstrap.js +1 -1
- package/dist/src/cli/shared/utils/claude-projects-path.js +32 -0
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
* Cleanup runs even on assertion failure so a fail doesn't leave orphaned
|
|
23
23
|
* agents/hive workers.
|
|
24
24
|
*/
|
|
25
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
25
26
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
27
|
+
import { mofloImport } from '../services/moflo-require.js';
|
|
28
|
+
import { memoryDbPath } from '../services/moflo-paths.js';
|
|
26
29
|
import { loadToolArrays, getTool, pushDetail, summarizeFunctional, } from './doctor-checks-functional-shared.js';
|
|
27
30
|
const MEMORY_ACCESS_CHECK = 'Memory Access Functional';
|
|
28
31
|
const MEMORY_ACCESS_FAIL_FIX = 'Run `flo doctor --json` for per-subcheck details. Common fixes: ensure fastembed installed (memory_store.hasEmbedding=false), explicit threshold:0 honored (#837), or rebuild HNSW index (`flo memory rebuild-index`)';
|
|
@@ -160,6 +163,164 @@ async function safeDelete(memoryTools, key, namespace) {
|
|
|
160
163
|
}
|
|
161
164
|
catch { /* best-effort */ }
|
|
162
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* #1053 S2: probe `memory_get_neighbors` round-trip.
|
|
168
|
+
*
|
|
169
|
+
* Same #798 protected-functionality posture as the swarm/agent/task probes:
|
|
170
|
+
* if a future refactor stubs the handler to literals (or unwires it from
|
|
171
|
+
* the metadata-passthrough plumbing in S1), this probe fails BEFORE the
|
|
172
|
+
* stub ships to consumers.
|
|
173
|
+
*
|
|
174
|
+
* Three chunks are stored via memory_store, then their `metadata` columns
|
|
175
|
+
* are written directly via sql.js to inject chunk-shaped nav fields
|
|
176
|
+
* (memory_store always sets metadata to '{}', so the chunker write path is
|
|
177
|
+
* the only producer in steady state — bypass it here for an in-process
|
|
178
|
+
* probe). The middle chunk's neighbors are then fetched and verified to
|
|
179
|
+
* carry navigation back, proving S1 + S2 + the metadata column passthrough
|
|
180
|
+
* survive end-to-end.
|
|
181
|
+
*/
|
|
182
|
+
async function probeMemoryGetNeighbors(memoryTools, details) {
|
|
183
|
+
const tool = getTool(memoryTools, 'memory_get_neighbors');
|
|
184
|
+
if (!tool?.handler) {
|
|
185
|
+
details.push({
|
|
186
|
+
id: 'neighbors.registered',
|
|
187
|
+
mcpTool: 'memory_get_neighbors',
|
|
188
|
+
status: 'fail',
|
|
189
|
+
observed: { registered: false },
|
|
190
|
+
expected: 'memory_get_neighbors registered in MCP tool surface (#1053 S2)',
|
|
191
|
+
message: 'memory_get_neighbors is not registered — has the tool been removed or its name changed?',
|
|
192
|
+
});
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
details.push({
|
|
196
|
+
id: 'neighbors.registered',
|
|
197
|
+
mcpTool: 'memory_get_neighbors',
|
|
198
|
+
status: 'pass',
|
|
199
|
+
observed: { registered: true },
|
|
200
|
+
expected: 'memory_get_neighbors registered',
|
|
201
|
+
});
|
|
202
|
+
const stamp = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
203
|
+
const namespace = `doctor-neighbors-${stamp}`;
|
|
204
|
+
const prefix = `chunk-doctor-neighbors-${stamp}`;
|
|
205
|
+
const chunkKeys = [`${prefix}-0`, `${prefix}-1`, `${prefix}-2`];
|
|
206
|
+
const middleKey = chunkKeys[1];
|
|
207
|
+
// Seed three chunks via DIRECT sql.js write (skipping memory_store).
|
|
208
|
+
// Going through memory_store would warm the bridge's TieredCache with
|
|
209
|
+
// metadata='{}', and a follow-up direct UPDATE would race the bridge's
|
|
210
|
+
// writeback (sql.js dump-on-flush hazard, see
|
|
211
|
+
// feedback_sqljs_writeback_clobber.md). Writing straight to disk before
|
|
212
|
+
// the bridge ever sees these keys means memory_get_neighbors → getEntry
|
|
213
|
+
// hits fresh disk state and our injected metadata is what comes back.
|
|
214
|
+
const dbPath = memoryDbPath(process.cwd());
|
|
215
|
+
if (!existsSync(dbPath)) {
|
|
216
|
+
details.push({
|
|
217
|
+
id: 'neighbors.seed',
|
|
218
|
+
mcpTool: 'memory_get_neighbors',
|
|
219
|
+
status: 'warn',
|
|
220
|
+
observed: { dbPath, exists: false },
|
|
221
|
+
expected: 'memory.db present so the neighbors probe can seed chunks',
|
|
222
|
+
message: 'memory.db missing — skip the neighbors probe (run `flo memory init` first)',
|
|
223
|
+
});
|
|
224
|
+
return { key: middleKey, namespace, chunkKeys };
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const initSqlJs = (await mofloImport('sql.js')).default;
|
|
228
|
+
const SQL = await initSqlJs();
|
|
229
|
+
const db = new SQL.Database(readFileSync(dbPath));
|
|
230
|
+
const insert = db.prepare(`INSERT OR REPLACE INTO memory_entries
|
|
231
|
+
(id, key, namespace, content, type, tags, metadata, created_at, updated_at, status)
|
|
232
|
+
VALUES (?, ?, ?, ?, 'semantic', '[]', ?, ?, ?, 'active')`);
|
|
233
|
+
const now = Date.now();
|
|
234
|
+
for (let i = 0; i < chunkKeys.length; i++) {
|
|
235
|
+
const meta = {
|
|
236
|
+
type: 'chunk',
|
|
237
|
+
parentDoc: 'doc-doctor-neighbors',
|
|
238
|
+
parentPath: '/doctor-neighbors.md',
|
|
239
|
+
chunkIndex: i,
|
|
240
|
+
totalChunks: chunkKeys.length,
|
|
241
|
+
prevChunk: i > 0 ? chunkKeys[i - 1] : null,
|
|
242
|
+
nextChunk: i < chunkKeys.length - 1 ? chunkKeys[i + 1] : null,
|
|
243
|
+
siblings: chunkKeys,
|
|
244
|
+
hierarchicalParent: null,
|
|
245
|
+
hierarchicalChildren: null,
|
|
246
|
+
chunkTitle: `Doctor Probe Chunk ${i}`,
|
|
247
|
+
headerLevel: 2,
|
|
248
|
+
docContentHash: stamp,
|
|
249
|
+
};
|
|
250
|
+
insert.run([
|
|
251
|
+
`memprobe-neighbors-${stamp}-${i}`,
|
|
252
|
+
chunkKeys[i],
|
|
253
|
+
namespace,
|
|
254
|
+
`chunk body ${i}`,
|
|
255
|
+
JSON.stringify(meta),
|
|
256
|
+
now, now,
|
|
257
|
+
]);
|
|
258
|
+
}
|
|
259
|
+
insert.free();
|
|
260
|
+
writeFileSync(dbPath, Buffer.from(db.export()));
|
|
261
|
+
db.close();
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
const msg = errorDetail(err, { firstLineOnly: true });
|
|
265
|
+
details.push({
|
|
266
|
+
id: 'neighbors.seed',
|
|
267
|
+
mcpTool: 'memory_get_neighbors',
|
|
268
|
+
status: 'fail',
|
|
269
|
+
observed: { error: msg },
|
|
270
|
+
expected: 'three chunk rows seeded with chunk-shaped metadata',
|
|
271
|
+
message: `seed failed: ${msg}`,
|
|
272
|
+
});
|
|
273
|
+
return { key: middleKey, namespace, chunkKeys };
|
|
274
|
+
}
|
|
275
|
+
// 3. The probe itself — fetch prev + next of the middle chunk.
|
|
276
|
+
let result;
|
|
277
|
+
try {
|
|
278
|
+
result = (await invokeOrThrow(memoryTools, 'memory_get_neighbors', {
|
|
279
|
+
key: middleKey,
|
|
280
|
+
namespace,
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
const msg = errorDetail(err, { firstLineOnly: true });
|
|
285
|
+
details.push({
|
|
286
|
+
id: 'neighbors.roundtrip',
|
|
287
|
+
mcpTool: 'memory_get_neighbors',
|
|
288
|
+
status: 'fail',
|
|
289
|
+
observed: { error: msg },
|
|
290
|
+
expected: 'memory_get_neighbors returns success=true with prev + next',
|
|
291
|
+
message: `handler threw: ${msg}`,
|
|
292
|
+
});
|
|
293
|
+
return { key: middleKey, namespace, chunkKeys };
|
|
294
|
+
}
|
|
295
|
+
const failReason = assertNeighbors(result, [chunkKeys[0], chunkKeys[2]]);
|
|
296
|
+
pushDetail(details, {
|
|
297
|
+
id: 'neighbors.roundtrip',
|
|
298
|
+
mcpTool: 'memory_get_neighbors',
|
|
299
|
+
expected: `success=true, total=2, neighbors include ${chunkKeys[0]} + ${chunkKeys[2]} with navigation`,
|
|
300
|
+
}, failReason ? result : { total: result.total, neighborKeys: result.neighbors?.map(n => n.key) }, failReason);
|
|
301
|
+
return { key: middleKey, namespace, chunkKeys };
|
|
302
|
+
}
|
|
303
|
+
function assertNeighbors(result, expectedKeys) {
|
|
304
|
+
if (!result?.success) {
|
|
305
|
+
return `success=${result?.success} (error: ${result?.error ?? 'unknown'}) — handler did not return success`;
|
|
306
|
+
}
|
|
307
|
+
if (result.total !== expectedKeys.length) {
|
|
308
|
+
return `expected total=${expectedKeys.length}, got ${result.total} — neighbors traversal returned wrong count`;
|
|
309
|
+
}
|
|
310
|
+
const got = (result.neighbors ?? []).map(n => n.key).sort();
|
|
311
|
+
const want = [...expectedKeys].sort();
|
|
312
|
+
if (JSON.stringify(got) !== JSON.stringify(want)) {
|
|
313
|
+
return `expected neighbor keys ${JSON.stringify(want)}, got ${JSON.stringify(got)} — wrong neighbors returned`;
|
|
314
|
+
}
|
|
315
|
+
// Every neighbor must carry navigation (S1 metadata passthrough). A stub
|
|
316
|
+
// that returns shaped envelopes but null nav would pass the count check;
|
|
317
|
+
// this catches it.
|
|
318
|
+
const missingNav = (result.neighbors ?? []).filter(n => !n.navigation);
|
|
319
|
+
if (missingNav.length > 0) {
|
|
320
|
+
return `${missingNav.length} neighbor(s) returned with navigation=null — S1 metadata passthrough may be broken`;
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
163
324
|
export async function checkMemoryAccessFunctional() {
|
|
164
325
|
const details = [];
|
|
165
326
|
const mods = await loadToolArrays({
|
|
@@ -182,6 +343,24 @@ export async function checkMemoryAccessFunctional() {
|
|
|
182
343
|
let spawnedAgentId;
|
|
183
344
|
let hiveInitialized = false;
|
|
184
345
|
try {
|
|
346
|
+
// ── Probe 0: memory_get_neighbors (#1053 S2) ──────────────────────────
|
|
347
|
+
// MUST RUN FIRST — before any memory_store call warms the bridge's
|
|
348
|
+
// in-memory sql.js snapshot. The probe injects chunk metadata via a
|
|
349
|
+
// direct file write to .moflo/moflo.db; once the bridge has loaded the
|
|
350
|
+
// DB into memory and done its first persist, external file writes are
|
|
351
|
+
// clobbered (sql.js dump-on-flush hazard, see
|
|
352
|
+
// feedback_sqljs_writeback_clobber.md). Running this probe first means
|
|
353
|
+
// the bridge instantiates AFTER our seed lands — its initial disk read
|
|
354
|
+
// sees our rows.
|
|
355
|
+
{
|
|
356
|
+
const probeResult = await probeMemoryGetNeighbors(memoryTools, details);
|
|
357
|
+
if (probeResult) {
|
|
358
|
+
const { namespace, chunkKeys } = probeResult;
|
|
359
|
+
for (const key of chunkKeys) {
|
|
360
|
+
cleanups.push(() => safeDelete(memoryTools, key, namespace));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
185
364
|
// ── Probe 1: subagent context ─────────────────────────────────────────
|
|
186
365
|
// The "subagent" path is what Claude's Task tool ends up calling: direct
|
|
187
366
|
// MCP tools with no surrounding coordinator state. Failures here indicate
|
|
@@ -242,7 +242,7 @@ const searchCommand = {
|
|
|
242
242
|
name: 'threshold',
|
|
243
243
|
description: 'Similarity threshold (0-1)',
|
|
244
244
|
type: 'number',
|
|
245
|
-
default: 0.
|
|
245
|
+
default: 0.5
|
|
246
246
|
},
|
|
247
247
|
{
|
|
248
248
|
name: 'type',
|
|
@@ -268,7 +268,8 @@ const searchCommand = {
|
|
|
268
268
|
const query = ctx.flags.query || ctx.args[0];
|
|
269
269
|
const namespace = ctx.flags.namespace || 'all';
|
|
270
270
|
const limit = ctx.flags.limit || 10;
|
|
271
|
-
|
|
271
|
+
// #1053 S6: align with MCP default — was 0.3 here vs 0.7 in option block.
|
|
272
|
+
const threshold = ctx.flags.threshold || 0.5;
|
|
272
273
|
const searchType = ctx.flags.type || 'semantic';
|
|
273
274
|
const buildHnsw = ctx.flags.buildHnsw;
|
|
274
275
|
if (!query) {
|
|
@@ -1612,16 +1613,16 @@ const indexGuidanceCommand = {
|
|
|
1612
1613
|
try {
|
|
1613
1614
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1614
1615
|
const contentHash_ = hashContent(content);
|
|
1615
|
-
//
|
|
1616
|
+
// #1053 S4: doc-* retired — read docContentHash off chunk-0 instead.
|
|
1616
1617
|
if (!forceReindex) {
|
|
1617
1618
|
const stmt = db.prepare('SELECT metadata FROM memory_entries WHERE key = ? AND namespace = ?');
|
|
1618
|
-
stmt.bind([
|
|
1619
|
+
stmt.bind([`${chunkPrefix}-0`, NAMESPACE]);
|
|
1619
1620
|
const entry = stmt.step() ? stmt.getAsObject() : null;
|
|
1620
1621
|
stmt.free();
|
|
1621
1622
|
if (entry?.metadata) {
|
|
1622
1623
|
try {
|
|
1623
1624
|
const meta = JSON.parse(entry.metadata);
|
|
1624
|
-
if (meta.
|
|
1625
|
+
if (meta.docContentHash === contentHash_) {
|
|
1625
1626
|
return { docKey, status: 'unchanged', chunks: 0 };
|
|
1626
1627
|
}
|
|
1627
1628
|
}
|
|
@@ -1630,19 +1631,12 @@ const indexGuidanceCommand = {
|
|
|
1630
1631
|
}
|
|
1631
1632
|
const stats = fs.statSync(filePath);
|
|
1632
1633
|
const relativePath = filePath.replace(cwd, '').replace(/\\/g, '/');
|
|
1633
|
-
// Delete old chunks
|
|
1634
|
+
// Delete old chunks. Also delete any legacy doc-* row (#1053 S4).
|
|
1634
1635
|
db.run(`DELETE FROM memory_entries WHERE namespace = ? AND key LIKE ?`, [NAMESPACE, `${chunkPrefix}%`]);
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
fileSize: stats.size,
|
|
1640
|
-
lastModified: stats.mtime.toISOString(),
|
|
1641
|
-
contentHash: contentHash_,
|
|
1642
|
-
indexedAt: new Date().toISOString(),
|
|
1643
|
-
ragVersion: '2.0',
|
|
1644
|
-
};
|
|
1645
|
-
batchStoreEntry(db, docKey, NAMESPACE, content, docMetadata, [keyPrefix, 'document']);
|
|
1636
|
+
db.run(`DELETE FROM memory_entries WHERE namespace = ? AND key = ?`, [NAMESPACE, docKey]);
|
|
1637
|
+
// #1053 S4: doc-* entries no longer written. parentDoc on chunks
|
|
1638
|
+
// remains as an identifier label; callers Read parentPath when
|
|
1639
|
+
// they need the source file (see shipped/moflo-memory-protocol.md).
|
|
1646
1640
|
// Chunk content
|
|
1647
1641
|
const chunks = chunkMarkdown(content, fileName);
|
|
1648
1642
|
if (chunks.length === 0) {
|
|
@@ -1650,26 +1644,24 @@ const indexGuidanceCommand = {
|
|
|
1650
1644
|
}
|
|
1651
1645
|
const hierarchy = buildHierarchy(chunks, chunkPrefix);
|
|
1652
1646
|
const siblings = chunks.map((_, i) => `${chunkPrefix}-${i}`);
|
|
1653
|
-
// Update doc with children refs
|
|
1654
|
-
const docChildrenMeta = { ...docMetadata, children: siblings, chunkCount: chunks.length };
|
|
1655
|
-
batchStoreEntry(db, docKey, NAMESPACE, content, docChildrenMeta, [keyPrefix, 'document']);
|
|
1656
1647
|
for (let i = 0; i < chunks.length; i++) {
|
|
1657
1648
|
const chunk = chunks[i];
|
|
1658
1649
|
const chunkKey = `${chunkPrefix}-${i}`;
|
|
1659
1650
|
const prevChunk = i > 0 ? `${chunkPrefix}-${i - 1}` : null;
|
|
1660
1651
|
const nextChunk = i < chunks.length - 1 ? `${chunkPrefix}-${i + 1}` : null;
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
: null;
|
|
1664
|
-
const contextAfter = i < chunks.length - 1
|
|
1665
|
-
? extractOverlapContext(chunks[i + 1].content, overlapPercent, 'start')
|
|
1666
|
-
: null;
|
|
1652
|
+
// #1053 S5: dropped prev/next preamble wrapping. Traversal happens
|
|
1653
|
+
// via memory_get_neighbors now (S2).
|
|
1667
1654
|
const hierInfo = hierarchy[chunkKey];
|
|
1668
1655
|
const chunkMetadata = {
|
|
1669
1656
|
type: 'chunk',
|
|
1670
1657
|
ragVersion: '2.0',
|
|
1658
|
+
// #1053 S4: parentDoc is an identifier label (target row no
|
|
1659
|
+
// longer exists); use parentPath for the actual source file.
|
|
1660
|
+
// docContentHash on every chunk lets the skip-if-unchanged
|
|
1661
|
+
// check read it off chunk-0.
|
|
1671
1662
|
parentDoc: docKey,
|
|
1672
1663
|
parentPath: relativePath,
|
|
1664
|
+
docContentHash: contentHash_,
|
|
1673
1665
|
chunkIndex: i,
|
|
1674
1666
|
totalChunks: chunks.length,
|
|
1675
1667
|
prevChunk,
|
|
@@ -1682,21 +1674,12 @@ const indexGuidanceCommand = {
|
|
|
1682
1674
|
headerLine: chunk.headerLine,
|
|
1683
1675
|
isPart: chunk.isPart || false,
|
|
1684
1676
|
partNum: chunk.partNum || null,
|
|
1685
|
-
contextOverlapPercent: overlapPercent,
|
|
1686
|
-
hasContextBefore: !!contextBefore,
|
|
1687
|
-
hasContextAfter: !!contextAfter,
|
|
1688
1677
|
contentLength: chunk.content.length,
|
|
1689
1678
|
contentHash: hashContent(chunk.content),
|
|
1690
1679
|
indexedAt: new Date().toISOString(),
|
|
1691
1680
|
};
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
searchableContent += `[Context from previous section:]\n${contextBefore}\n\n---\n\n`;
|
|
1695
|
-
}
|
|
1696
|
-
searchableContent += chunk.content;
|
|
1697
|
-
if (contextAfter) {
|
|
1698
|
-
searchableContent += `\n\n---\n\n[Context from next section:]\n${contextAfter}`;
|
|
1699
|
-
}
|
|
1681
|
+
// #1053 S5: title heading + chunk body. No prev/next preamble.
|
|
1682
|
+
const searchableContent = `# ${chunk.title}\n\n${chunk.content}`;
|
|
1700
1683
|
batchStoreEntry(db, chunkKey, NAMESPACE, searchableContent, chunkMetadata, [keyPrefix, 'chunk', `level-${chunk.level}`, chunk.title.toLowerCase().replace(/[^a-z0-9]+/g, '-')]);
|
|
1701
1684
|
}
|
|
1702
1685
|
return { docKey, status: 'indexed', chunks: chunks.length };
|
|
@@ -1753,22 +1736,28 @@ const indexGuidanceCommand = {
|
|
|
1753
1736
|
errors++;
|
|
1754
1737
|
}
|
|
1755
1738
|
}
|
|
1756
|
-
// Clean stale
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1739
|
+
// #1053 S4: Clean stale chunks for deleted files.
|
|
1740
|
+
// doc-* markers are gone — derive prefixes from chunk keys directly.
|
|
1741
|
+
// Chunk key shape: chunk-guidance-<filename>-<index>; group by stripping
|
|
1742
|
+
// the trailing -<index>.
|
|
1743
|
+
const chunksStmt = db.prepare(`SELECT DISTINCT key FROM memory_entries WHERE namespace = ? AND key LIKE 'chunk-guidance-%'`);
|
|
1744
|
+
chunksStmt.bind([NAMESPACE]);
|
|
1745
|
+
const seenPrefixes = new Set();
|
|
1746
|
+
while (chunksStmt.step()) {
|
|
1747
|
+
const { key } = chunksStmt.getAsObject();
|
|
1748
|
+
const prefix = key.replace(/-\d+$/, '');
|
|
1749
|
+
seenPrefixes.add(prefix);
|
|
1750
|
+
}
|
|
1751
|
+
chunksStmt.free();
|
|
1752
|
+
for (const prefix of seenPrefixes) {
|
|
1753
|
+
const filename = prefix.replace('chunk-guidance-', '') + '.md';
|
|
1754
|
+
const checkPath = pathModule.resolve(cwd, '.claude/guidance', filename);
|
|
1767
1755
|
if (!fs.existsSync(checkPath)) {
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1756
|
+
db.run(`DELETE FROM memory_entries WHERE namespace = ? AND key LIKE ?`, [NAMESPACE, `${prefix}-%`]);
|
|
1757
|
+
// Also sweep any legacy doc-* row for this prefix (one-time tidy).
|
|
1758
|
+
const legacyDocKey = prefix.replace('chunk-', 'doc-');
|
|
1759
|
+
db.run(`DELETE FROM memory_entries WHERE namespace = ? AND key = ?`, [NAMESPACE, legacyDocKey]);
|
|
1760
|
+
output.writeln(output.dim(` Removed stale: ${prefix}-* (file ${filename} not found)`));
|
|
1772
1761
|
}
|
|
1773
1762
|
}
|
|
1774
1763
|
}
|
|
@@ -31,6 +31,10 @@ function mofloSection() {
|
|
|
31
31
|
|
|
32
32
|
Your first tool call MUST be \`mcp__moflo__memory_search\` — before any Glob/Grep/Read. Search \`guidance\`, \`patterns\`, and \`learnings\` every prompt; add \`code-map\` when navigating code, \`tests\` when looking for test inventory or coverage. When the user says "remember this", call \`mcp__moflo__memory_store\` with namespace \`learnings\`.
|
|
33
33
|
|
|
34
|
+
### Traverse chunks, don't bulk-retrieve
|
|
35
|
+
|
|
36
|
+
Search results carry a compact \`navigation\` crumb (parentDoc, prev/next, chunkTitle). For adjacent/sibling/hierarchical context use \`mcp__moflo__memory_get_neighbors\`; for full chunk content use \`mcp__moflo__memory_retrieve\`; \`Read\` the source doc only via \`parentPath\` when truly needed. Full protocol: \`.claude/guidance/moflo-memory-protocol.md\`.
|
|
37
|
+
|
|
34
38
|
### Auto-enforced gates
|
|
35
39
|
|
|
36
40
|
- **TaskCreate-first**: Call \`TaskCreate\` before spawning the Agent tool
|
|
@@ -435,7 +435,7 @@ function generateClaudeMd(root, _force) {
|
|
|
435
435
|
// scriptFiles array in bin/session-start-launcher.mjs — first-init drops any
|
|
436
436
|
// script missing here, and the launcher's manifest cleanup later treats it as
|
|
437
437
|
// orphan residue and deletes it (#777, feedback_scriptfiles_sync.md).
|
|
438
|
-
const SCRIPT_MAP = [
|
|
438
|
+
export const SCRIPT_MAP = [
|
|
439
439
|
'hooks.mjs',
|
|
440
440
|
'session-start-launcher.mjs',
|
|
441
441
|
'index-guidance.mjs',
|
|
@@ -496,17 +496,25 @@ function isStale(srcPath, destPath) {
|
|
|
496
496
|
// ============================================================================
|
|
497
497
|
// Step 6: .gitignore
|
|
498
498
|
// ============================================================================
|
|
499
|
-
function updateGitignore(root) {
|
|
499
|
+
export function updateGitignore(root) {
|
|
500
500
|
const gitignorePath = path.join(root, '.gitignore');
|
|
501
501
|
const entries = [
|
|
502
502
|
'.claude-epic/',
|
|
503
503
|
'.moflo/',
|
|
504
504
|
'.swarm/',
|
|
505
|
-
'.moflo/',
|
|
506
505
|
'.claude/settings.local.json',
|
|
507
506
|
'.claude/scheduled_tasks.lock',
|
|
508
507
|
'**/workflow-state.json',
|
|
508
|
+
// Leading `/` anchors to gitignore root — bare `.claude/guidance/` once
|
|
509
|
+
// swallowed shipped/internal subdirs and broke `npm pack`
|
|
510
|
+
// (guidance-gitignore-shipped-trap).
|
|
511
|
+
'/.claude/guidance/moflo-*.md',
|
|
512
|
+
...SCRIPT_MAP.map(name => `/.claude/scripts/${name}`),
|
|
509
513
|
];
|
|
514
|
+
// Treat `/.foo` and `.foo` as the same rule when checking for prior presence
|
|
515
|
+
// — both forms anchor at gitignore root, so a consumer who wrote either
|
|
516
|
+
// shouldn't get a duplicate appended.
|
|
517
|
+
const normalize = (s) => s.replace(/^\//, '');
|
|
510
518
|
if (!fs.existsSync(gitignorePath)) {
|
|
511
519
|
const defaultEntries = ['node_modules/', 'dist/', '.env', '.env.*', ''];
|
|
512
520
|
const content = '# Dependencies\n' + defaultEntries.join('\n') + '\n# MoFlo state\n' + entries.join('\n') + '\n';
|
|
@@ -514,8 +522,8 @@ function updateGitignore(root) {
|
|
|
514
522
|
return { name: '.gitignore', status: 'created', detail: 'Created with node_modules, .env, and MoFlo entries' };
|
|
515
523
|
}
|
|
516
524
|
const existing = fs.readFileSync(gitignorePath, 'utf-8');
|
|
517
|
-
const existingLines = new Set(existing.split(/\r?\n/).map(l => l.trim()).filter(l => l && !l.startsWith('#')));
|
|
518
|
-
const toAdd = entries.filter(e => !existingLines.has(e));
|
|
525
|
+
const existingLines = new Set(existing.split(/\r?\n/).map(l => normalize(l.trim())).filter(l => l && !l.startsWith('#')));
|
|
526
|
+
const toAdd = entries.filter(e => !existingLines.has(normalize(e)));
|
|
519
527
|
if (toAdd.length === 0) {
|
|
520
528
|
return { name: '.gitignore', status: 'skipped', detail: 'Entries already present' };
|
|
521
529
|
}
|