mindforge-cc 2.3.5 → 3.0.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.
Files changed (82) hide show
  1. package/.agent/skills/mindforge-plan-phase/SKILL.md +1 -0
  2. package/.agent/skills/mindforge-system-architecture/SKILL.md +136 -0
  3. package/.agent/skills/mindforge-system-architecture/examples.md +120 -0
  4. package/.agent/skills/mindforge-system-architecture/scaling-checklist.md +76 -0
  5. package/.agent/skills/mindforge-tdd/SKILL.md +112 -0
  6. package/.agent/skills/mindforge-tdd/deep-modules.md +21 -0
  7. package/.agent/skills/mindforge-tdd/interface-design.md +22 -0
  8. package/.agent/skills/mindforge-tdd/mocking.md +24 -0
  9. package/.agent/skills/mindforge-tdd/refactoring.md +21 -0
  10. package/.agent/skills/mindforge-tdd/tests.md +28 -0
  11. package/.agent/workflows/mindforge-plan-phase.md +30 -1
  12. package/.agent/workflows/mindforge:architecture.md +40 -0
  13. package/.agent/workflows/mindforge:executor.md +18 -0
  14. package/.agent/workflows/mindforge:identity.md +18 -0
  15. package/.agent/workflows/mindforge:memory.md +18 -0
  16. package/.agent/workflows/mindforge:planner.md +18 -0
  17. package/.agent/workflows/mindforge:researcher.md +18 -0
  18. package/.agent/workflows/mindforge:reviewer.md +18 -0
  19. package/.agent/workflows/mindforge:tdd.md +41 -0
  20. package/.agent/workflows/mindforge:tool.md +18 -0
  21. package/.mindforge/engine/ads-protocol.md +54 -0
  22. package/.mindforge/engine/compaction-protocol.md +21 -36
  23. package/.mindforge/engine/context-injector.md +26 -0
  24. package/.mindforge/engine/knowledge-graph-protocol.md +125 -0
  25. package/.mindforge/engine/shard-controller.md +53 -0
  26. package/.mindforge/engine/temporal-protocol.md +40 -0
  27. package/.mindforge/personas/mf-executor.md +40 -0
  28. package/.mindforge/personas/mf-memory.md +33 -0
  29. package/.mindforge/personas/mf-planner.md +45 -0
  30. package/.mindforge/personas/mf-researcher.md +39 -0
  31. package/.mindforge/personas/mf-reviewer.md +35 -0
  32. package/.mindforge/personas/mf-tool.md +33 -0
  33. package/.planning/AUDIT.jsonl +1 -0
  34. package/.planning/TEMPORAL-TEST.md +1 -0
  35. package/.planning/history/36525e1d9da1b674/ARCHITECTURE.md +0 -0
  36. package/.planning/history/36525e1d9da1b674/HANDOFF.json +8 -0
  37. package/.planning/history/36525e1d9da1b674/PROJECT.md +33 -0
  38. package/.planning/history/36525e1d9da1b674/RELEASE-CHECKLIST.md +68 -0
  39. package/.planning/history/36525e1d9da1b674/REQUIREMENTS.md +0 -0
  40. package/.planning/history/36525e1d9da1b674/ROADMAP.md +12 -0
  41. package/.planning/history/36525e1d9da1b674/SNAPSHOT-META.json +18 -0
  42. package/.planning/history/36525e1d9da1b674/STATE.md +31 -0
  43. package/.planning/history/36525e1d9da1b674/TEMPORAL-TEST.md +1 -0
  44. package/.planning/history/36525e1d9da1b674/jira-sync.json +5 -0
  45. package/.planning/history/36525e1d9da1b674/slack-threads.json +3 -0
  46. package/.planning/history/test-audit-001/ARCHITECTURE.md +0 -0
  47. package/.planning/history/test-audit-001/HANDOFF.json +8 -0
  48. package/.planning/history/test-audit-001/PROJECT.md +33 -0
  49. package/.planning/history/test-audit-001/RELEASE-CHECKLIST.md +68 -0
  50. package/.planning/history/test-audit-001/REQUIREMENTS.md +0 -0
  51. package/.planning/history/test-audit-001/ROADMAP.md +12 -0
  52. package/.planning/history/test-audit-001/SNAPSHOT-META.json +17 -0
  53. package/.planning/history/test-audit-001/STATE.md +31 -0
  54. package/.planning/history/test-audit-001/TEMPORAL-TEST.md +1 -0
  55. package/.planning/history/test-audit-001/jira-sync.json +5 -0
  56. package/.planning/history/test-audit-001/slack-threads.json +3 -0
  57. package/CHANGELOG.md +101 -0
  58. package/README.md +57 -23
  59. package/bin/autonomous/auto-runner.js +23 -0
  60. package/bin/dashboard/server.js +2 -0
  61. package/bin/dashboard/temporal-api.js +82 -0
  62. package/bin/engine/temporal-cli.js +52 -0
  63. package/bin/engine/temporal-hub.js +138 -0
  64. package/bin/hindsight-injector.js +59 -0
  65. package/bin/memory/auto-shadow.js +274 -0
  66. package/bin/memory/embedding-engine.js +326 -0
  67. package/bin/memory/knowledge-capture.js +122 -5
  68. package/bin/memory/knowledge-graph.js +572 -0
  69. package/bin/memory/knowledge-store.js +15 -3
  70. package/bin/mindforge-cli.js +19 -0
  71. package/bin/models/model-router.js +1 -0
  72. package/bin/review/ads-engine.js +126 -0
  73. package/bin/review/ads-synthesizer.js +117 -0
  74. package/bin/shard-helper.js +134 -0
  75. package/bin/spawn-agent.js +61 -0
  76. package/docs/PERSONAS.md +71 -5
  77. package/docs/adr/ADR-042-ads-protocol.md +30 -0
  78. package/docs/architecture/README.md +55 -0
  79. package/docs/architecture/V3-CORE.md +52 -0
  80. package/docs/commands-reference.md +3 -2
  81. package/docs/usp-features.md +33 -15
  82. package/package.json +1 -1
@@ -0,0 +1,572 @@
1
+ /**
2
+ * MindForge v2.4.0 — Knowledge Graph Engine (RAG 2.0)
3
+ * Graph-aware knowledge management with nodes, edges, and traversal.
4
+ *
5
+ * Design:
6
+ * - Nodes = KnowledgeEntry items from knowledge-store.js
7
+ * - Edges = Typed relationships stored in graph-edges.jsonl
8
+ * - Adjacency index rebuilt on load for O(1) neighbor lookups
9
+ * - Traversal via BFS with configurable depth and edge type filters
10
+ * - All edge writes are append-only with SHA-256 integrity checksums
11
+ */
12
+ 'use strict';
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const crypto = require('crypto');
17
+ const Store = require('./knowledge-store');
18
+ const Embedder = require('./embedding-engine');
19
+
20
+ // ── Edge Types ────────────────────────────────────────────────────────────────
21
+ const EDGE_TYPES = Object.freeze({
22
+ RELATED_TO: 'RELATED_TO', // Semantic similarity (auto-inferred)
23
+ CAUSED_BY: 'CAUSED_BY', // Bug → Root cause
24
+ SUPERSEDES: 'SUPERSEDES', // New decision → Old decision
25
+ DEPENDS_ON: 'DEPENDS_ON', // Pattern → Required pattern
26
+ INFORMS: 'INFORMS', // Review finding → Decision
27
+ CONTRADICTS: 'CONTRADICTS', // Conflicting knowledge
28
+ });
29
+
30
+ const EDGE_SCHEMA_VERSION = '1.0.0';
31
+ const DEFAULT_EDGE_WEIGHT = 1.0;
32
+ const DECAY_RATE = 0.10; // 10% weight decay per cycle
33
+ const DECAY_THRESHOLD_DAYS = 30; // Decay edges not traversed in 30 days
34
+
35
+ // ── Path Configuration ────────────────────────────────────────────────────────
36
+ let baseDir = process.cwd();
37
+ let testMemoryDir = null;
38
+
39
+ function setBaseDir(dir) { baseDir = dir; }
40
+
41
+ /**
42
+ * Set a flat memory directory for testing (bypasses .mindforge/memory/ nesting).
43
+ * @param {string|null} dir - Flat directory path, or null to reset
44
+ */
45
+ function setTestMode(dir) { testMemoryDir = dir; }
46
+
47
+ function getPaths() {
48
+ const memoryDir = testMemoryDir || path.join(baseDir, '.mindforge', 'memory');
49
+ return {
50
+ MEMORY_DIR: memoryDir,
51
+ EDGES_PATH: path.join(memoryDir, 'graph-edges.jsonl'),
52
+ CACHE_PATH: path.join(memoryDir, 'embeddings.json'),
53
+ GRAPH_STATS: path.join(memoryDir, 'graph-stats.json'),
54
+ };
55
+ }
56
+
57
+ function ensureDir(dir) {
58
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
59
+ }
60
+
61
+ // ── Edge CRUD ─────────────────────────────────────────────────────────────────
62
+
63
+ /**
64
+ * Add a directed edge between two knowledge nodes.
65
+ * @param {object} edge
66
+ * @param {string} edge.sourceId - Source node ID
67
+ * @param {string} edge.targetId - Target node ID
68
+ * @param {string} edge.type - Edge type (from EDGE_TYPES)
69
+ * @param {number} [edge.weight] - Edge weight (default: 1.0)
70
+ * @param {string} [edge.reason] - Why this edge exists
71
+ * @param {object} [edge.metadata] - Additional metadata
72
+ * @returns {string} Edge ID
73
+ */
74
+ function addEdge(edge) {
75
+ const paths = getPaths();
76
+ ensureDir(paths.MEMORY_DIR);
77
+
78
+ if (!edge.sourceId) throw new Error('Edge requires sourceId');
79
+ if (!edge.targetId) throw new Error('Edge requires targetId');
80
+ if (!edge.type || !EDGE_TYPES[edge.type]) {
81
+ throw new Error(`Invalid edge type: ${edge.type}. Must be one of: ${Object.keys(EDGE_TYPES).join(', ')}`);
82
+ }
83
+ if (edge.sourceId === edge.targetId) {
84
+ throw new Error('Self-referencing edges are not allowed');
85
+ }
86
+
87
+ const id = crypto.randomUUID();
88
+ const now = new Date().toISOString();
89
+
90
+ const record = {
91
+ id,
92
+ schema_version: EDGE_SCHEMA_VERSION,
93
+ sourceId: edge.sourceId,
94
+ targetId: edge.targetId,
95
+ type: edge.type,
96
+ weight: Math.min(2.0, Math.max(0.0, edge.weight ?? DEFAULT_EDGE_WEIGHT)),
97
+ reason: edge.reason || '',
98
+ metadata: edge.metadata || {},
99
+ created_at: now,
100
+ last_traversed: null,
101
+ traversal_count: 0,
102
+ deprecated: false,
103
+ checksum: '',
104
+ };
105
+
106
+ // Compute integrity checksum
107
+ const payload = JSON.stringify({ ...record, checksum: '' });
108
+ record.checksum = crypto.createHash('sha256').update(payload).digest('hex');
109
+
110
+ fs.appendFileSync(paths.EDGES_PATH, JSON.stringify(record) + '\n');
111
+ return id;
112
+ }
113
+
114
+ /**
115
+ * Read all edges from the graph-edges.jsonl file.
116
+ * Later entries with the same ID supersede earlier ones (append-only pattern).
117
+ * @returns {object[]} Edges
118
+ */
119
+ function readAllEdges() {
120
+ const paths = getPaths();
121
+ if (!fs.existsSync(paths.EDGES_PATH)) return [];
122
+
123
+ const lines = fs.readFileSync(paths.EDGES_PATH, 'utf8').split('\n').filter(Boolean);
124
+ const byId = new Map();
125
+
126
+ for (const line of lines) {
127
+ try {
128
+ const edge = JSON.parse(line);
129
+ byId.set(edge.id, edge);
130
+ } catch {
131
+ // Skip malformed lines
132
+ }
133
+ }
134
+
135
+ return [...byId.values()].filter(e => !e.deprecated);
136
+ }
137
+
138
+ /**
139
+ * Deprecate an edge.
140
+ * @param {string} edgeId - Edge to deprecate
141
+ * @param {string} reason - Why
142
+ */
143
+ function deprecateEdge(edgeId, reason) {
144
+ const paths = getPaths();
145
+ const edges = readAllEdges();
146
+ const edge = edges.find(e => e.id === edgeId);
147
+ if (!edge) return;
148
+
149
+ const deprecated = {
150
+ ...edge,
151
+ deprecated: true,
152
+ deprecated_reason: reason,
153
+ deprecated_at: new Date().toISOString(),
154
+ };
155
+
156
+ fs.appendFileSync(paths.EDGES_PATH, JSON.stringify(deprecated) + '\n');
157
+ }
158
+
159
+ /**
160
+ * Reinforce an edge (increase weight, update traversal stats).
161
+ * @param {string} edgeId
162
+ */
163
+ function reinforceEdge(edgeId) {
164
+ const paths = getPaths();
165
+ const edges = readAllEdges();
166
+ const edge = edges.find(e => e.id === edgeId);
167
+ if (!edge) return;
168
+
169
+ const reinforced = {
170
+ ...edge,
171
+ weight: Math.min(2.0, edge.weight + 0.1),
172
+ last_traversed: new Date().toISOString(),
173
+ traversal_count: (edge.traversal_count || 0) + 1,
174
+ };
175
+
176
+ // Recompute checksum
177
+ const payload = JSON.stringify({ ...reinforced, checksum: '' });
178
+ reinforced.checksum = crypto.createHash('sha256').update(payload).digest('hex');
179
+
180
+ fs.appendFileSync(paths.EDGES_PATH, JSON.stringify(reinforced) + '\n');
181
+ }
182
+
183
+ // ── Adjacency Index ───────────────────────────────────────────────────────────
184
+
185
+ /**
186
+ * Build an in-memory adjacency index for O(1) neighbor lookups.
187
+ * @param {object[]} edges - All active edges
188
+ * @returns {Map<string, object[]>} nodeId → [{ edge, neighborId }]
189
+ */
190
+ function buildAdjacencyIndex(edges) {
191
+ const index = new Map();
192
+
193
+ for (const edge of edges) {
194
+ // Forward direction
195
+ if (!index.has(edge.sourceId)) index.set(edge.sourceId, []);
196
+ index.get(edge.sourceId).push({
197
+ edge,
198
+ neighborId: edge.targetId,
199
+ direction: 'outgoing',
200
+ });
201
+
202
+ // Reverse direction (for bidirectional traversal)
203
+ if (!index.has(edge.targetId)) index.set(edge.targetId, []);
204
+ index.get(edge.targetId).push({
205
+ edge,
206
+ neighborId: edge.sourceId,
207
+ direction: 'incoming',
208
+ });
209
+ }
210
+
211
+ return index;
212
+ }
213
+
214
+ // ── Graph Traversal ───────────────────────────────────────────────────────────
215
+
216
+ /**
217
+ * BFS traversal from a node, returning all reachable nodes within depth.
218
+ * @param {string} startId - Starting node ID
219
+ * @param {number} maxDepth - Maximum hop count (default: 2)
220
+ * @param {object} opts
221
+ * @param {string[]} [opts.edgeTypes] - Filter by edge types
222
+ * @param {number} [opts.minWeight] - Minimum edge weight
223
+ * @returns {Array<{id: string, depth: number, path: string[]}>}
224
+ */
225
+ function traverse(startId, maxDepth = 2, opts = {}) {
226
+ const { edgeTypes, minWeight = 0 } = opts;
227
+ const edges = readAllEdges();
228
+ const adjacency = buildAdjacencyIndex(edges);
229
+
230
+ const visited = new Set();
231
+ const results = [];
232
+ const queue = [{ id: startId, depth: 0, path: [startId] }];
233
+
234
+ while (queue.length > 0) {
235
+ const { id, depth, path: currentPath } = queue.shift();
236
+
237
+ if (visited.has(id)) continue;
238
+ visited.add(id);
239
+
240
+ if (id !== startId) {
241
+ results.push({ id, depth, path: currentPath });
242
+ }
243
+
244
+ if (depth >= maxDepth) continue;
245
+
246
+ const neighbors = adjacency.get(id) || [];
247
+ for (const { edge, neighborId } of neighbors) {
248
+ if (visited.has(neighborId)) continue;
249
+ if (edgeTypes && !edgeTypes.includes(edge.type)) continue;
250
+ if (edge.weight < minWeight) continue;
251
+
252
+ queue.push({
253
+ id: neighborId,
254
+ depth: depth + 1,
255
+ path: [...currentPath, neighborId],
256
+ });
257
+ }
258
+ }
259
+
260
+ return results;
261
+ }
262
+
263
+ /**
264
+ * Find related nodes via both graph traversal AND embedding similarity.
265
+ * Returns a combined, deduplicated, scored result set.
266
+ * @param {string} queryText - Natural language query
267
+ * @param {Map<string, object>} vectors - Precomputed embeddings
268
+ * @param {Map<string, number>} df - Document frequency
269
+ * @param {number} N - Corpus size
270
+ * @param {object} opts
271
+ * @param {number} [opts.maxHops] - Graph traversal depth
272
+ * @param {number} [opts.topK] - Max results
273
+ * @returns {Array<{id: string, score: number, source: string}>}
274
+ */
275
+ function findRelated(queryText, vectors, df, N, opts = {}) {
276
+ const { maxHops = 2, topK = 10 } = opts;
277
+
278
+ // 1. Embedding-based similarity
279
+ const embeddingResults = Embedder.findSimilar(queryText, vectors, df, N, topK * 2);
280
+
281
+ // 2. Graph-based traversal from top embedding matches
282
+ const graphResults = new Map();
283
+ for (const { id } of embeddingResults.slice(0, 3)) {
284
+ const neighbors = traverse(id, maxHops);
285
+ for (const n of neighbors) {
286
+ if (!graphResults.has(n.id) || graphResults.get(n.id).depth > n.depth) {
287
+ graphResults.set(n.id, n);
288
+ }
289
+ }
290
+ }
291
+
292
+ // 3. Merge and score
293
+ const combined = new Map();
294
+
295
+ // Embedding scores (0.0 - 1.0)
296
+ for (const r of embeddingResults) {
297
+ combined.set(r.id, {
298
+ id: r.id,
299
+ embeddingScore: r.similarity,
300
+ graphScore: 0,
301
+ source: 'embedding',
302
+ });
303
+ }
304
+
305
+ // Graph scores (inversely proportional to depth)
306
+ for (const [id, result] of graphResults) {
307
+ const graphScore = 1.0 / (result.depth + 1);
308
+ if (combined.has(id)) {
309
+ const existing = combined.get(id);
310
+ existing.graphScore = graphScore;
311
+ existing.source = 'hybrid';
312
+ } else {
313
+ combined.set(id, {
314
+ id,
315
+ embeddingScore: 0,
316
+ graphScore,
317
+ source: 'graph',
318
+ });
319
+ }
320
+ }
321
+
322
+ // Final score: weighted combination
323
+ const scored = [...combined.values()].map(r => ({
324
+ id: r.id,
325
+ score: r.embeddingScore * 0.6 + r.graphScore * 0.4,
326
+ source: r.source,
327
+ }));
328
+
329
+ return scored
330
+ .filter(r => r.score > 0)
331
+ .sort((a, b) => b.score - a.score)
332
+ .slice(0, topK);
333
+ }
334
+
335
+ /**
336
+ * Get all edges for a specific node.
337
+ * @param {string} nodeId
338
+ * @param {object} opts
339
+ * @param {string} [opts.direction] - 'outgoing', 'incoming', or 'both' (default)
340
+ * @param {string[]} [opts.edgeTypes] - Filter by types
341
+ * @returns {object[]} Edges
342
+ */
343
+ function getNodeEdges(nodeId, opts = {}) {
344
+ const { direction = 'both', edgeTypes } = opts;
345
+ const edges = readAllEdges();
346
+
347
+ return edges.filter(e => {
348
+ const matchesNode =
349
+ direction === 'outgoing' ? e.sourceId === nodeId :
350
+ direction === 'incoming' ? e.targetId === nodeId :
351
+ e.sourceId === nodeId || e.targetId === nodeId;
352
+
353
+ const matchesType = !edgeTypes || edgeTypes.includes(e.type);
354
+
355
+ return matchesNode && matchesType;
356
+ });
357
+ }
358
+
359
+ // ── Auto-Edge Creation ────────────────────────────────────────────────────────
360
+
361
+ /**
362
+ * When a new entry is added, auto-create RELATED_TO edges
363
+ * for entries with cosine similarity above the threshold.
364
+ * @param {string} entryId - Newly added entry's ID
365
+ * @param {Map<string, object>} vectors - Precomputed entry vectors
366
+ * @returns {string[]} Created edge IDs
367
+ */
368
+ function autoCreateEdges(entryId, vectors) {
369
+ const candidates = Embedder.inferEdges(entryId, vectors);
370
+ const created = [];
371
+
372
+ // Check for existing edges to avoid duplicates
373
+ const existingEdges = getNodeEdges(entryId);
374
+ const existingTargets = new Set(existingEdges.map(e =>
375
+ e.sourceId === entryId ? e.targetId : e.sourceId
376
+ ));
377
+
378
+ for (const { targetId, similarity } of candidates.slice(0, 5)) {
379
+ if (existingTargets.has(targetId)) continue;
380
+
381
+ const edgeId = addEdge({
382
+ sourceId: entryId,
383
+ targetId,
384
+ type: EDGE_TYPES.RELATED_TO,
385
+ weight: similarity,
386
+ reason: `Auto-inferred: cosine similarity ${similarity.toFixed(3)}`,
387
+ metadata: { auto_inferred: true, similarity },
388
+ });
389
+
390
+ created.push(edgeId);
391
+ }
392
+
393
+ return created;
394
+ }
395
+
396
+ // ── Edge Weight Decay ─────────────────────────────────────────────────────────
397
+
398
+ /**
399
+ * Apply weight decay to edges not traversed in DECAY_THRESHOLD_DAYS.
400
+ * Edges that decay to weight ≤ 0.1 are deprecated.
401
+ * @returns {{ decayed: number, pruned: number }}
402
+ */
403
+ function applyDecay() {
404
+ const edges = readAllEdges();
405
+ const now = Date.now();
406
+ let decayed = 0;
407
+ let pruned = 0;
408
+
409
+ for (const edge of edges) {
410
+ const lastUsed = edge.last_traversed
411
+ ? new Date(edge.last_traversed).getTime()
412
+ : new Date(edge.created_at).getTime();
413
+
414
+ const daysSince = (now - lastUsed) / 86_400_000;
415
+ if (daysSince < DECAY_THRESHOLD_DAYS) continue;
416
+
417
+ const cycles = Math.floor(daysSince / DECAY_THRESHOLD_DAYS);
418
+ const newWeight = edge.weight * Math.pow(1 - DECAY_RATE, cycles);
419
+
420
+ if (newWeight <= 0.1) {
421
+ deprecateEdge(edge.id, `Pruned: weight decayed to ${newWeight.toFixed(3)} after ${cycles} cycles`);
422
+ pruned++;
423
+ } else if (newWeight < edge.weight) {
424
+ // Append updated weight
425
+ const paths = getPaths();
426
+ const updated = {
427
+ ...edge,
428
+ weight: parseFloat(newWeight.toFixed(4)),
429
+ };
430
+ const payload = JSON.stringify({ ...updated, checksum: '' });
431
+ updated.checksum = crypto.createHash('sha256').update(payload).digest('hex');
432
+ fs.appendFileSync(paths.EDGES_PATH, JSON.stringify(updated) + '\n');
433
+ decayed++;
434
+ }
435
+ }
436
+
437
+ return { decayed, pruned };
438
+ }
439
+
440
+ // ── Cycle Detection ───────────────────────────────────────────────────────────
441
+
442
+ /**
443
+ * Detect cycles in directed edge types (CAUSED_BY, SUPERSEDES).
444
+ * Uses DFS to find back-edges.
445
+ * @returns {Array<string[]>} List of cycles (as node ID arrays)
446
+ */
447
+ function detectCycles() {
448
+ const edges = readAllEdges().filter(e =>
449
+ e.type === EDGE_TYPES.CAUSED_BY || e.type === EDGE_TYPES.SUPERSEDES
450
+ );
451
+
452
+ const adj = new Map();
453
+ for (const e of edges) {
454
+ if (!adj.has(e.sourceId)) adj.set(e.sourceId, []);
455
+ adj.get(e.sourceId).push(e.targetId);
456
+ }
457
+
458
+ const visited = new Set();
459
+ const inStack = new Set();
460
+ const cycles = [];
461
+
462
+ function dfs(node, path) {
463
+ if (inStack.has(node)) {
464
+ const cycleStart = path.indexOf(node);
465
+ cycles.push(path.slice(cycleStart));
466
+ return;
467
+ }
468
+ if (visited.has(node)) return;
469
+
470
+ visited.add(node);
471
+ inStack.add(node);
472
+
473
+ for (const neighbor of (adj.get(node) || [])) {
474
+ dfs(neighbor, [...path, neighbor]);
475
+ }
476
+
477
+ inStack.delete(node);
478
+ }
479
+
480
+ for (const nodeId of adj.keys()) {
481
+ if (!visited.has(nodeId)) {
482
+ dfs(nodeId, [nodeId]);
483
+ }
484
+ }
485
+
486
+ return cycles;
487
+ }
488
+
489
+ // ── Graph Statistics ──────────────────────────────────────────────────────────
490
+
491
+ /**
492
+ * Compute and return graph statistics.
493
+ * @returns {object} Graph stats
494
+ */
495
+ function graphStats() {
496
+ const edges = readAllEdges();
497
+ const entries = Store.readAll();
498
+ const activeEntries = entries.filter(e => !e.deprecated);
499
+
500
+ const byType = {};
501
+ for (const e of edges) {
502
+ byType[e.type] = (byType[e.type] || 0) + 1;
503
+ }
504
+
505
+ // Find orphan nodes (no edges)
506
+ const connectedNodes = new Set();
507
+ for (const e of edges) {
508
+ connectedNodes.add(e.sourceId);
509
+ connectedNodes.add(e.targetId);
510
+ }
511
+ const orphans = activeEntries.filter(e => !connectedNodes.has(e.id));
512
+
513
+ return {
514
+ total_nodes: activeEntries.length,
515
+ total_edges: edges.length,
516
+ edges_by_type: byType,
517
+ orphan_nodes: orphans.length,
518
+ avg_weight: edges.length
519
+ ? edges.reduce((s, e) => s + e.weight, 0) / edges.length
520
+ : 0,
521
+ connected_ratio: activeEntries.length
522
+ ? connectedNodes.size / activeEntries.length
523
+ : 0,
524
+ };
525
+ }
526
+
527
+ // ── Edge Integrity Verification ───────────────────────────────────────────────
528
+
529
+ /**
530
+ * Verify SHA-256 checksums on all edges.
531
+ * @returns {{ valid: number, corrupted: string[] }}
532
+ */
533
+ function verifyEdgeIntegrity() {
534
+ const edges = readAllEdges();
535
+ let valid = 0;
536
+ const corrupted = [];
537
+
538
+ for (const edge of edges) {
539
+ const storedChecksum = edge.checksum;
540
+ const payload = JSON.stringify({ ...edge, checksum: '' });
541
+ const computed = crypto.createHash('sha256').update(payload).digest('hex');
542
+
543
+ if (storedChecksum === computed) {
544
+ valid++;
545
+ } else {
546
+ corrupted.push(edge.id);
547
+ }
548
+ }
549
+
550
+ return { valid, corrupted };
551
+ }
552
+
553
+ // ── Exports ───────────────────────────────────────────────────────────────────
554
+ module.exports = {
555
+ EDGE_TYPES,
556
+ setBaseDir,
557
+ setTestMode,
558
+ getPaths,
559
+ addEdge,
560
+ readAllEdges,
561
+ deprecateEdge,
562
+ reinforceEdge,
563
+ buildAdjacencyIndex,
564
+ traverse,
565
+ findRelated,
566
+ getNodeEdges,
567
+ autoCreateEdges,
568
+ applyDecay,
569
+ detectCycles,
570
+ graphStats,
571
+ verifyEdgeIntegrity,
572
+ };
@@ -33,9 +33,21 @@ function setGlobalDir(dir) {
33
33
  globalBaseDir = dir;
34
34
  }
35
35
 
36
+ // Test-mode override: flat memory dir without .mindforge/ nesting
37
+ let testMemoryDir = null;
38
+
39
+ /**
40
+ * Set a flat memory directory for testing (bypasses .mindforge/memory/ nesting).
41
+ * This avoids macOS App Sandbox EPERM on dot-prefixed directories.
42
+ * @param {string|null} dir - Flat directory path, or null to reset
43
+ */
44
+ function setTestMode(dir) {
45
+ testMemoryDir = dir;
46
+ }
47
+
36
48
  function getPaths() {
37
- const memoryDir = path.join(baseDir, '.mindforge', 'memory');
38
- const globalDir = path.join(globalBaseDir, '.mindforge');
49
+ const memoryDir = testMemoryDir || path.join(baseDir, '.mindforge', 'memory');
50
+ const globalDir = testMemoryDir || path.join(globalBaseDir, '.mindforge');
39
51
  return {
40
52
  MEMORY_DIR: memoryDir,
41
53
  GLOBAL_DIR: globalDir,
@@ -315,5 +327,5 @@ function stats() {
315
327
  module.exports = {
316
328
  add, deprecate, reinforce,
317
329
  readAll, readByType, readFile, query, stats,
318
- setBaseDir, setGlobalDir, getPaths,
330
+ setBaseDir, setGlobalDir, setTestMode, getPaths,
319
331
  };
@@ -80,6 +80,25 @@ const COMMANDS = {
80
80
  'marketplace': {
81
81
  script: 'bin/skills-builder/marketplace-cli.js',
82
82
  description: 'Search and install community skills from the marketplace'
83
+ },
84
+ 'spawn': {
85
+ script: 'bin/spawn-agent.js',
86
+ description: 'Spawn a persona essence (e.g., mf-planner)',
87
+ defaultArgs: ['spawn']
88
+ },
89
+ 'identity': {
90
+ script: 'bin/spawn-agent.js',
91
+ description: 'Invoke a specialized identity from /agents/',
92
+ defaultArgs: ['identity']
93
+ },
94
+ 'temporal': {
95
+ script: 'bin/engine/temporal-cli.js',
96
+ description: 'Manage time-travel debugging and state history'
97
+ },
98
+ 'hindsight': {
99
+ script: 'bin/engine/temporal-cli.js',
100
+ description: 'Inject a fix into a past point and regenerate state',
101
+ defaultArgs: ['inject']
83
102
  }
84
103
  };
85
104
 
@@ -30,6 +30,7 @@ const PERSONA_MAP = {
30
30
  'qa-engineer': 'QA_MODEL',
31
31
  'research-agent': 'RESEARCH_MODEL',
32
32
  'debug-specialist': 'DEBUG_MODEL',
33
+ 'decision-architect': 'PLANNER_MODEL',
33
34
  };
34
35
 
35
36
  let _settingsCache = null;