mindforge-cc 1.0.5 → 2.0.0-alpha.4
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/.agent/CLAUDE.md +53 -0
- package/.agent/mindforge/auto.md +22 -0
- package/.agent/mindforge/browse.md +26 -0
- package/.agent/mindforge/costs.md +11 -0
- package/.agent/mindforge/cross-review.md +17 -0
- package/.agent/mindforge/execute-phase.md +5 -3
- package/.agent/mindforge/qa.md +16 -0
- package/.agent/mindforge/remember.md +14 -0
- package/.agent/mindforge/research.md +11 -0
- package/.agent/mindforge/steer.md +13 -0
- package/.agent/workflows/publish-release.md +36 -0
- package/.claude/CLAUDE.md +53 -0
- package/.claude/commands/mindforge/auto.md +22 -0
- package/.claude/commands/mindforge/browse.md +26 -0
- package/.claude/commands/mindforge/costs.md +11 -0
- package/.claude/commands/mindforge/cross-review.md +17 -0
- package/.claude/commands/mindforge/execute-phase.md +5 -3
- package/.claude/commands/mindforge/qa.md +16 -0
- package/.claude/commands/mindforge/remember.md +14 -0
- package/.claude/commands/mindforge/research.md +11 -0
- package/.claude/commands/mindforge/steer.md +13 -0
- package/.mindforge/MINDFORGE-V2-SCHEMA.json +47 -0
- package/.mindforge/browser/daemon-protocol.md +24 -0
- package/.mindforge/browser/qa-engine.md +16 -0
- package/.mindforge/browser/session-manager.md +18 -0
- package/.mindforge/browser/visual-verify-spec.md +31 -0
- package/.mindforge/engine/autonomous/auto-executor.md +266 -0
- package/.mindforge/engine/autonomous/headless-adapter.md +66 -0
- package/.mindforge/engine/autonomous/node-repair.md +190 -0
- package/.mindforge/engine/autonomous/progress-reporter.md +58 -0
- package/.mindforge/engine/autonomous/steering-manager.md +64 -0
- package/.mindforge/engine/autonomous/stuck-detector.md +89 -0
- package/.mindforge/memory/MEMORY-SCHEMA.md +155 -0
- package/.mindforge/memory/decision-library.jsonl +0 -0
- package/.mindforge/memory/engine/capture-protocol.md +36 -0
- package/.mindforge/memory/engine/global-sync-spec.md +42 -0
- package/.mindforge/memory/engine/retrieval-spec.md +44 -0
- package/.mindforge/memory/knowledge-base.jsonl +7 -0
- package/.mindforge/memory/pattern-library.jsonl +1 -0
- package/.mindforge/memory/team-preferences.jsonl +4 -0
- package/.mindforge/models/model-registry.md +48 -0
- package/.mindforge/models/model-router.md +30 -0
- package/.mindforge/personas/research-agent.md +24 -0
- package/.planning/browser-daemon.log +32 -0
- package/.planning/decisions/ADR-021-autonomy-boundary.md +17 -0
- package/.planning/decisions/ADR-022-node-repair-hierarchy.md +19 -0
- package/.planning/decisions/ADR-023-gate-3-timing.md +15 -0
- package/CHANGELOG.md +68 -0
- package/MINDFORGE.md +26 -3
- package/README.md +54 -18
- package/bin/autonomous/auto-runner.js +95 -0
- package/bin/autonomous/headless.js +36 -0
- package/bin/autonomous/progress-stream.js +49 -0
- package/bin/autonomous/repair-operator.js +213 -0
- package/bin/autonomous/steer.js +71 -0
- package/bin/autonomous/stuck-monitor.js +77 -0
- package/bin/browser/browser-daemon.js +139 -0
- package/bin/browser/daemon-manager.js +91 -0
- package/bin/browser/qa-engine.js +47 -0
- package/bin/browser/qa-report-writer.js +32 -0
- package/bin/browser/regression-writer.js +27 -0
- package/bin/browser/screenshot-store.js +49 -0
- package/bin/browser/session-manager.js +93 -0
- package/bin/browser/visual-verify-executor.js +89 -0
- package/bin/install.js +4 -4
- package/bin/installer-core.js +24 -24
- package/bin/memory/cli.js +99 -0
- package/bin/memory/global-sync.js +107 -0
- package/bin/memory/knowledge-capture.js +278 -0
- package/bin/memory/knowledge-indexer.js +172 -0
- package/bin/memory/knowledge-store.js +319 -0
- package/bin/memory/session-memory-loader.js +137 -0
- package/bin/migrations/0.1.0-to-0.5.0.js +2 -3
- package/bin/migrations/0.5.0-to-0.6.0.js +1 -1
- package/bin/migrations/0.6.0-to-1.0.0.js +3 -3
- package/bin/migrations/migrate.js +15 -11
- package/bin/models/anthropic-provider.js +77 -0
- package/bin/models/cost-tracker.js +118 -0
- package/bin/models/gemini-provider.js +79 -0
- package/bin/models/model-client.js +98 -0
- package/bin/models/model-router.js +111 -0
- package/bin/models/openai-provider.js +78 -0
- package/bin/research/research-engine.js +115 -0
- package/bin/review/cross-review-engine.js +81 -0
- package/bin/review/finding-synthesizer.js +116 -0
- package/bin/review/review-report-writer.js +49 -0
- package/bin/updater/self-update.js +13 -13
- package/docs/adr/ADR-024-browser-localhost-only.md +17 -0
- package/docs/adr/ADR-025-visual-verify-failure-treatment.md +19 -0
- package/docs/adr/ADR-026-session-persistence-security.md +20 -0
- package/docs/architecture/README.md +4 -2
- package/docs/publishing-guide.md +78 -0
- package/docs/reference/commands.md +17 -2
- package/docs/reference/sdk-api.md +6 -1
- package/docs/user-guide.md +93 -9
- package/docs/usp-features.md +56 -8
- package/package.json +3 -2
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MindForge v2 — Knowledge Indexer
|
|
3
|
+
* TF-IDF inspired relevance scoring for fast knowledge retrieval.
|
|
4
|
+
* Provides tag-based and text-based search across the knowledge graph.
|
|
5
|
+
*
|
|
6
|
+
* Design note: We use a simple in-memory index rebuilt on each query
|
|
7
|
+
* (not persisted) because the knowledge base stays small (< 10K entries
|
|
8
|
+
* for a typical project). Rebuild time < 50ms for 1K entries.
|
|
9
|
+
*/
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const Store = require('./knowledge-store');
|
|
13
|
+
|
|
14
|
+
// ── Stopwords (excluded from TF-IDF scoring) ──────────────────────────────────
|
|
15
|
+
const STOPWORDS = new Set([
|
|
16
|
+
'the', 'a', 'an', 'is', 'it', 'in', 'on', 'at', 'to', 'for', 'of', 'and',
|
|
17
|
+
'or', 'but', 'not', 'this', 'that', 'with', 'from', 'by', 'be', 'are',
|
|
18
|
+
'was', 'were', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would',
|
|
19
|
+
'could', 'should', 'may', 'might', 'can', 'use', 'using', 'used', 'when',
|
|
20
|
+
'where', 'which', 'what', 'how', 'why', 'who', 'all', 'any', 'some', 'we',
|
|
21
|
+
'our', 'they', 'their', 'we', 'you', 'your', 'my', 'its',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
// ── Tokenizer ─────────────────────────────────────────────────────────────────
|
|
25
|
+
function tokenize(text) {
|
|
26
|
+
if (!text) return [];
|
|
27
|
+
return text
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/[^a-z0-9\s_-]/g, ' ')
|
|
30
|
+
.split(/\s+/)
|
|
31
|
+
.filter(w => w.length > 2 && !STOPWORDS.has(w));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Build in-memory index ─────────────────────────────────────────────────────
|
|
35
|
+
function buildIndex(entries) {
|
|
36
|
+
const index = new Map(); // token → [{ id, count }]
|
|
37
|
+
const docTokenCounts = new Map(); // id → token count
|
|
38
|
+
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
if (entry.deprecated) continue;
|
|
41
|
+
|
|
42
|
+
const text = `${entry.topic} ${entry.content} ${(entry.tags || []).join(' ')}`;
|
|
43
|
+
const tokens = tokenize(text);
|
|
44
|
+
const counts = {};
|
|
45
|
+
|
|
46
|
+
for (const tok of tokens) {
|
|
47
|
+
counts[tok] = (counts[tok] || 0) + 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
docTokenCounts.set(entry.id, tokens.length);
|
|
51
|
+
|
|
52
|
+
for (const [tok, count] of Object.entries(counts)) {
|
|
53
|
+
if (!index.has(tok)) index.set(tok, []);
|
|
54
|
+
index.get(tok).push({ id: entry.id, count });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { index, docTokenCounts, N: entries.length };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── TF-IDF scoring ────────────────────────────────────────────────────────────
|
|
62
|
+
function tfidfScore(queryTokens, entryId, index, docTokenCounts, N) {
|
|
63
|
+
let score = 0;
|
|
64
|
+
const docLen = docTokenCounts.get(entryId) || 1;
|
|
65
|
+
|
|
66
|
+
for (const qTok of queryTokens) {
|
|
67
|
+
const postings = index.get(qTok) || [];
|
|
68
|
+
const df = postings.length; // Document frequency
|
|
69
|
+
if (df === 0) continue;
|
|
70
|
+
|
|
71
|
+
const posting = postings.find(p => p.id === entryId);
|
|
72
|
+
if (!posting) continue;
|
|
73
|
+
|
|
74
|
+
const tf = posting.count / docLen; // Term frequency (normalized)
|
|
75
|
+
const idf = Math.log((N + 1) / (df + 1)) + 1; // Smoothed IDF
|
|
76
|
+
score += tf * idf;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return score;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Main search function ──────────────────────────────────────────────────────
|
|
83
|
+
/**
|
|
84
|
+
* Search knowledge base with TF-IDF scoring.
|
|
85
|
+
* @param {string} queryText - Natural language query
|
|
86
|
+
* @param {object} filters - Optional filters { type, tags, minConfidence }
|
|
87
|
+
* @param {number} limit - Max results to return
|
|
88
|
+
* @returns {object[]} Ranked results
|
|
89
|
+
*/
|
|
90
|
+
function search(queryText, filters = {}, limit = 10) {
|
|
91
|
+
const allEntries = Store.readAll(filters.includeGlobal);
|
|
92
|
+
const active = allEntries.filter(e => !e.deprecated);
|
|
93
|
+
|
|
94
|
+
// Apply filters
|
|
95
|
+
let candidates = active;
|
|
96
|
+
if (filters.type) candidates = candidates.filter(e => e.type === filters.type);
|
|
97
|
+
if (filters.minConfidence) candidates = candidates.filter(e => e.confidence >= filters.minConfidence);
|
|
98
|
+
if (filters.tags?.length) {
|
|
99
|
+
const filterTags = filters.tags.map(t => t.toLowerCase());
|
|
100
|
+
candidates = candidates.filter(e =>
|
|
101
|
+
(e.tags || []).some(t => filterTags.includes(t.toLowerCase()))
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (candidates.length === 0) return [];
|
|
106
|
+
|
|
107
|
+
const queryTokens = tokenize(queryText);
|
|
108
|
+
if (queryTokens.length === 0) {
|
|
109
|
+
// No meaningful query tokens — return by confidence
|
|
110
|
+
return candidates
|
|
111
|
+
.sort((a, b) => b.confidence - a.confidence)
|
|
112
|
+
.slice(0, limit);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const { index, docTokenCounts, N } = buildIndex(candidates);
|
|
116
|
+
|
|
117
|
+
// Score each candidate
|
|
118
|
+
const scored = candidates.map(entry => {
|
|
119
|
+
const textScore = tfidfScore(queryTokens, entry.id, index, docTokenCounts, N);
|
|
120
|
+
// Combine TF-IDF score with confidence, but only if there's a text match
|
|
121
|
+
const finalScore = textScore > 0
|
|
122
|
+
? textScore * 0.7 + entry.confidence * 0.3
|
|
123
|
+
: 0;
|
|
124
|
+
return { entry, score: finalScore };
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return scored
|
|
128
|
+
.filter(s => s.score > 0)
|
|
129
|
+
.sort((a, b) => b.score - a.score)
|
|
130
|
+
.slice(0, limit)
|
|
131
|
+
.map(s => s.entry);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Load session context: retrieve the most relevant memories for the current session.
|
|
136
|
+
* @param {object} context - { techStack, phase, topic, project }
|
|
137
|
+
* @returns {object} Categorized memories for session start display
|
|
138
|
+
*/
|
|
139
|
+
function loadSessionContext(context = {}) {
|
|
140
|
+
const { techStack = [], phase, topic = '', project } = context;
|
|
141
|
+
|
|
142
|
+
const allEntries = Store.readAll(true); // Include global knowledge
|
|
143
|
+
const active = allEntries.filter(e => !e.deprecated && e.confidence >= 0.5);
|
|
144
|
+
|
|
145
|
+
// Build query from context
|
|
146
|
+
const queryText = [
|
|
147
|
+
topic,
|
|
148
|
+
...(techStack || []),
|
|
149
|
+
].join(' ');
|
|
150
|
+
|
|
151
|
+
const { index, docTokenCounts, N } = buildIndex(active);
|
|
152
|
+
const queryTokens = tokenize(queryText);
|
|
153
|
+
|
|
154
|
+
// Score all active entries
|
|
155
|
+
const scored = active.map(e => ({
|
|
156
|
+
entry: e,
|
|
157
|
+
score: queryTokens.length > 0
|
|
158
|
+
? tfidfScore(queryTokens, e.id, index, docTokenCounts, N) * 0.6 + e.confidence * 0.4
|
|
159
|
+
: e.confidence,
|
|
160
|
+
})).sort((a, b) => b.score - a.score);
|
|
161
|
+
|
|
162
|
+
// Bucket by type, top N per bucket
|
|
163
|
+
const preferences = scored.filter(s => s.entry.type === 'team_preference').slice(0, 5).map(s => s.entry);
|
|
164
|
+
const decisions = scored.filter(s => s.entry.type === 'architectural_decision').slice(0, 8).map(s => s.entry);
|
|
165
|
+
const bugPatterns = scored.filter(s => s.entry.type === 'bug_pattern').slice(0, 5).map(s => s.entry);
|
|
166
|
+
const codePatterns = scored.filter(s => s.entry.type === 'code_pattern').slice(0, 5).map(s => s.entry);
|
|
167
|
+
const domain = scored.filter(s => s.entry.type === 'domain_knowledge').slice(0, 3).map(s => s.entry);
|
|
168
|
+
|
|
169
|
+
return { preferences, decisions, bugPatterns, codePatterns, domain };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = { search, loadSessionContext, buildIndex, tfidfScore, tokenize };
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MindForge v2 — Knowledge Store
|
|
3
|
+
* Append-only JSONL knowledge base with CRUD-like operations.
|
|
4
|
+
*
|
|
5
|
+
* Philosophy:
|
|
6
|
+
* - NEVER delete entries — deprecate instead (audit trail)
|
|
7
|
+
* - NEVER update entries in-place — append new version, deprecate old
|
|
8
|
+
* - All writes are atomic (append to JSONL is atomic on POSIX)
|
|
9
|
+
* - Reads are always full scan + in-memory filter (files stay small)
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
|
|
18
|
+
// ── Paths ─────────────────────────────────────────────────────────────────────
|
|
19
|
+
let baseDir = process.cwd();
|
|
20
|
+
let globalBaseDir = os.homedir();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Configure the base directory for memory (useful for testing).
|
|
24
|
+
*/
|
|
25
|
+
function setBaseDir(dir) {
|
|
26
|
+
baseDir = dir;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Configure the global base directory (useful for testing).
|
|
31
|
+
*/
|
|
32
|
+
function setGlobalDir(dir) {
|
|
33
|
+
globalBaseDir = dir;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getPaths() {
|
|
37
|
+
const memoryDir = path.join(baseDir, '.mindforge', 'memory');
|
|
38
|
+
const globalDir = path.join(globalBaseDir, '.mindforge');
|
|
39
|
+
return {
|
|
40
|
+
MEMORY_DIR: memoryDir,
|
|
41
|
+
GLOBAL_DIR: globalDir,
|
|
42
|
+
KB_PATH: path.join(memoryDir, 'knowledge-base.jsonl'),
|
|
43
|
+
GLOBAL_KB_PATH: path.join(globalDir, 'global-knowledge-base.jsonl'),
|
|
44
|
+
DECISION_PATH: path.join(memoryDir, 'decision-library.jsonl'),
|
|
45
|
+
PATTERN_PATH: path.join(memoryDir, 'pattern-library.jsonl'),
|
|
46
|
+
PREFERENCES_PATH: path.join(memoryDir, 'team-preferences.jsonl'),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generates a UUID v4.
|
|
52
|
+
*/
|
|
53
|
+
function generateId() {
|
|
54
|
+
return crypto.randomUUID();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ensureDir(dir) {
|
|
58
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getFilePath(type) {
|
|
62
|
+
const paths = getPaths();
|
|
63
|
+
switch (type) {
|
|
64
|
+
case 'architectural_decision': return paths.DECISION_PATH;
|
|
65
|
+
case 'code_pattern': return paths.PATTERN_PATH;
|
|
66
|
+
case 'team_preference': return paths.PREFERENCES_PATH;
|
|
67
|
+
default: return paths.KB_PATH; // bug_pattern, domain_knowledge, all others
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Write operations ──────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Append a new knowledge entry.
|
|
75
|
+
* @param {object} entry - Entry data (without id, timestamp, times_referenced)
|
|
76
|
+
* @returns {string} The new entry's ID
|
|
77
|
+
*/
|
|
78
|
+
function add(entry) {
|
|
79
|
+
const paths = getPaths();
|
|
80
|
+
ensureDir(paths.MEMORY_DIR);
|
|
81
|
+
|
|
82
|
+
if (!entry.type) throw new Error('Knowledge entry requires a "type" field');
|
|
83
|
+
if (!entry.topic) throw new Error('Knowledge entry requires a "topic" field');
|
|
84
|
+
if (!entry.content) throw new Error('Knowledge entry requires a "content" field');
|
|
85
|
+
|
|
86
|
+
const id = entry.id || generateId();
|
|
87
|
+
const now = new Date().toISOString();
|
|
88
|
+
|
|
89
|
+
const full = {
|
|
90
|
+
id,
|
|
91
|
+
timestamp: now,
|
|
92
|
+
type: entry.type,
|
|
93
|
+
topic: entry.topic.slice(0, 80), // Enforce max topic length
|
|
94
|
+
content: entry.content,
|
|
95
|
+
source: entry.source || 'manual',
|
|
96
|
+
project: entry.project || readProjectName(),
|
|
97
|
+
confidence: Math.min(1.0, Math.max(0.0, entry.confidence ?? 0.7)),
|
|
98
|
+
tags: Array.isArray(entry.tags) ? entry.tags : [],
|
|
99
|
+
linked_adrs: Array.isArray(entry.linked_adrs) ? entry.linked_adrs : [],
|
|
100
|
+
times_referenced: entry.times_referenced || 0,
|
|
101
|
+
last_referenced: entry.last_referenced || null,
|
|
102
|
+
deprecated: false,
|
|
103
|
+
deprecated_by: null,
|
|
104
|
+
// Type-specific fields
|
|
105
|
+
...(entry.decision && { decision: entry.decision }),
|
|
106
|
+
...(entry.rationale && { rationale: entry.rationale }),
|
|
107
|
+
...(entry.alternatives && { alternatives: entry.alternatives }),
|
|
108
|
+
...(entry.adr_reference && { adr_reference: entry.adr_reference }),
|
|
109
|
+
...(entry.pattern_type && { pattern_type: entry.pattern_type }),
|
|
110
|
+
...(entry.language && { language: entry.language }),
|
|
111
|
+
...(entry.example_good && { example_good: entry.example_good }),
|
|
112
|
+
...(entry.example_bad && { example_bad: entry.example_bad }),
|
|
113
|
+
...(entry.bug_category && { bug_category: entry.bug_category }),
|
|
114
|
+
...(entry.root_cause && { root_cause: entry.root_cause }),
|
|
115
|
+
...(entry.fix && { fix: entry.fix }),
|
|
116
|
+
...(entry.preference && { preference: entry.preference }),
|
|
117
|
+
...(entry.strength && { strength: entry.strength }),
|
|
118
|
+
...(entry.domain && { domain: entry.domain }),
|
|
119
|
+
...(entry.tech_stack && { tech_stack: entry.tech_stack }),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const filePath = getFilePath(entry.type);
|
|
123
|
+
fs.appendFileSync(filePath, JSON.stringify(full) + '\n');
|
|
124
|
+
|
|
125
|
+
// Also append to unified knowledge-base.jsonl for cross-type queries
|
|
126
|
+
if (filePath !== paths.KB_PATH) {
|
|
127
|
+
fs.appendFileSync(paths.KB_PATH, JSON.stringify(full) + '\n');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return id;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Deprecate an entry (never hard-delete).
|
|
135
|
+
*/
|
|
136
|
+
function deprecate(id, reason, supersededBy = null) {
|
|
137
|
+
const paths = getPaths();
|
|
138
|
+
const entries = readAll();
|
|
139
|
+
const entry = entries.find(e => e.id === id);
|
|
140
|
+
if (!entry) throw new Error(`Knowledge entry not found: ${id}`);
|
|
141
|
+
|
|
142
|
+
// Append a deprecation marker (new entry with same id, deprecated=true)
|
|
143
|
+
const filePath = getFilePath(entry.type);
|
|
144
|
+
const deprecated = {
|
|
145
|
+
...entry,
|
|
146
|
+
deprecated: true,
|
|
147
|
+
deprecated_by: supersededBy,
|
|
148
|
+
deprecated_reason: reason,
|
|
149
|
+
deprecated_at: new Date().toISOString(),
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
fs.appendFileSync(filePath, JSON.stringify(deprecated) + '\n');
|
|
153
|
+
if (filePath !== paths.KB_PATH) {
|
|
154
|
+
fs.appendFileSync(paths.KB_PATH, JSON.stringify(deprecated) + '\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return id;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Reinforce an entry (increase confidence, increment reference count).
|
|
162
|
+
*/
|
|
163
|
+
function reinforce(id) {
|
|
164
|
+
const paths = getPaths();
|
|
165
|
+
const entries = readAll();
|
|
166
|
+
const entry = entries.find(e => e.id === id && !e.deprecated);
|
|
167
|
+
if (!entry) return;
|
|
168
|
+
|
|
169
|
+
const reinforced = {
|
|
170
|
+
...entry,
|
|
171
|
+
confidence: Math.min(1.0, entry.confidence + 0.05),
|
|
172
|
+
times_referenced: entry.times_referenced + 1,
|
|
173
|
+
last_referenced: new Date().toISOString(),
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const filePath = getFilePath(entry.type);
|
|
177
|
+
fs.appendFileSync(filePath, JSON.stringify(reinforced) + '\n');
|
|
178
|
+
if (filePath !== paths.KB_PATH) {
|
|
179
|
+
fs.appendFileSync(paths.KB_PATH, JSON.stringify(reinforced) + '\n');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Read operations ───────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Read all entries from a JSONL file.
|
|
187
|
+
* Handles the append pattern: later entries with same ID supersede earlier ones.
|
|
188
|
+
*/
|
|
189
|
+
function readFile(filePath) {
|
|
190
|
+
if (!fs.existsSync(filePath)) return [];
|
|
191
|
+
|
|
192
|
+
const lines = fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean);
|
|
193
|
+
const byId = new Map(); // Later entries (same ID) win
|
|
194
|
+
|
|
195
|
+
for (const line of lines) {
|
|
196
|
+
try {
|
|
197
|
+
const entry = JSON.parse(line);
|
|
198
|
+
byId.set(entry.id, entry); // Last write wins
|
|
199
|
+
} catch {
|
|
200
|
+
// Skip malformed lines — never crash on corrupt JSONL
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return [...byId.values()];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function readAll(includeGlobal = false) {
|
|
208
|
+
const paths = getPaths();
|
|
209
|
+
let entries = readFile(paths.KB_PATH);
|
|
210
|
+
if (includeGlobal && fs.existsSync(paths.GLOBAL_KB_PATH)) {
|
|
211
|
+
const globalEntries = readFile(paths.GLOBAL_KB_PATH).map(e => ({ ...e, global: true }));
|
|
212
|
+
entries = [...entries, ...globalEntries];
|
|
213
|
+
}
|
|
214
|
+
return entries;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function readByType(type) {
|
|
218
|
+
return readFile(getFilePath(type)).filter(e => e.type === type);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Query operations ──────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Query the knowledge base.
|
|
225
|
+
* Returns entries sorted by relevance score (confidence × recency × tag overlap).
|
|
226
|
+
*/
|
|
227
|
+
function query(params = {}) {
|
|
228
|
+
const {
|
|
229
|
+
tags = [],
|
|
230
|
+
topic,
|
|
231
|
+
type,
|
|
232
|
+
minConfidence = 0.3,
|
|
233
|
+
limit = 20,
|
|
234
|
+
includeGlobal = false,
|
|
235
|
+
includeDeprecated = false,
|
|
236
|
+
project,
|
|
237
|
+
} = params;
|
|
238
|
+
|
|
239
|
+
let entries = readAll(includeGlobal);
|
|
240
|
+
|
|
241
|
+
// Filter
|
|
242
|
+
if (!includeDeprecated) entries = entries.filter(e => !e.deprecated);
|
|
243
|
+
if (type) entries = entries.filter(e => e.type === type);
|
|
244
|
+
if (project) entries = entries.filter(e => !e.project || e.project === project);
|
|
245
|
+
entries = entries.filter(e => e.confidence >= minConfidence);
|
|
246
|
+
|
|
247
|
+
// Score entries by relevance
|
|
248
|
+
const scored = entries.map(e => ({
|
|
249
|
+
entry: e,
|
|
250
|
+
score: scoreRelevance(e, { tags, topic }),
|
|
251
|
+
}));
|
|
252
|
+
|
|
253
|
+
return scored
|
|
254
|
+
.filter(s => s.score > 0)
|
|
255
|
+
.sort((a, b) => b.score - a.score)
|
|
256
|
+
.slice(0, limit)
|
|
257
|
+
.map(s => s.entry);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function scoreRelevance(entry, { tags = [], topic = '' }) {
|
|
261
|
+
let score = entry.confidence; // Base score from confidence
|
|
262
|
+
|
|
263
|
+
// Tag overlap
|
|
264
|
+
const entryTags = entry.tags || [];
|
|
265
|
+
const tagOverlap = tags.filter(t => entryTags.some(et => et.toLowerCase() === t.toLowerCase())).length;
|
|
266
|
+
score += tagOverlap * 0.2;
|
|
267
|
+
|
|
268
|
+
// Topic text match
|
|
269
|
+
if (topic) {
|
|
270
|
+
const topicWords = topic.toLowerCase().split(/\s+/);
|
|
271
|
+
const entryText = `${entry.topic} ${entry.content}`.toLowerCase();
|
|
272
|
+
const wordMatches = topicWords.filter(w => w.length > 3 && entryText.includes(w)).length;
|
|
273
|
+
score += (wordMatches / Math.max(topicWords.length, 1)) * 0.3;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Recency boost (entries referenced in last 30 days get a small boost)
|
|
277
|
+
if (entry.last_referenced) {
|
|
278
|
+
const daysSince = (Date.now() - new Date(entry.last_referenced).getTime()) / 86_400_000;
|
|
279
|
+
if (daysSince < 30) score += 0.1 * (1 - daysSince / 30);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Penalty for very low reference count (may be noisy)
|
|
283
|
+
if (entry.times_referenced === 0) score *= 0.9;
|
|
284
|
+
|
|
285
|
+
return score;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── Project name helper ───────────────────────────────────────────────────────
|
|
289
|
+
function readProjectName() {
|
|
290
|
+
const projectMd = path.join(process.cwd(), '.planning', 'PROJECT.md');
|
|
291
|
+
if (!fs.existsSync(projectMd)) return 'unknown';
|
|
292
|
+
const match = fs.readFileSync(projectMd, 'utf8').match(/^# (.+)/m);
|
|
293
|
+
return match?.[1]?.trim().slice(0, 50) || 'unknown';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ── Statistics ────────────────────────────────────────────────────────────────
|
|
297
|
+
function stats() {
|
|
298
|
+
const all = readAll();
|
|
299
|
+
const active = all.filter(e => !e.deprecated);
|
|
300
|
+
const byType = {};
|
|
301
|
+
for (const e of active) {
|
|
302
|
+
byType[e.type] = (byType[e.type] || 0) + 1;
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
total_entries: all.length,
|
|
306
|
+
active_entries: active.length,
|
|
307
|
+
deprecated_entries: all.length - active.length,
|
|
308
|
+
by_type: byType,
|
|
309
|
+
avg_confidence: active.length
|
|
310
|
+
? active.reduce((s, e) => s + e.confidence, 0) / active.length
|
|
311
|
+
: 0,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
module.exports = {
|
|
316
|
+
add, deprecate, reinforce,
|
|
317
|
+
readAll, readByType, readFile, query, stats,
|
|
318
|
+
setBaseDir, setGlobalDir, getPaths,
|
|
319
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MindForge v2 — Session Memory Loader
|
|
3
|
+
* Loads relevant knowledge at session start and formats it for CLAUDE.md injection.
|
|
4
|
+
*
|
|
5
|
+
* Called at session boot to populate the agent with accumulated knowledge
|
|
6
|
+
* before any task begins.
|
|
7
|
+
*/
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const Indexer = require('./knowledge-indexer');
|
|
13
|
+
const Store = require('./knowledge-store');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Load relevant session context from the knowledge graph.
|
|
17
|
+
* Returns a formatted string for injection into agent context at session start.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} opts
|
|
20
|
+
* @param {string[]} opts.techStack - Tech stack from PROJECT.md (for relevance filtering)
|
|
21
|
+
* @param {string} opts.phase - Current phase description
|
|
22
|
+
* @param {string} opts.topic - Current task/topic focus
|
|
23
|
+
* @param {number} opts.maxEntries - Maximum entries to load (default: 20)
|
|
24
|
+
*/
|
|
25
|
+
function loadForSession(opts = {}) {
|
|
26
|
+
const { techStack = [], phase = '', topic = '', maxEntries = 20 } = opts;
|
|
27
|
+
|
|
28
|
+
const context = Indexer.loadSessionContext({ techStack, phase, topic });
|
|
29
|
+
const allLoaded = [
|
|
30
|
+
...context.preferences,
|
|
31
|
+
...context.decisions,
|
|
32
|
+
...context.bugPatterns,
|
|
33
|
+
...context.codePatterns,
|
|
34
|
+
...context.domain,
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
if (allLoaded.length === 0) {
|
|
38
|
+
return { formatted: '', entries: [], count: 0 };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Reinforce all loaded entries (they are being actively used)
|
|
42
|
+
for (const entry of allLoaded) {
|
|
43
|
+
try { Store.reinforce(entry.id); } catch (e) { /* ignore cleanup errors */ }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const formatted = formatForContext(context);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
formatted,
|
|
50
|
+
entries: allLoaded,
|
|
51
|
+
count: allLoaded.length,
|
|
52
|
+
preferences: context.preferences.length,
|
|
53
|
+
decisions: context.decisions.length,
|
|
54
|
+
bugPatterns: context.bugPatterns.length,
|
|
55
|
+
codePatterns: context.codePatterns.length,
|
|
56
|
+
domain: context.domain.length,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Format loaded knowledge entries for agent context injection.
|
|
62
|
+
*/
|
|
63
|
+
function formatForContext(context) {
|
|
64
|
+
const sections = [];
|
|
65
|
+
|
|
66
|
+
if (context.preferences.length > 0) {
|
|
67
|
+
sections.push('### Team Preferences');
|
|
68
|
+
context.preferences.forEach(e => {
|
|
69
|
+
sections.push(`- [${(e.confidence * 100).toFixed(0)}% confidence] ${e.topic}: ${e.content.slice(0, 200)}`);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (context.decisions.length > 0) {
|
|
74
|
+
sections.push('\n### Architectural Decisions (from this project)');
|
|
75
|
+
context.decisions.forEach(e => {
|
|
76
|
+
const adr = e.adr_reference ? ` (${e.adr_reference})` : '';
|
|
77
|
+
sections.push(`- ${e.topic}${adr}: ${e.content.slice(0, 200)}`);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (context.bugPatterns.length > 0) {
|
|
82
|
+
sections.push('\n### Bug Patterns to Avoid');
|
|
83
|
+
context.bugPatterns.forEach(e => {
|
|
84
|
+
sections.push(`- ⚠️ ${e.topic}: ${e.root_cause?.slice(0, 150) || e.content.slice(0, 150)}`);
|
|
85
|
+
if (e.fix) sections.push(` Fix: ${e.fix.slice(0, 100)}`);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (context.domain.length > 0) {
|
|
90
|
+
sections.push('\n### Domain Knowledge');
|
|
91
|
+
context.domain.forEach(e => {
|
|
92
|
+
sections.push(`- ${e.topic}: ${e.content.slice(0, 200)}`);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return sections.join('\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Read tech stack from PROJECT.md for relevance filtering.
|
|
101
|
+
*/
|
|
102
|
+
function readTechStack() {
|
|
103
|
+
const projectMd = path.join(process.cwd(), '.planning', 'PROJECT.md');
|
|
104
|
+
if (!fs.existsSync(projectMd)) return [];
|
|
105
|
+
const content = fs.readFileSync(projectMd, 'utf8');
|
|
106
|
+
// Extract tech stack section
|
|
107
|
+
const techSection = content.match(/## Tech stack\n+([\s\S]*?)(?=\n##|$)/i)?.[1] || '';
|
|
108
|
+
return techSection
|
|
109
|
+
.split('\n')
|
|
110
|
+
.map(l => l.replace(/^[-*•]\s*/, '').split(/[\s,/]/).filter(w => w.length > 2))
|
|
111
|
+
.flat()
|
|
112
|
+
.filter(Boolean)
|
|
113
|
+
.slice(0, 20);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Generate the memory header displayed at session start.
|
|
118
|
+
*/
|
|
119
|
+
function generateSessionHeader(loadResult) {
|
|
120
|
+
if (loadResult.count === 0) {
|
|
121
|
+
return '🧠 Knowledge Base — no relevant memories for this session\n';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const lines = [
|
|
125
|
+
`🧠 Knowledge Base — ${loadResult.count} relevant memories loaded:`,
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
if (loadResult.preferences > 0) lines.push(` Preferences : ${loadResult.preferences}`);
|
|
129
|
+
if (loadResult.decisions > 0) lines.push(` Decisions : ${loadResult.decisions}`);
|
|
130
|
+
if (loadResult.bugPatterns > 0) lines.push(` Bug patterns : ${loadResult.bugPatterns}`);
|
|
131
|
+
if (loadResult.codePatterns > 0) lines.push(` Code patterns: ${loadResult.codePatterns}`);
|
|
132
|
+
if (loadResult.domain > 0) lines.push(` Domain : ${loadResult.domain}`);
|
|
133
|
+
|
|
134
|
+
return lines.join('\n');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { loadForSession, readTechStack, generateSessionHeader, formatForContext };
|
|
@@ -13,13 +13,12 @@ module.exports = {
|
|
|
13
13
|
if (!handoff.implicit_knowledge) handoff.implicit_knowledge = [];
|
|
14
14
|
if (!handoff.quality_signals) handoff.quality_signals = [];
|
|
15
15
|
fs.writeFileSync(paths.handoff, JSON.stringify(handoff, null, 2) + '\n');
|
|
16
|
-
console.log(
|
|
16
|
+
console.log(' • HANDOFF.json: added intelligence layer fields');
|
|
17
17
|
},
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
// bin/migrations/0.5.0-to-0.6.0.js
|
|
21
21
|
'use strict';
|
|
22
|
-
const fs = require('fs');
|
|
23
22
|
module.exports = {
|
|
24
23
|
fromVersion: '0.5.0',
|
|
25
24
|
toVersion: '0.6.0',
|
|
@@ -32,6 +31,6 @@ module.exports = {
|
|
|
32
31
|
if (!Array.isArray(handoff.recent_commits)) handoff.recent_commits = [];
|
|
33
32
|
if (!Array.isArray(handoff.recent_files)) handoff.recent_files = [];
|
|
34
33
|
fs.writeFileSync(paths.handoff, JSON.stringify(handoff, null, 2) + '\n');
|
|
35
|
-
console.log(
|
|
34
|
+
console.log(' • HANDOFF.json: added distribution platform fields');
|
|
36
35
|
},
|
|
37
36
|
};
|
|
@@ -12,6 +12,6 @@ module.exports = {
|
|
|
12
12
|
if (!Array.isArray(handoff.recent_commits)) handoff.recent_commits = [];
|
|
13
13
|
if (!Array.isArray(handoff.recent_files)) handoff.recent_files = [];
|
|
14
14
|
fs.writeFileSync(paths.handoff, JSON.stringify(handoff, null, 2) + '\n');
|
|
15
|
-
console.log(
|
|
15
|
+
console.log(' • HANDOFF.json: added distribution platform fields');
|
|
16
16
|
},
|
|
17
17
|
};
|
|
@@ -32,7 +32,7 @@ module.exports = {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
fs.writeFileSync(paths.handoff, JSON.stringify(handoff, null, 2) + '\n');
|
|
35
|
-
console.log(
|
|
35
|
+
console.log(' • HANDOFF.json: added plugin_api_version, normalised arrays');
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// ── 2. AUDIT.jsonl ────────────────────────────────────────────────────────
|
|
@@ -59,7 +59,7 @@ module.exports = {
|
|
|
59
59
|
fs.writeFileSync(paths.audit, updated.join('\n') + '\n');
|
|
60
60
|
console.log(` • AUDIT.jsonl: backfilled session_id in ${modified} of ${lines.length} entries`);
|
|
61
61
|
} else {
|
|
62
|
-
console.log(
|
|
62
|
+
console.log(' • AUDIT.jsonl: all entries already have session_id');
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -93,7 +93,7 @@ module.exports = {
|
|
|
93
93
|
fs.appendFileSync(paths.state,
|
|
94
94
|
`\n\n---\n*Migrated to MindForge v1.0.0 schema on ${new Date().toISOString().slice(0,10)}*\n`
|
|
95
95
|
);
|
|
96
|
-
console.log(
|
|
96
|
+
console.log(' • STATE.md: added v1.0.0 migration note');
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
},
|