wicked-brain 0.3.7 → 0.4.2
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/README.md +7 -4
- package/package.json +1 -1
- package/server/bin/wicked-brain-server.mjs +62 -3
- package/server/lib/sqlite-search.mjs +269 -6
- package/server/lib/wikilinks.mjs +1 -0
- package/server/package.json +1 -1
- package/skills/wicked-brain-compile/SKILL.md +70 -0
- package/skills/wicked-brain-confirm/SKILL.md +83 -0
- package/skills/wicked-brain-ingest/SKILL.md +23 -11
- package/skills/wicked-brain-init/SKILL.md +30 -22
- package/skills/wicked-brain-lint/SKILL.md +52 -0
- package/skills/wicked-brain-search/SKILL.md +34 -1
- package/skills/wicked-brain-status/SKILL.md +56 -0
- package/skills/wicked-brain-synonyms/SKILL.md +133 -0
package/README.md
CHANGED
|
@@ -33,6 +33,7 @@ Your documents ──> Structured chunks ──> Synthesized wiki ──>
|
|
|
33
33
|
|
|
34
34
|
- **Chunks** are source-faithful extractions with rich metadata (entities, themes, tags)
|
|
35
35
|
- **Wiki articles** are LLM-synthesized concepts with `[[backlinks]]` to source chunks
|
|
36
|
+
- **Links carry confidence** — confirmed connections rank higher, contradictions surface for review
|
|
36
37
|
- **Every claim traces back** to a specific file you can read, edit, or delete
|
|
37
38
|
|
|
38
39
|
The brain is plain markdown on your filesystem. Open it in Obsidian, VS Code, or `cat`. No black box.
|
|
@@ -117,7 +118,7 @@ Every operation uses **progressive loading** — the agent never pulls more than
|
|
|
117
118
|
| **Connections** | None (just similarity scores) | Explicit `[[backlinks]]` between concepts |
|
|
118
119
|
| **Auditability** | Low (why did it retrieve this?) | High (every claim links to a source file) |
|
|
119
120
|
| **Infrastructure** | Vector DB + embedding pipeline + retrieval service | One SQLite file + markdown |
|
|
120
|
-
| **Maintenance** | Re-embed on changes, tune thresholds | Agent self-heals via lint and
|
|
121
|
+
| **Maintenance** | Re-embed on changes, tune thresholds | Agent self-heals via lint, enhance, and confidence tracking |
|
|
121
122
|
| **Cost to start** | Embedding API calls for entire corpus | Zero (deterministic chunking is free) |
|
|
122
123
|
| **Ideal scale** | Millions of documents | 100 - 10,000 high-signal documents |
|
|
123
124
|
|
|
@@ -125,16 +126,18 @@ Every operation uses **progressive loading** — the agent never pulls more than
|
|
|
125
126
|
|
|
126
127
|
| Skill | What it does |
|
|
127
128
|
|---|---|
|
|
128
|
-
| `wicked-brain:init` | Set up a new brain — creates
|
|
129
|
+
| `wicked-brain:init` | Set up a new brain — creates structure, starts the server, and ingests your project in one shot |
|
|
129
130
|
| `wicked-brain:ingest` | Add source files — text extracted deterministically, binary docs read via LLM vision |
|
|
130
131
|
| `wicked-brain:search` | Parallel search across your brain and linked brains |
|
|
131
132
|
| `wicked-brain:read` | Progressive loading: depth 0 (stats), depth 1 (summary), depth 2 (full content) |
|
|
132
133
|
| `wicked-brain:query` | Answer questions with source citations |
|
|
133
134
|
| `wicked-brain:compile` | Synthesize wiki articles from chunks |
|
|
134
|
-
| `wicked-brain:lint` | Find broken links, orphan chunks, inconsistencies; auto-fix where possible |
|
|
135
|
+
| `wicked-brain:lint` | Find broken links, orphan chunks, inconsistencies, tag synonyms, low-confidence links; auto-fix where possible |
|
|
135
136
|
| `wicked-brain:enhance` | Identify and fill knowledge gaps with inferred content |
|
|
136
137
|
| `wicked-brain:memory` | Store and recall experiential learnings across sessions (working / episodic / semantic tiers) |
|
|
137
|
-
| `wicked-brain:status` | Brain health, stats,
|
|
138
|
+
| `wicked-brain:status` | Brain health, stats, convergence debt detection, contradiction hotspots |
|
|
139
|
+
| `wicked-brain:confirm` | Confirm or contradict a brain link — adjusts confidence score and tracks evidence |
|
|
140
|
+
| `wicked-brain:synonyms` | Manage search synonym mappings; auto-suggest from search misses and tag frequency |
|
|
138
141
|
| `wicked-brain:server` | Manage the background search server (auto-triggered) |
|
|
139
142
|
| `wicked-brain:configure` | Write brain-aware context into your CLI's config (CLAUDE.md, GEMINI.md, etc.) |
|
|
140
143
|
| `wicked-brain:batch` | Generate scripts for bulk operations — avoids burning context on repetitive tool calls |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
|
-
import { readFileSync, writeFileSync, unlinkSync,
|
|
3
|
+
import { readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { argv, pid, exit } from "node:process";
|
|
6
6
|
import { FileWatcher } from "../lib/file-watcher.mjs";
|
|
@@ -15,9 +15,27 @@ function getArg(name) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const brainPath = resolve(getArg("brain") || ".");
|
|
18
|
-
const
|
|
18
|
+
const preferredPort = parseInt(getArg("port") || "4242", 10);
|
|
19
19
|
const configPath = join(brainPath, "brain.json");
|
|
20
20
|
|
|
21
|
+
/** Find a free TCP port starting from `start`. */
|
|
22
|
+
function findFreePort(start) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const tryPort = (p) => {
|
|
25
|
+
const probe = createServer();
|
|
26
|
+
probe.once("error", (err) => {
|
|
27
|
+
if (err.code === "EADDRINUSE") tryPort(p + 1);
|
|
28
|
+
else reject(err);
|
|
29
|
+
});
|
|
30
|
+
probe.once("listening", () => {
|
|
31
|
+
probe.close(() => resolve(p));
|
|
32
|
+
});
|
|
33
|
+
probe.listen(p, "127.0.0.1");
|
|
34
|
+
};
|
|
35
|
+
tryPort(start);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
// Read brain config
|
|
22
40
|
let brainId = "unknown";
|
|
23
41
|
try {
|
|
@@ -66,9 +84,37 @@ const actions = {
|
|
|
66
84
|
forward_links: (p) => ({ links: db.forwardLinks(p.id) }),
|
|
67
85
|
stats: () => db.stats(),
|
|
68
86
|
candidates: (p) => ({ candidates: db.candidates(p) }),
|
|
87
|
+
symbols: async (p) => {
|
|
88
|
+
// Prefer LSP workspace symbols (structured, language-aware)
|
|
89
|
+
try {
|
|
90
|
+
const lspResult = await lsp.workspaceSymbols({ query: p.name || p.query || "" });
|
|
91
|
+
if (lspResult.symbols && lspResult.symbols.length > 0) {
|
|
92
|
+
return {
|
|
93
|
+
results: lspResult.symbols.map(s => ({
|
|
94
|
+
id: `${s.file}::${s.name}`,
|
|
95
|
+
name: s.name,
|
|
96
|
+
type: s.kind,
|
|
97
|
+
file_path: s.file,
|
|
98
|
+
line_start: s.line,
|
|
99
|
+
})),
|
|
100
|
+
source: "lsp",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// LSP unavailable or errored (e.g. no tsconfig.json) — fall through to FTS
|
|
105
|
+
}
|
|
106
|
+
// Fall back to FTS-based symbol search
|
|
107
|
+
return { ...db.symbols(p), source: "fts" };
|
|
108
|
+
},
|
|
109
|
+
dependents: (p) => db.dependents(p),
|
|
110
|
+
refs: async (p) => lsp.references(p),
|
|
69
111
|
access_log: (p) => db.accessLog(p.id),
|
|
70
112
|
recent_memories: (p) => ({ memories: db.recentMemories(p) }),
|
|
71
113
|
contradictions: () => ({ links: db.contradictions() }),
|
|
114
|
+
confirm_link: (p) => db.confirmLink(p.source_id, p.target_path, p.verdict),
|
|
115
|
+
link_health: () => db.linkHealth(),
|
|
116
|
+
tag_frequency: () => ({ tags: db.tagFrequency() }),
|
|
117
|
+
search_misses: (p) => ({ misses: db.searchMisses(p) }),
|
|
72
118
|
// LSP actions
|
|
73
119
|
"lsp-health": () => lsp.health(),
|
|
74
120
|
"lsp-symbols": (p) => lsp.symbols(p),
|
|
@@ -123,9 +169,10 @@ const server = createServer((req, res) => {
|
|
|
123
169
|
});
|
|
124
170
|
|
|
125
171
|
// Read project directories from config
|
|
172
|
+
const metaConfigPath = join(brainPath, "_meta", "config.json");
|
|
126
173
|
let projects = [];
|
|
127
174
|
try {
|
|
128
|
-
const metaConfig = JSON.parse(readFileSync(
|
|
175
|
+
const metaConfig = JSON.parse(readFileSync(metaConfigPath, "utf-8"));
|
|
129
176
|
projects = metaConfig.projects || [];
|
|
130
177
|
} catch {}
|
|
131
178
|
|
|
@@ -136,6 +183,18 @@ watcher.onFileChange((relPath, absPath, content, eventType) => {
|
|
|
136
183
|
lsp.handleFileChange(relPath, absPath, content, eventType);
|
|
137
184
|
});
|
|
138
185
|
|
|
186
|
+
const port = await findFreePort(preferredPort);
|
|
187
|
+
|
|
188
|
+
// Write actual port back to config so skills can always find the server
|
|
189
|
+
try {
|
|
190
|
+
let metaConfig = {};
|
|
191
|
+
try { metaConfig = JSON.parse(readFileSync(metaConfigPath, "utf-8")); } catch {}
|
|
192
|
+
metaConfig.server_port = port;
|
|
193
|
+
writeFileSync(metaConfigPath, JSON.stringify(metaConfig, null, 2) + "\n");
|
|
194
|
+
} catch (err) {
|
|
195
|
+
console.error(`Warning: could not write port to config: ${err.message}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
139
198
|
server.listen(port, () => {
|
|
140
199
|
console.log(`wicked-brain-server running on port ${port} (brain: ${brainId}, pid: ${pid})`);
|
|
141
200
|
watcher.start();
|
|
@@ -24,6 +24,9 @@ function escapeFtsQuery(query) {
|
|
|
24
24
|
/** Weight factor for backlink count in search ranking (PageRank-lite). */
|
|
25
25
|
const BACKLINK_WEIGHT = 0.5;
|
|
26
26
|
|
|
27
|
+
/** Weight factor for average backlink confidence in search ranking. */
|
|
28
|
+
const CONFIDENCE_WEIGHT = 0.3;
|
|
29
|
+
|
|
27
30
|
/** Weight factor for access count in search ranking. */
|
|
28
31
|
const SEARCH_ACCESS_WEIGHT = 0.1;
|
|
29
32
|
|
|
@@ -73,7 +76,9 @@ export class SqliteSearch {
|
|
|
73
76
|
target_path TEXT NOT NULL,
|
|
74
77
|
target_brain TEXT,
|
|
75
78
|
rel TEXT,
|
|
76
|
-
link_text TEXT
|
|
79
|
+
link_text TEXT,
|
|
80
|
+
confidence REAL DEFAULT 0.5,
|
|
81
|
+
evidence_count INTEGER DEFAULT 0
|
|
77
82
|
);
|
|
78
83
|
|
|
79
84
|
CREATE INDEX IF NOT EXISTS idx_links_source ON links(source_id);
|
|
@@ -86,6 +91,12 @@ export class SqliteSearch {
|
|
|
86
91
|
);
|
|
87
92
|
CREATE INDEX IF NOT EXISTS idx_access_doc ON access_log(doc_id);
|
|
88
93
|
CREATE INDEX IF NOT EXISTS idx_access_session ON access_log(session_id);
|
|
94
|
+
|
|
95
|
+
CREATE TABLE IF NOT EXISTS search_misses (
|
|
96
|
+
query TEXT NOT NULL,
|
|
97
|
+
searched_at INTEGER NOT NULL,
|
|
98
|
+
session_id TEXT
|
|
99
|
+
);
|
|
89
100
|
`);
|
|
90
101
|
|
|
91
102
|
this.#migrate();
|
|
@@ -124,8 +135,23 @@ export class SqliteSearch {
|
|
|
124
135
|
currentVersion = 1;
|
|
125
136
|
}
|
|
126
137
|
|
|
127
|
-
//
|
|
128
|
-
|
|
138
|
+
// Migration 2: add confidence + evidence_count to links, add search_misses table
|
|
139
|
+
if (currentVersion < 2) {
|
|
140
|
+
try { this.#db.prepare(`SELECT confidence FROM links LIMIT 0`).get(); } catch {
|
|
141
|
+
this.#db.exec(`ALTER TABLE links ADD COLUMN confidence REAL DEFAULT 0.5`);
|
|
142
|
+
}
|
|
143
|
+
try { this.#db.prepare(`SELECT evidence_count FROM links LIMIT 0`).get(); } catch {
|
|
144
|
+
this.#db.exec(`ALTER TABLE links ADD COLUMN evidence_count INTEGER DEFAULT 0`);
|
|
145
|
+
}
|
|
146
|
+
this.#db.exec(`
|
|
147
|
+
CREATE TABLE IF NOT EXISTS search_misses (
|
|
148
|
+
query TEXT NOT NULL,
|
|
149
|
+
searched_at INTEGER NOT NULL,
|
|
150
|
+
session_id TEXT
|
|
151
|
+
)
|
|
152
|
+
`);
|
|
153
|
+
currentVersion = 2;
|
|
154
|
+
}
|
|
129
155
|
|
|
130
156
|
// Persist the current version
|
|
131
157
|
this.#db.exec(`DELETE FROM _schema_version`);
|
|
@@ -142,8 +168,16 @@ export class SqliteSearch {
|
|
|
142
168
|
}
|
|
143
169
|
}
|
|
144
170
|
|
|
171
|
+
/** Extract YAML frontmatter block from content if present. Returns null if none. */
|
|
172
|
+
static #extractFrontmatter(content) {
|
|
173
|
+
const m = content.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
|
|
174
|
+
return m ? m[1] : null;
|
|
175
|
+
}
|
|
176
|
+
|
|
145
177
|
index(doc) {
|
|
146
|
-
const { id, path, content
|
|
178
|
+
const { id, path, content } = doc;
|
|
179
|
+
// Auto-extract frontmatter from content when not provided explicitly
|
|
180
|
+
const frontmatter = doc.frontmatter ?? SqliteSearch.#extractFrontmatter(content);
|
|
147
181
|
const brainId = this.#brainId;
|
|
148
182
|
const indexedAt = Date.now();
|
|
149
183
|
|
|
@@ -221,7 +255,8 @@ export class SqliteSearch {
|
|
|
221
255
|
snippet(documents_fts, 2, '<b>', '</b>', '…', 32) AS snippet,
|
|
222
256
|
SUBSTR(d.content, 1, 1000) AS raw_content,
|
|
223
257
|
COALESCE(link_count.cnt, 0) AS backlink_count,
|
|
224
|
-
COALESCE(ac.cnt, 0) AS access_count
|
|
258
|
+
COALESCE(ac.cnt, 0) AS access_count,
|
|
259
|
+
COALESCE(link_conf.avg_conf, 0.5) AS avg_backlink_confidence
|
|
225
260
|
FROM documents_fts f
|
|
226
261
|
JOIN documents d ON d.id = f.id
|
|
227
262
|
LEFT JOIN (
|
|
@@ -229,6 +264,11 @@ export class SqliteSearch {
|
|
|
229
264
|
FROM links
|
|
230
265
|
GROUP BY target_path
|
|
231
266
|
) link_count ON d.path = link_count.target_path
|
|
267
|
+
LEFT JOIN (
|
|
268
|
+
SELECT target_path, AVG(confidence) AS avg_conf
|
|
269
|
+
FROM links
|
|
270
|
+
GROUP BY target_path
|
|
271
|
+
) link_conf ON d.path = link_conf.target_path
|
|
232
272
|
LEFT JOIN (
|
|
233
273
|
SELECT doc_id, COUNT(*) AS cnt
|
|
234
274
|
FROM access_log
|
|
@@ -236,7 +276,7 @@ export class SqliteSearch {
|
|
|
236
276
|
) ac ON d.id = ac.doc_id
|
|
237
277
|
WHERE documents_fts MATCH ?
|
|
238
278
|
${sinceClause}
|
|
239
|
-
ORDER BY (f.rank - (COALESCE(link_count.cnt, 0) * ${BACKLINK_WEIGHT}) - (COALESCE(ac.cnt, 0) * ${SEARCH_ACCESS_WEIGHT}))
|
|
279
|
+
ORDER BY (f.rank - (COALESCE(link_count.cnt, 0) * ${BACKLINK_WEIGHT}) - (COALESCE(ac.cnt, 0) * ${SEARCH_ACCESS_WEIGHT}) - (COALESCE(link_conf.avg_conf, 0.5) * ${CONFIDENCE_WEIGHT}))
|
|
240
280
|
LIMIT ? OFFSET ?
|
|
241
281
|
`)
|
|
242
282
|
.all(escaped, ...sinceParams, limit, offset)
|
|
@@ -257,6 +297,13 @@ export class SqliteSearch {
|
|
|
257
297
|
|
|
258
298
|
const total_matches = countRow ? countRow.cnt : 0;
|
|
259
299
|
|
|
300
|
+
// Log search miss when no results returned
|
|
301
|
+
if (total_matches === 0) {
|
|
302
|
+
this.#db.prepare(
|
|
303
|
+
`INSERT INTO search_misses (query, searched_at, session_id) VALUES (?, ?, ?)`
|
|
304
|
+
).run(query, Date.now(), session_id ?? null);
|
|
305
|
+
}
|
|
306
|
+
|
|
260
307
|
// Log access for each returned document if session_id provided
|
|
261
308
|
if (session_id && rows.length > 0) {
|
|
262
309
|
const logAccess = this.#db.prepare(
|
|
@@ -476,6 +523,222 @@ export class SqliteSearch {
|
|
|
476
523
|
.all();
|
|
477
524
|
}
|
|
478
525
|
|
|
526
|
+
/**
|
|
527
|
+
* Confirm or contradict a link, adjusting its confidence score.
|
|
528
|
+
* verdict: "confirm" → confidence += 0.1 (capped at 1.0)
|
|
529
|
+
* verdict: "contradict" → confidence -= 0.2 (floored at 0.0)
|
|
530
|
+
* Returns the updated link row, or null if no matching link was found.
|
|
531
|
+
*/
|
|
532
|
+
confirmLink(sourceId, targetPath, verdict) {
|
|
533
|
+
const link = this.#db.prepare(`
|
|
534
|
+
SELECT rowid, confidence, evidence_count
|
|
535
|
+
FROM links
|
|
536
|
+
WHERE source_id = ? AND target_path = ?
|
|
537
|
+
LIMIT 1
|
|
538
|
+
`).get(sourceId, targetPath);
|
|
539
|
+
|
|
540
|
+
if (!link) return null;
|
|
541
|
+
|
|
542
|
+
let newConfidence;
|
|
543
|
+
if (verdict === "confirm") {
|
|
544
|
+
newConfidence = Math.min(link.confidence + 0.1, 1.0);
|
|
545
|
+
} else if (verdict === "contradict") {
|
|
546
|
+
newConfidence = Math.max(link.confidence - 0.2, 0.0);
|
|
547
|
+
} else {
|
|
548
|
+
throw new Error(`Unknown verdict: ${verdict}. Expected "confirm" or "contradict".`);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
this.#db.prepare(`
|
|
552
|
+
UPDATE links
|
|
553
|
+
SET confidence = ?, evidence_count = evidence_count + 1
|
|
554
|
+
WHERE rowid = ?
|
|
555
|
+
`).run(newConfidence, link.rowid);
|
|
556
|
+
|
|
557
|
+
return this.#db.prepare(`
|
|
558
|
+
SELECT source_id, target_path, confidence, evidence_count
|
|
559
|
+
FROM links
|
|
560
|
+
WHERE rowid = ?
|
|
561
|
+
`).get(link.rowid);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Returns link health statistics for the lint skill.
|
|
566
|
+
* - broken_links: links where target_path doesn't exist in documents table
|
|
567
|
+
* - low_confidence_links: links where confidence < 0.3
|
|
568
|
+
* - total_links: total link count
|
|
569
|
+
* - avg_confidence: average confidence across all links
|
|
570
|
+
*/
|
|
571
|
+
linkHealth() {
|
|
572
|
+
const totalsRow = this.#db.prepare(`
|
|
573
|
+
SELECT COUNT(*) AS total_links, AVG(confidence) AS avg_confidence
|
|
574
|
+
FROM links
|
|
575
|
+
`).get();
|
|
576
|
+
|
|
577
|
+
const brokenRow = this.#db.prepare(`
|
|
578
|
+
SELECT COUNT(*) AS cnt
|
|
579
|
+
FROM links l
|
|
580
|
+
WHERE NOT EXISTS (
|
|
581
|
+
SELECT 1 FROM documents d WHERE d.path = l.target_path
|
|
582
|
+
)
|
|
583
|
+
`).get();
|
|
584
|
+
|
|
585
|
+
const lowConfRow = this.#db.prepare(`
|
|
586
|
+
SELECT COUNT(*) AS cnt
|
|
587
|
+
FROM links
|
|
588
|
+
WHERE confidence < 0.3
|
|
589
|
+
`).get();
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
total_links: totalsRow.total_links ?? 0,
|
|
593
|
+
avg_confidence: totalsRow.avg_confidence ?? null,
|
|
594
|
+
broken_links: brokenRow.cnt ?? 0,
|
|
595
|
+
low_confidence_links: lowConfRow.cnt ?? 0,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Returns tag frequency data for synonym detection.
|
|
601
|
+
* Parses frontmatter from all documents and extracts `contains` arrays.
|
|
602
|
+
* Returns [{tag, count}] sorted by count descending.
|
|
603
|
+
*/
|
|
604
|
+
tagFrequency() {
|
|
605
|
+
const rows = this.#db.prepare(`
|
|
606
|
+
SELECT frontmatter FROM documents WHERE frontmatter IS NOT NULL
|
|
607
|
+
`).all();
|
|
608
|
+
|
|
609
|
+
const counts = new Map();
|
|
610
|
+
|
|
611
|
+
for (const row of rows) {
|
|
612
|
+
const fm = row.frontmatter;
|
|
613
|
+
// Match: contains: tag1 tag2 tag3 (space-separated inline)
|
|
614
|
+
// or contains: ["tag1","tag2"] (JSON array)
|
|
615
|
+
// or multi-line YAML list (- tag per line)
|
|
616
|
+
// Note: \S required after contains: to avoid matching block-list headers
|
|
617
|
+
const inlineMatch = fm.match(/^contains:[ \t]+(\S.*)$/m);
|
|
618
|
+
if (inlineMatch) {
|
|
619
|
+
const raw = inlineMatch[1].trim();
|
|
620
|
+
let tags = [];
|
|
621
|
+
// Try JSON array first
|
|
622
|
+
if (raw.startsWith("[")) {
|
|
623
|
+
try {
|
|
624
|
+
tags = JSON.parse(raw).map(String);
|
|
625
|
+
} catch {
|
|
626
|
+
tags = raw.replace(/[\[\]"]/g, "").split(/[\s,]+/).filter(Boolean);
|
|
627
|
+
}
|
|
628
|
+
} else {
|
|
629
|
+
tags = raw.split(/\s+/).filter(Boolean);
|
|
630
|
+
}
|
|
631
|
+
for (const tag of tags) {
|
|
632
|
+
counts.set(tag, (counts.get(tag) ?? 0) + 1);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Also handle YAML block list: lines starting with " - tag" after "contains:"
|
|
637
|
+
const blockMatch = fm.match(/^contains:\s*\n((?:\s+-\s+.+\n?)+)/m);
|
|
638
|
+
if (!inlineMatch && blockMatch) {
|
|
639
|
+
const listLines = blockMatch[1].match(/^\s+-\s+(.+)$/gm) || [];
|
|
640
|
+
for (const line of listLines) {
|
|
641
|
+
const tag = line.replace(/^\s+-\s+/, "").trim();
|
|
642
|
+
if (tag) counts.set(tag, (counts.get(tag) ?? 0) + 1);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return Array.from(counts.entries())
|
|
648
|
+
.map(([tag, count]) => ({ tag, count }))
|
|
649
|
+
.sort((a, b) => b.count - a.count);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Symbol lookup: FTS search for a symbol name, returning structured results
|
|
654
|
+
* with file path and position extracted from chunk frontmatter.
|
|
655
|
+
* Used as fallback when no LSP server is running.
|
|
656
|
+
*/
|
|
657
|
+
symbols({ name, limit = 10 }) {
|
|
658
|
+
const escaped = escapeFtsQuery(name);
|
|
659
|
+
if (!escaped) return { results: [] };
|
|
660
|
+
|
|
661
|
+
const rows = this.#db.prepare(`
|
|
662
|
+
SELECT d.id, d.path, d.frontmatter, d.content
|
|
663
|
+
FROM documents_fts f
|
|
664
|
+
JOIN documents d ON d.id = f.id
|
|
665
|
+
WHERE documents_fts MATCH ?
|
|
666
|
+
ORDER BY rank
|
|
667
|
+
LIMIT ?
|
|
668
|
+
`).all(escaped, limit * 4); // overfetch — we may skip rows with no source_path
|
|
669
|
+
|
|
670
|
+
const seen = new Set();
|
|
671
|
+
const results = [];
|
|
672
|
+
|
|
673
|
+
for (const row of rows) {
|
|
674
|
+
if (results.length >= limit) break;
|
|
675
|
+
const fm = row.frontmatter || SqliteSearch.#extractFrontmatter(row.content) || "";
|
|
676
|
+
const sourcePathMatch = fm.match(/^source_path:\s*(.+)$/m);
|
|
677
|
+
const sourcePath = sourcePathMatch ? sourcePathMatch[1].trim() : null;
|
|
678
|
+
const key = `${sourcePath ?? row.path}::${name}`;
|
|
679
|
+
if (seen.has(key)) continue;
|
|
680
|
+
seen.add(key);
|
|
681
|
+
results.push({
|
|
682
|
+
id: key,
|
|
683
|
+
name,
|
|
684
|
+
type: "unknown",
|
|
685
|
+
file_path: sourcePath,
|
|
686
|
+
chunk_path: row.path,
|
|
687
|
+
line_start: null,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return { results };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Dependent files: find all files (by source_path) that mention the given name.
|
|
696
|
+
* Purely FTS-based — no LSP required.
|
|
697
|
+
*/
|
|
698
|
+
dependents({ name, limit = 20 }) {
|
|
699
|
+
const escaped = escapeFtsQuery(name);
|
|
700
|
+
if (!escaped) return { files: [] };
|
|
701
|
+
|
|
702
|
+
const rows = this.#db.prepare(`
|
|
703
|
+
SELECT d.frontmatter, d.content, d.path
|
|
704
|
+
FROM documents_fts f
|
|
705
|
+
JOIN documents d ON d.id = f.id
|
|
706
|
+
WHERE documents_fts MATCH ?
|
|
707
|
+
LIMIT ?
|
|
708
|
+
`).all(escaped, limit * 5);
|
|
709
|
+
|
|
710
|
+
const files = new Map(); // source_path → {file_path, chunk_path}
|
|
711
|
+
for (const row of rows) {
|
|
712
|
+
if (files.size >= limit) break;
|
|
713
|
+
const fm = row.frontmatter || SqliteSearch.#extractFrontmatter(row.content) || "";
|
|
714
|
+
const m = fm.match(/^source_path:\s*(.+)$/m);
|
|
715
|
+
const sourcePath = m ? m[1].trim() : null;
|
|
716
|
+
if (sourcePath && !files.has(sourcePath)) {
|
|
717
|
+
files.set(sourcePath, row.path);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return { files: [...files.keys()] };
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Retrieve logged search misses.
|
|
726
|
+
* @param {object} opts
|
|
727
|
+
* @param {number} [opts.limit=50]
|
|
728
|
+
* @param {string} [opts.since] - ISO 8601 timestamp
|
|
729
|
+
*/
|
|
730
|
+
searchMisses({ limit = 50, since = null } = {}) {
|
|
731
|
+
const sinceClause = since ? `WHERE searched_at >= ?` : "";
|
|
732
|
+
const sinceParams = since ? [new Date(since).getTime()] : [];
|
|
733
|
+
return this.#db.prepare(`
|
|
734
|
+
SELECT query, searched_at, session_id
|
|
735
|
+
FROM search_misses
|
|
736
|
+
${sinceClause}
|
|
737
|
+
ORDER BY searched_at DESC
|
|
738
|
+
LIMIT ?
|
|
739
|
+
`).all(...sinceParams, limit);
|
|
740
|
+
}
|
|
741
|
+
|
|
479
742
|
close() {
|
|
480
743
|
this.#db.close();
|
|
481
744
|
}
|
package/server/lib/wikilinks.mjs
CHANGED
package/server/package.json
CHANGED
|
@@ -86,6 +86,26 @@ file, update `authored_at` and `source_hashes`).
|
|
|
86
86
|
Read uncovered chunks (frontmatter + body) to understand their content.
|
|
87
87
|
Group them by topic/concept.
|
|
88
88
|
|
|
89
|
+
## Step 3.5: Identify Cluster Persona
|
|
90
|
+
|
|
91
|
+
For each concept cluster identified in Step 3, analyze the chunk tags and content
|
|
92
|
+
to select the most appropriate synthesis persona:
|
|
93
|
+
|
|
94
|
+
| Chunk tags/content signals | Persona | Focus |
|
|
95
|
+
|---------------------------|---------|-------|
|
|
96
|
+
| architecture, system-design, components, topology | Architect | Emphasize component relationships, trade-offs, scalability |
|
|
97
|
+
| testing, quality, coverage, assertions | QE Specialist | Emphasize test strategies, edge cases, quality metrics |
|
|
98
|
+
| security, auth, encryption, vulnerabilities | Security Analyst | Emphasize threat models, attack surfaces, mitigations |
|
|
99
|
+
| api, endpoints, contracts, integration | API Designer | Emphasize interface contracts, versioning, consumer experience |
|
|
100
|
+
| operations, deployment, monitoring, incidents | SRE | Emphasize reliability, observability, runbooks |
|
|
101
|
+
| data, schema, migration, storage | Data Engineer | Emphasize data integrity, schema evolution, performance |
|
|
102
|
+
| Default (no strong signal) | Generalist | Balanced synthesis across all dimensions |
|
|
103
|
+
|
|
104
|
+
When writing the wiki article in Step 4, adopt this persona's perspective:
|
|
105
|
+
- Frame the article's structure around the persona's priorities
|
|
106
|
+
- Include a "Considerations" section written from the persona's viewpoint
|
|
107
|
+
- Add the persona to the article's frontmatter: `synthesis_persona: {persona_name}`
|
|
108
|
+
|
|
89
109
|
## Step 4: Write wiki articles
|
|
90
110
|
|
|
91
111
|
For each concept cluster, write a wiki article to `{brain_path}/wiki/concepts/{concept-name}.md`
|
|
@@ -96,6 +116,7 @@ or `{brain_path}/wiki/topics/{topic-name}.md`:
|
|
|
96
116
|
authored_by: llm
|
|
97
117
|
authored_at: {ISO timestamp}
|
|
98
118
|
confidence: {average of source chunk confidences, rounded to 2 decimals}
|
|
119
|
+
synthesis_persona: generalist
|
|
99
120
|
source_chunks:
|
|
100
121
|
- {chunk-path-1}
|
|
101
122
|
- {chunk-path-2}
|
|
@@ -122,6 +143,55 @@ explicit lineage trail so the brain can track how concepts evolve over time.
|
|
|
122
143
|
- [[brain-id::cross-brain-concept]] (if applicable)
|
|
123
144
|
```
|
|
124
145
|
|
|
146
|
+
## Step 4.5: Consensus Compilation (high-importance clusters only)
|
|
147
|
+
|
|
148
|
+
A cluster is "high-importance" when it has:
|
|
149
|
+
- 5+ source chunks, OR
|
|
150
|
+
- Source chunks with backlink_count >= 3 (check via backlinks API), OR
|
|
151
|
+
- Source chunks spanning 3+ distinct path prefixes (cross-cutting concern)
|
|
152
|
+
|
|
153
|
+
For high-importance clusters, enhance the article with a second perspective:
|
|
154
|
+
|
|
155
|
+
1. After writing the initial article with the primary persona (Step 4), identify
|
|
156
|
+
a **contrasting** persona that would view the same content differently:
|
|
157
|
+
- Architect ↔ SRE (design vs. operational reality)
|
|
158
|
+
- QE Specialist ↔ Developer (testing rigor vs. implementation pragmatism)
|
|
159
|
+
- Security Analyst ↔ API Designer (lockdown vs. usability)
|
|
160
|
+
- For other combinations, choose the most relevant contrasting view.
|
|
161
|
+
|
|
162
|
+
2. Re-read the source chunks from the contrasting persona's perspective.
|
|
163
|
+
|
|
164
|
+
3. Add a "## Alternative Perspectives" section to the article:
|
|
165
|
+
```
|
|
166
|
+
## Alternative Perspectives
|
|
167
|
+
|
|
168
|
+
### {Contrasting Persona} View
|
|
169
|
+
|
|
170
|
+
{2-3 paragraphs examining the topic from the contrasting perspective.
|
|
171
|
+
Highlight where this view agrees with the primary synthesis, and where
|
|
172
|
+
it raises different priorities or concerns.}
|
|
173
|
+
|
|
174
|
+
### Points of Tension
|
|
175
|
+
|
|
176
|
+
{Bullet list of specific disagreements or trade-offs between the two views.
|
|
177
|
+
Use typed wikilinks where relevant:}
|
|
178
|
+
- [[questions::wiki/concepts/{related-concept}]] — {what the tension is about}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
4. Update the article frontmatter to reflect consensus compilation:
|
|
182
|
+
```yaml
|
|
183
|
+
synthesis_persona: {primary_persona}
|
|
184
|
+
contrasting_persona: {secondary_persona}
|
|
185
|
+
consensus: true
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
If the cluster does NOT meet the high-importance threshold, skip this step
|
|
189
|
+
(the single-persona article from Step 4 is sufficient).
|
|
190
|
+
|
|
191
|
+
Note: The `questions` typed link (`[[questions::wiki/concepts/X]]`) is recognized
|
|
192
|
+
by the brain server's wikilink parser alongside existing types (contradicts,
|
|
193
|
+
supersedes, supports, caused-by, extends, depends-on).
|
|
194
|
+
|
|
125
195
|
## Step 5: Index new articles
|
|
126
196
|
|
|
127
197
|
For each article written:
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wicked-brain:confirm
|
|
3
|
+
description: |
|
|
4
|
+
Confirm or contradict a brain link, adjusting its confidence score.
|
|
5
|
+
Increases confidence when a link is confirmed by evidence, decreases it
|
|
6
|
+
when contradicted. Tracks evidence_count for audit purposes.
|
|
7
|
+
|
|
8
|
+
Use when: "confirm this link", "contradict this connection", "adjust link
|
|
9
|
+
confidence", "mark link as confirmed", "this link is wrong".
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# wicked-brain:confirm
|
|
13
|
+
|
|
14
|
+
You adjust the confidence score of a brain link based on user feedback.
|
|
15
|
+
|
|
16
|
+
## Cross-Platform Notes
|
|
17
|
+
|
|
18
|
+
Commands in this skill work on macOS, Linux, and Windows. `curl` is available
|
|
19
|
+
on Windows 10+ and macOS/Linux — it is used here for API calls.
|
|
20
|
+
|
|
21
|
+
For the brain path default:
|
|
22
|
+
- macOS/Linux: ~/.wicked-brain
|
|
23
|
+
- Windows: %USERPROFILE%\.wicked-brain
|
|
24
|
+
|
|
25
|
+
## Config
|
|
26
|
+
|
|
27
|
+
Read `_meta/config.json` for brain path and server port.
|
|
28
|
+
If it doesn't exist, trigger wicked-brain:init.
|
|
29
|
+
|
|
30
|
+
## Parameters
|
|
31
|
+
|
|
32
|
+
- **source_id** (required): the ID of the source document that contains the link
|
|
33
|
+
- **target_path** (required): the target path the link points to
|
|
34
|
+
- **verdict** (required): `confirm` or `contradict`
|
|
35
|
+
|
|
36
|
+
## Process
|
|
37
|
+
|
|
38
|
+
### Step 1: Validate parameters
|
|
39
|
+
|
|
40
|
+
Ensure `source_id`, `target_path`, and `verdict` are provided.
|
|
41
|
+
`verdict` must be exactly `"confirm"` or `"contradict"`.
|
|
42
|
+
|
|
43
|
+
### Step 2: Submit the verdict to the server
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
curl -s -X POST http://localhost:{port}/api \
|
|
47
|
+
-H "Content-Type: application/json" \
|
|
48
|
+
-d '{"action":"confirm_link","params":{"source_id":"{source_id}","target_path":"{target_path}","verdict":"{verdict}"}}'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Step 3: Report the result
|
|
52
|
+
|
|
53
|
+
If the response contains a `confidence` value, report back to the user:
|
|
54
|
+
|
|
55
|
+
- What the verdict was (`confirmed` or `contradicted`)
|
|
56
|
+
- The updated confidence score (e.g., `0.6`)
|
|
57
|
+
- The evidence_count (how many times this link has been evaluated)
|
|
58
|
+
|
|
59
|
+
Example success response:
|
|
60
|
+
```
|
|
61
|
+
Link {source_id} → {target_path} {verdict}ed.
|
|
62
|
+
Updated confidence: {confidence} (based on {evidence_count} evaluations)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
If the API returns `null` (link not found), report:
|
|
66
|
+
```
|
|
67
|
+
No link found from {source_id} to {target_path}.
|
|
68
|
+
Use wicked-brain:search to verify the source document ID and target path.
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
If the API returns an error, report the error message.
|
|
72
|
+
|
|
73
|
+
### Step 4: Log the action
|
|
74
|
+
|
|
75
|
+
Append an event to `{brain_path}/_meta/log.jsonl`:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{"ts":"{ISO}","op":"link_{verdict}","source_id":"{source_id}","target_path":"{target_path}","confidence":{new_confidence},"evidence_count":{evidence_count},"author":"agent:confirm"}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Use your Write tool or append via shell:
|
|
82
|
+
- macOS/Linux: `echo '...' >> {brain_path}/_meta/log.jsonl`
|
|
83
|
+
- Windows PowerShell: `Add-Content -Path "{brain_path}\_meta\log.jsonl" -Value '...'`
|
|
@@ -25,7 +25,7 @@ For the brain path default:
|
|
|
25
25
|
|
|
26
26
|
## Config
|
|
27
27
|
|
|
28
|
-
Read `_meta/config.json` for brain path and server port.
|
|
28
|
+
Read `{brain_path}/_meta/config.json` for brain path and server port.
|
|
29
29
|
If it doesn't exist, trigger wicked-brain:init.
|
|
30
30
|
|
|
31
31
|
## Parameters
|
|
@@ -34,6 +34,20 @@ If it doesn't exist, trigger wicked-brain:init.
|
|
|
34
34
|
|
|
35
35
|
## Process
|
|
36
36
|
|
|
37
|
+
### Step 0: Ensure server is running
|
|
38
|
+
|
|
39
|
+
Before doing anything else, health-check the server:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
curl -s -f -X POST http://localhost:{port}/api \
|
|
43
|
+
-H "Content-Type: application/json" \
|
|
44
|
+
-d '{"action":"health","params":{}}'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If this fails (connection refused or non-2xx), invoke `wicked-brain:server` to start it
|
|
48
|
+
before continuing. Re-read `_meta/config.json` after the server starts to get the
|
|
49
|
+
actual port it bound to.
|
|
50
|
+
|
|
37
51
|
### Step 1: Assess scope
|
|
38
52
|
|
|
39
53
|
Determine if the source is a single file or a directory.
|
|
@@ -169,9 +183,8 @@ Instead, write a batch script and run it. This preserves context and is dramatic
|
|
|
169
183
|
|
|
170
184
|
```javascript
|
|
171
185
|
#!/usr/bin/env node
|
|
172
|
-
import { readFileSync, writeFileSync, mkdirSync, readdirSync,
|
|
186
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync, renameSync } from "node:fs";
|
|
173
187
|
import { join, extname, basename, relative } from "node:path";
|
|
174
|
-
import { createHash } from "node:crypto";
|
|
175
188
|
|
|
176
189
|
const BRAIN = "{brain_path}";
|
|
177
190
|
const PORT = {port};
|
|
@@ -273,7 +286,6 @@ async function removeOldChunks(name) {
|
|
|
273
286
|
}
|
|
274
287
|
}
|
|
275
288
|
// Archive the old directory
|
|
276
|
-
const { renameSync } = await import("node:fs");
|
|
277
289
|
renameSync(chunkDir, `${chunkDir}.archived-${ts}`);
|
|
278
290
|
console.log(` Archived old chunks: ${name}`);
|
|
279
291
|
}
|
|
@@ -345,17 +357,17 @@ function walk(dir, callback) {
|
|
|
345
357
|
|
|
346
358
|
console.log(`Ingesting from ${SOURCE_DIR}...`);
|
|
347
359
|
|
|
360
|
+
// Collect files first, then process serially so every ingestFile is awaited
|
|
361
|
+
const textFiles = [];
|
|
348
362
|
walk(SOURCE_DIR, (filePath) => {
|
|
349
363
|
const ext = extname(filePath).toLowerCase();
|
|
350
|
-
if (TEXT_EXT.has(ext))
|
|
351
|
-
|
|
352
|
-
} else if (BINARY_EXT.has(ext)) {
|
|
353
|
-
binaryFiles.push(filePath);
|
|
354
|
-
}
|
|
364
|
+
if (TEXT_EXT.has(ext)) textFiles.push(filePath);
|
|
365
|
+
else if (BINARY_EXT.has(ext)) binaryFiles.push(filePath);
|
|
355
366
|
});
|
|
356
367
|
|
|
357
|
-
|
|
358
|
-
await
|
|
368
|
+
for (const filePath of textFiles) {
|
|
369
|
+
try { await ingestFile(filePath); } catch (e) { console.error(` Error: ${filePath}: ${e.message}`); }
|
|
370
|
+
}
|
|
359
371
|
|
|
360
372
|
console.log(`\nDone: ${totalFiles} files, ${totalChunks} chunks indexed`);
|
|
361
373
|
if (binaryFiles.length > 0) {
|
|
@@ -10,7 +10,7 @@ description: |
|
|
|
10
10
|
|
|
11
11
|
# wicked-brain:init
|
|
12
12
|
|
|
13
|
-
You initialize a new digital brain on the filesystem.
|
|
13
|
+
You initialize a new digital brain on the filesystem and get it fully operational.
|
|
14
14
|
|
|
15
15
|
## Cross-Platform Notes
|
|
16
16
|
|
|
@@ -38,23 +38,16 @@ Ask these questions (provide defaults):
|
|
|
38
38
|
- Default (Windows): `%USERPROFILE%\.wicked-brain`
|
|
39
39
|
2. "What should this brain be called?" — Default: directory name
|
|
40
40
|
|
|
41
|
-
### Step 2:
|
|
41
|
+
### Step 2: Check for existing brain
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
If `{brain_path}/_meta/config.json` already exists, tell the user:
|
|
44
|
+
"A brain already exists at `{brain_path}`. Do you want to re-initialize it (keeps existing chunks) or pick a different path?"
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
- `brain_path`: the path confirmed in Step 1
|
|
47
|
-
- `project_path`: the current working directory
|
|
48
|
-
|
|
49
|
-
**Sequencing rationale:** Onboard starts with a read-only scanning phase (Glob, Grep, Read across the project). That scanning takes meaningful time. Steps 3–6 below are fast — just creating a handful of files and directories. They will complete well before onboard finishes scanning and reaches its write phase (where it needs `brain_path` dirs to exist). So it is safe to fire onboard now and proceed immediately with Steps 3–6; the brain dirs will be in place long before onboard needs them.
|
|
50
|
-
|
|
51
|
-
Continue with Steps 3–6 immediately after dispatching.
|
|
46
|
+
Stop and wait for their answer before continuing.
|
|
52
47
|
|
|
53
48
|
### Step 3: Create directory structure
|
|
54
49
|
|
|
55
|
-
Use your native Write
|
|
56
|
-
|
|
57
|
-
Directories to create (create each with its parent directories):
|
|
50
|
+
Use your native Write tool to create these directories (write a `.gitkeep` placeholder in each):
|
|
58
51
|
- `{brain_path}/raw`
|
|
59
52
|
- `{brain_path}/chunks/extracted`
|
|
60
53
|
- `{brain_path}/chunks/inferred`
|
|
@@ -99,21 +92,36 @@ Write to `{brain_path}/_meta/config.json`:
|
|
|
99
92
|
}
|
|
100
93
|
```
|
|
101
94
|
|
|
95
|
+
`server_port: 4242` is the *preferred* port. The server will find a free port starting
|
|
96
|
+
from this value on startup and write the actual port back to this file. You do not
|
|
97
|
+
need to find a free port manually.
|
|
98
|
+
|
|
102
99
|
### Step 6: Initialize the event log
|
|
103
100
|
|
|
104
101
|
Use your Write tool to create an empty file at `{brain_path}/_meta/log.jsonl`.
|
|
105
102
|
|
|
106
|
-
|
|
103
|
+
### Step 7: Start the server
|
|
104
|
+
|
|
105
|
+
Invoke `wicked-brain:server` to start the server against this brain path.
|
|
106
|
+
The server will pick a free port and write it back to `_meta/config.json`.
|
|
107
|
+
|
|
107
108
|
```bash
|
|
108
|
-
|
|
109
|
-
touch {brain_path}/_meta/log.jsonl
|
|
110
|
-
```
|
|
111
|
-
```powershell
|
|
112
|
-
# Windows PowerShell
|
|
113
|
-
New-Item -ItemType File -Force -Path "{brain_path}\_meta\log.jsonl"
|
|
109
|
+
npx wicked-brain-server --brain {brain_path} &
|
|
114
110
|
```
|
|
115
111
|
|
|
116
|
-
|
|
112
|
+
Wait for the health check to confirm it's up before continuing.
|
|
113
|
+
|
|
114
|
+
### Step 8: Ingest the project
|
|
115
|
+
|
|
116
|
+
Invoke `wicked-brain:ingest` with:
|
|
117
|
+
- `brain_path`: `{brain_path}`
|
|
118
|
+
- `source`: the current working directory
|
|
119
|
+
|
|
120
|
+
This indexes the project files so the brain is immediately queryable.
|
|
121
|
+
|
|
122
|
+
### Step 9: Confirm
|
|
117
123
|
|
|
118
124
|
Tell the user:
|
|
119
|
-
"Brain
|
|
125
|
+
"Brain `{name}` is ready at `{brain_path}` — {N} files ingested, {M} chunks indexed.
|
|
126
|
+
|
|
127
|
+
Run `/wicked-brain-compile` to synthesize wiki articles from the indexed content."
|
|
@@ -80,6 +80,58 @@ For each wiki article with source_hashes in frontmatter:
|
|
|
80
80
|
### Missing frontmatter
|
|
81
81
|
Check each chunk has required frontmatter fields (source, chunk_id, confidence, indexed_at).
|
|
82
82
|
|
|
83
|
+
### Tag synonym candidates
|
|
84
|
+
|
|
85
|
+
Call the server to get all tag frequencies:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
curl -s -X POST http://localhost:{port}/api \
|
|
89
|
+
-H "Content-Type: application/json" \
|
|
90
|
+
-d '{"action":"tag_frequency","params":{}}'
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The response contains `tags: [{tag, count}]`. Identify potential synonyms:
|
|
94
|
+
|
|
95
|
+
1. **Substring pairs**: if tag A is a substring of tag B (e.g., "auth" is a substring
|
|
96
|
+
of "authentication"), they may be synonyms. Flag pairs where both appear in the brain.
|
|
97
|
+
|
|
98
|
+
2. **Edit distance ≤ 2**: tags that differ by at most 2 character insertions, deletions,
|
|
99
|
+
or substitutions (e.g., "authentification" vs "authentication") may be typos or synonyms.
|
|
100
|
+
|
|
101
|
+
For each candidate pair, report as `info` severity with type `synonym_candidate`:
|
|
102
|
+
- **path**: `_meta` (brain-level issue, not file-specific)
|
|
103
|
+
- **message**: `Possible synonym: "{tagA}" ({countA} uses) and "{tagB}" ({countB} uses) — consider merging`
|
|
104
|
+
- **fix**: `Run wicked-brain:retag to consolidate tags`
|
|
105
|
+
|
|
106
|
+
Only report pairs where both tags have at least 1 use.
|
|
107
|
+
|
|
108
|
+
### Link confidence report
|
|
109
|
+
|
|
110
|
+
Call the server for link health:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
curl -s -X POST http://localhost:{port}/api \
|
|
114
|
+
-H "Content-Type: application/json" \
|
|
115
|
+
-d '{"action":"link_health","params":{}}'
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The response contains:
|
|
119
|
+
- `broken_links`: count of links whose target is not in the index
|
|
120
|
+
- `low_confidence_links`: count of links with confidence < 0.3
|
|
121
|
+
- `total_links`: total link count
|
|
122
|
+
- `avg_confidence`: average confidence across all links
|
|
123
|
+
|
|
124
|
+
Report findings:
|
|
125
|
+
|
|
126
|
+
- If `broken_links > 0`: severity `error`, type `broken_link`:
|
|
127
|
+
`{broken_links} links point to targets not in the index. Use wicked-brain:search to verify targets.`
|
|
128
|
+
|
|
129
|
+
- If `low_confidence_links > 0`: severity `warning`, type `low_confidence`:
|
|
130
|
+
`{low_confidence_links} links have confidence < 0.3. Use wicked-brain:confirm to evaluate them.`
|
|
131
|
+
|
|
132
|
+
- Always include summary stats in the report:
|
|
133
|
+
`Total links: {total_links}, avg confidence: {avg_confidence:.2f}`
|
|
134
|
+
|
|
83
135
|
## Pass 2: Semantic analysis
|
|
84
136
|
|
|
85
137
|
Read a sample of chunks and wiki articles. Check:
|
|
@@ -37,6 +37,25 @@ If it doesn't exist, trigger wicked-brain:init.
|
|
|
37
37
|
|
|
38
38
|
## Process
|
|
39
39
|
|
|
40
|
+
### Step 0: Load synonyms (optional)
|
|
41
|
+
|
|
42
|
+
Check if `{brain_path}/_meta/synonyms.json` exists using the Read tool.
|
|
43
|
+
If it exists, parse it. Format:
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"jwt": ["json web token", "auth token"],
|
|
47
|
+
"auth": ["authentication", "authorization"],
|
|
48
|
+
"k8s": ["kubernetes"]
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
When searching, expand the query: if any word in the query matches a synonym key,
|
|
53
|
+
add the synonym values as additional OR terms.
|
|
54
|
+
|
|
55
|
+
Example: query "jwt validation" → search for "jwt validation" first, then also
|
|
56
|
+
search for "json web token validation" and "auth token validation" if initial
|
|
57
|
+
results are sparse (fewer than 3 results).
|
|
58
|
+
|
|
40
59
|
### Step 1: Discover brains to search
|
|
41
60
|
|
|
42
61
|
Use the Read tool on `{brain_path}/brain.json` to get parents and links.
|
|
@@ -101,7 +120,21 @@ After all subagents return:
|
|
|
101
120
|
3. Sort by score descending
|
|
102
121
|
4. Tag each result with its brain origin
|
|
103
122
|
|
|
104
|
-
### Step 4:
|
|
123
|
+
### Step 4: Log search miss (if applicable)
|
|
124
|
+
|
|
125
|
+
If the merged results have 0 matches across all brains, the query is a "search miss."
|
|
126
|
+
Log it so the brain can learn:
|
|
127
|
+
```bash
|
|
128
|
+
curl -s -X POST http://localhost:{port}/api \
|
|
129
|
+
-H "Content-Type: application/json" \
|
|
130
|
+
-d '{"action":"search_misses","params":{"query":"{original_query}","session_id":"{session_id}"}}'
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Note: This logging happens server-side automatically when search returns 0 results.
|
|
134
|
+
The explicit call here is only needed if synonym-expanded searches found results
|
|
135
|
+
but the original query did not.
|
|
136
|
+
|
|
137
|
+
### Step 5: Return at requested depth
|
|
105
138
|
|
|
106
139
|
**Depth 0 (default):**
|
|
107
140
|
```
|
|
@@ -78,8 +78,64 @@ Depth 0 plus:
|
|
|
78
78
|
- List the top 10 most common tags
|
|
79
79
|
- Flag any staleness warnings (sources modified after last ingest)
|
|
80
80
|
|
|
81
|
+
**Convergence Debt:**
|
|
82
|
+
Detect chunks that are frequently accessed but have never been compiled into wiki articles:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
curl -s -X POST http://localhost:{port}/api \
|
|
86
|
+
-H "Content-Type: application/json" \
|
|
87
|
+
-d '{"action":"candidates","params":{"mode":"promote","limit":50}}'
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For each result where `access_count >= 5` and `session_diversity >= 3`, check whether any wiki article references it. Use the Grep tool on `{brain_path}/wiki/` searching for the chunk path string. If no wiki article references it, flag it as convergence debt:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
⚠ Convergence debt: {path} (accessed {access_count} times across {session_diversity} sessions, no wiki citation)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
If any convergence debt exists, suggest running `wicked-brain:compile` to promote high-value chunks.
|
|
97
|
+
|
|
98
|
+
**Contradiction Hotspots:**
|
|
99
|
+
Detect path prefixes that concentrate multiple contradictions:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
curl -s -X POST http://localhost:{port}/api \
|
|
103
|
+
-H "Content-Type: application/json" \
|
|
104
|
+
-d '{"action":"contradictions","params":{}}'
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Group the returned contradiction links by path prefix: take the first two path segments of each linked path (e.g., a path `chunks/extracted/auth/session.md` yields prefix `chunks/extracted/auth/`). If any prefix has 2 or more contradiction links, flag it as a hotspot:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
⚠ Contradiction hotspot: {prefix} ({N} contradictions) — consider dispatching wicked-brain:compile or manual review
|
|
111
|
+
```
|
|
112
|
+
|
|
81
113
|
**Depth 2:**
|
|
82
114
|
Depth 1 plus:
|
|
83
115
|
- Read `_meta/log.jsonl` fully for recent activity (last 7 days)
|
|
84
116
|
- List coverage gaps (chunks with no wiki article referencing them)
|
|
85
117
|
- Full linked brain details
|
|
118
|
+
|
|
119
|
+
**Link Health (Depth 2 only):**
|
|
120
|
+
Check link integrity and surface knowledge gaps using two additional API calls.
|
|
121
|
+
|
|
122
|
+
Get link health:
|
|
123
|
+
```bash
|
|
124
|
+
curl -s -X POST http://localhost:{port}/api \
|
|
125
|
+
-H "Content-Type: application/json" \
|
|
126
|
+
-d '{"action":"link_health","params":{}}'
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Report:
|
|
130
|
+
- Total links checked and number of broken links
|
|
131
|
+
- Links with confidence below 0.5 (low confidence links)
|
|
132
|
+
- Average confidence score across all links
|
|
133
|
+
|
|
134
|
+
Get recent search misses:
|
|
135
|
+
```bash
|
|
136
|
+
curl -s -X POST http://localhost:{port}/api \
|
|
137
|
+
-H "Content-Type: application/json" \
|
|
138
|
+
-d '{"action":"search_misses","params":{"limit":20}}'
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Report the top recurring search miss queries to identify knowledge gaps. If a query appears multiple times, that topic is a strong candidate for ingestion or wiki article creation.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wicked-brain:synonyms
|
|
3
|
+
description: |
|
|
4
|
+
Manage the brain's synonym map for search expansion. Add, remove, or review
|
|
5
|
+
synonym mappings. Can also auto-suggest synonyms from search miss data and
|
|
6
|
+
tag frequency analysis.
|
|
7
|
+
|
|
8
|
+
Use when: "add synonym", "manage synonyms", "brain synonyms",
|
|
9
|
+
"why can't I find X", "search isn't finding".
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# wicked-brain:synonyms
|
|
13
|
+
|
|
14
|
+
You manage the brain's synonym map for improved search recall.
|
|
15
|
+
|
|
16
|
+
## Cross-Platform Notes
|
|
17
|
+
|
|
18
|
+
Commands in this skill work on macOS, Linux, and Windows. When a command has
|
|
19
|
+
platform differences, alternatives are shown. Your native tools (Read, Write,
|
|
20
|
+
Grep, Glob) work everywhere — prefer them over shell commands when possible.
|
|
21
|
+
|
|
22
|
+
For the brain path default:
|
|
23
|
+
- macOS/Linux: ~/.wicked-brain
|
|
24
|
+
- Windows: %USERPROFILE%\.wicked-brain
|
|
25
|
+
|
|
26
|
+
## Config
|
|
27
|
+
|
|
28
|
+
Read `_meta/config.json` for brain path and server port.
|
|
29
|
+
If it doesn't exist, trigger wicked-brain:init.
|
|
30
|
+
|
|
31
|
+
## Synonym File
|
|
32
|
+
|
|
33
|
+
Location: `{brain_path}/_meta/synonyms.json`
|
|
34
|
+
|
|
35
|
+
Format:
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"jwt": ["json web token", "auth token"],
|
|
39
|
+
"auth": ["authentication", "authorization"]
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Keys are the short/common form. Values are expansions to try when the key
|
|
44
|
+
appears in a search query. The search skill reads this file before executing
|
|
45
|
+
queries and automatically expands sparse results using these mappings.
|
|
46
|
+
|
|
47
|
+
## Commands
|
|
48
|
+
|
|
49
|
+
### Add a synonym
|
|
50
|
+
|
|
51
|
+
Parameters: `term`, `expansions` (comma-separated list)
|
|
52
|
+
|
|
53
|
+
1. Read `{brain_path}/_meta/synonyms.json` using the Read tool (or start with `{}` if the file does not exist).
|
|
54
|
+
2. Parse the JSON.
|
|
55
|
+
3. If the key already exists, merge the new expansions with the existing list (deduplicate).
|
|
56
|
+
4. If the key is new, add it with the provided expansions as an array.
|
|
57
|
+
5. Write the updated JSON back using the Write tool.
|
|
58
|
+
6. Confirm: `Synonym added: "{term}" → [{expansions}]`
|
|
59
|
+
|
|
60
|
+
### Remove a synonym
|
|
61
|
+
|
|
62
|
+
Parameters: `term`
|
|
63
|
+
|
|
64
|
+
1. Read `{brain_path}/_meta/synonyms.json` using the Read tool.
|
|
65
|
+
2. Parse the JSON.
|
|
66
|
+
3. Delete the key matching `term`. If the key does not exist, report that and stop.
|
|
67
|
+
4. Write the updated JSON back using the Write tool.
|
|
68
|
+
5. Confirm: `Synonym removed: "{term}"`
|
|
69
|
+
|
|
70
|
+
### Review synonyms
|
|
71
|
+
|
|
72
|
+
No parameters.
|
|
73
|
+
|
|
74
|
+
Read `{brain_path}/_meta/synonyms.json` using the Read tool and display the
|
|
75
|
+
current synonym map in a readable table:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
Synonym map ({N} entries):
|
|
79
|
+
|
|
80
|
+
jwt → json web token, auth token
|
|
81
|
+
auth → authentication, authorization
|
|
82
|
+
k8s → kubernetes
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
If the file does not exist, report: "No synonyms defined yet. Use 'add synonym' to create mappings."
|
|
86
|
+
|
|
87
|
+
### Auto-suggest
|
|
88
|
+
|
|
89
|
+
Analyze search misses and tag frequency to surface candidate synonym mappings
|
|
90
|
+
for user review.
|
|
91
|
+
|
|
92
|
+
**Step 1: Get recent search misses**
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
curl -s -X POST http://localhost:{port}/api \
|
|
96
|
+
-H "Content-Type: application/json" \
|
|
97
|
+
-d '{"action":"search_misses","params":{"limit":50}}'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Step 2: Get tag frequency**
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
curl -s -X POST http://localhost:{port}/api \
|
|
104
|
+
-H "Content-Type: application/json" \
|
|
105
|
+
-d '{"action":"tag_frequency","params":{}}'
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Step 3: Cross-reference and suggest**
|
|
109
|
+
|
|
110
|
+
For each search miss query:
|
|
111
|
+
- Check whether any word in the query is a substring of an existing tag or vice versa.
|
|
112
|
+
Example: miss query "k8s deploy" — tag "kubernetes" contains "k8s" as a known abbreviation.
|
|
113
|
+
- If a match is found, propose: `"{miss_word}" → ["{tag}"]`
|
|
114
|
+
|
|
115
|
+
For tags that appear to be alternate forms of each other (e.g., "auth" and
|
|
116
|
+
"authentication" both present as tags), suggest consolidating them:
|
|
117
|
+
- `"auth" → ["authentication"]`
|
|
118
|
+
|
|
119
|
+
**Step 4: Present suggestions for approval**
|
|
120
|
+
|
|
121
|
+
List all suggestions in a numbered table before writing anything:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
Suggested synonyms (review before applying):
|
|
125
|
+
|
|
126
|
+
1. "k8s" → ["kubernetes"] (from search miss: "k8s deploy")
|
|
127
|
+
2. "auth" → ["authentication"] (tag consolidation)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Ask: "Apply all, apply some (specify numbers), or cancel?"
|
|
131
|
+
|
|
132
|
+
Only write to `synonyms.json` after explicit user approval. Merge approved
|
|
133
|
+
suggestions with any existing entries using the same add-synonym logic above.
|