@gcunharodrigues/wrxn 0.3.0 → 0.4.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 +42 -0
- package/lib/brain.cjs +295 -0
- package/migrations/003-serve-http-door.cjs +44 -0
- package/package.json +2 -2
- package/payload/.claude/hooks/recall-surface.cjs +260 -80
- package/payload/.mcp.json +1 -1
- package/payload/.recon-wrxn.json +2 -0
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
|
+
};
|
|
@@ -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
|
+
"version": "0.4.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.
|
|
19
|
+
"recon-wrxn": "6.0.0-wrxn.3"
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=20"
|
|
@@ -1,43 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
// WRXN recall-surface hook —
|
|
5
|
-
// UserPromptSubmit.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// recall it before re-deriving / re-asking the operator". Gated → silent on non-matching prompts.
|
|
4
|
+
// WRXN recall-surface hook — proactive PROSE Recall via the warm Brain door (recon-brain-recall-04).
|
|
5
|
+
// UserPromptSubmit. Replaces the old wiki-substring engine: on each prompt it discovers recon-wrxn's
|
|
6
|
+
// warm serve door, POSTs a prose-scoped hybrid query, and — ONLY when a hit clears the relevance gate
|
|
7
|
+
// — injects a compact <recall-surface> block. Implements ADR 0002.
|
|
9
8
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
9
|
+
// The gate (per arm, NEVER the fused RRF score): a prose hit qualifies on the semantic cosine FLOOR
|
|
10
|
+
// (>= 0.4) OR on CONSENSUS (it surfaced in both the BM25 and the dense arm). Nothing clears ⇒ Abstain.
|
|
11
|
+
// Prose only — hits are post-filtered to Page/Section, so code symbols never surface here (they stay
|
|
12
|
+
// on the agent's on-demand recon_* / `wrxn brain query` path).
|
|
13
|
+
//
|
|
14
|
+
// Self-contained: ships into installs, MUST NOT import the kernel lib or recon — node stdlib ONLY
|
|
15
|
+
// (http / fs / path). Fail-open SILENT: a cold/missing/dead door, a slow door, a non-200, malformed
|
|
16
|
+
// JSON, or ANY fault emits {} — the hook NEVER blocks a prompt and never delays it past the hard
|
|
17
|
+
// client timeout. There is NO substring fallback (a weak fallback can itself harm — ADR 0002).
|
|
13
18
|
//
|
|
14
19
|
// Contract: UserPromptSubmit event JSON on stdin → envelope JSON on stdout (exit 0).
|
|
20
|
+
// inject → { "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": "<recall-surface>…" } }
|
|
21
|
+
// abstain → {}
|
|
15
22
|
|
|
16
23
|
const fs = require('fs');
|
|
24
|
+
const http = require('http');
|
|
17
25
|
const path = require('path');
|
|
18
26
|
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const MAX_BLOCK_CHARS = 600;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
'into', 'just', 'like', 'more', 'most', 'only', 'other', 'over', 'same', 'should', 'some', 'such',
|
|
32
|
-
'than', 'that', 'their', 'them', 'then', 'there', 'these', 'they', 'this', 'those', 'through', 'under',
|
|
33
|
-
'until', 'very', 'want', 'what', 'when', 'where', 'which', 'while', 'with', 'would', 'your', 'today',
|
|
34
|
-
'tell', 'show', 'give', 'make', 'need', 'know', 'help', 'please', 'thing', 'stuff', 'really', 'going']);
|
|
27
|
+
const MIN_PROMPT_LEN = 8; // skip trivial prompts ("ok", "yes")
|
|
28
|
+
const MAX_QUERY_CHARS = 512; // trim the prompt before querying the door
|
|
29
|
+
const FETCH_LIMIT = 15; // ask the door WIDE — fetch is decoupled from inject so prose ranked
|
|
30
|
+
// below the whole-brain code hits is not truncated before we filter
|
|
31
|
+
const TIMEOUT_MS = 150; // hard client budget — never delay a prompt past this
|
|
32
|
+
const MAX_RESPONSE_BYTES = 256 * 1024; // hard cap on an accumulated door response body (anti-flood)
|
|
33
|
+
const TOP_N = 3; // inject at most 3 hits (the wide fetch is post-filtered down to this)
|
|
34
|
+
const MAX_BLOCK_CHARS = 600; // injection size cap (ADR 0002: inject little, high-signal)
|
|
35
|
+
const SEMANTIC_FLOOR = 0.4; // dense cosine floor (reused from P1.5)
|
|
36
|
+
const PROSE_TYPES = new Set(['Page', 'Section']); // prose scope — drop code symbols
|
|
37
|
+
const ENDPOINT_REL = path.join('.recon-wrxn', 'serve-endpoint.json');
|
|
38
|
+
const FIND_PATH = '/api/tools/recon_find';
|
|
35
39
|
|
|
36
40
|
function emit(envelope) {
|
|
37
41
|
process.stdout.write(JSON.stringify(envelope));
|
|
38
42
|
process.exit(0);
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
// Walk up from cwd / CLAUDE_PROJECT_DIR to the install root carrying wrxn.install.json.
|
|
41
46
|
function findInstallRoot(startDir) {
|
|
42
47
|
let dir = startDir || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
43
48
|
for (let i = 0; i < 12; i++) {
|
|
@@ -49,79 +54,254 @@ function findInstallRoot(startDir) {
|
|
|
49
54
|
return null;
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
// ── the gate (PURE) ────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
function isProse(hit) {
|
|
60
|
+
return !!hit && PROSE_TYPES.has(hit.type);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Consensus = the hit surfaced in BOTH the BM25 and the dense arm (the find response's `sources`
|
|
64
|
+
// provenance). A consensus hit qualifies even below the cosine floor.
|
|
65
|
+
function hasConsensus(hit) {
|
|
66
|
+
const s = hit && hit.sources;
|
|
67
|
+
return Array.isArray(s) && s.includes('bm25') && s.includes('semantic');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Qualify on the PER-ARM signal only: the semantic cosine floor OR consensus. NEVER the fused
|
|
71
|
+
// `score` (RRF is a rank-based consensus, not a relevance magnitude — ADR 0002). The floor clause
|
|
72
|
+
// requires the dense arm to actually be PRESENT in `sources` (not just a stray semanticScore) —
|
|
73
|
+
// today these co-occur, but this is a defense against a future producer emitting a score without the
|
|
74
|
+
// 'semantic' tag. An absent/NaN semanticScore can never clear the floor; only consensus rescues it.
|
|
75
|
+
function qualifies(hit) {
|
|
76
|
+
const sem = Number(hit && hit.semanticScore);
|
|
77
|
+
const s = hit && hit.sources;
|
|
78
|
+
const hasSemantic = Array.isArray(s) && s.includes('semantic');
|
|
79
|
+
const floorOk = Number.isFinite(sem) && sem >= SEMANTIC_FLOOR && hasSemantic;
|
|
80
|
+
return floorOk || hasConsensus(hit);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function slugify(s) {
|
|
84
|
+
return String(s || '')
|
|
85
|
+
.toLowerCase()
|
|
86
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
87
|
+
.replace(/^-+|-+$/g, '')
|
|
88
|
+
.slice(0, 48);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// A stable slug for a hit: the prose file's basename (sans extension), else a slugified name.
|
|
92
|
+
function slugOf(hit) {
|
|
93
|
+
if (hit.file) {
|
|
94
|
+
const base = path.basename(String(hit.file)).replace(/\.[^.]+$/, '');
|
|
95
|
+
if (base) return base.slice(0, 48);
|
|
57
96
|
}
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
97
|
+
return slugify(hit.name) || 'untitled';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// The one-line descriptor. NOTE: a recon FindHit carries NO body text — only name/file/line/scores —
|
|
101
|
+
// so the snippet is the hit's NAME (the page title / section heading), the most descriptive line
|
|
102
|
+
// available. (Follow-up: if the door later surfaces a per-hit text excerpt, prefer it here.)
|
|
103
|
+
function snippetOf(hit) {
|
|
104
|
+
const s = String(hit.name || hit.file || '').replace(/\s+/g, ' ').trim();
|
|
105
|
+
return s.length > 100 ? s.slice(0, 99) + '…' : s;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Render the qualifying hits into the <recall-surface> block, guaranteed <= MAX_BLOCK_CHARS and
|
|
109
|
+
// always closed. Drops trailing bullets first, then hard-truncates the last line if it still overflows.
|
|
110
|
+
function renderBlock(hits) {
|
|
111
|
+
const head = '<recall-surface>';
|
|
112
|
+
const intro = 'Knowledge already in your Brain, recalled for this prompt — read it before re-deriving or re-asking the operator:';
|
|
113
|
+
const foot = '</recall-surface>';
|
|
114
|
+
const bullets = hits.map((h) => `- ${slugOf(h)} — ${snippetOf(h)}`);
|
|
115
|
+
const assemble = (bs) => [head, intro, ...bs, foot].join('\n');
|
|
116
|
+
const kept = bullets.slice();
|
|
117
|
+
while (kept.length > 1 && assemble(kept).length > MAX_BLOCK_CHARS) kept.pop();
|
|
118
|
+
let block = assemble(kept);
|
|
119
|
+
if (block.length > MAX_BLOCK_CHARS) {
|
|
120
|
+
block = block.slice(0, MAX_BLOCK_CHARS - foot.length - 1).replace(/\s+\S*$/, '').trimEnd() + '\n' + foot;
|
|
121
|
+
}
|
|
122
|
+
return block;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// PURE: prose-filter → gate → top-N → format. Returns the block string, or null (Abstain).
|
|
126
|
+
function decideRecall(hits) {
|
|
127
|
+
const list = Array.isArray(hits) ? hits : [];
|
|
128
|
+
const qualified = list.filter((h) => isProse(h) && qualifies(h)).slice(0, TOP_N);
|
|
129
|
+
if (!qualified.length) return null;
|
|
130
|
+
return renderBlock(qualified);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── the door (IO shell, injectable transport) ───────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
// A pid is alive unless process.kill(pid,0) throws ESRCH. EPERM means it exists (owned by another
|
|
136
|
+
// user) — still alive. Mirrors the cross-repo discovery contract.
|
|
137
|
+
function pidAlive(pid) {
|
|
138
|
+
try {
|
|
139
|
+
process.kill(pid, 0);
|
|
140
|
+
return true;
|
|
141
|
+
} catch (e) {
|
|
142
|
+
return !!e && e.code === 'EPERM';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Refuse a discovery file another user could have planted, or that is group/world-writable — trusting
|
|
147
|
+
// it would let a hostile workspace point the door host/port at an exfil/injection sink (the hook feeds
|
|
148
|
+
// the door's response into the prompt context). lstat (not stat) so a symlink's OWN ownership/mode is
|
|
149
|
+
// judged. A platform without getuid skips the uid check but still enforces the mode check. Any fault →
|
|
150
|
+
// not trusted (treated as not-warm).
|
|
151
|
+
function endpointTrusted(file) {
|
|
152
|
+
let st;
|
|
153
|
+
try {
|
|
154
|
+
st = fs.lstatSync(file);
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
82
157
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return
|
|
158
|
+
if (typeof process.getuid === 'function' && st.uid !== process.getuid()) return false; // foreign owner
|
|
159
|
+
if ((st.mode & 0o022) !== 0) return false; // group/world-writable
|
|
160
|
+
return true;
|
|
86
161
|
}
|
|
87
162
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
163
|
+
// Discover the warm serve door from <root>/.recon-wrxn/serve-endpoint.json = {pid,port}. Returns
|
|
164
|
+
// {pid,port} only when the file is well-owned (not planted), present, well-formed, and the pid is
|
|
165
|
+
// alive — else null (not warm).
|
|
166
|
+
function discoverEndpoint(root) {
|
|
167
|
+
const file = path.join(root, ENDPOINT_REL);
|
|
168
|
+
if (!endpointTrusted(file)) return null; // absent, foreign-owned, or loose perms → not warm
|
|
169
|
+
let raw;
|
|
170
|
+
try {
|
|
171
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
172
|
+
} catch {
|
|
173
|
+
return null; // absent (race)
|
|
174
|
+
}
|
|
175
|
+
let obj;
|
|
176
|
+
try {
|
|
177
|
+
obj = JSON.parse(raw);
|
|
178
|
+
} catch {
|
|
179
|
+
return null; // malformed
|
|
180
|
+
}
|
|
181
|
+
const pid = Number(obj && obj.pid);
|
|
182
|
+
const port = Number(obj && obj.port);
|
|
183
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
184
|
+
if (!Number.isInteger(port) || port <= 0) return null;
|
|
185
|
+
if (!pidAlive(pid)) return null; // dead pid → not warm
|
|
186
|
+
return { pid, port };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Default transport: a real POST over http with a hard timeout. Resolves {statusCode, body}; rejects
|
|
190
|
+
// on socket error or timeout. Injectable so unit tests never touch the network (mirrors connect.cjs).
|
|
191
|
+
function httpTransport({ port, path: reqPath, body, timeoutMs }) {
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
const payload = Buffer.from(JSON.stringify(body));
|
|
194
|
+
const deadline = timeoutMs || TIMEOUT_MS;
|
|
195
|
+
let settled = false;
|
|
196
|
+
let wall = null;
|
|
197
|
+
const done = (fn, arg) => {
|
|
198
|
+
if (settled) return;
|
|
199
|
+
settled = true;
|
|
200
|
+
if (wall) clearTimeout(wall);
|
|
201
|
+
fn(arg);
|
|
202
|
+
};
|
|
203
|
+
const req = http.request(
|
|
204
|
+
{
|
|
205
|
+
host: '127.0.0.1',
|
|
206
|
+
port,
|
|
207
|
+
path: reqPath,
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': payload.length },
|
|
210
|
+
},
|
|
211
|
+
(res) => {
|
|
212
|
+
const chunks = [];
|
|
213
|
+
let total = 0;
|
|
214
|
+
res.on('data', (c) => {
|
|
215
|
+
total += c.length;
|
|
216
|
+
if (total > MAX_RESPONSE_BYTES) { req.destroy(new Error('recall door response too large')); return; }
|
|
217
|
+
chunks.push(c);
|
|
218
|
+
});
|
|
219
|
+
res.on('end', () => done(resolve, { statusCode: res.statusCode, body: Buffer.concat(chunks).toString('utf8') }));
|
|
220
|
+
res.on('error', (e) => done(reject, e));
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
req.on('error', (e) => done(reject, e));
|
|
224
|
+
// Idle timeout (no bytes for `deadline`) AND an independent wall-clock: the latter bounds a trickle
|
|
225
|
+
// attacker that dribbles bytes to keep the idle timer from ever firing past the hook budget.
|
|
226
|
+
req.setTimeout(deadline, () => req.destroy(new Error('recall door timeout')));
|
|
227
|
+
wall = setTimeout(() => req.destroy(new Error('recall door wall-clock timeout')), deadline);
|
|
228
|
+
req.write(payload);
|
|
229
|
+
req.end();
|
|
230
|
+
});
|
|
97
231
|
}
|
|
98
232
|
|
|
99
|
-
|
|
233
|
+
// IO shell: discover the door, POST the prose query, gate the hits. Returns the block string or null.
|
|
234
|
+
// `transport` is injected in tests; production uses httpTransport. Sends NO `type` (recon_find takes a
|
|
235
|
+
// single NodeType, not an array) — prose scope is enforced by decideRecall's post-filter.
|
|
236
|
+
async function recallFromDoor(root, prompt, { transport, timeoutMs } = {}) {
|
|
237
|
+
const door = discoverEndpoint(root);
|
|
238
|
+
if (!door) return null; // not warm → Abstain (silent)
|
|
239
|
+
const query = String(prompt || '').trim().slice(0, MAX_QUERY_CHARS);
|
|
240
|
+
if (!query) return null;
|
|
241
|
+
let resp;
|
|
242
|
+
try {
|
|
243
|
+
resp = await (transport || httpTransport)({
|
|
244
|
+
port: door.port,
|
|
245
|
+
path: FIND_PATH,
|
|
246
|
+
body: { query, limit: FETCH_LIMIT },
|
|
247
|
+
timeoutMs: timeoutMs || TIMEOUT_MS,
|
|
248
|
+
});
|
|
249
|
+
} catch {
|
|
250
|
+
return null; // timeout / connection refused / abort → silent
|
|
251
|
+
}
|
|
252
|
+
if (!resp || resp.statusCode !== 200) return null;
|
|
253
|
+
let parsed;
|
|
254
|
+
try {
|
|
255
|
+
parsed = JSON.parse(resp.body);
|
|
256
|
+
} catch {
|
|
257
|
+
return null; // malformed body → silent
|
|
258
|
+
}
|
|
259
|
+
return decideRecall(Array.isArray(parsed.hits) ? parsed.hits : []);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── entrypoint ──────────────────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
async function main() {
|
|
100
265
|
let event = {};
|
|
101
266
|
try {
|
|
102
267
|
const stdin = fs.readFileSync(0, 'utf8');
|
|
103
268
|
if (stdin.trim()) event = JSON.parse(stdin);
|
|
104
269
|
} catch {
|
|
105
|
-
emit({});
|
|
270
|
+
return emit({});
|
|
106
271
|
}
|
|
107
272
|
|
|
108
|
-
const root = findInstallRoot();
|
|
109
|
-
if (!root) emit({});
|
|
273
|
+
const root = findInstallRoot(event.cwd);
|
|
274
|
+
if (!root) return emit({});
|
|
110
275
|
|
|
111
276
|
const prompt = typeof event.prompt === 'string' ? event.prompt : '';
|
|
112
|
-
if (prompt.trim().length < MIN_PROMPT_LEN) emit({});
|
|
113
|
-
|
|
114
|
-
const terms = salientTerms(prompt);
|
|
115
|
-
if (!terms.length) emit({});
|
|
277
|
+
if (prompt.trim().length < MIN_PROMPT_LEN) return emit({});
|
|
116
278
|
|
|
117
|
-
|
|
118
|
-
|
|
279
|
+
let block = null;
|
|
280
|
+
try {
|
|
281
|
+
block = await recallFromDoor(root, prompt.trim());
|
|
282
|
+
} catch {
|
|
283
|
+
return emit({});
|
|
284
|
+
}
|
|
285
|
+
if (!block) return emit({}); // nothing cleared the gate → Abstain
|
|
119
286
|
|
|
120
|
-
emit({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: block
|
|
287
|
+
return emit({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: block } });
|
|
121
288
|
}
|
|
122
289
|
|
|
123
|
-
|
|
124
|
-
main();
|
|
125
|
-
} catch {
|
|
126
|
-
emit({});
|
|
290
|
+
if (require.main === module) {
|
|
291
|
+
main().catch(() => emit({}));
|
|
127
292
|
}
|
|
293
|
+
|
|
294
|
+
module.exports = {
|
|
295
|
+
decideRecall,
|
|
296
|
+
recallFromDoor,
|
|
297
|
+
discoverEndpoint,
|
|
298
|
+
httpTransport,
|
|
299
|
+
pidAlive,
|
|
300
|
+
isProse,
|
|
301
|
+
hasConsensus,
|
|
302
|
+
qualifies,
|
|
303
|
+
renderBlock,
|
|
304
|
+
findInstallRoot,
|
|
305
|
+
SEMANTIC_FLOOR,
|
|
306
|
+
PROSE_TYPES,
|
|
307
|
+
};
|
package/payload/.mcp.json
CHANGED