wicked-brain 0.15.3 → 0.16.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.
package/package.json
CHANGED
|
@@ -161,6 +161,28 @@ async function healthInfo(port, { timeoutMs = 800 } = {}) {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
// Raised by ensureServer when a server answers the persisted port but it
|
|
165
|
+
// belongs to a DIFFERENT brain than the one we resolved. The data path
|
|
166
|
+
// (search/index/remove/forget/query/...) routes through ensureServer, so this
|
|
167
|
+
// guard is the single choke point that stops a destructive op from silently
|
|
168
|
+
// hitting the wrong brain on a shared/recycled port. Fail closed — never
|
|
169
|
+
// operate on a mismatched brain.
|
|
170
|
+
class PortConflictError extends Error {
|
|
171
|
+
constructor({ port, expectedBrainId, actualBrainId }) {
|
|
172
|
+
super(
|
|
173
|
+
`port_conflict: port ${port} is held by a different brain ` +
|
|
174
|
+
`(expected brain_id "${expectedBrainId}", got "${actualBrainId ?? "unknown"}"). ` +
|
|
175
|
+
`Refusing the operation so it can't hit the wrong brain. ` +
|
|
176
|
+
`Stop the other server, free the port, or pass the correct --port for this brain.`,
|
|
177
|
+
);
|
|
178
|
+
this.name = "PortConflictError";
|
|
179
|
+
this.code = "port_conflict";
|
|
180
|
+
this.port = port;
|
|
181
|
+
this.expectedBrainId = expectedBrainId;
|
|
182
|
+
this.actualBrainId = actualBrainId ?? null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
164
186
|
// ---------- spawn lock ----------
|
|
165
187
|
// Cross-platform exclusive-create lock via { flag: "wx" }. Stale entries
|
|
166
188
|
// (older than STALE_LOCK_MS) are reaped on contention so a crashed CLI
|
|
@@ -233,12 +255,42 @@ function openServerLog(brainPath) {
|
|
|
233
255
|
}
|
|
234
256
|
}
|
|
235
257
|
|
|
258
|
+
// Reconcile the responding server's brain_id against the brain we resolved.
|
|
259
|
+
// Returns true when the port is ours (or when expectedBrainId is unknown and
|
|
260
|
+
// we choose not to gate). Throws PortConflictError when a DIFFERENT brain
|
|
261
|
+
// answers — the fail-closed path that protects the data plane. One health
|
|
262
|
+
// round-trip per resolution; cheap enough that we don't cache across calls
|
|
263
|
+
// (each CLI invocation resolves the server exactly once anyway).
|
|
264
|
+
function reconcileHealth(health, { port, expectedBrainId }) {
|
|
265
|
+
// No expected id to compare against (no brain.json id, no basename) — can't
|
|
266
|
+
// meaningfully gate, so don't. In practice readExpectedBrainId always yields
|
|
267
|
+
// at least the basename, so this is a defensive fallback only.
|
|
268
|
+
if (!expectedBrainId) return true;
|
|
269
|
+
if (health.brain_id === expectedBrainId) return true;
|
|
270
|
+
throw new PortConflictError({
|
|
271
|
+
port,
|
|
272
|
+
expectedBrainId,
|
|
273
|
+
actualBrainId: health.brain_id,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
236
277
|
async function ensureServer(brainPath, opts) {
|
|
237
278
|
const { explicitPort, sourceOverride, noSpawn, log, spawnTimeoutMs = 10_000 } = opts;
|
|
238
279
|
const meta = readMetaConfig(brainPath);
|
|
239
280
|
const port = explicitPort || meta.server_port || 4242;
|
|
240
|
-
|
|
241
|
-
|
|
281
|
+
// The brain we EXPECT on this port — same source --status/--stop reconcile
|
|
282
|
+
// against (brain.json `id`, falling back to the per-project basename).
|
|
283
|
+
const expectedBrainId = opts.expectedBrainId ?? readExpectedBrainId(brainPath);
|
|
284
|
+
|
|
285
|
+
// Warm path: a server already answers the port. Confirm it's OUR brain before
|
|
286
|
+
// handing the port to the data plane. A foreign brain can occupy this port
|
|
287
|
+
// (stale config, manual restart, EADDRINUSE probe-up), and routing by port
|
|
288
|
+
// alone would let a destructive op (remove/forget) silently hit it.
|
|
289
|
+
const warmHealth = await healthInfo(port);
|
|
290
|
+
if (warmHealth) {
|
|
291
|
+
reconcileHealth(warmHealth, { port, expectedBrainId });
|
|
292
|
+
return port;
|
|
293
|
+
}
|
|
242
294
|
if (noSpawn) {
|
|
243
295
|
throw new Error(`server not reachable on port ${port} and --no-spawn was set`);
|
|
244
296
|
}
|
|
@@ -248,14 +300,26 @@ async function ensureServer(brainPath, opts) {
|
|
|
248
300
|
|
|
249
301
|
if (!tryLock(lockPath)) {
|
|
250
302
|
log(`another process is starting the server; waiting...`);
|
|
251
|
-
|
|
303
|
+
const concurrentHealth = await waitForHealthInfo(port, spawnTimeoutMs);
|
|
304
|
+
if (concurrentHealth) {
|
|
305
|
+
// The peer that held the lock brought a server up — confirm it's OUR
|
|
306
|
+
// brain before we route the data plane at it.
|
|
307
|
+
reconcileHealth(concurrentHealth, { port, expectedBrainId });
|
|
308
|
+
return port;
|
|
309
|
+
}
|
|
252
310
|
throw new Error(`concurrent spawn timed out on port ${port}`);
|
|
253
311
|
}
|
|
254
312
|
|
|
255
313
|
try {
|
|
256
314
|
// Re-check after acquiring the lock — another process might have started
|
|
257
|
-
// and finished while we were contending.
|
|
258
|
-
|
|
315
|
+
// and finished while we were contending. Reconcile brain_id here too: the
|
|
316
|
+
// server that came up while we waited could be a different brain probing up
|
|
317
|
+
// onto this port.
|
|
318
|
+
const relockHealth = await healthInfo(port);
|
|
319
|
+
if (relockHealth) {
|
|
320
|
+
reconcileHealth(relockHealth, { port, expectedBrainId });
|
|
321
|
+
return port;
|
|
322
|
+
}
|
|
259
323
|
|
|
260
324
|
const pidPath = join(brainPath, "_meta", "server.pid");
|
|
261
325
|
if (existsSync(pidPath)) {
|
|
@@ -299,7 +363,14 @@ async function ensureServer(brainPath, opts) {
|
|
|
299
363
|
}
|
|
300
364
|
if (logFd !== null) log(`server logs -> ${logPath}`);
|
|
301
365
|
|
|
302
|
-
|
|
366
|
+
const ready = await waitForHealthInfo(port, spawnTimeoutMs);
|
|
367
|
+
if (ready) {
|
|
368
|
+
// We spawned with --brain brainPath, so the server that comes up should
|
|
369
|
+
// be ours. Reconcile anyway: a foreign brain could have won the bind in
|
|
370
|
+
// the race window before our child listened. Fail closed if so.
|
|
371
|
+
reconcileHealth(ready, { port, expectedBrainId });
|
|
372
|
+
return port;
|
|
373
|
+
}
|
|
303
374
|
throw new Error(
|
|
304
375
|
`server did not become ready within ${spawnTimeoutMs}ms on port ${port}` +
|
|
305
376
|
(logFd !== null ? ` — see ${logPath} for the cause` : ""),
|
|
@@ -318,6 +389,18 @@ async function waitForHealth(port, timeoutMs) {
|
|
|
318
389
|
return false;
|
|
319
390
|
}
|
|
320
391
|
|
|
392
|
+
// Like waitForHealth but returns the health body (carrying brain_id) once the
|
|
393
|
+
// port answers, so the spawn path can confirm WHICH brain came up.
|
|
394
|
+
async function waitForHealthInfo(port, timeoutMs) {
|
|
395
|
+
const deadline = Date.now() + timeoutMs;
|
|
396
|
+
while (Date.now() < deadline) {
|
|
397
|
+
const h = await healthInfo(port, { timeoutMs: 500 });
|
|
398
|
+
if (h) return h;
|
|
399
|
+
await sleep(150);
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
321
404
|
function sleep(ms) {
|
|
322
405
|
return new Promise(r => setTimeout(r, ms));
|
|
323
406
|
}
|
|
@@ -644,13 +727,43 @@ Examples:
|
|
|
644
727
|
}
|
|
645
728
|
}
|
|
646
729
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
730
|
+
let port;
|
|
731
|
+
try {
|
|
732
|
+
port = await ensureServer(brainPath, {
|
|
733
|
+
explicitPort: args.flags.port,
|
|
734
|
+
sourceOverride: args.flags.source,
|
|
735
|
+
noSpawn: args.flags.noSpawn,
|
|
736
|
+
spawnTimeoutMs: args.flags.spawnTimeoutMs,
|
|
737
|
+
log,
|
|
738
|
+
});
|
|
739
|
+
} catch (err) {
|
|
740
|
+
if (err instanceof PortConflictError) {
|
|
741
|
+
// Fail closed: the persisted port is held by a different brain. Refuse the
|
|
742
|
+
// op (read OR mutate) so a destructive call (remove/forget/index) can't
|
|
743
|
+
// silently hit the wrong brain. Emit a structured payload on stdout that
|
|
744
|
+
// mirrors --stop/--status, plus a non-zero exit for scripted callers.
|
|
745
|
+
const payload = {
|
|
746
|
+
error: err.message,
|
|
747
|
+
action,
|
|
748
|
+
refused: true,
|
|
749
|
+
reason: "port_conflict",
|
|
750
|
+
port: err.port,
|
|
751
|
+
brain_id: err.expectedBrainId,
|
|
752
|
+
actual_brain_id: err.actualBrainId,
|
|
753
|
+
};
|
|
754
|
+
// Leave a refusal breadcrumb so the audit trail shows the op was blocked.
|
|
755
|
+
if (auditEnabled(args.flags.noAudit)) {
|
|
756
|
+
const a = writeAuditOpen(brainPath, action, params, err.port);
|
|
757
|
+
writeAuditClose(a, { exitCode: 1, durationMs: 0, response: payload, error: err.message });
|
|
758
|
+
}
|
|
759
|
+
process.stdout.write(
|
|
760
|
+
(args.flags.pretty ? JSON.stringify(payload, null, 2) : JSON.stringify(payload)) + "\n",
|
|
761
|
+
);
|
|
762
|
+
process.exitCode = 1;
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
die(err.message);
|
|
766
|
+
}
|
|
654
767
|
|
|
655
768
|
// Open audit BEFORE the call so a crash mid-flight still leaves a partial
|
|
656
769
|
// record. Audit is best-effort — write failures never block the request.
|
|
@@ -141,14 +141,18 @@ const actions = {
|
|
|
141
141
|
emitEvent("wicked.search.executed", "brain.search", {
|
|
142
142
|
query: p.query, result_count: result.total_matches, brain_id: brainId,
|
|
143
143
|
});
|
|
144
|
-
|
|
144
|
+
// Stamp the responding brain on the envelope so a `wicked-brain-call search`
|
|
145
|
+
// makes WHICH brain answered visible without a separate health round-trip.
|
|
146
|
+
// (A wrong-port hit is caught upstream by reconcileHealth, but this keeps
|
|
147
|
+
// the answer self-describing for the operator and the rendering skill.)
|
|
148
|
+
return { ...result, brain_id: brainId };
|
|
145
149
|
},
|
|
146
150
|
federated_search: (p) => {
|
|
147
151
|
const result = db.federatedSearch(p);
|
|
148
152
|
emitEvent("wicked.search.executed", "brain.search", {
|
|
149
153
|
query: p.query, federated: true, brain_id: brainId,
|
|
150
154
|
});
|
|
151
|
-
return result;
|
|
155
|
+
return { ...result, brain_id: brainId };
|
|
152
156
|
},
|
|
153
157
|
index: (p) => {
|
|
154
158
|
db.index(p);
|
package/server/package.json
CHANGED
|
@@ -13,14 +13,16 @@ description: |
|
|
|
13
13
|
|
|
14
14
|
# wicked-brain:query
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Answer questions from the brain's content by dispatching a query subagent.
|
|
17
|
+
The default path is a direct search → read → synthesize. Reserve the heavier
|
|
18
|
+
steps (synonym expansion, grep, backlinks) for when the direct search is thin.
|
|
17
19
|
|
|
18
20
|
## Config
|
|
19
21
|
|
|
20
22
|
Brain discovery + server lifecycle are handled by `wicked-brain-call`. Pass
|
|
21
23
|
`--brain <path>` to override the auto-detected brain, or set
|
|
22
|
-
`WICKED_BRAIN_PATH`. The CLI starts the server on first call (no manual
|
|
23
|
-
|
|
24
|
+
`WICKED_BRAIN_PATH`. The CLI starts the server on first call (no manual init)
|
|
25
|
+
and writes an audit record to `{brain}/calls/` per call.
|
|
24
26
|
|
|
25
27
|
## Parameters
|
|
26
28
|
|
|
@@ -36,137 +38,72 @@ Server interactions: use `npx wicked-brain-call <action> [--param k=v ...]`.
|
|
|
36
38
|
|
|
37
39
|
Question: "{question}"
|
|
38
40
|
|
|
39
|
-
## Step
|
|
41
|
+
## Step 1: Search (default path)
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
full sentences or common words.
|
|
43
|
+
Extract 2-4 key terms from the question (noun phrases, named entities,
|
|
44
|
+
technical terms — not full sentences or common words). Search the brain:
|
|
44
45
|
|
|
45
|
-
Example:
|
|
46
|
-
Question: "What was the reasoning behind choosing PostgreSQL over SQLite?"
|
|
47
|
-
Key terms: ["PostgreSQL", "SQLite", "database decision", "API layer"]
|
|
48
|
-
|
|
49
|
-
### Check learned synonyms first
|
|
50
|
-
|
|
51
|
-
Before generating synonyms, check if `{brain_path}/_meta/synonyms.json` exists.
|
|
52
|
-
If it does, read it. If it does not exist, skip synonym expansion and proceed
|
|
53
|
-
with LLM-generated synonyms only — `synonyms.json` is auto-generated by
|
|
54
|
-
`wicked-brain:retag` and will be absent on fresh brains.
|
|
55
|
-
Format:
|
|
56
|
-
|
|
57
|
-
```json
|
|
58
|
-
{
|
|
59
|
-
"testing": ["unit-test", "integration-test", "test-coverage"],
|
|
60
|
-
"auth": ["authentication", "jwt", "session"],
|
|
61
|
-
"bootcamp": ["pod-exercises", "hands-on-lab"]
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
For each key term, look it up in the synonym map. Use learned synonyms first,
|
|
66
|
-
then supplement with LLM-generated synonyms for terms not in the map.
|
|
67
|
-
Learned synonyms are more reliable — they come from actual brain usage.
|
|
68
|
-
|
|
69
|
-
### LLM synonym expansion
|
|
70
|
-
|
|
71
|
-
For terms not covered by the learned synonym map, generate 1-2 synonyms or related terms:
|
|
72
|
-
- "auth" → also search "authentication", "credentials"
|
|
73
|
-
- "DB" → also search "database", "datastore"
|
|
74
|
-
- "K8s" → also search "kubernetes", "container-orchestration"
|
|
75
|
-
|
|
76
|
-
Run searches for both the original key terms AND the expanded terms.
|
|
77
|
-
Deduplicate results before proceeding to Step 1.
|
|
78
|
-
|
|
79
|
-
Example:
|
|
80
|
-
Question: "How does our auth system handle sessions?"
|
|
81
|
-
Key terms: ["auth", "sessions"]
|
|
82
|
-
Expanded: ["authentication", "credentials", "session-management", "tokens"]
|
|
83
|
-
Search queries: ["auth", "sessions", "authentication", "credentials", "session-management", "tokens"]
|
|
84
|
-
|
|
85
|
-
Use the key terms for FTS search queries (Step 1).
|
|
86
|
-
Use the full original question for synthesis context (Step 4).
|
|
87
|
-
Run multiple searches if key terms suggest different angles.
|
|
88
|
-
|
|
89
|
-
## Step 1: Search
|
|
90
|
-
|
|
91
|
-
Search the brain for relevant content:
|
|
92
46
|
```bash
|
|
93
47
|
npx wicked-brain-call search --param query={term} --param limit=10 --param session_id={session_id}
|
|
94
48
|
```
|
|
95
49
|
|
|
96
|
-
Pass a session_id
|
|
97
|
-
|
|
98
|
-
`session_id` is any string that identifies the current conversation or session
|
|
99
|
-
(e.g., a timestamp like `"1712345678"` or a UUID like `"a1b2-c3d4"`). It is
|
|
100
|
-
used for access-log tracking and diversity ranking across repeated searches.
|
|
50
|
+
Pass a consistent session_id for the whole conversation (any stable string —
|
|
51
|
+
a timestamp or UUID). It drives access tracking and diversity ranking.
|
|
101
52
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
```
|
|
53
|
+
The envelope carries `brain_id` — note WHICH brain answered; cite it in the
|
|
54
|
+
final answer. If the question implies recency ("recently", "this week",
|
|
55
|
+
"latest"), add `--param since={iso8601_date}` (e.g. the date 7 days ago).
|
|
106
56
|
|
|
107
|
-
|
|
108
|
-
it is cross-platform and preferred over shell commands):
|
|
57
|
+
If the searches return enough to answer the question, go straight to Step 2.
|
|
109
58
|
|
|
110
|
-
|
|
111
|
-
```bash
|
|
112
|
-
grep -rl "{key_terms}" {brain_path}/chunks/ {brain_path}/wiki/ 2>/dev/null | head -10
|
|
113
|
-
```
|
|
59
|
+
### Fallback: expand only when results are thin (0-2 hits)
|
|
114
60
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
61
|
+
Only if the direct searches came back sparse:
|
|
62
|
+
- Read `{brain_path}/_meta/synonyms.json` if it exists (skip on fresh brains —
|
|
63
|
+
it's auto-generated by `wicked-brain:retag`). For each key term that matches
|
|
64
|
+
a synonym key, re-search with the learned synonyms. Learned synonyms beat
|
|
65
|
+
guesses — they come from real usage.
|
|
66
|
+
- For terms not in the map, add 1-2 LLM-generated synonyms ("auth" →
|
|
67
|
+
"authentication", "credentials"; "DB" → "database") and re-search.
|
|
68
|
+
- Still thin? Grep for exact phrases (prefer your Grep tool — cross-platform):
|
|
69
|
+
macOS/Linux: `grep -rl "{terms}" {brain_path}/chunks/ {brain_path}/wiki/ | head -10`
|
|
70
|
+
Windows: `Get-ChildItem -Recurse -Path "{brain_path}\chunks","{brain_path}\wiki" -Filter *.md | Select-String -Pattern "{terms}" -List | Select-Object -First 10 -ExpandProperty Path`
|
|
120
71
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
For each synonym-expanded search term, log whether it produced results:
|
|
124
|
-
|
|
125
|
-
If the expansion returned results that the user accessed (appeared in final answer):
|
|
126
|
-
Append to `{brain_path}/_meta/log.jsonl`:
|
|
127
|
-
{"ts":"{ISO}","op":"synonym_hit","original":"{original term}","expansion":"{expanded term}","results_found":{count},"author":"agent:query"}
|
|
128
|
-
|
|
129
|
-
If the expansion returned 0 results:
|
|
130
|
-
Append to `{brain_path}/_meta/log.jsonl`:
|
|
131
|
-
{"ts":"{ISO}","op":"synonym_miss","original":"{original term}","expansion":"{expanded term}","results_found":0,"author":"agent:query"}
|
|
72
|
+
Dedupe results before reading.
|
|
132
73
|
|
|
133
74
|
## Step 2: Progressive read
|
|
134
75
|
|
|
135
|
-
Read the top 3-5 results at depth 1
|
|
136
|
-
|
|
76
|
+
Read the top 3-5 results at depth 1 (frontmatter + summary), then the most
|
|
77
|
+
promising 1-3 at depth 2 (full content). Use the Read tool; parse frontmatter
|
|
78
|
+
between `---` lines.
|
|
137
79
|
|
|
138
|
-
|
|
80
|
+
## Step 3: Follow links (only if context is incomplete)
|
|
139
81
|
|
|
140
|
-
|
|
82
|
+
If the answer still has gaps, follow [[wikilinks]] in the content you read
|
|
83
|
+
(local [[path]] → read it; cross-brain [[brain::path]] → read if accessible),
|
|
84
|
+
and check backlinks for what else references a key result:
|
|
141
85
|
|
|
142
|
-
Check the content for [[wikilinks]]. If following them would provide useful context:
|
|
143
|
-
- For local links [[path]]: read that file
|
|
144
|
-
- For cross-brain links [[brain::path]]: check if that brain is accessible
|
|
145
|
-
|
|
146
|
-
Check backlinks — what else references the content you found:
|
|
147
86
|
```bash
|
|
148
87
|
npx wicked-brain-call backlinks --param id={result_path}
|
|
149
88
|
```
|
|
150
89
|
|
|
151
90
|
## Step 4: Synthesize answer
|
|
152
91
|
|
|
153
|
-
|
|
154
|
-
-
|
|
155
|
-
- If
|
|
156
|
-
-
|
|
157
|
-
- Keep the answer concise — the user asked a question, not for a report
|
|
92
|
+
- Cite sources: [source: {path}] for every factual claim.
|
|
93
|
+
- If evidence is insufficient, say so explicitly.
|
|
94
|
+
- If sources conflict, note the contradiction.
|
|
95
|
+
- Keep it concise — the user asked a question, not for a report.
|
|
158
96
|
|
|
159
97
|
## Report format
|
|
160
98
|
|
|
161
|
-
|
|
99
|
+
Lead with which brain answered, then the answer and sources:
|
|
162
100
|
|
|
163
|
-
"{Answer text with [source: path] citations}"
|
|
101
|
+
"(brain: {brain_id}) {Answer text with [source: path] citations}"
|
|
164
102
|
|
|
165
103
|
Sources:
|
|
166
104
|
- {path}: {one-line description of what it contributed}
|
|
167
|
-
- {path}: {one-line description}
|
|
168
105
|
|
|
169
|
-
## Step 5: Emit bus event
|
|
106
|
+
## Step 5: Emit bus event (fire-and-forget)
|
|
170
107
|
|
|
171
108
|
```bash
|
|
172
109
|
npx wicked-bus emit \
|
|
@@ -176,13 +113,10 @@ npx wicked-bus emit \
|
|
|
176
113
|
--payload '{"question":"{question}","sources_found":{count},"brain_id":"{brain_id}"}' 2>/dev/null || true
|
|
177
114
|
```
|
|
178
115
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
## Step 6: Log search effectiveness
|
|
116
|
+
If the bus is not installed, silently skip.
|
|
182
117
|
|
|
183
|
-
|
|
184
|
-
search-miss event to the brain's log:
|
|
118
|
+
## Step 6: Log a search miss (only if evidence was insufficient)
|
|
185
119
|
|
|
186
|
-
Append
|
|
187
|
-
{"ts":"{ISO}","op":"search_miss","query":"{original question}","key_terms":[{
|
|
120
|
+
Append to {brain_path}/_meta/log.jsonl:
|
|
121
|
+
{"ts":"{ISO}","op":"search_miss","query":"{original question}","key_terms":[{terms}],"results_found":{count},"author":"agent:query"}
|
|
188
122
|
```
|
|
@@ -1,39 +1,35 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wicked-brain:search
|
|
3
3
|
description: |
|
|
4
|
-
Search the digital brain for relevant content.
|
|
5
|
-
|
|
6
|
-
deeper hints.
|
|
4
|
+
Search the digital brain for relevant content. A single CLI call for the
|
|
5
|
+
common single-brain case; fans out to linked brains only when they exist.
|
|
7
6
|
|
|
8
7
|
Use instead of Grep/Glob/Agent(Explore) for any open-ended search or
|
|
9
8
|
exploration: "find X", "search for Y", "look for Z", "where is W used",
|
|
10
9
|
"show me anything about X", "explore Y", "what files relate to Z".
|
|
11
|
-
|
|
10
|
+
|
|
12
11
|
Only fall back to Grep/Glob for exact symbol or pattern lookup when the
|
|
13
12
|
brain returns no results.
|
|
14
13
|
---
|
|
15
14
|
|
|
16
15
|
# wicked-brain:search
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
Search the brain. The default path is ONE direct CLI call — no synonym load,
|
|
18
|
+
no brain.json round-trip, no subagent fan-out. Reserve the heavier paths
|
|
19
|
+
(synonym fallback, multi-brain fan-out) for when they actually pay off.
|
|
20
20
|
|
|
21
21
|
## Cross-Platform Notes
|
|
22
22
|
|
|
23
|
-
Commands
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
For the brain path default:
|
|
28
|
-
- macOS/Linux: ~/.wicked-brain
|
|
29
|
-
- Windows: %USERPROFILE%\.wicked-brain
|
|
23
|
+
Commands here work on macOS, Linux, and Windows. The `npx wicked-brain-call`
|
|
24
|
+
CLI is cross-platform. Brain path default: `~/.wicked-brain/projects/{name}`
|
|
25
|
+
(macOS/Linux), `%USERPROFILE%\.wicked-brain\projects\{name}` (Windows).
|
|
30
26
|
|
|
31
27
|
## Config
|
|
32
28
|
|
|
33
29
|
Brain discovery + server lifecycle are handled by `wicked-brain-call`. Pass
|
|
34
30
|
`--brain <path>` to override the auto-detected brain, or set
|
|
35
|
-
`WICKED_BRAIN_PATH`. The CLI starts the server on first call (no manual
|
|
36
|
-
|
|
31
|
+
`WICKED_BRAIN_PATH`. The CLI starts the server on first call (no manual init)
|
|
32
|
+
and writes an audit record to `{brain}/calls/` per call.
|
|
37
33
|
|
|
38
34
|
## Parameters
|
|
39
35
|
|
|
@@ -41,121 +37,73 @@ init required) and writes an audit record to `{brain}/calls/` per call.
|
|
|
41
37
|
- **limit** (default: 10): max results per brain
|
|
42
38
|
- **depth** (default: 0): result detail level
|
|
43
39
|
|
|
44
|
-
##
|
|
45
|
-
|
|
46
|
-
### Step 0: Load synonyms (optional)
|
|
47
|
-
|
|
48
|
-
Check if `{brain_path}/_meta/synonyms.json` exists using the Read tool.
|
|
49
|
-
If it exists, parse it. Format:
|
|
50
|
-
```json
|
|
51
|
-
{
|
|
52
|
-
"jwt": ["json web token", "auth token"],
|
|
53
|
-
"auth": ["authentication", "authorization"],
|
|
54
|
-
"k8s": ["kubernetes"]
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
When searching, expand the query: if any word in the query matches a synonym key,
|
|
59
|
-
add the synonym values as additional OR terms.
|
|
60
|
-
|
|
61
|
-
Example: query "jwt validation" → search for "jwt validation" first, then also
|
|
62
|
-
search for "json web token validation" and "auth token validation" if initial
|
|
63
|
-
results are sparse (fewer than 3 results).
|
|
64
|
-
|
|
65
|
-
### Step 1: Discover brains to search
|
|
66
|
-
|
|
67
|
-
Use the Read tool on `{brain_path}/brain.json` to get parents and links.
|
|
68
|
-
For each parent/link, check if it's accessible by reading `{brain_path}/{relative_path}/brain.json`.
|
|
69
|
-
|
|
70
|
-
Build a list of accessible brains with their absolute paths.
|
|
71
|
-
|
|
72
|
-
### Step 2: Ensure server is running
|
|
73
|
-
|
|
74
|
-
`wicked-brain-call` auto-starts the server on first invocation. If you want
|
|
75
|
-
to be defensive, run a probe up front:
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
npx wicked-brain-call health
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Exit code 0 means the server is up. Exit code 2 indicates an infra failure
|
|
82
|
-
(server could not be reached or spawned).
|
|
83
|
-
|
|
84
|
-
### Step 3: Dispatch search subagents in parallel
|
|
85
|
-
|
|
86
|
-
Launch one subagent per accessible brain using parallel dispatch:
|
|
87
|
-
|
|
88
|
-
- **Claude Code:** use the Agent tool, launching all subagents in a single message so they run concurrently.
|
|
89
|
-
- **Other CLIs with subagent support:** use the CLI's native parallel dispatch mechanism (e.g., Gemini CLI's parallel tool calls).
|
|
90
|
-
- **No subagent support:** run each brain search sequentially and collect results before merging.
|
|
91
|
-
|
|
92
|
-
Each subagent call passes the brain-specific instructions below.
|
|
93
|
-
|
|
94
|
-
Each search subagent receives these instructions:
|
|
95
|
-
|
|
96
|
-
```
|
|
97
|
-
You are a search agent for the "{brain_id}" brain at {brain_path}.
|
|
40
|
+
## Default path — direct search (do this first)
|
|
98
41
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
## Step 1: Server search (FTS5)
|
|
42
|
+
One call. The CLI auto-starts the server and reconciles the responding
|
|
43
|
+
brain, so no probe is needed.
|
|
102
44
|
|
|
103
45
|
```bash
|
|
104
|
-
npx wicked-brain-call search --param query={query} --param limit={limit}
|
|
46
|
+
npx wicked-brain-call search --param query={query} --param limit={limit}
|
|
105
47
|
```
|
|
106
48
|
|
|
107
|
-
|
|
49
|
+
The JSON envelope is `{ results, total_matches, showing, collapsed, brain_id }`.
|
|
50
|
+
`brain_id` names WHICH brain answered — always surface it so the result is
|
|
51
|
+
unambiguous (see Report format). Each result row also carries its own
|
|
52
|
+
`brain_id` (the brain that owns that document).
|
|
108
53
|
|
|
109
|
-
|
|
54
|
+
If `total_matches > 0`, render and return. You are done — skip everything
|
|
55
|
+
below.
|
|
110
56
|
|
|
111
|
-
|
|
57
|
+
## Fallback A — synonym expansion (only when results are sparse)
|
|
112
58
|
|
|
113
|
-
|
|
114
|
-
BRAIN: {brain_id}
|
|
115
|
-
RESULTS:
|
|
116
|
-
- {path} | score: {score} | {one-line summary}
|
|
117
|
-
- {path} | score: {score} | {one-line summary}
|
|
118
|
-
TOTAL: {count}
|
|
119
|
-
```
|
|
59
|
+
Trigger ONLY when the direct search returned 0–2 results.
|
|
120
60
|
|
|
121
|
-
|
|
61
|
+
1. Read `{brain_path}/_meta/synonyms.json` (skip if absent — fresh brains
|
|
62
|
+
won't have it). Format: `{ "jwt": ["json web token", "auth token"], ... }`.
|
|
63
|
+
2. For each query word matching a synonym key, re-run the search with the
|
|
64
|
+
synonym values OR'd in (e.g. "jwt validation" → also try "json web token
|
|
65
|
+
validation"). Merge results, dedupe by path, keep higher score.
|
|
122
66
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
2. Deduplicate by path (keep higher score)
|
|
126
|
-
3. Sort by score descending
|
|
127
|
-
4. Tag each result with its brain origin
|
|
67
|
+
Server-side miss logging is automatic when a search returns 0 results — no
|
|
68
|
+
explicit call needed.
|
|
128
69
|
|
|
129
|
-
|
|
70
|
+
## Fallback B — multi-brain fan-out (only when linked brains exist)
|
|
130
71
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
```bash
|
|
134
|
-
npx wicked-brain-call search_misses --param query={original_query} --param session_id={session_id}
|
|
135
|
-
```
|
|
72
|
+
Trigger ONLY when this brain has accessible parents/links. Most brains have
|
|
73
|
+
none — skip this entirely for a single local brain.
|
|
136
74
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
75
|
+
1. Read `{brain_path}/brain.json`. If it has no `parents`/`links`, STOP —
|
|
76
|
+
the default-path result is complete.
|
|
77
|
+
2. For each parent/link, confirm it's reachable by reading
|
|
78
|
+
`{linked_brain_path}/brain.json`.
|
|
79
|
+
3. Dispatch one subagent per reachable brain IN PARALLEL (Claude Code: one
|
|
80
|
+
message, multiple Agent calls). Each subagent runs:
|
|
81
|
+
```bash
|
|
82
|
+
npx wicked-brain-call search --param query={query} --param limit={limit} --brain {linked_brain_path}
|
|
83
|
+
```
|
|
84
|
+
and returns `BRAIN: {brain_id}` plus `{path} | score | one-line summary`
|
|
85
|
+
per row.
|
|
86
|
+
4. Merge: collect all rows, dedupe by path (keep higher score), sort by score
|
|
87
|
+
descending, tag each with its origin `brain_id`.
|
|
140
88
|
|
|
141
|
-
|
|
89
|
+
## Report format
|
|
142
90
|
|
|
143
91
|
**Depth 0 (default):**
|
|
144
92
|
```
|
|
145
|
-
|
|
93
|
+
Brain: {brain_id}{, +N linked} — {N} matches (top {limit}):
|
|
146
94
|
|
|
147
|
-
1. {path}
|
|
95
|
+
1. {path} ({score})
|
|
148
96
|
{one-line summary}
|
|
149
|
-
|
|
150
|
-
2. {path} [{brain}] ({score})
|
|
97
|
+
2. {path} ({score})
|
|
151
98
|
{one-line summary}
|
|
152
|
-
|
|
153
99
|
...
|
|
154
100
|
|
|
155
|
-
Unreachable brains: {list, if any}
|
|
101
|
+
Unreachable brains: {list, if any — fan-out only}
|
|
156
102
|
|
|
157
103
|
To read any result: wicked-brain:read {path} --depth 2
|
|
158
104
|
```
|
|
159
105
|
|
|
160
|
-
|
|
161
|
-
|
|
106
|
+
Lead with `brain_id` so it's always clear which brain produced the answer.
|
|
107
|
+
|
|
108
|
+
**Depth 1:** also include frontmatter + first paragraph per result.
|
|
109
|
+
**Depth 2:** include full content per result (use sparingly — high token cost).
|