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.
@@ -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.7
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
- const threshold = ctx.flags.threshold || 0.3;
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
- // Check if content changed
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([docKey, NAMESPACE]);
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.contentHash === contentHash_) {
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
- // Store full document
1636
- const docMetadata = {
1637
- type: 'document',
1638
- filePath: relativePath,
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
- const contextBefore = i > 0
1662
- ? extractOverlapContext(chunks[i - 1].content, overlapPercent, 'end')
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
- let searchableContent = `# ${chunk.title}\n\n`;
1693
- if (contextBefore) {
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 entries for deleted files
1757
- const docsStmt = db.prepare(`SELECT DISTINCT key FROM memory_entries WHERE namespace = ? AND key LIKE 'doc-%'`);
1758
- docsStmt.bind([NAMESPACE]);
1759
- const docs = [];
1760
- while (docsStmt.step())
1761
- docs.push(docsStmt.getAsObject());
1762
- docsStmt.free();
1763
- for (const { key } of docs) {
1764
- if (!key.startsWith('doc-guidance-'))
1765
- continue;
1766
- const checkPath = pathModule.resolve(cwd, '.claude/guidance', key.replace('doc-guidance-', '') + '.md');
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
- const cp = key.replace('doc-', 'chunk-');
1769
- db.run(`DELETE FROM memory_entries WHERE namespace = ? AND key LIKE ?`, [NAMESPACE, `${cp}%`]);
1770
- db.run(`DELETE FROM memory_entries WHERE namespace = ? AND key = ?`, [NAMESPACE, key]);
1771
- output.writeln(output.dim(` Removed stale: ${key}`));
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
  }