@gcunharodrigues/wrxn 0.13.2 → 0.14.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 +10 -1
- package/lib/executor.cjs +110 -3
- package/lib/install.cjs +13 -0
- package/lib/release.cjs +13 -1
- package/manifest.json +30 -0
- package/package.json +2 -2
- package/payload/.claude/hooks/emit-event.cjs +181 -0
- package/payload/.claude/hooks/prune.cjs +252 -0
- package/payload/.claude/hooks/recall-surface.cjs +465 -52
- package/payload/.claude/hooks/reward.cjs +244 -0
- package/payload/.claude/hooks/session-end-reward.cjs +265 -0
- package/payload/.claude/hooks/session-start.cjs +87 -2
- package/payload/.claude/hooks/sidecar.cjs +93 -0
- package/payload/.claude/hooks/synapse-engine.cjs +45 -3
- package/payload/.claude/hooks/wiki-lint.cjs +70 -1
- package/payload/.claude/settings.json +9 -2
- package/payload/.mcp.json +1 -1
- package/payload/.wrxn/dream.cjs +329 -5
- package/payload/.wrxn/events/.gitkeep +0 -0
- package/payload/.wrxn/harvest.cjs +159 -4
- package/payload/.wrxn/memory-synth.cjs +158 -31
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// WRXN shared coalesced-sidecar helper (kernel #12 / S1). The common mechanism behind recall-surface's
|
|
4
|
+
// runtime STATE sidecars (.wrxn/reinforce.json, .wrxn/surfaced.json): read a JSON object map → let the
|
|
5
|
+
// caller mutate it → REWRITE (not append) the whole map back, but ONLY when it actually changed
|
|
6
|
+
// (coalesced: <= 1 write per real change, the file never grows on a no-op).
|
|
7
|
+
//
|
|
8
|
+
// Self-contained: ships into installs alongside the hooks — node stdlib ONLY (fs / path), NO kernel-lib
|
|
9
|
+
// or recon import. FAIL-OPEN SILENT: any fault (absent dir, malformed existing sidecar, unwritable path)
|
|
10
|
+
// is swallowed so the caller's primary effect always proceeds and nothing ever throws.
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
// secretScan — replicated here because each self-contained install-only module imports no shared
|
|
16
|
+
// kernel-lib (node stdlib only), exactly as it is duplicated across dream.cjs / sync.cjs. CASE-SENSITIVE:
|
|
17
|
+
// the token shapes are case-specific. A coalesced sidecar must never harden a credential onto disk.
|
|
18
|
+
const SECRET_PATTERNS = [
|
|
19
|
+
/AKIA[0-9A-Z]{16}/, // AWS access key id
|
|
20
|
+
/gh[pousr]_[A-Za-z0-9]{36}/, // GitHub token (ghp_/gho_/ghu_/ghs_/ghr_)
|
|
21
|
+
/npm_[A-Za-z0-9]{36}/, // npm automation token
|
|
22
|
+
/sk-[A-Za-z0-9]{20,}/, // OpenAI-style secret key
|
|
23
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/, // FULL PEM block (BEGIN…END) — MUST precede the header-only line so redaction consumes the body, not just the header
|
|
24
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM private-key header (fallback: a lone/truncated header with no END)
|
|
25
|
+
/Bearer\s+[A-Za-z0-9\-._~+/]{16,}=*/i, // HTTP bearer / Authorization token (case-insensitive scheme)
|
|
26
|
+
// password=/pwd= assignment (=, :, quoted JSON/YAML). The keyword is EITHER fully quoted ("password":)
|
|
27
|
+
// OR bare (password=) — never a lone trailing quote, so a path ending in "passwd" used as a JSON key
|
|
28
|
+
// (e.g. "../../etc/passwd": …) is NOT misread as an assignment (it would falsely refuse a sidecar write).
|
|
29
|
+
/(?:["'](?:password|passwd|pwd)["']|(?:password|passwd|pwd))\s*[:=]\s*["']?[^\s"',;)&}{]+/i,
|
|
30
|
+
/[a-z][a-z0-9+.\-]+:\/\/[^\s:/@]+:[^\s/@]+@\S+/i, // URI connection string with inline creds (scheme://user:pass@host)
|
|
31
|
+
/eyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}/, // JWT: base64url header(eyJ…).payload.signature
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function secretScan(text) {
|
|
35
|
+
const s = String(text || ''); // NOT lowercased — the token shapes are case-sensitive.
|
|
36
|
+
for (const re of SECRET_PATTERNS) if (re.test(s)) return 'contains_secret';
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// redactSecrets — scrub known secret shapes OUT of free text before it is persisted, built on the SAME
|
|
41
|
+
// SECRET_PATTERNS as secretScan (one source of truth: when a shape is added, both detection and redaction
|
|
42
|
+
// follow). Every match is replaced with a fixed placeholder while the surrounding text is preserved
|
|
43
|
+
// (metadata-grade redaction, not a whole-value drop) — so a persisted prompt stays useful for analysis
|
|
44
|
+
// yet never hardens a credential onto disk. Global-flagged clones of the patterns so EVERY occurrence on a
|
|
45
|
+
// line is scrubbed, not just the first; String#replace resets a global regex's lastIndex per call, so
|
|
46
|
+
// reusing these module-level clones is safe. TOTAL: a non-string coerces (null/undefined → ''). The clone
|
|
47
|
+
// PRESERVES each pattern's own flags (e.g. /i) and only ADDS the global flag — so a case-insensitive shape
|
|
48
|
+
// scans and redacts identically (dropping the flags here would let detection and redaction diverge).
|
|
49
|
+
const SECRET_PATTERNS_GLOBAL = SECRET_PATTERNS.map((re) => new RegExp(re.source, re.flags.includes('g') ? re.flags : re.flags + 'g'));
|
|
50
|
+
const SECRET_PLACEHOLDER = '[redacted]';
|
|
51
|
+
|
|
52
|
+
function redactSecrets(text) {
|
|
53
|
+
let s = String(text == null ? '' : text); // NOT lowercased — the token shapes are case-sensitive.
|
|
54
|
+
for (const re of SECRET_PATTERNS_GLOBAL) s = s.replace(re, SECRET_PLACEHOLDER);
|
|
55
|
+
return s;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Read the JSON object map at `file`, hand it to `mutate(map)`, and rewrite the file iff mutate signals
|
|
59
|
+
// a change (returns truthy). Returns true when a write happened, false otherwise (including every
|
|
60
|
+
// fail-open path). `mutate` mutates the map in place and returns whether it changed it. The fully
|
|
61
|
+
// serialized map is secret-scanned before the write: if any value would harden a credential onto disk,
|
|
62
|
+
// the write is REFUSED (fail-open — the sidecar stays as it was, the caller proceeds).
|
|
63
|
+
function coalesceSidecar(file, mutate) {
|
|
64
|
+
try {
|
|
65
|
+
let map = {};
|
|
66
|
+
let raw = null;
|
|
67
|
+
try {
|
|
68
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
69
|
+
} catch {
|
|
70
|
+
raw = null; // absent → fresh map (normal, not a fault)
|
|
71
|
+
}
|
|
72
|
+
if (raw !== null) {
|
|
73
|
+
let parsed;
|
|
74
|
+
try {
|
|
75
|
+
parsed = JSON.parse(raw);
|
|
76
|
+
} catch {
|
|
77
|
+
return false; // malformed existing sidecar → skip silently, leave it untouched (never clobber)
|
|
78
|
+
}
|
|
79
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false; // not a map → skip
|
|
80
|
+
map = parsed;
|
|
81
|
+
}
|
|
82
|
+
if (!mutate(map)) return false; // nothing changed → coalesced no-op, no write
|
|
83
|
+
const body = JSON.stringify(map, null, 2) + '\n';
|
|
84
|
+
if (secretScan(body)) return false; // never harden a credential onto disk → refuse the write
|
|
85
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
86
|
+
fs.writeFileSync(file, body);
|
|
87
|
+
return true;
|
|
88
|
+
} catch {
|
|
89
|
+
return false; // best-effort: a sidecar fault must never alter or break the caller
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { coalesceSidecar, secretScan, redactSecrets };
|
|
@@ -276,11 +276,47 @@ function hasCurationDebt(root) {
|
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
// The starved-useful watchdog's EMISSION bar (kernel #15 / S4): the number of starved-useful pages at or
|
|
280
|
+
// above which the handoff carries the canary line. Distinct from the watchdog's MATH thresholds (R_HIGH /
|
|
281
|
+
// S_LOW live with the pure reward.starvedUseful) — this is the "how many before we bug the operator" bar.
|
|
282
|
+
// A PLACEHOLDER pending the lift gate (recorded with the gate verdict, like the decay half-life); it just
|
|
283
|
+
// keeps the canary quiet until a few pages have genuinely starved. Kept local so the engine never
|
|
284
|
+
// HARD-requires the reward sibling at module load (a missing sibling must not break constitution injection).
|
|
285
|
+
const STARVED_NUDGE_THRESHOLD = 3;
|
|
286
|
+
|
|
287
|
+
// The starved-useful WATCHDOG probe (kernel #15 / S4) — the sibling of hasCurationDebt for the learning
|
|
288
|
+
// moat. A CHEAP, fail-open, READ-ONLY read of the two STATE sidecars (.wrxn/reward.json per-page
|
|
289
|
+
// Beta-Bernoulli counts + .wrxn/surfaced.json per-session surfaced-log) → the pure reward.starvedUseful
|
|
290
|
+
// count (pages learned high-reward but rarely surfaced). The reward sibling is required LAZILY inside the
|
|
291
|
+
// try/catch so a (theoretically) absent sibling can never break the engine's constitution injection. Any
|
|
292
|
+
// fault (missing/corrupt sidecar, missing sibling) → 0 (no nudge, no throw). Only invoked when a handoff
|
|
293
|
+
// is actually firing, so the two extra reads are doubly bounded. CANARY: reads only — it never mutates a
|
|
294
|
+
// sidecar and never touches recall ranking or reward counts.
|
|
295
|
+
function starvedUsefulSignal(root) {
|
|
296
|
+
try {
|
|
297
|
+
const { starvedUseful } = require('./reward.cjs');
|
|
298
|
+
const readMap = (rel) => {
|
|
299
|
+
try {
|
|
300
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(root, '.wrxn', rel), 'utf8'));
|
|
301
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
302
|
+
} catch {
|
|
303
|
+
return {}; // absent / malformed → empty (fail-open)
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
return starvedUseful(readMap('reward.json'), readMap('surfaced.json')).count;
|
|
307
|
+
} catch {
|
|
308
|
+
return 0; // any fault (incl. a missing reward sibling) → no signal, never a throw
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
279
312
|
// The NON-BLOCKING forced-handoff directive (never refuses work — orders the agent to wrap up cleanly).
|
|
280
313
|
// `hasDebt` (harvest-05) appends a harvest curation nudge AFTER the dream line — emitted ONLY when the
|
|
281
314
|
// latest health-check found curation debt, so a clean knowledge set never sees it. Ordered after dream:
|
|
282
|
-
// dream consolidates the session first, then harvest curates the enlarged knowledge set.
|
|
283
|
-
|
|
315
|
+
// dream consolidates the session first, then harvest curates the enlarged knowledge set. `starvedCount`
|
|
316
|
+
// (S4) appends ONE canary line LAST — emitted ONLY when the starved-useful count >= STARVED_NUDGE_THRESHOLD
|
|
317
|
+
// (below / omitted / garbage → silent, totality). The canary states the count and that it is (b)-pressure
|
|
318
|
+
// (data toward graduating a recon reward term); it is informational only and changes no recall/reward state.
|
|
319
|
+
function handoffDirective(consumed, pct, hasDebt, starvedCount) {
|
|
284
320
|
const now = Math.round(consumed * 100);
|
|
285
321
|
const thresh = Math.round(pct * 100);
|
|
286
322
|
const lines = [
|
|
@@ -293,6 +329,10 @@ function handoffDirective(consumed, pct, hasDebt) {
|
|
|
293
329
|
if (hasDebt) {
|
|
294
330
|
lines.push(' Then (optional, only because the last health-check found curation debt): run the harvest skill to review the flagged near-dups / decay-candidates / malformed pages — a suggestion only; harvest never auto-deletes, every change is proposed for your confirmation.');
|
|
295
331
|
}
|
|
332
|
+
const starved = Number(starvedCount);
|
|
333
|
+
if (Number.isFinite(starved) && starved >= STARVED_NUDGE_THRESHOLD) {
|
|
334
|
+
lines.push(` Watchdog (canary, informational): ${starved} starved-useful page(s) — learned high-reward but rarely surfaced, sitting below recon's floor where the kernel-side re-rank can't lift them. This is (b)-pressure: data toward graduating a recon reward term (option b). Informational only — nothing auto-changes; no recall or reward state is touched.`);
|
|
335
|
+
}
|
|
296
336
|
return lines.join('\n');
|
|
297
337
|
}
|
|
298
338
|
|
|
@@ -360,7 +400,7 @@ function compose(root, event) {
|
|
|
360
400
|
const window = modelWindow(ev.cwd || root, process.env.HOME || os.homedir(), manifestText, ev.session_id, resident);
|
|
361
401
|
const consumed = resident / window;
|
|
362
402
|
const pct = resolveHandoffPct(manifestText);
|
|
363
|
-
if (consumed >= pct) out.push(handoffDirective(consumed, pct, hasCurationDebt(root)));
|
|
403
|
+
if (consumed >= pct) out.push(handoffDirective(consumed, pct, hasCurationDebt(root), starvedUsefulSignal(root)));
|
|
364
404
|
}
|
|
365
405
|
}
|
|
366
406
|
|
|
@@ -411,5 +451,7 @@ module.exports = {
|
|
|
411
451
|
modelWindow,
|
|
412
452
|
resolveHandoffPct,
|
|
413
453
|
hasCurationDebt,
|
|
454
|
+
starvedUsefulSignal,
|
|
414
455
|
handoffDirective,
|
|
456
|
+
STARVED_NUDGE_THRESHOLD,
|
|
415
457
|
};
|
|
@@ -50,8 +50,71 @@ function lintPage(text) {
|
|
|
50
50
|
return null;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// A page's identity is its `name:` frontmatter slug — it is the wikilink target ([[name]]), the
|
|
54
|
+
// filename, and the dedup key. Returns the slug, or null when the page has no readable name.
|
|
55
|
+
function pageName(text) {
|
|
56
|
+
const m = /^name\s*:\s*(.+)$/m.exec(String(text || ''));
|
|
57
|
+
return m ? m[1].trim() : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The body is everything after the frontmatter block (where prose [[wikilinks]] live).
|
|
61
|
+
function pageBody(text) {
|
|
62
|
+
const src = String(text || '');
|
|
63
|
+
if (!src.startsWith('---')) return src;
|
|
64
|
+
const end = src.indexOf('\n---', 3);
|
|
65
|
+
return end < 0 ? '' : src.slice(end + 4);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Wikilinks are written `[[slug]]` (Obsidian style), where slug equals a target page's `name:`. An
|
|
69
|
+
// optional `|alias` or `#anchor` is stripped to the bare target slug. Returns the distinct slugs.
|
|
70
|
+
// The inner class excludes BOTH `]` and `[`: a real `[[slug]]` target never contains `[`, and the
|
|
71
|
+
// unbounded `[^\]]+` form backtracked O(n²) on runs of `[`, hanging this Stop hook on a `[`-heavy
|
|
72
|
+
// page (ReDoS, S2 #29). Bounding the class is the root-cause fix — linear AND more correct.
|
|
73
|
+
function wikilinkTargets(body) {
|
|
74
|
+
const out = new Set();
|
|
75
|
+
const re = /\[\[([^\]\[]+)\]\]/g;
|
|
76
|
+
let m;
|
|
77
|
+
while ((m = re.exec(String(body || '')))) {
|
|
78
|
+
const slug = m[1].split('|')[0].split('#')[0].trim();
|
|
79
|
+
if (slug) out.add(slug);
|
|
80
|
+
}
|
|
81
|
+
return [...out];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Corpus-level checks (need every page, so they run over the swept corpus, not per-page).
|
|
85
|
+
// `corpus` is an array of { ref, name, body }; `ref` is the human-readable `tier/file.md` label.
|
|
86
|
+
// Returns dead-wikilink findings: a [[slug]] whose slug matches no existing page name.
|
|
87
|
+
function deadLinkFindings(corpus) {
|
|
88
|
+
const names = new Set(corpus.map((p) => p.name).filter(Boolean));
|
|
89
|
+
const findings = [];
|
|
90
|
+
for (const page of corpus) {
|
|
91
|
+
for (const slug of wikilinkTargets(page.body)) {
|
|
92
|
+
if (!names.has(slug)) findings.push(`${page.ref} — dead wikilink [[${slug}]]`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return findings;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Duplicate page titles: two+ pages sharing the same `name:` identity slug — an identity collision
|
|
99
|
+
// (write-page even refuses to overwrite by slug), the tell that the pages should have been merged.
|
|
100
|
+
// One finding per colliding title, naming every page that claims it.
|
|
101
|
+
function duplicateTitleFindings(corpus) {
|
|
102
|
+
const byName = new Map();
|
|
103
|
+
for (const page of corpus) {
|
|
104
|
+
if (!page.name) continue; // a name-less page is already caught by the malformed check
|
|
105
|
+
if (!byName.has(page.name)) byName.set(page.name, []);
|
|
106
|
+
byName.get(page.name).push(page.ref);
|
|
107
|
+
}
|
|
108
|
+
const findings = [];
|
|
109
|
+
for (const [name, refs] of byName) {
|
|
110
|
+
if (refs.length > 1) findings.push(`duplicate title "${name}" — ${refs.join(', ')}`);
|
|
111
|
+
}
|
|
112
|
+
return findings;
|
|
113
|
+
}
|
|
114
|
+
|
|
53
115
|
function sweep(root) {
|
|
54
116
|
const flagged = [];
|
|
117
|
+
const corpus = [];
|
|
55
118
|
for (const tier of TIERS) {
|
|
56
119
|
const dir = path.join(root, '.wrxn', 'wiki', tier);
|
|
57
120
|
let names;
|
|
@@ -67,6 +130,7 @@ function sweep(root) {
|
|
|
67
130
|
} catch {
|
|
68
131
|
continue;
|
|
69
132
|
}
|
|
133
|
+
corpus.push({ ref: `${tier}/${name}`, name: pageName(text), body: pageBody(text) });
|
|
70
134
|
const reason = lintPage(text);
|
|
71
135
|
if (reason) {
|
|
72
136
|
flagged.push(`${tier}/${name} — ${reason}`);
|
|
@@ -74,6 +138,11 @@ function sweep(root) {
|
|
|
74
138
|
}
|
|
75
139
|
}
|
|
76
140
|
}
|
|
141
|
+
// Corpus-level checks run after every page is read (they need the full set of page names).
|
|
142
|
+
for (const f of [...deadLinkFindings(corpus), ...duplicateTitleFindings(corpus)]) {
|
|
143
|
+
flagged.push(f);
|
|
144
|
+
if (flagged.length >= MAX_FLAGGED) return flagged;
|
|
145
|
+
}
|
|
77
146
|
return flagged;
|
|
78
147
|
}
|
|
79
148
|
|
|
@@ -92,7 +161,7 @@ function main() {
|
|
|
92
161
|
|
|
93
162
|
const ctx = [
|
|
94
163
|
'<wiki-lint>',
|
|
95
|
-
`${flagged.length}
|
|
164
|
+
`${flagged.length} wiki integrity issue(s) — malformed frontmatter, dead [[wikilinks]], or duplicate page titles:`,
|
|
96
165
|
...flagged.map((f) => `- ${f}`),
|
|
97
166
|
'</wiki-lint>',
|
|
98
167
|
].join('\n');
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"SessionEnd": [
|
|
11
11
|
{
|
|
12
12
|
"hooks": [
|
|
13
|
-
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/memory-synth-spawn.cjs\"" }
|
|
13
|
+
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/memory-synth-spawn.cjs\"" },
|
|
14
|
+
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/session-end-reward.cjs\"" }
|
|
14
15
|
]
|
|
15
16
|
}
|
|
16
17
|
],
|
|
@@ -19,7 +20,8 @@
|
|
|
19
20
|
"hooks": [
|
|
20
21
|
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/synapse-engine.cjs\"" },
|
|
21
22
|
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/reference-detect.cjs\"" },
|
|
22
|
-
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/recall-surface.cjs\"" }
|
|
23
|
+
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/recall-surface.cjs\"" },
|
|
24
|
+
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/emit-event.cjs\"" }
|
|
23
25
|
]
|
|
24
26
|
}
|
|
25
27
|
],
|
|
@@ -50,6 +52,11 @@
|
|
|
50
52
|
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/code-intel-push.cjs\"" },
|
|
51
53
|
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/drift-detect.cjs\"" }
|
|
52
54
|
]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"hooks": [
|
|
58
|
+
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/emit-event.cjs\"" }
|
|
59
|
+
]
|
|
53
60
|
}
|
|
54
61
|
],
|
|
55
62
|
"Stop": [
|