@gcunharodrigues/wrxn 0.3.0 → 0.5.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/bin/wrxn.cjs CHANGED
@@ -11,6 +11,7 @@ const worktree = require('../lib/worktree.cjs');
11
11
  const executor = require('../lib/executor.cjs');
12
12
  const onboard = require('../lib/onboard.cjs');
13
13
  const connect = require('../lib/connect.cjs');
14
+ const brain = require('../lib/brain.cjs');
14
15
  const statusline = require('../lib/statusline.cjs');
15
16
  const { convert } = require('../lib/convert.cjs');
16
17
  const { ingest } = require('../lib/ingest.cjs');
@@ -58,6 +59,10 @@ function parseArgs(argv) {
58
59
  args.flags.probe = argv[++i];
59
60
  } else if (a === '--distillation') {
60
61
  args.flags.distillation = argv[++i];
62
+ } else if (a === '--limit') {
63
+ args.flags.limit = argv[++i];
64
+ } else if (a === '--type') {
65
+ args.flags.type = argv[++i];
61
66
  } else if (a === '--check-report') {
62
67
  args.flags['check-report'] = true;
63
68
  } else if (a.startsWith('--')) {
@@ -109,6 +114,17 @@ Usage:
109
114
  list print all registered connections (agent-readable JSON)
110
115
  get <name> print one connection by name
111
116
 
117
+ wrxn brain query "<q>" [--json] [--limit <n>] [--type <prose|code|NodeType>] [--neighbors] [--root <dir>]
118
+ ask the warm Brain (recon-wrxn's code+prose graph) from the terminal.
119
+ WHOLE-BRAIN by default. Discovers the live serve door via
120
+ .recon-wrxn/serve-endpoint.json and POSTs the query; prints ranked
121
+ hits (name · type · file:line). If the Brain is not warm (no live
122
+ serve), prints a clear error and exits non-zero — no cold load.
123
+ --json emits the structured hits · --limit asks the door for top n ·
124
+ --type post-filters (prose=Page/Section, code=the rest, or an exact
125
+ NodeType) · --neighbors expands each hit to its 1-hop graph neighbors
126
+ (callers/callees/relationships via recon_explain).
127
+
112
128
  wrxn statusline [--inject [--path <script>]]
113
129
  SYNAPSE live-window writer. With no flag: report whether a statusline
114
130
  is configured (~/.claude/settings.json) + print the marker-bounded
@@ -408,6 +424,32 @@ async function main(argv) {
408
424
  }
409
425
  }
410
426
 
427
+ if (cmd === 'brain') {
428
+ const sub = args._[1];
429
+ if (sub !== 'query') {
430
+ process.stderr.write(`wrxn: unknown brain subcommand "${sub || ''}" (expected: query)\n\n${USAGE}\n`);
431
+ return 2;
432
+ }
433
+ const q = args._[2];
434
+ if (!q) { process.stderr.write('wrxn: brain query requires "<query>"\n'); return 2; }
435
+ const opts = { json: !!args.flags.json, neighbors: !!args.flags.neighbors };
436
+ if (args.flags.limit != null) {
437
+ const n = parseInt(args.flags.limit, 10);
438
+ if (!Number.isInteger(n) || n <= 0) { process.stderr.write('wrxn: --limit requires a positive integer\n'); return 2; }
439
+ opts.limit = n;
440
+ }
441
+ if (args.flags.type) opts.type = String(args.flags.type);
442
+ const root = path.resolve(args.flags.root || process.cwd());
443
+ try {
444
+ const res = await brain.query(q, opts, { root });
445
+ process.stdout.write(brain.formatHits(res.hits, opts) + '\n');
446
+ return 0;
447
+ } catch (err) {
448
+ process.stderr.write(`wrxn: ${err.message}\n`);
449
+ return 2;
450
+ }
451
+ }
452
+
411
453
  if (cmd === 'statusline') {
412
454
  const home = process.env.HOME || os.homedir();
413
455
  const detection = statusline.detectStatusLine(home);
package/lib/brain.cjs ADDED
@@ -0,0 +1,295 @@
1
+ 'use strict';
2
+
3
+ // WRXN brain query (recon-brain-recall-03) — interrogate the warm Brain from the terminal.
4
+ //
5
+ // The Brain is recon-wrxn's unified code+prose knowledge graph, loaded WARM inside the `recon serve`
6
+ // process Claude Code boots for a session. This command reaches it over the loopback find door that
7
+ // serve announces via a discovery file — it is WHOLE-BRAIN (code AND prose, no scope filter by
8
+ // default), the operator's on-demand counterpart to the prose-only proactive Recall hook.
9
+ //
10
+ // Endpoint-first (v1): if no warm door is discoverable, we raise a clear, actionable error and the CLI
11
+ // exits non-zero — there is NO cold one-shot load (that would pay the index + embedder cost the warm
12
+ // serve already absorbs).
13
+ //
14
+ // The query path takes an INJECTED transport + endpoint reader (deps) so its behavior is unit-testable
15
+ // with no live serve — mirrors the injected-invoker seam in lib/connect.cjs and the recall hook's
16
+ // httpTransport. lib/brain.cjs is PACKAGE code (invoked via bin/wrxn.cjs), NOT payload — no manifest
17
+ // entry, consistent with lib/connect.cjs / lib/executor.cjs / lib/onboard.cjs.
18
+ //
19
+ // The discovery contract (serve-endpoint.json {pid,port}, pid-liveness) is duplicated here from the
20
+ // payload recall-surface hook ON PURPOSE: that hook must be node-stdlib-only and self-contained (it
21
+ // ships into installs without the kernel lib), so package code cannot import it. The contract is ~20
22
+ // stable lines — duplicating it across the install boundary is the same self-containment trade the
23
+ // payload hooks make for findInstallRoot.
24
+
25
+ const fs = require('fs');
26
+ const http = require('http');
27
+ const path = require('path');
28
+
29
+ const ENDPOINT_REL = path.join('.recon-wrxn', 'serve-endpoint.json');
30
+ const FIND_PATH = '/api/tools/recon_find';
31
+ const EXPLAIN_PATH = '/api/tools/recon_explain';
32
+ const TIMEOUT_MS = 5000; // generous: an interactive CLI, not the per-prompt 150ms recall budget
33
+ const MAX_RESPONSE_BYTES = 256 * 1024; // hard cap on an accumulated door response body (anti-flood)
34
+ const PROSE_TYPES = new Set(['Page', 'Section']);
35
+ const WALK_UP_LIMIT = 12;
36
+
37
+ // ── discovery (the cross-repo warm-door contract) ────────────────────────────────────
38
+
39
+ // A pid is alive unless process.kill(pid,0) throws. ESRCH = gone; EPERM = owned by another user but
40
+ // alive. Mirrors the cross-repo discovery contract (and the recall hook).
41
+ function pidAlive(pid) {
42
+ try {
43
+ process.kill(pid, 0);
44
+ return true;
45
+ } catch (e) {
46
+ return !!e && e.code === 'EPERM';
47
+ }
48
+ }
49
+
50
+ // Refuse a discovery file another user could have planted, or that is group/world-writable — trusting
51
+ // it would let a hostile workspace point the door host/port at an exfil/injection sink. lstat (not
52
+ // stat) so a symlink's OWN ownership/mode is judged, not its target's. A platform without getuid (no
53
+ // POSIX ownership) skips the uid check but still enforces the mode check. Any fault → not trusted.
54
+ function endpointTrusted(file) {
55
+ let st;
56
+ try {
57
+ st = fs.lstatSync(file);
58
+ } catch {
59
+ return false;
60
+ }
61
+ if (typeof process.getuid === 'function' && st.uid !== process.getuid()) return false; // foreign owner
62
+ if ((st.mode & 0o022) !== 0) return false; // group/world-writable
63
+ return true;
64
+ }
65
+
66
+ // Walk up from startDir to the first directory carrying .recon-wrxn/serve-endpoint.json; read and
67
+ // validate {pid,port}; trust it only when it is well-owned (not planted), well-formed, and the pid is
68
+ // alive. Returns {pid,port,root} or null (the Brain is not warm: absent, untrusted, malformed, missing
69
+ // fields, or a dead process).
70
+ function discoverEndpoint(startDir) {
71
+ let dir = startDir || process.cwd();
72
+ for (let i = 0; i < WALK_UP_LIMIT; i++) {
73
+ const file = path.join(dir, ENDPOINT_REL);
74
+ if (fs.existsSync(file)) {
75
+ if (!endpointTrusted(file)) return null; // foreign-owned or loose perms → not warm
76
+ let obj;
77
+ try {
78
+ obj = JSON.parse(fs.readFileSync(file, 'utf8'));
79
+ } catch {
80
+ return null; // malformed → not warm
81
+ }
82
+ const pid = Number(obj && obj.pid);
83
+ const port = Number(obj && obj.port);
84
+ if (!Number.isInteger(pid) || pid <= 0) return null;
85
+ if (!Number.isInteger(port) || port <= 0) return null;
86
+ if (!pidAlive(pid)) return null; // dead process → not warm
87
+ return { pid, port, root: dir };
88
+ }
89
+ const up = path.dirname(dir);
90
+ if (up === dir) break;
91
+ dir = up;
92
+ }
93
+ return null;
94
+ }
95
+
96
+ // ── transport (injectable; default = real loopback POST) ─────────────────────────────
97
+
98
+ // Default transport: a real loopback POST with a hard timeout. Injectable so unit tests never touch
99
+ // the network (mirrors lib/connect.cjs's invoke seam). Resolves {statusCode, body}; rejects on socket
100
+ // error or timeout.
101
+ function httpTransport({ port, path: reqPath, body, timeoutMs }) {
102
+ return new Promise((resolve, reject) => {
103
+ const payload = Buffer.from(JSON.stringify(body));
104
+ const deadline = timeoutMs || TIMEOUT_MS;
105
+ let settled = false;
106
+ let wall = null;
107
+ const done = (fn, arg) => {
108
+ if (settled) return;
109
+ settled = true;
110
+ if (wall) clearTimeout(wall);
111
+ fn(arg);
112
+ };
113
+ const req = http.request(
114
+ {
115
+ host: '127.0.0.1',
116
+ port,
117
+ path: reqPath,
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json', 'Content-Length': payload.length },
120
+ },
121
+ (res) => {
122
+ const chunks = [];
123
+ let total = 0;
124
+ res.on('data', (c) => {
125
+ total += c.length;
126
+ if (total > MAX_RESPONSE_BYTES) { req.destroy(new Error('brain door response too large')); return; }
127
+ chunks.push(c);
128
+ });
129
+ res.on('end', () => done(resolve, { statusCode: res.statusCode, body: Buffer.concat(chunks).toString('utf8') }));
130
+ res.on('error', (e) => done(reject, e));
131
+ }
132
+ );
133
+ req.on('error', (e) => done(reject, e));
134
+ // Idle timeout (no bytes for `deadline`) AND an independent wall-clock — the latter bounds a trickle
135
+ // attacker that dribbles bytes to keep the idle timer from ever firing.
136
+ req.setTimeout(deadline, () => req.destroy(new Error('brain door timeout')));
137
+ wall = setTimeout(() => req.destroy(new Error('brain door wall-clock timeout')), deadline);
138
+ req.write(payload);
139
+ req.end();
140
+ });
141
+ }
142
+
143
+ // POST a door tool and return the parsed JSON body. Raises a clean error (never a crash) on a transport
144
+ // fault, a non-200 status, or a non-JSON body.
145
+ async function postTool(transport, port, reqPath, body, timeoutMs) {
146
+ let resp;
147
+ try {
148
+ resp = await transport({ port, path: reqPath, body, timeoutMs: timeoutMs || TIMEOUT_MS });
149
+ } catch (err) {
150
+ throw new Error(`Brain door request to ${reqPath} failed: ${err.message}`);
151
+ }
152
+ if (!resp || resp.statusCode !== 200) {
153
+ throw new Error(`Brain door returned HTTP ${resp ? resp.statusCode : 'no-response'} for ${reqPath}`);
154
+ }
155
+ try {
156
+ return JSON.parse(resp.body);
157
+ } catch {
158
+ throw new Error(`Brain door returned a malformed (non-JSON) response for ${reqPath}`);
159
+ }
160
+ }
161
+
162
+ // ── pure helpers ─────────────────────────────────────────────────────────────────────
163
+
164
+ function isProse(hit) {
165
+ return !!hit && PROSE_TYPES.has(hit.type);
166
+ }
167
+
168
+ // Post-filter hits by --type (the find request can't carry a type ARRAY, so prose=Page+Section is
169
+ // always a post-filter): 'prose' → Page/Section, 'code' → everything else, else an exact NodeType.
170
+ function filterByType(hits, type) {
171
+ if (!type) return hits;
172
+ if (type === 'prose') return hits.filter(isProse);
173
+ if (type === 'code') return hits.filter((h) => !isProse(h));
174
+ const t = String(type).toLowerCase();
175
+ return hits.filter((h) => String(h && h.type).toLowerCase() === t);
176
+ }
177
+
178
+ // Extract a hit's 1-hop neighbors from the door's structured recon_explain response:
179
+ // { result, neighbors: NeighborHit[] }, NeighborHit = { name, type, file, line, relationship }
180
+ // relationship ∈ caller | callee | import | importedBy | method | implementedBy | usedBy | testRef
181
+ // Strictly 1-hop. Consumes that real shape directly — no relationship-bucket guesswork. A missing or
182
+ // non-array `neighbors` (e.g. a degraded/empty explain) yields [] so the hit simply has no neighbors.
183
+ function extractNeighbors(resp) {
184
+ if (!resp || typeof resp !== 'object' || !Array.isArray(resp.neighbors)) return [];
185
+ return resp.neighbors.map((n) => {
186
+ const r = n || {};
187
+ const out = { name: r.name, type: r.type, file: r.file };
188
+ if (r.line != null) out.line = r.line;
189
+ if (r.relationship) out.relationship = r.relationship;
190
+ return out;
191
+ });
192
+ }
193
+
194
+ // ── formatting (pure) ────────────────────────────────────────────────────────────────
195
+
196
+ function hitLine(h) {
197
+ const name = h.name || '(unnamed)';
198
+ const type = h.type || '?';
199
+ const loc = h.file ? `${h.file}${h.line != null ? ':' + h.line : ''}` : '';
200
+ return loc ? `${name} · ${type} · ${loc}` : `${name} · ${type}`;
201
+ }
202
+
203
+ function neighborLine(n) {
204
+ const rel = n.relationship ? ` [${n.relationship}]` : '';
205
+ return ` - ${hitLine(n)}${rel}`;
206
+ }
207
+
208
+ // Render results: --json re-emits the structured hits; default is a human text list. With --neighbors,
209
+ // each hit's 1-hop neighbors are listed indented beneath it.
210
+ function formatHits(hits, opts = {}) {
211
+ const list = Array.isArray(hits) ? hits : [];
212
+ if (opts.json) return JSON.stringify(list, null, 2);
213
+ if (!list.length) return 'no results';
214
+ const lines = [];
215
+ for (const h of list) {
216
+ lines.push(hitLine(h));
217
+ if (opts.neighbors) {
218
+ const ns = Array.isArray(h.neighbors) ? h.neighbors : [];
219
+ if (ns.length) for (const n of ns) lines.push(neighborLine(n));
220
+ else lines.push(' (no 1-hop neighbors)');
221
+ }
222
+ }
223
+ return lines.join('\n');
224
+ }
225
+
226
+ // ── the query (IO shell over the injected seam) ──────────────────────────────────────
227
+
228
+ const NOT_WARM =
229
+ 'Brain is not warm — no live recon serve door found (.recon-wrxn/serve-endpoint.json is absent, ' +
230
+ 'malformed, or its process is dead). Open a Claude Code session (which boots recon serve), or run ' +
231
+ '`recon serve` with the find door enabled, then retry.';
232
+
233
+ /**
234
+ * Query the warm Brain. Whole-brain (code+prose) by default.
235
+ * @param {string} q the query string
236
+ * @param {object} opts { json?, limit?, type?, neighbors? }
237
+ * @param {object} deps { root?, discover?, transport?, timeoutMs? } — injected seam for tests
238
+ * @returns {Promise<{hits: object[]}>}
239
+ * @throws a clear error when the Brain is not warm, or on a malformed/non-200 door response.
240
+ */
241
+ async function query(q, opts = {}, deps = {}) {
242
+ const term = String(q == null ? '' : q).trim();
243
+ if (!term) throw new Error('wrxn brain query requires a non-empty query string');
244
+
245
+ const startDir = deps.root || process.env.CLAUDE_PROJECT_DIR || process.cwd();
246
+ const discover = deps.discover || discoverEndpoint;
247
+ const transport = deps.transport || httpTransport;
248
+ const timeoutMs = deps.timeoutMs || TIMEOUT_MS;
249
+
250
+ const door = discover(startDir);
251
+ if (!door) throw new Error(NOT_WARM);
252
+
253
+ const findBody = { query: term };
254
+ if (Number.isInteger(opts.limit) && opts.limit > 0) findBody.limit = opts.limit;
255
+
256
+ const found = await postTool(transport, door.port, FIND_PATH, findBody, timeoutMs);
257
+ if (!Array.isArray(found.hits)) {
258
+ throw new Error(
259
+ 'Brain door returned an unexpected response shape (no structured `hits` array) — the recon-wrxn ' +
260
+ 'serve door may predate the structured find response.'
261
+ );
262
+ }
263
+
264
+ let hits = filterByType(found.hits, opts.type);
265
+
266
+ // --neighbors: 1-hop expansion per hit via recon_explain — the ONLY place 1-hop lives. A per-hit
267
+ // explain failure degrades to empty neighbors (the find already succeeded); it never crashes.
268
+ if (opts.neighbors) {
269
+ for (const h of hits) {
270
+ const explainBody = { name: h.name };
271
+ if (h.file) explainBody.file = h.file;
272
+ try {
273
+ h.neighbors = extractNeighbors(await postTool(transport, door.port, EXPLAIN_PATH, explainBody, timeoutMs));
274
+ } catch {
275
+ h.neighbors = [];
276
+ }
277
+ }
278
+ }
279
+
280
+ return { hits };
281
+ }
282
+
283
+ module.exports = {
284
+ query,
285
+ formatHits,
286
+ discoverEndpoint,
287
+ pidAlive,
288
+ httpTransport,
289
+ filterByType,
290
+ extractNeighbors,
291
+ isProse,
292
+ FIND_PATH,
293
+ EXPLAIN_PATH,
294
+ PROSE_TYPES,
295
+ };
package/manifest.json CHANGED
@@ -98,6 +98,11 @@
98
98
  "class": "managed",
99
99
  "profile": "project"
100
100
  },
101
+ {
102
+ "path": ".claude/skills/dream/SKILL.md",
103
+ "class": "managed",
104
+ "profile": "project"
105
+ },
101
106
  {
102
107
  "path": ".claude/skills/grill-me/SKILL.md",
103
108
  "class": "managed",
@@ -408,6 +413,16 @@
408
413
  "class": "state",
409
414
  "profile": "project"
410
415
  },
416
+ {
417
+ "path": ".wrxn/dream.cjs",
418
+ "class": "managed",
419
+ "profile": "project"
420
+ },
421
+ {
422
+ "path": ".wrxn/dream/.gitkeep",
423
+ "class": "state",
424
+ "profile": "project"
425
+ },
411
426
  {
412
427
  "path": ".wrxn/wiki.cjs",
413
428
  "class": "managed",
@@ -438,6 +453,16 @@
438
453
  "class": "state",
439
454
  "profile": "project"
440
455
  },
456
+ {
457
+ "path": ".wrxn/wiki/_rules/.gitkeep",
458
+ "class": "state",
459
+ "profile": "project"
460
+ },
461
+ {
462
+ "path": ".wrxn/wiki/_slots/.gitkeep",
463
+ "class": "state",
464
+ "profile": "project"
465
+ },
441
466
  {
442
467
  "path": "docs/agents/domain.md",
443
468
  "class": "seeded",
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * 003 — open the warm-brain HTTP find door on existing installs (recon-brain-recall-05).
8
+ *
9
+ * The door is the concurrent, read-only, loopback HTTP transport `recon serve` runs alongside its
10
+ * stdio MCP transport when `.recon-wrxn.json` carries `serveHttp:true` (recon-wrxn ADR 0003) — the one
11
+ * warm index the per-prompt Recall hook and `wrxn brain query` reach. A fresh install gets the door
12
+ * from the updated seed, but `.recon-wrxn.json` is SEEDED (operator-owned), so `wrxn update` never
13
+ * overwrites it — an install created before this release would keep the door shut forever unless a
14
+ * migration flips it.
15
+ *
16
+ * up() sets `serveHttp:true` in place, PRESERVING every existing operator field (projects, ignore,
17
+ * watch, …) — it touches only the door bit, it does not retrofit the rest of the new template.
18
+ * Defensive like 002/001: a missing config is a no-op (a non-recon install has nothing to migrate), an
19
+ * already-open door is an idempotent no-op, and a corrupt/unparseable (or non-object) config is left
20
+ * untouched — never clobber a hand-edited file. `version` is a frozen 0.4.0: it runs via `wrxn update`
21
+ * once the install reaches the release that carries the door (the same release that bumps the recon-wrxn
22
+ * pin to the wrxn.3 build whose serve actually honors serveHttp).
23
+ */
24
+ module.exports = {
25
+ id: '003',
26
+ version: '0.4.0',
27
+ up(ctx) {
28
+ const cfgPath = path.join(ctx.target, '.recon-wrxn.json');
29
+ if (!fs.existsSync(cfgPath)) return; // no recon config → nothing to migrate
30
+
31
+ let cfg;
32
+ try {
33
+ cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
34
+ } catch {
35
+ return; // malformed operator config → leave it untouched, never clobber
36
+ }
37
+ if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) return; // not a config object → leave it
38
+
39
+ if (cfg.serveHttp === true) return; // door already open → idempotent no-op
40
+
41
+ cfg.serveHttp = true; // open the door; every other operator field is preserved
42
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
43
+ },
44
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gcunharodrigues/wrxn",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "WRXN Kernel — installable AI operating system. Two profiles (project | workspace), pull-based updates, managed/seeded/state file classes.",
5
5
  "bin": {
6
6
  "wrxn": "bin/wrxn.cjs"
@@ -16,7 +16,7 @@
16
16
  "test": "node --test"
17
17
  },
18
18
  "dependencies": {
19
- "recon-wrxn": "6.0.0-wrxn.2"
19
+ "recon-wrxn": "6.0.0-wrxn.3"
20
20
  },
21
21
  "engines": {
22
22
  "node": ">=20"