@gcunharodrigues/wrxn 0.6.0 → 0.7.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/lib/install.cjs +2 -0
- package/manifest.json +15 -15
- package/migrations/004-retire-session-capture.cjs +106 -0
- package/package.json +1 -1
- package/payload/.claude/hooks/recall-surface.cjs +90 -4
- package/payload/.claude/hooks/session-start.cjs +4 -23
- package/payload/.claude/hooks/synapse-engine.cjs +41 -4
- package/payload/.claude/settings.json +0 -8
- package/payload/.claude/skills/harvest/SKILL.md +210 -0
- package/payload/.wrxn/dream.cjs +43 -1
- package/payload/.wrxn/harvest.cjs +1190 -0
- package/payload/.wrxn/wiki.cjs +28 -1
- package/payload/.claude/hooks/session-end.cjs +0 -172
- package/payload/.claude/hooks/session-history.cjs +0 -76
- /package/payload/.wrxn/{wiki/sessions → harvest}/.gitkeep +0 -0
package/lib/install.cjs
CHANGED
|
@@ -84,6 +84,8 @@ function init(opts) {
|
|
|
84
84
|
|
|
85
85
|
// recon-wrxn writes its index into a fixed `.recon-wrxn/` dir — keep it out of version control.
|
|
86
86
|
ensureGitignoreLine(target, '.recon-wrxn/');
|
|
87
|
+
// the recall-surface hook writes per-install access-recency state here (harvest-08) — runtime, not committed.
|
|
88
|
+
ensureGitignoreLine(target, '.wrxn/reinforce.json');
|
|
87
89
|
|
|
88
90
|
writeReceipt(target, { version, profile, laid, skipped, merged, brownfield });
|
|
89
91
|
|
package/manifest.json
CHANGED
|
@@ -63,16 +63,6 @@
|
|
|
63
63
|
"class": "managed",
|
|
64
64
|
"profile": "project"
|
|
65
65
|
},
|
|
66
|
-
{
|
|
67
|
-
"path": ".claude/hooks/session-end.cjs",
|
|
68
|
-
"class": "managed",
|
|
69
|
-
"profile": "project"
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
"path": ".claude/hooks/session-history.cjs",
|
|
73
|
-
"class": "managed",
|
|
74
|
-
"profile": "project"
|
|
75
|
-
},
|
|
76
66
|
{
|
|
77
67
|
"path": ".claude/hooks/session-start.cjs",
|
|
78
68
|
"class": "managed",
|
|
@@ -133,6 +123,11 @@
|
|
|
133
123
|
"class": "managed",
|
|
134
124
|
"profile": "project"
|
|
135
125
|
},
|
|
126
|
+
{
|
|
127
|
+
"path": ".claude/skills/harvest/SKILL.md",
|
|
128
|
+
"class": "managed",
|
|
129
|
+
"profile": "project"
|
|
130
|
+
},
|
|
136
131
|
{
|
|
137
132
|
"path": ".claude/skills/improve-codebase-architecture/DEEPENING.md",
|
|
138
133
|
"class": "managed",
|
|
@@ -438,6 +433,16 @@
|
|
|
438
433
|
"class": "managed",
|
|
439
434
|
"profile": "project"
|
|
440
435
|
},
|
|
436
|
+
{
|
|
437
|
+
"path": ".wrxn/harvest.cjs",
|
|
438
|
+
"class": "managed",
|
|
439
|
+
"profile": "project"
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
"path": ".wrxn/harvest/.gitkeep",
|
|
443
|
+
"class": "state",
|
|
444
|
+
"profile": "project"
|
|
445
|
+
},
|
|
441
446
|
{
|
|
442
447
|
"path": ".wrxn/wiki.cjs",
|
|
443
448
|
"class": "managed",
|
|
@@ -463,11 +468,6 @@
|
|
|
463
468
|
"class": "state",
|
|
464
469
|
"profile": "project"
|
|
465
470
|
},
|
|
466
|
-
{
|
|
467
|
-
"path": ".wrxn/wiki/sessions/.gitkeep",
|
|
468
|
-
"class": "state",
|
|
469
|
-
"profile": "project"
|
|
470
|
-
},
|
|
471
471
|
{
|
|
472
472
|
"path": ".wrxn/wiki/_rules/.gitkeep",
|
|
473
473
|
"class": "state",
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 004 — retire the session-capture subsystem (harvest-01).
|
|
8
|
+
*
|
|
9
|
+
* Phase 5 (harvest) drops the low-value mechanical session-capture layer: the `session-end` episodic
|
|
10
|
+
* breadcrumb writer, the `session-history` turn-trail recorder, and the `sessions` wiki tier they fed.
|
|
11
|
+
* The deliberate handoff (continuity baton) + dream consolidation are the close-out moment now — the
|
|
12
|
+
* automatic breadcrumb no longer earns its keep. The new payload no longer SHIPS the two hooks, but a
|
|
13
|
+
* pre-0.7.0 install still carries them: `wrxn update` overwrites a managed file in place, it never
|
|
14
|
+
* PRUNES one that was removed. So an existing install keeps the two hook files, a settings.json still
|
|
15
|
+
* wired for them, a populated `sessions` tier, and now-orphaned history scratch. up() sweeps all of it.
|
|
16
|
+
*
|
|
17
|
+
* Steps: (1) remove the two retired hook files; (2) unwire them from the install settings.json — drop
|
|
18
|
+
* the SessionEnd event whose only hook was session-end, and the session-history command from the
|
|
19
|
+
* UserPromptSubmit chain (synapse-engine + the rest are preserved); (3) remove the whole
|
|
20
|
+
* `.wrxn/wiki/sessions/` tier (dated pages + the gitkeep); (4) reap the now-orphaned
|
|
21
|
+
* `.wrxn/history/*.trail` (no writer/reader left) + `*.touched` markers. The `.wrxn/history/` dir
|
|
22
|
+
* itself STAYS — code-intel-push still writes `.touched` markers there.
|
|
23
|
+
*
|
|
24
|
+
* Defensive like 002/003: every step is existence-guarded and best-effort (force-rm ignores a missing
|
|
25
|
+
* file), a missing/clean target is a no-op, and a corrupt settings.json is left untouched (never
|
|
26
|
+
* clobber a hand-edited file — the other sweeps still run). Idempotent (a second run finds nothing to
|
|
27
|
+
* do) and never throws on an already-clean install. `version` 0.7.0 = the harvest release that carries
|
|
28
|
+
* the retirement (the same release whose payload stops shipping the two hooks).
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const RETIRED_HOOKS = ['session-end.cjs', 'session-history.cjs'];
|
|
32
|
+
|
|
33
|
+
// Remove every hook command referencing `basename` across all settings events; drop any group left
|
|
34
|
+
// with no hooks, and any event left with no groups. Returns true iff the config changed. Preserves
|
|
35
|
+
// every other hook (synapse-engine, reference-detect, recall-surface, session-start, …).
|
|
36
|
+
function unwireHook(cfg, basename) {
|
|
37
|
+
const hooks = cfg && cfg.hooks;
|
|
38
|
+
if (!hooks || typeof hooks !== 'object') return false;
|
|
39
|
+
let changed = false;
|
|
40
|
+
for (const event of Object.keys(hooks)) {
|
|
41
|
+
const groups = hooks[event];
|
|
42
|
+
if (!Array.isArray(groups)) continue;
|
|
43
|
+
for (const group of groups) {
|
|
44
|
+
if (!group || !Array.isArray(group.hooks)) continue;
|
|
45
|
+
const before = group.hooks.length;
|
|
46
|
+
group.hooks = group.hooks.filter(
|
|
47
|
+
(h) => !(h && typeof h.command === 'string' && h.command.includes(basename)),
|
|
48
|
+
);
|
|
49
|
+
if (group.hooks.length !== before) changed = true;
|
|
50
|
+
}
|
|
51
|
+
const kept = groups.filter((g) => g && Array.isArray(g.hooks) && g.hooks.length > 0);
|
|
52
|
+
if (kept.length !== groups.length) {
|
|
53
|
+
changed = true;
|
|
54
|
+
if (kept.length === 0) delete hooks[event];
|
|
55
|
+
else hooks[event] = kept;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return changed;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
id: '004',
|
|
63
|
+
version: '0.7.0',
|
|
64
|
+
up(ctx) {
|
|
65
|
+
const target = ctx.target;
|
|
66
|
+
|
|
67
|
+
// 1. remove the two retired hook files (force = absent is a no-op)
|
|
68
|
+
for (const h of RETIRED_HOOKS) {
|
|
69
|
+
fs.rmSync(path.join(target, '.claude', 'hooks', h), { force: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 2. unwire them from settings.json — only while still wired; a corrupt file is left untouched
|
|
73
|
+
const settingsPath = path.join(target, '.claude', 'settings.json');
|
|
74
|
+
if (fs.existsSync(settingsPath)) {
|
|
75
|
+
let cfg = null;
|
|
76
|
+
try {
|
|
77
|
+
cfg = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
78
|
+
} catch {
|
|
79
|
+
cfg = null; // hand-corrupted operator file → never clobber, never crash
|
|
80
|
+
}
|
|
81
|
+
if (cfg && typeof cfg === 'object') {
|
|
82
|
+
let changed = false;
|
|
83
|
+
for (const h of RETIRED_HOOKS) if (unwireHook(cfg, h)) changed = true;
|
|
84
|
+
if (changed) fs.writeFileSync(settingsPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 3. sweep the retired `sessions` wiki tier (the whole dir: dated pages + the gitkeep)
|
|
89
|
+
fs.rmSync(path.join(target, '.wrxn', 'wiki', 'sessions'), { recursive: true, force: true });
|
|
90
|
+
|
|
91
|
+
// 4. reap orphaned history scratch — the *.trail (no writer/reader left) + *.touched markers.
|
|
92
|
+
// The `.wrxn/history/` dir itself stays: code-intel-push still records .touched markers there.
|
|
93
|
+
const histDir = path.join(target, '.wrxn', 'history');
|
|
94
|
+
let names = [];
|
|
95
|
+
try {
|
|
96
|
+
names = fs.readdirSync(histDir);
|
|
97
|
+
} catch {
|
|
98
|
+
names = []; // no history dir → nothing to reap
|
|
99
|
+
}
|
|
100
|
+
for (const n of names) {
|
|
101
|
+
if (n.endsWith('.trail') || n.endsWith('.touched')) {
|
|
102
|
+
fs.rmSync(path.join(histDir, n), { force: true });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gcunharodrigues/wrxn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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"
|
|
@@ -36,6 +36,8 @@ const SEMANTIC_FLOOR = 0.4; // dense cosine floor (reused from P1.5)
|
|
|
36
36
|
const PROSE_TYPES = new Set(['Page', 'Section']); // prose scope — drop code symbols
|
|
37
37
|
const ENDPOINT_REL = path.join('.recon-wrxn', 'serve-endpoint.json');
|
|
38
38
|
const FIND_PATH = '/api/tools/recon_find';
|
|
39
|
+
const REINFORCE_REL = path.join('.wrxn', 'reinforce.json'); // coalesced access-recency sidecar (STATE)
|
|
40
|
+
const WIKI_PREFIX = '.wrxn/wiki/'; // the wiki root — stripped to form the D1 join key
|
|
39
41
|
|
|
40
42
|
function emit(envelope) {
|
|
41
43
|
process.stdout.write(JSON.stringify(envelope));
|
|
@@ -122,14 +124,90 @@ function renderBlock(hits) {
|
|
|
122
124
|
return block;
|
|
123
125
|
}
|
|
124
126
|
|
|
127
|
+
// PURE: the prose hits that clear the gate, capped at TOP_N — exactly the hits decideRecall renders
|
|
128
|
+
// (and the pages reinforce stamps). Factored out so the IO shell can stamp the surfaced pages by path.
|
|
129
|
+
function qualifyingHits(hits) {
|
|
130
|
+
const list = Array.isArray(hits) ? hits : [];
|
|
131
|
+
return list.filter((h) => isProse(h) && qualifies(h)).slice(0, TOP_N);
|
|
132
|
+
}
|
|
133
|
+
|
|
125
134
|
// PURE: prose-filter → gate → top-N → format. Returns the block string, or null (Abstain).
|
|
126
135
|
function decideRecall(hits) {
|
|
127
|
-
const
|
|
128
|
-
const qualified = list.filter((h) => isProse(h) && qualifies(h)).slice(0, TOP_N);
|
|
136
|
+
const qualified = qualifyingHits(hits);
|
|
129
137
|
if (!qualified.length) return null;
|
|
130
138
|
return renderBlock(qualified);
|
|
131
139
|
}
|
|
132
140
|
|
|
141
|
+
// ── reinforce: the coalesced access-recency sidecar (harvest-08 / D2) ─────────────────
|
|
142
|
+
//
|
|
143
|
+
// When Recall actually surfaces prose pages, stamp each page's "last used" day into
|
|
144
|
+
// <root>/.wrxn/reinforce.json — a COMPACT MAP { "<wiki-rel-path>": "YYYY-MM-DD" }, NOT page frontmatter
|
|
145
|
+
// (no churn) and NOT an append log (no growth). recon harvest-07/D1 reads this sidecar to compute
|
|
146
|
+
// recency for decay-weighted retrieval; the join key MUST be the wiki-root-relative path on BOTH sides
|
|
147
|
+
// (a slug-vs-path mismatch silently breaks recency). COALESCED to <= 1 write per page per day: when
|
|
148
|
+
// every surfaced page already carries today's date the map is unchanged and NOTHING is written.
|
|
149
|
+
// BEST-EFFORT + NON-BLOCKING: this is a pure side effect of recall — any fault (absent dir, malformed
|
|
150
|
+
// existing sidecar, unwritable path) is swallowed so the surfacing always proceeds.
|
|
151
|
+
|
|
152
|
+
// The wiki-root-relative join key for a prose hit's file: tolerate a leading './', normalize separators,
|
|
153
|
+
// then strip the '.wrxn/wiki/' prefix → e.g. 'concepts/foo.md'. Returns null when the file is not under
|
|
154
|
+
// the wiki root (no join key — never stamped).
|
|
155
|
+
function wikiRelPath(file) {
|
|
156
|
+
const f = String(file || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
157
|
+
const i = f.indexOf(WIKI_PREFIX);
|
|
158
|
+
if (i === -1) return null;
|
|
159
|
+
return f.slice(i + WIKI_PREFIX.length) || null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// A day-granular UTC stamp (YYYY-MM-DD): the coalescing grain AND D1's recency value. Injectable clock
|
|
163
|
+
// (`now` = a Date/ms/iso, default real time) so day-granularity is deterministic under test.
|
|
164
|
+
function dayStamp(now) {
|
|
165
|
+
const d = now instanceof Date ? now : new Date(now == null ? Date.now() : now);
|
|
166
|
+
return d.toISOString().slice(0, 10);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Stamp each surfaced prose hit's wiki-rel path → today into <root>/.wrxn/reinforce.json. Writes only
|
|
170
|
+
// when the map actually changes (coalesced). Wholly best-effort: never throws, never blocks recall.
|
|
171
|
+
function reinforce(root, hits, now) {
|
|
172
|
+
try {
|
|
173
|
+
const list = Array.isArray(hits) ? hits : [];
|
|
174
|
+
if (!root || !list.length) return;
|
|
175
|
+
const file = path.join(root, REINFORCE_REL);
|
|
176
|
+
let map = {};
|
|
177
|
+
let raw = null;
|
|
178
|
+
try {
|
|
179
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
180
|
+
} catch {
|
|
181
|
+
raw = null; // absent → fresh map (normal, not a fault)
|
|
182
|
+
}
|
|
183
|
+
if (raw !== null) {
|
|
184
|
+
let parsed;
|
|
185
|
+
try {
|
|
186
|
+
parsed = JSON.parse(raw);
|
|
187
|
+
} catch {
|
|
188
|
+
return; // malformed existing sidecar → skip silently, leave it untouched (never clobber)
|
|
189
|
+
}
|
|
190
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return; // not a map → skip
|
|
191
|
+
map = parsed;
|
|
192
|
+
}
|
|
193
|
+
const day = dayStamp(now);
|
|
194
|
+
let changed = false;
|
|
195
|
+
for (const h of list) {
|
|
196
|
+
const key = wikiRelPath(h && h.file);
|
|
197
|
+
if (!key) continue; // not under the wiki root → no D1 join key
|
|
198
|
+
if (map[key] !== day) {
|
|
199
|
+
map[key] = day;
|
|
200
|
+
changed = true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (!changed) return; // coalesced no-op → file stays byte-identical (<= 1 write/page/day)
|
|
204
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
205
|
+
fs.writeFileSync(file, JSON.stringify(map, null, 2) + '\n');
|
|
206
|
+
} catch {
|
|
207
|
+
/* best-effort: a reinforce fault must NEVER alter or break the recall surfacing */
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
133
211
|
// ── the door (IO shell, injectable transport) ───────────────────────────────────────
|
|
134
212
|
|
|
135
213
|
// A pid is alive unless process.kill(pid,0) throws ESRCH. EPERM means it exists (owned by another
|
|
@@ -233,7 +311,7 @@ function httpTransport({ port, path: reqPath, body, timeoutMs }) {
|
|
|
233
311
|
// IO shell: discover the door, POST the prose query, gate the hits. Returns the block string or null.
|
|
234
312
|
// `transport` is injected in tests; production uses httpTransport. Sends NO `type` (recon_find takes a
|
|
235
313
|
// single NodeType, not an array) — prose scope is enforced by decideRecall's post-filter.
|
|
236
|
-
async function recallFromDoor(root, prompt, { transport, timeoutMs } = {}) {
|
|
314
|
+
async function recallFromDoor(root, prompt, { transport, timeoutMs, now } = {}) {
|
|
237
315
|
const door = discoverEndpoint(root);
|
|
238
316
|
if (!door) return null; // not warm → Abstain (silent)
|
|
239
317
|
const query = String(prompt || '').trim().slice(0, MAX_QUERY_CHARS);
|
|
@@ -256,7 +334,11 @@ async function recallFromDoor(root, prompt, { transport, timeoutMs } = {}) {
|
|
|
256
334
|
} catch {
|
|
257
335
|
return null; // malformed body → silent
|
|
258
336
|
}
|
|
259
|
-
|
|
337
|
+
const hits = Array.isArray(parsed.hits) ? parsed.hits : [];
|
|
338
|
+
const block = decideRecall(hits);
|
|
339
|
+
// Side effect: stamp access-recency for the pages we actually surfaced (best-effort, never blocks).
|
|
340
|
+
if (block) reinforce(root, qualifyingHits(hits), now);
|
|
341
|
+
return block;
|
|
260
342
|
}
|
|
261
343
|
|
|
262
344
|
// ── entrypoint ──────────────────────────────────────────────────────────────────────
|
|
@@ -293,7 +375,11 @@ if (require.main === module) {
|
|
|
293
375
|
|
|
294
376
|
module.exports = {
|
|
295
377
|
decideRecall,
|
|
378
|
+
qualifyingHits,
|
|
296
379
|
recallFromDoor,
|
|
380
|
+
reinforce,
|
|
381
|
+
wikiRelPath,
|
|
382
|
+
dayStamp,
|
|
297
383
|
discoverEndpoint,
|
|
298
384
|
httpTransport,
|
|
299
385
|
pidAlive,
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
// WRXN session-start hook — the orientation surface (wrxn-kernel-10).
|
|
5
5
|
// SessionStart. Injects identity + resume as additionalContext so every new session opens
|
|
6
|
-
// oriented. The resume
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// oriented. The resume surfaces the DELIBERATE handoff baton at .wrxn/continuity/latest.md (single
|
|
7
|
+
// writer = the handoff skill); absent a baton there is no prior handoff to resume. (The automatic
|
|
8
|
+
// episodic session-page fallback was retired with the session-capture subsystem in harvest-01.)
|
|
9
9
|
//
|
|
10
10
|
// Self-contained: ships into installs, MUST NOT import the kernel lib (node stdlib only).
|
|
11
11
|
// Fail-open: any fault emits {} (no orientation) — the hook NEVER blocks a session opening.
|
|
@@ -62,20 +62,6 @@ function readBaton(root) {
|
|
|
62
62
|
return readFileOr(path.join(root, '.wrxn', 'continuity', 'latest.md'), null);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
// The automatic episodic record: the most-recent dated session page (sessions tier).
|
|
66
|
-
function latestSessionPage(root) {
|
|
67
|
-
const dir = path.join(root, '.wrxn', 'wiki', 'sessions');
|
|
68
|
-
let names;
|
|
69
|
-
try {
|
|
70
|
-
names = fs.readdirSync(dir).filter((n) => n.endsWith('.md'));
|
|
71
|
-
} catch {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
if (names.length === 0) return null;
|
|
75
|
-
names.sort(); // dated `YYYY-MM-DD-…` slugs sort chronologically
|
|
76
|
-
return names[names.length - 1];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
65
|
function main() {
|
|
80
66
|
let consumed = '';
|
|
81
67
|
try {
|
|
@@ -94,12 +80,7 @@ function main() {
|
|
|
94
80
|
if (baton && baton.trim()) {
|
|
95
81
|
parts.push('', 'Resume — deliberate handoff baton (.wrxn/continuity/latest.md):', baton.trim());
|
|
96
82
|
} else {
|
|
97
|
-
|
|
98
|
-
if (page) {
|
|
99
|
-
parts.push('', `Resume — last session: .wrxn/wiki/sessions/${page}`);
|
|
100
|
-
} else {
|
|
101
|
-
parts.push('', 'Resume — fresh install, no prior session recorded.');
|
|
102
|
-
}
|
|
83
|
+
parts.push('', 'Resume — no prior handoff.');
|
|
103
84
|
}
|
|
104
85
|
|
|
105
86
|
emit({
|
|
@@ -247,18 +247,54 @@ function resolveHandoffPct(manifestText) {
|
|
|
247
247
|
return Number.isFinite(m) && m > 0 ? m : 0.40;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
// A CHEAP, fail-open presence-probe for curation debt — the debt-gate on the handoff harvest nudge
|
|
251
|
+
// (harvest-05). It does NOT recompute health: no recon-door query, no scan of the knowledge tiers (that
|
|
252
|
+
// is harvest.cjs `check`, an operator-invoked command far too heavy for a per-prompt hook). It reads ONLY
|
|
253
|
+
// the single latest `.wrxn/harvest/<ts>.jsonl` report a prior `check` already wrote and asks "did the last
|
|
254
|
+
// health-check find real debt?". Report filenames are timestamp-derived (ISO with `:`/`.` → `-`), so the
|
|
255
|
+
// lexically-greatest name is the newest report. A real finding = any record EXCEPT the near_dup
|
|
256
|
+
// "unavailable" marker (a cold-door "couldn't check", not debt); an empty report / only-unavailable / no
|
|
257
|
+
// reports / missing dir all read as no-debt → silent. Any fault → false (never a spurious nudge, never a
|
|
258
|
+
// throw). Only invoked when a handoff is actually firing, so the one extra file read is doubly bounded.
|
|
259
|
+
function hasCurationDebt(root) {
|
|
260
|
+
try {
|
|
261
|
+
const dir = path.join(root, '.wrxn', 'harvest');
|
|
262
|
+
const reports = fs.readdirSync(dir).filter((n) => n.endsWith('.jsonl'));
|
|
263
|
+
if (!reports.length) return false;
|
|
264
|
+
const latest = reports.sort()[reports.length - 1];
|
|
265
|
+
const text = fs.readFileSync(path.join(dir, latest), 'utf8');
|
|
266
|
+
for (const line of text.split('\n')) {
|
|
267
|
+
const t = line.trim();
|
|
268
|
+
if (!t) continue;
|
|
269
|
+
let rec;
|
|
270
|
+
try { rec = JSON.parse(t); } catch { continue; } // a malformed line contributes nothing
|
|
271
|
+
if (rec && rec.type && rec.status !== 'unavailable') return true;
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
274
|
+
} catch {
|
|
275
|
+
return false; // missing dir / unreadable report / any fault → fail-open: no debt, no throw
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
250
279
|
// The NON-BLOCKING forced-handoff directive (never refuses work — orders the agent to wrap up cleanly).
|
|
251
|
-
|
|
280
|
+
// `hasDebt` (harvest-05) appends a harvest curation nudge AFTER the dream line — emitted ONLY when the
|
|
281
|
+
// 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
|
+
function handoffDirective(consumed, pct, hasDebt) {
|
|
252
284
|
const now = Math.round(consumed * 100);
|
|
253
285
|
const thresh = Math.round(pct * 100);
|
|
254
|
-
|
|
286
|
+
const lines = [
|
|
255
287
|
'[HANDOFF REQUIRED]',
|
|
256
288
|
` Context is at ~${now}% of the model window (>= the ${thresh}% handoff threshold). NON-BLOCKING — do NOT stop work:`,
|
|
257
289
|
' 1. Finish the current request.',
|
|
258
290
|
' 2. Run the handoff skill to write the baton (a compact handoff document).',
|
|
259
291
|
' 3. Tell the operator to /clear and open a fresh session, where the baton injects on resume.',
|
|
260
292
|
' Suggestion (optional, before step 2): run the dream skill to consolidate this session\'s durable learnings into wiki memory — a suggestion only; dream never auto-runs, it acts only when you invoke it.',
|
|
261
|
-
]
|
|
293
|
+
];
|
|
294
|
+
if (hasDebt) {
|
|
295
|
+
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.');
|
|
296
|
+
}
|
|
297
|
+
return lines.join('\n');
|
|
262
298
|
}
|
|
263
299
|
|
|
264
300
|
// ── assembly ────────────────────────────────────────────────────────────────────
|
|
@@ -325,7 +361,7 @@ function compose(root, event) {
|
|
|
325
361
|
const window = modelWindow(ev.cwd || root, process.env.HOME || os.homedir(), manifestText, ev.session_id, resident);
|
|
326
362
|
const consumed = resident / window;
|
|
327
363
|
const pct = resolveHandoffPct(manifestText);
|
|
328
|
-
if (consumed >= pct) out.push(handoffDirective(consumed, pct));
|
|
364
|
+
if (consumed >= pct) out.push(handoffDirective(consumed, pct, hasCurationDebt(root)));
|
|
329
365
|
}
|
|
330
366
|
}
|
|
331
367
|
|
|
@@ -375,5 +411,6 @@ module.exports = {
|
|
|
375
411
|
readStatuslineWindow,
|
|
376
412
|
modelWindow,
|
|
377
413
|
resolveHandoffPct,
|
|
414
|
+
hasCurationDebt,
|
|
378
415
|
handoffDirective,
|
|
379
416
|
};
|
|
@@ -7,18 +7,10 @@
|
|
|
7
7
|
]
|
|
8
8
|
}
|
|
9
9
|
],
|
|
10
|
-
"SessionEnd": [
|
|
11
|
-
{
|
|
12
|
-
"hooks": [
|
|
13
|
-
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/session-end.cjs\"" }
|
|
14
|
-
]
|
|
15
|
-
}
|
|
16
|
-
],
|
|
17
10
|
"UserPromptSubmit": [
|
|
18
11
|
{
|
|
19
12
|
"hooks": [
|
|
20
13
|
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/synapse-engine.cjs\"" },
|
|
21
|
-
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/session-history.cjs\"" },
|
|
22
14
|
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/reference-detect.cjs\"" },
|
|
23
15
|
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/recall-surface.cjs\"" }
|
|
24
16
|
]
|