@gcunharodrigues/wrxn 0.11.0 → 0.13.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 CHANGED
@@ -86,6 +86,9 @@ function init(opts) {
86
86
  ensureGitignoreLine(target, '.recon-wrxn/');
87
87
  // the recall-surface hook writes per-install access-recency state here (harvest-08) — runtime, not committed.
88
88
  ensureGitignoreLine(target, '.wrxn/reinforce.json');
89
+ // the memory-synth gemini fallback reads GEMINI_API_KEY from `.env` — ignore it so the secret is
90
+ // never committed (the synth's doc-comment calls it the install's gitignored `.env`).
91
+ ensureGitignoreLine(target, '.env');
89
92
 
90
93
  writeReceipt(target, { version, profile, laid, skipped, merged, brownfield });
91
94
 
package/manifest.json CHANGED
@@ -73,6 +73,11 @@
73
73
  "class": "managed",
74
74
  "profile": "project"
75
75
  },
76
+ {
77
+ "path": ".claude/hooks/memory-synth-spawn.cjs",
78
+ "class": "managed",
79
+ "profile": "project"
80
+ },
76
81
  {
77
82
  "path": ".claude/hooks/recall-surface.cjs",
78
83
  "class": "managed",
@@ -143,11 +148,6 @@
143
148
  "class": "managed",
144
149
  "profile": "project"
145
150
  },
146
- {
147
- "path": ".claude/skills/handoff/SKILL.md",
148
- "class": "managed",
149
- "profile": "project"
150
- },
151
151
  {
152
152
  "path": ".claude/skills/harvest/SKILL.md",
153
153
  "class": "managed",
@@ -488,6 +488,16 @@
488
488
  "class": "state",
489
489
  "profile": "project"
490
490
  },
491
+ {
492
+ "path": ".wrxn/memory-synth.cjs",
493
+ "class": "managed",
494
+ "profile": "project"
495
+ },
496
+ {
497
+ "path": ".wrxn/memory.config.json",
498
+ "class": "seeded",
499
+ "profile": "project"
500
+ },
491
501
  {
492
502
  "path": ".wrxn/sync.cjs",
493
503
  "class": "managed",
@@ -0,0 +1,136 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * 007 — transition existing installs onto auto-memory (auto-memory-05).
8
+ *
9
+ * The auto-memory release makes memory automatic: a SessionEnd hook (memory-synth-spawn.cjs) spawns a
10
+ * background synth that writes the continuity baton and consolidates dream pages — so the manual
11
+ * `handoff` skill is removed (the synth is the sole baton writer) and the stale `_slots/current-focus`
12
+ * slot + its `set-focus` op are dropped. The new payload stops shipping the handoff skill and now wires
13
+ * SessionEnd + ships a seeded `memory.config.json`. But `wrxn update` overwrites managed files in place —
14
+ * it never PRUNES a removed one — and a seeded file already present is preserved, so a pre-0.12.0 install
15
+ * still carries the old `handoff` skill files and the stale focus slot, and may lack the new wiring/seed
16
+ * (e.g. a hand-edited settings.json the managed overwrite left alone, or an install that never had a
17
+ * config). up() transitions it.
18
+ *
19
+ * Steps: (1) remove the install's `handoff` skill dir; (2) wire SessionEnd → memory-synth-spawn.cjs into
20
+ * the install settings.json IDEMPOTENTLY (add only if absent — if update's managed overwrite already laid
21
+ * it, this is a safe no-op); a corrupt settings.json is left untouched; (3) seed `memory.config.json` if
22
+ * absent (the slice-02 default shape); (4) remove the stale `_slots/current-focus.md` focus slot;
23
+ * (5) backfill the install `.gitignore` for the `.env` secret and the continuity runtime temps the synth/
24
+ * dream now write (`.wrxn/continuity/.pending`, `.pending-handoff`, the baton `.tmp`, `.dream.*.tmp`) —
25
+ * slice-02 added `.env` for NEW installs; this closes the gap for OLD ones (slice-04 reviewer F1).
26
+ *
27
+ * Defensive like 004: every step is existence-guarded and best-effort (force-rm ignores a missing file),
28
+ * a missing/clean target is a no-op, a corrupt settings.json is left untouched (never clobber a hand-
29
+ * edited file — the other steps still run), and the gitignore backfill adds each line at most once.
30
+ * Idempotent (a second run finds nothing to do) and never throws on an already-clean install. `version`
31
+ * 0.12.0 = the auto-memory release that carries the transition (the same release whose payload stops
32
+ * shipping the handoff skill and starts wiring the SessionEnd synth).
33
+ */
34
+
35
+ // The .gitignore lines auto-memory needs an install to carry: the `.env` secret (slice-02 added it for
36
+ // NEW installs — backfilled here for OLD ones) + the continuity runtime markers/temps the synth and dream
37
+ // write and clean in a finally, which a SIGKILL could leave behind UNTRACKED (slice-04 reviewer F1). The
38
+ // tracked baton `latest.md` is deliberately NOT ignored — only its dot-prefixed `.tmp` is.
39
+ const GITIGNORE_LINES = [
40
+ '.env',
41
+ '.wrxn/continuity/.pending*', // the .pending + .pending-handoff synth markers
42
+ '.wrxn/continuity/.dream.*.tmp', // dream's per-call blob/batch/stage/approved temps
43
+ '.wrxn/continuity/.latest.md.*.tmp', // the atomic baton-write temp (rename-over target)
44
+ ];
45
+
46
+ /** Append `line` to `<target>/.gitignore` (create if absent) exactly once — mirrors install.cjs. */
47
+ function ensureGitignoreLine(target, line) {
48
+ const giPath = path.join(target, '.gitignore');
49
+ let body = '';
50
+ try {
51
+ body = fs.readFileSync(giPath, 'utf8');
52
+ } catch {
53
+ body = '';
54
+ }
55
+ if (body.split('\n').some((l) => l.trim() === line)) return; // already ignored
56
+ const prefix = body.length && !body.endsWith('\n') ? '\n' : '';
57
+ fs.writeFileSync(giPath, body + prefix + line + '\n');
58
+ }
59
+
60
+ // The SessionEnd spawn hook the auto-memory payload wires (must match payload/.claude/settings.json).
61
+ const SPAWN_HOOK_BASENAME = 'memory-synth-spawn.cjs';
62
+ const SPAWN_HOOK_COMMAND = 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/memory-synth-spawn.cjs"';
63
+
64
+ // Idempotently wire SessionEnd → the spawn hook into a parsed settings config. Adds the hook ONLY if no
65
+ // existing hook command across any event already references it — so a config the managed overwrite already
66
+ // laid (or a prior run) is a safe no-op. Returns true iff the config changed. Preserves every other event.
67
+ function wireSessionEndSpawn(cfg) {
68
+ if (!cfg || typeof cfg !== 'object') return false;
69
+ cfg.hooks = cfg.hooks && typeof cfg.hooks === 'object' ? cfg.hooks : {};
70
+ // already wired anywhere? (mirror unwireHook's whole-config scan) → no-op.
71
+ for (const groups of Object.values(cfg.hooks)) {
72
+ if (!Array.isArray(groups)) continue;
73
+ for (const group of groups) {
74
+ if (!group || !Array.isArray(group.hooks)) continue;
75
+ if (group.hooks.some((h) => h && typeof h.command === 'string' && h.command.includes(SPAWN_HOOK_BASENAME))) {
76
+ return false;
77
+ }
78
+ }
79
+ }
80
+ const event = Array.isArray(cfg.hooks.SessionEnd) ? cfg.hooks.SessionEnd : [];
81
+ event.push({ hooks: [{ type: 'command', command: SPAWN_HOOK_COMMAND }] });
82
+ cfg.hooks.SessionEnd = event;
83
+ return true;
84
+ }
85
+
86
+ module.exports = {
87
+ id: '007',
88
+ version: '0.12.0',
89
+ up(ctx) {
90
+ const target = ctx.target;
91
+
92
+ // 1. remove the retired `handoff` skill dir (the synth is the sole baton writer now). recursive +
93
+ // force = an absent dir is a no-op.
94
+ fs.rmSync(path.join(target, '.claude', 'skills', 'handoff'), { recursive: true, force: true });
95
+
96
+ // 2. wire SessionEnd → memory-synth-spawn.cjs into the install settings.json, idempotently. A corrupt
97
+ // file is left untouched (never clobber a hand-edited config); an absent file is left absent (the
98
+ // managed overwrite lays the wired settings — the migration only backfills a hand-edited one).
99
+ const settingsPath = path.join(target, '.claude', 'settings.json');
100
+ if (fs.existsSync(settingsPath)) {
101
+ let cfg = null;
102
+ try {
103
+ cfg = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
104
+ } catch {
105
+ cfg = null; // hand-corrupted operator file → never clobber, never crash
106
+ }
107
+ if (cfg && typeof cfg === 'object' && wireSessionEndSpawn(cfg)) {
108
+ fs.writeFileSync(settingsPath, JSON.stringify(cfg, null, 2) + '\n');
109
+ }
110
+ }
111
+
112
+ // 3. seed memory.config.json if absent (the slice-02 default). Copied from THIS package's payload so
113
+ // the seeded shape can never drift from what a fresh install ships. Already present ⇒ preserved
114
+ // (it is a seeded, operator-owned file). A missing payload source is swallowed (best-effort).
115
+ const cfgPath = path.join(target, '.wrxn', 'memory.config.json');
116
+ if (!fs.existsSync(cfgPath)) {
117
+ const seedSrc = path.join(__dirname, '..', 'payload', '.wrxn', 'memory.config.json');
118
+ try {
119
+ const seed = fs.readFileSync(seedSrc, 'utf8');
120
+ fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
121
+ fs.writeFileSync(cfgPath, seed);
122
+ } catch {
123
+ // no payload seed reachable → skip (the managed/seeded update path lays it anyway)
124
+ }
125
+ }
126
+
127
+ // 4. remove the stale `_slots/current-focus.md` focus slot (the slot + its set-focus op are dropped).
128
+ // Only the slot PAGE goes — the empty `_slots` tier dir + its gitkeep stay (the tier is retained).
129
+ // force = an absent slot is a no-op.
130
+ fs.rmSync(path.join(target, '.wrxn', 'wiki', '_slots', 'current-focus.md'), { force: true });
131
+
132
+ // 5. gitignore backfill — add the `.env` secret + continuity runtime-temp lines (each at most once).
133
+ // Closes the gap for installs created before slice-02 added `.env` / before the synth wrote temps.
134
+ for (const line of GITIGNORE_LINES) ensureGitignoreLine(target, line);
135
+ },
136
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gcunharodrigues/wrxn",
3
- "version": "0.11.0",
3
+ "version": "0.13.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 --require ./test/setup.cjs"
17
17
  },
18
18
  "dependencies": {
19
- "recon-wrxn": "6.0.0-wrxn.6"
19
+ "recon-wrxn": "6.0.0-wrxn.7"
20
20
  },
21
21
  "engines": {
22
22
  "node": ">=20"
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // WRXN memory-synth-spawn — the SessionEnd hook that launches the background synthesizer (auto-memory-03).
5
+ // SessionEnd is otherwise unwired, so this is its sole hook. Its ONLY job: return {} immediately and
6
+ // spawn memory-synth.cjs DETACHED, so closing a session is NEVER blocked by summarization (PRD story 16).
7
+ //
8
+ // Recursion guard (PRD story 17): the synth runs `claude -p`, whose own session fires SessionEnd again —
9
+ // a SessionEnd→claude→SessionEnd fork-bomb. The synth sets WRXN_MEMORY_SYNTH=1 on every engine spawn;
10
+ // this hook no-ops (spawns nothing, writes no markers) when it sees that sentinel set.
11
+ //
12
+ // Before launching, it stashes the SessionEnd payload as the `.pending` marker and writes a
13
+ // `.pending-handoff` marker under .wrxn/continuity/ — both so SessionStart can detect an in-flight synth
14
+ // (and hold on the handoff marker) even before the detached child has done anything. The synth clears
15
+ // them on exit (memory-synth.cjs), so the markers describe "a synth is running for this session".
16
+ //
17
+ // Self-contained: ships into installs, MUST NOT import the kernel lib (node stdlib only).
18
+ // Fail-open: any fault emits {} (no spawn) — the hook NEVER blocks a session closing.
19
+ //
20
+ // Contract: SessionEnd event JSON on stdin → {} on stdout (exit 0).
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const { spawn } = require('child_process');
25
+
26
+ const SENTINEL = 'WRXN_MEMORY_SYNTH'; // recursion guard (mirrors memory-synth.cjs's SENTINEL).
27
+
28
+ function findInstallRoot(startDir) {
29
+ let dir = startDir || process.env.CLAUDE_PROJECT_DIR || process.cwd();
30
+ for (let i = 0; i < 12; i++) {
31
+ if (fs.existsSync(path.join(dir, 'wrxn.install.json'))) return dir;
32
+ const up = path.dirname(dir);
33
+ if (up === dir) break;
34
+ dir = up;
35
+ }
36
+ return null;
37
+ }
38
+
39
+ /**
40
+ * The testable core. Given the SessionEnd payload, the install root, the env (for the recursion guard),
41
+ * and an injectable `spawn`, it stashes the payload + writes the pending markers and launches the synth
42
+ * detached — returning {} immediately. With the recursion sentinel set it no-ops (spawns nothing, writes
43
+ * no markers). Fail-open: any fault → {} (session close is never blocked).
44
+ * @param {{ payload:object, root:string, env:object, spawn:Function }} opts
45
+ * @returns {object} the hook envelope — always {}
46
+ */
47
+ function run({ payload, root, env = process.env, spawn: spawnFn = spawn }) {
48
+ try {
49
+ if (env && env[SENTINEL]) return {}; // inside a synth-spawned session — do not recurse.
50
+ if (!root) return {};
51
+
52
+ const dir = path.join(root, '.wrxn', 'continuity');
53
+ fs.mkdirSync(dir, { recursive: true });
54
+
55
+ // Stash the payload as .pending (the synth reads it for transcript_path) and raise the handoff gate.
56
+ fs.writeFileSync(path.join(dir, '.pending'), JSON.stringify(payload || {}));
57
+ fs.writeFileSync(path.join(dir, '.pending-handoff'), String(Date.now()));
58
+
59
+ // Launch the synth detached: stdio ignored + unref() so the parent's event loop never waits on it.
60
+ const synth = path.join(root, '.wrxn', 'memory-synth.cjs');
61
+ const child = spawnFn('node', [synth, '--from-spawn', '--root', root], {
62
+ detached: true,
63
+ stdio: 'ignore',
64
+ env: { ...env, [SENTINEL]: '1' },
65
+ });
66
+ if (child && typeof child.unref === 'function') child.unref();
67
+ } catch {
68
+ // fail-open: never block session close.
69
+ }
70
+ return {};
71
+ }
72
+
73
+ function main() {
74
+ let consumed = '';
75
+ try {
76
+ consumed = fs.readFileSync(0, 'utf8');
77
+ } catch {
78
+ /* no stdin → nothing to synthesize */
79
+ }
80
+ let payload = {};
81
+ try {
82
+ payload = consumed.trim() ? JSON.parse(consumed) : {};
83
+ } catch {
84
+ payload = {};
85
+ }
86
+ const root = findInstallRoot(payload && payload.cwd);
87
+ const out = run({ payload, root, env: process.env, spawn });
88
+ process.stdout.write(JSON.stringify(out));
89
+ process.exit(0);
90
+ }
91
+
92
+ if (require.main === module) {
93
+ try {
94
+ main();
95
+ } catch {
96
+ process.stdout.write('{}');
97
+ process.exit(0);
98
+ }
99
+ }
100
+
101
+ module.exports = { run };
@@ -3,9 +3,10 @@
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 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.)
6
+ // oriented. The resume surfaces the handoff baton at .wrxn/continuity/latest.md (single writer =
7
+ // the memory synth `memory-synth.cjs`, which writes the baton automatically on SessionEnd); absent
8
+ // a baton there is no prior handoff to resume. (The automatic episodic session-page fallback was
9
+ // retired with the session-capture subsystem in harvest-01.)
9
10
  //
10
11
  // Self-contained: ships into installs, MUST NOT import the kernel lib (node stdlib only).
11
12
  // Fail-open: any fault emits {} (no orientation) — the hook NEVER blocks a session opening.
@@ -57,11 +58,76 @@ function identityLine(root) {
57
58
  }
58
59
 
59
60
  // The deliberate handoff baton — the intent-carrying continuity slot. Single writer: the
60
- // handoff skill. Read-only here; its presence takes precedence over the episodic record.
61
+ // auto-handoff synth (memory-synth.cjs, auto-memory-03). Read-only here; its presence is the resume.
61
62
  function readBaton(root) {
62
63
  return readFileOr(path.join(root, '.wrxn', 'continuity', 'latest.md'), null);
63
64
  }
64
65
 
66
+ // ── the SessionEnd-synth hold (auto-memory-03) ──────────────────────────────────
67
+ // When a session ends, memory-synth-spawn.cjs raises a `.pending-handoff` marker and launches the synth
68
+ // detached; the synth writes the baton then clears the marker. So a back-to-back /clear could start
69
+ // BEFORE the fresh baton exists. We hold: poll the marker until it clears (synth done), bounded by a
70
+ // crash safety-cap so a SIGKILLed synth (marker never cleared) can never hang start forever. The
71
+ // poll-decision is pure and the loop takes an injected clock, so it is unit-tested with no wall sleep.
72
+ const HANDOFF_MARKER_REL = ['.wrxn', 'continuity', '.pending-handoff'];
73
+ const HOLD_CAP_MS = 60000; // crash safety-cap: never wait past this for an in-flight synth.
74
+ const HOLD_POLL_MS = 250; // poll cadence (the real sleep step; tests inject their own).
75
+
76
+ // A synchronous sleep for the real poll loop (the hook must finish before it emits; tests inject their
77
+ // own sleep and never reach this). Atomics.wait blocks the thread without a busy-spin (node stdlib).
78
+ function sleepMs() {
79
+ try {
80
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HOLD_POLL_MS);
81
+ } catch {
82
+ /* SharedArrayBuffer unavailable → degrade to no wait (the loop's wall-cap still bounds it) */
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Pure poll-decision: wait vs proceed, from the marker's presence + age + the cap. No marker → proceed
88
+ * (no synth in flight). Marker present and younger than the cap → wait. Marker at/over the cap → proceed
89
+ * anyway (a crashed synth must not hang start). PURE.
90
+ * @param {{ markerExists:boolean, markerAgeMs:number, capMs:number }} s
91
+ * @returns {'wait'|'proceed'}
92
+ */
93
+ function holdDecision({ markerExists, markerAgeMs, capMs }) {
94
+ if (!markerExists) return 'proceed';
95
+ return markerAgeMs >= capMs ? 'proceed' : 'wait';
96
+ }
97
+
98
+ /** Age (ms) of the handoff marker per the injected clock; Infinity if it is absent/unreadable. */
99
+ function markerAgeMs(root, now) {
100
+ try {
101
+ const st = fs.statSync(path.join(root, ...HANDOFF_MARKER_REL));
102
+ return Math.max(0, now() - st.mtimeMs);
103
+ } catch {
104
+ return Infinity; // absent → treated as "no marker" by the caller.
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Hold until the in-flight synth clears the handoff marker, or the safety-cap elapses. The clock is
110
+ * injected (now()/sleep()) so the loop is deterministic in tests — NO wall-clock sleep here. Returns
111
+ * 'cleared' (marker gone → fresh baton ready) or 'capped' (gave up at the cap). Never throws.
112
+ * @param {{ root:string, capMs?:number, now?:Function, sleep?:Function }} opts
113
+ * @returns {'cleared'|'capped'}
114
+ */
115
+ function holdForHandoff({ root, capMs = HOLD_CAP_MS, now = Date.now, sleep } = {}) {
116
+ const marker = path.join(root, ...HANDOFF_MARKER_REL);
117
+ const wait = sleep || (() => {}); // with no injected waiter the loop takes a single pass then caps (line below).
118
+ const started = now();
119
+ for (;;) {
120
+ const exists = fs.existsSync(marker);
121
+ if (!exists) return 'cleared';
122
+ const age = markerAgeMs(root, now);
123
+ if (holdDecision({ markerExists: true, markerAgeMs: age, capMs }) === 'proceed') return 'capped';
124
+ // also cap on wall-elapsed-since-entry, so a marker with a future/odd mtime still can't hang us.
125
+ if (now() - started >= capMs) return 'capped';
126
+ wait();
127
+ if (!sleep) return 'capped'; // no real waiter injected → don't spin; proceed.
128
+ }
129
+ }
130
+
65
131
  function main() {
66
132
  let consumed = '';
67
133
  try {
@@ -74,6 +140,14 @@ function main() {
74
140
  const root = findInstallRoot();
75
141
  if (!root) emit({});
76
142
 
143
+ // Hold for an in-flight SessionEnd synth (auto-memory-03) so a back-to-back /clear resumes on the
144
+ // FRESH baton, bounded by the crash safety-cap. Fail-open: any fault here must not block orientation.
145
+ try {
146
+ holdForHandoff({ root, sleep: sleepMs });
147
+ } catch {
148
+ /* never block the session opening on the hold */
149
+ }
150
+
77
151
  const parts = [identityLine(root)];
78
152
 
79
153
  const baton = readBaton(root);
@@ -91,8 +165,12 @@ function main() {
91
165
  });
92
166
  }
93
167
 
94
- try {
95
- main();
96
- } catch {
97
- emit({}); // fail-open: never block a session opening
168
+ if (require.main === module) {
169
+ try {
170
+ main();
171
+ } catch {
172
+ emit({}); // fail-open: never block a session opening
173
+ }
98
174
  }
175
+
176
+ module.exports = { holdDecision, holdForHandoff };
@@ -287,9 +287,8 @@ function handoffDirective(consumed, pct, hasDebt) {
287
287
  '[HANDOFF REQUIRED]',
288
288
  ` Context is at ~${now}% of the model window (>= the ${thresh}% handoff threshold). NON-BLOCKING — do NOT stop work:`,
289
289
  ' 1. Finish the current request.',
290
- ' 2. Run the handoff skill to write the baton (a compact handoff document).',
291
- ' 3. Tell the operator to /clear and open a fresh session, where the baton injects on resume.',
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.',
290
+ ' 2. Tell the operator to /clear and open a fresh session. No manual step: the continuity baton writes automatically when this session ends (the memory synth) and injects on resume.',
291
+ ' Suggestion (optional): the session also auto-consolidates its durable learnings into wiki memory on close (auto-dream); to consolidate explicitly or mid-session, invoke the dream skill a suggestion only, never required.',
293
292
  ];
294
293
  if (hasDebt) {
295
294
  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.');
@@ -7,6 +7,13 @@
7
7
  ]
8
8
  }
9
9
  ],
10
+ "SessionEnd": [
11
+ {
12
+ "hooks": [
13
+ { "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/memory-synth-spawn.cjs\"" }
14
+ ]
15
+ }
16
+ ],
10
17
  "UserPromptSubmit": [
11
18
  {
12
19
  "hooks": [
@@ -162,40 +162,11 @@ recalls them automatically next session. Spot-check with a wiki query:
162
162
  node .wrxn/wiki.cjs query "<a phrase from a page you just wrote>"
163
163
  ```
164
164
 
165
- ## Refreshing the focus slot
166
-
167
- `_slots/current-focus.md` is the project's **durable standing focus** — a short statement of what the
168
- project is centered on right now, recall-surfaced like any other page. It is the **lone updatable wiki
169
- page**: every knowledge page is additive + dedup-skip, but the focus slot may be **overwritten in
170
- place**.
171
-
172
- This is **not** the knowledge-proposal loop — do not run a focus update through `check` / `stage` /
173
- `commit` (those are for evidence-backed concept/decision/gotcha/rule pages). The slot has its **own op**:
174
-
175
- 1. Draft a short standing-focus statement (a few lines of markdown, body starting with `# `).
176
- 2. **Present it to the operator and wait for confirmation** — like every dream write.
177
- 3. On approval, write it via the dedicated op — it overwrites the slot in place:
178
-
179
- ```bash
180
- node .wrxn/dream.cjs set-focus /tmp/dream-focus.json # { "title": "Current focus", "body": "# Current focus\n\n…" }
181
- ```
182
-
183
- The focus slot is **gated** too: `set-focus` runs the anti-superstition negative filters and the
184
- credential secret-scan over the focus body and **refuses** (writing nothing) if either fires. Redact
185
- secrets and pin durable standing context, not a transient note.
186
-
187
- **Continuity doctrine — do not cross these wires.** The focus slot is **disjoint** from the handoff
188
- **baton** (`.wrxn/continuity/latest.md`): different path, different writer. `set-focus` NEVER reads or
189
- writes the baton, and the **handoff** skill remains its sole writer. The baton is ephemeral cross-session
190
- resume; the focus slot is durable standing context. Keeping their paths and writers separate is the
191
- structural fix that stops a deliberate handoff from being clobbered.
192
-
193
165
  ## Boundaries
194
166
 
195
167
  - **Current session only.** No transcript mining, no cross-session backlog.
196
- - **Additive only, save one slot.** dream creates net-new knowledge pages; merging or refreshing an
197
- existing page is out of scope (that is harvest, a later phase). The **lone exception** is the focus
198
- slot `_slots/current-focus.md`, which `set-focus` overwrites in place (see *Refreshing the focus slot*).
168
+ - **Additive only.** dream creates net-new knowledge pages; merging or refreshing an existing page is
169
+ out of scope (that is harvest, a later phase).
199
170
  - **Never autonomous.** dream is a deliberate, attended, operator-confirmed skill — never a background
200
171
  run, never a write without confirmation.
201
172
  - **`_rules` ≠ SYNAPSE.** A `rule` page is *recalled knowledge* — the Brain surfaces it like a concept
@@ -62,9 +62,10 @@ marker records what was dropped. One budget, applied flat. See
62
62
 
63
63
  When real consumed context reaches the handoff threshold (`HANDOFF_PCT`, default 0.40; override
64
64
  `WRXN_HANDOFF_PCT`) of the model window, SYNAPSE appends a **non-blocking** `[HANDOFF REQUIRED]`
65
- directive: finish the current request, run the handoff skill to write the baton, then `/clear` and
66
- resume in a fresh session. It never refuses work. The math runs on real token usage (resident tokens
67
- from the transcript ÷ the resolved model window), not an assumed window. See
65
+ directive: finish the current request, then `/clear` and resume in a fresh session the continuity
66
+ baton writes automatically on session end (the memory synth) and injects on resume, so there is no
67
+ manual step. It never refuses work. The math runs on real token usage (resident tokens from the
68
+ transcript ÷ the resolved model window), not an assumed window. See
68
69
  [token budget & handoff](references/brackets.md).
69
70
 
70
71
  ## Output shape
@@ -89,8 +90,7 @@ Article I — Agent Authority (NON-NEGOTIABLE)
89
90
  [HANDOFF REQUIRED]
90
91
  Context is at ~42% of the model window (>= the 40% handoff threshold). NON-BLOCKING — do NOT stop work:
91
92
  1. Finish the current request.
92
- 2. Run the handoff skill to write the baton (a compact handoff document).
93
- 3. Tell the operator to /clear and open a fresh session, where the baton injects on resume.
93
+ 2. Tell the operator to /clear and open a fresh session. No manual step: the continuity baton writes automatically when this session ends (the memory synth) and injects on resume.
94
94
 
95
95
  </synapse-rules>
96
96
  ```
@@ -30,8 +30,7 @@ When the real consumed context reaches the handoff threshold, SYNAPSE appends a
30
30
  [HANDOFF REQUIRED]
31
31
  Context is at ~42% of the model window (>= the 40% handoff threshold). NON-BLOCKING — do NOT stop work:
32
32
  1. Finish the current request.
33
- 2. Run the handoff skill to write the baton (a compact handoff document).
34
- 3. Tell the operator to /clear and open a fresh session, where the baton injects on resume.
33
+ 2. Tell the operator to /clear and open a fresh session. No manual step: the continuity baton writes automatically when this session ends (the memory synth) and injects on resume.
35
34
  ```
36
35
 
37
36
  Like the constitution, the handoff directive is outside the budget — it is never trimmed.
@@ -79,8 +79,7 @@ Article I — Agent Authority (NON-NEGOTIABLE)
79
79
  [HANDOFF REQUIRED]
80
80
  Context is at ~42% of the model window (>= the 40% handoff threshold). NON-BLOCKING — do NOT stop work:
81
81
  1. Finish the current request.
82
- 2. Run the handoff skill to write the baton (a compact handoff document).
83
- 3. Tell the operator to /clear and open a fresh session, where the baton injects on resume.
82
+ 2. Tell the operator to /clear and open a fresh session. No manual step: the continuity baton writes automatically when this session ends (the memory synth) and injects on resume.
84
83
 
85
84
  </synapse-rules>
86
85
  ```
package/payload/.mcp.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "recon-wrxn": {
4
4
  "command": "npx",
5
- "args": ["-y", "recon-wrxn@6.0.0-wrxn.6", "serve"]
5
+ "args": ["-y", "recon-wrxn@6.0.0-wrxn.7", "serve"]
6
6
  }
7
7
  }
8
8
  }