@gcunharodrigues/wrxn 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/wrxn.cjs CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
+ const os = require('os');
6
7
 
7
8
  const { init } = require('../lib/install.cjs');
8
9
  const { update } = require('../lib/update.cjs');
@@ -10,6 +11,7 @@ const worktree = require('../lib/worktree.cjs');
10
11
  const executor = require('../lib/executor.cjs');
11
12
  const onboard = require('../lib/onboard.cjs');
12
13
  const connect = require('../lib/connect.cjs');
14
+ const statusline = require('../lib/statusline.cjs');
13
15
 
14
16
  const PKG_ROOT = path.join(__dirname, '..');
15
17
 
@@ -103,6 +105,13 @@ Usage:
103
105
  list print all registered connections (agent-readable JSON)
104
106
  get <name> print one connection by name
105
107
 
108
+ wrxn statusline [--inject [--path <script>]]
109
+ SYNAPSE live-window writer. With no flag: report whether a statusline
110
+ is configured (~/.claude/settings.json) + print the marker-bounded
111
+ sidecar block + how to enable. With --inject: append the block to the
112
+ resolved (or --path) statusline script, idempotently (append-only,
113
+ never overwrites). init NEVER touches your statusline.
114
+
106
115
  wrxn onboard [--root <dir>] scaffold the Day-1 operator file set under context/ from a filled
107
116
  aios-intake.md (the deterministic half of the onboard skill;
108
117
  workspace installs only). Idempotent.
@@ -148,10 +157,19 @@ function main(argv) {
148
157
  for (const f of report.skipped) {
149
158
  process.stdout.write(` skipped [${f.class}] ${f.path} (${f.collision ? 'collision — existing file preserved' : 'exists'})\n`);
150
159
  }
151
- process.stdout.write(`${report.laid.length} laid, ${report.skipped.length} unchanged.\n`);
160
+ for (const f of report.merged || []) {
161
+ process.stdout.write(` merged [${f.class}] ${f.path} (recon-wrxn server added to your existing config)\n`);
162
+ }
163
+ process.stdout.write(`${report.laid.length} laid, ${report.skipped.length} unchanged${report.merged && report.merged.length ? `, ${report.merged.length} merged` : ''}.\n`);
152
164
  if (report.brownfield) {
153
165
  process.stdout.write(`brownfield install — ${report.collisions.length} existing file(s) preserved (never overwritten): ${report.collisions.map((c) => c.path).join(', ')}\n`);
154
166
  }
167
+ if (report.adoptHint) {
168
+ process.stdout.write(`${report.adoptHint}\n`);
169
+ }
170
+ if (report.statuslineHint) {
171
+ process.stdout.write(`${report.statuslineHint}\n`);
172
+ }
155
173
  return 0;
156
174
  }
157
175
 
@@ -335,6 +353,46 @@ function main(argv) {
335
353
  }
336
354
  }
337
355
 
356
+ if (cmd === 'statusline') {
357
+ const home = process.env.HOME || os.homedir();
358
+ const detection = statusline.detectStatusLine(home);
359
+
360
+ // --inject: append the sidecar block to the resolved (or --path) statusline script, idempotently.
361
+ if (args.flags.inject) {
362
+ const target = args.flags.path || detection.scriptPath;
363
+ if (!target) {
364
+ process.stderr.write('wrxn: no statusline script to inject into — pass --path <script>, or configure statusLine in ~/.claude/settings.json first\n');
365
+ return 2;
366
+ }
367
+ try {
368
+ const r = statusline.injectSnippet(target);
369
+ process.stdout.write(r.injected
370
+ ? `wrxn sidecar appended to ${r.path}\n`
371
+ : `wrxn sidecar already present in ${r.path} — no change\n`);
372
+ return 0;
373
+ } catch (err) {
374
+ process.stderr.write(`wrxn: ${err.message}\n`);
375
+ return 2;
376
+ }
377
+ }
378
+
379
+ // Default: report detection + print the snippet + how to enable.
380
+ if (detection.configured) {
381
+ process.stdout.write(`statusline detected: ${detection.command}\n`);
382
+ process.stdout.write(detection.scriptPath
383
+ ? ` script: ${detection.scriptPath}\n`
384
+ : ' (not a bash <path> command — cannot auto-resolve a script to inject into)\n');
385
+ } else {
386
+ process.stdout.write('no statusline configured in ~/.claude/settings.json\n');
387
+ }
388
+ process.stdout.write('\nThe SYNAPSE live-window block (host statusline must read stdin into $input and set $session_id):\n\n');
389
+ process.stdout.write(statusline.snippet() + '\n');
390
+ process.stdout.write(detection.scriptPath
391
+ ? `Enable: wrxn statusline --inject (appends idempotently to ${detection.scriptPath})\n`
392
+ : 'Enable: wrxn statusline --inject --path <your-statusline-script>\n');
393
+ return 0;
394
+ }
395
+
338
396
  process.stderr.write(`wrxn: unknown command "${cmd}"\n\n${USAGE}\n`);
339
397
  return 2;
340
398
  }
package/lib/install.cjs CHANGED
@@ -4,8 +4,10 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
 
6
6
  const { loadManifest, inProfile } = require('./manifest.cjs');
7
+ const { STATUSLINE_HINT } = require('./statusline.cjs');
7
8
 
8
9
  const RECEIPT = 'wrxn.install.json';
10
+ const MCP_PATH = '.mcp.json';
9
11
 
10
12
  /**
11
13
  * Lay the kernel payload into a target root, governed by the file-class manifest.
@@ -33,8 +35,13 @@ function init(opts) {
33
35
 
34
36
  const laid = [];
35
37
  const skipped = [];
38
+ const merged = [];
36
39
 
37
40
  const version = packageVersion(pkgRoot);
41
+ // Did the target hold project content BEFORE we laid anything? Drives the adopt-hint: a non-empty
42
+ // repo has existing code worth priming the recon-wrxn index over now (.git and our own receipt
43
+ // don't count). Captured up front so a re-init's own laid files don't read as "pre-existing".
44
+ const wasNonEmpty = dirHasContent(target);
38
45
  // What a PRIOR wrxn install laid here — used to tell a re-init skip (wrxn's own file) apart from a
39
46
  // BROWNFIELD collision (a pre-existing PROJECT file that happens to clash with a payload path).
40
47
  const priorPaths = priorReceiptPaths(target);
@@ -55,6 +62,14 @@ function init(opts) {
55
62
  // A collision = the file existed but was NOT laid by a prior wrxn install (it is the operator's
56
63
  // own pre-existing project file). A re-init skip of wrxn's own file is `exists`, not a collision.
57
64
  const collision = !priorPaths.has(entry.path);
65
+ // .mcp.json is the one payload file we MERGE rather than preserve on a brownfield collision: the
66
+ // operator's other MCP servers must survive while gaining the recon-wrxn server. A re-init
67
+ // (not a collision) is left as a plain `exists` skip. A malformed operator file is preserved
68
+ // untouched (we never clobber or crash) and reported as an ordinary collision.
69
+ if (entry.path === MCP_PATH && collision && mergeMcpServer(src, dest)) {
70
+ merged.push({ path: entry.path, class: entry.class });
71
+ continue;
72
+ }
58
73
  skipped.push({ path: entry.path, class: entry.class, reason: collision ? 'collision' : 'exists', collision });
59
74
  continue;
60
75
  }
@@ -67,9 +82,65 @@ function init(opts) {
67
82
  const collisions = skipped.filter((s) => s.collision);
68
83
  const brownfield = collisions.length > 0;
69
84
 
70
- writeReceipt(target, { version, profile, laid, skipped, brownfield });
85
+ // recon-wrxn writes its index into a fixed `.recon-wrxn/` dir — keep it out of version control.
86
+ ensureGitignoreLine(target, '.recon-wrxn/');
71
87
 
72
- return { profile, laid, skipped, collisions, brownfield, receipt: path.join(target, RECEIPT) };
88
+ writeReceipt(target, { version, profile, laid, skipped, merged, brownfield });
89
+
90
+ // No synchronous index here (AC-5): `recon-wrxn serve` auto-indexes lazily on first use. On a
91
+ // non-empty repo we only HINT that the operator can prime it now with `recon-wrxn index`.
92
+ const adoptHint = wasNonEmpty
93
+ ? 'recon-wrxn indexes lazily on first use — run `recon-wrxn index` to prime it over your existing code now.'
94
+ : null;
95
+
96
+ // init NEVER touches a statusline (that would risk overwriting the operator's). It only surfaces
97
+ // the opt-in path so installs discover the SYNAPSE live-window writer.
98
+ return { profile, laid, skipped, merged, collisions, brownfield, adoptHint, statuslineHint: STATUSLINE_HINT, receipt: path.join(target, RECEIPT) };
99
+ }
100
+
101
+ /** Does the target hold content other than `.git` and wrxn's own receipt? (Missing dir → empty.) */
102
+ function dirHasContent(target) {
103
+ let entries;
104
+ try {
105
+ entries = fs.readdirSync(target);
106
+ } catch {
107
+ return false;
108
+ }
109
+ return entries.some((e) => e !== '.git' && e !== RECEIPT);
110
+ }
111
+
112
+ /** Append `line` to `<target>/.gitignore` (create if absent), exactly once — idempotent. */
113
+ function ensureGitignoreLine(target, line) {
114
+ const giPath = path.join(target, '.gitignore');
115
+ let body = '';
116
+ try {
117
+ body = fs.readFileSync(giPath, 'utf8');
118
+ } catch {
119
+ body = '';
120
+ }
121
+ if (body.split('\n').some((l) => l.trim() === line)) return; // already ignored
122
+ const prefix = body.length && !body.endsWith('\n') ? '\n' : '';
123
+ fs.writeFileSync(giPath, body + prefix + line + '\n');
124
+ }
125
+
126
+ /**
127
+ * Merge the recon-wrxn server key from the payload `.mcp.json` into an operator's existing one.
128
+ * Returns true on a successful merge, false when the operator file is not parseable JSON (in which
129
+ * case the caller preserves it untouched — wrxn never clobbers or crashes on a hand-written config).
130
+ */
131
+ function mergeMcpServer(src, dest) {
132
+ let operator;
133
+ try {
134
+ operator = JSON.parse(fs.readFileSync(dest, 'utf8'));
135
+ } catch {
136
+ return false; // unparseable operator file → preserve as a plain collision, don't merge
137
+ }
138
+ if (!operator || typeof operator !== 'object') return false;
139
+ const payload = JSON.parse(fs.readFileSync(src, 'utf8'));
140
+ operator.mcpServers = operator.mcpServers || {};
141
+ operator.mcpServers['recon-wrxn'] = payload.mcpServers['recon-wrxn'];
142
+ fs.writeFileSync(dest, JSON.stringify(operator, null, 2) + '\n');
143
+ return true;
73
144
  }
74
145
 
75
146
  /** Paths a prior wrxn install recorded in the receipt (empty when there is no prior install). */
@@ -97,9 +168,9 @@ function writeReceipt(target, data) {
97
168
  existing.kernelVersion = data.version;
98
169
  existing.profile = data.profile;
99
170
  existing.brownfield = !!data.brownfield;
100
- existing.files = [...data.laid, ...data.skipped].map((f) => ({ path: f.path, class: f.class }));
101
- existing.installs.push({ laidCount: data.laid.length, skippedCount: data.skipped.length });
171
+ existing.files = [...data.laid, ...data.skipped, ...(data.merged || [])].map((f) => ({ path: f.path, class: f.class }));
172
+ existing.installs.push({ laidCount: data.laid.length, skippedCount: data.skipped.length, mergedCount: (data.merged || []).length });
102
173
  fs.writeFileSync(receiptPath, JSON.stringify(existing, null, 2) + '\n');
103
174
  }
104
175
 
105
- module.exports = { init, RECEIPT, packageVersion };
176
+ module.exports = { init, RECEIPT, MCP_PATH, packageVersion, mergeMcpServer };
@@ -0,0 +1,97 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ // The sidecar block is marker-bounded so injection is idempotent: the START marker's presence in a
7
+ // host statusline means the block is already there. The canonical text lives in the managed payload
8
+ // doc (single source of truth) — snippet() extracts the marker region from it, so the doc and the
9
+ // injected block can never drift.
10
+ const MARKER_START = '# >>> wrxn sidecar >>>';
11
+ const MARKER_END = '# <<< wrxn sidecar <<<';
12
+
13
+ // The one-line adopt-hint `wrxn init` surfaces. init must NEVER modify a statusline; it only points
14
+ // the operator at the opt-in command.
15
+ const STATUSLINE_HINT = 'SYNAPSE live-window: run `wrxn statusline` to enable';
16
+
17
+ // The canonical doc inside the package payload (ships in dev and in the installed package alike).
18
+ const DOC_PATH = path.join(__dirname, '..', 'payload', 'docs', 'statusline-sidecar.sh');
19
+
20
+ /** The marker-bounded sidecar block, extracted from the canonical payload doc. */
21
+ function snippet(docPath) {
22
+ const text = fs.readFileSync(docPath || DOC_PATH, 'utf8');
23
+ const start = text.indexOf(MARKER_START);
24
+ const end = text.indexOf(MARKER_END);
25
+ if (start < 0 || end < 0 || end < start) {
26
+ throw new Error('statusline doc is missing its sidecar markers');
27
+ }
28
+ return text.slice(start, end + MARKER_END.length) + '\n';
29
+ }
30
+
31
+ /**
32
+ * Resolve the statusline script path from a settings `statusLine.command`.
33
+ * Returns a path for a bare `<path>` or a `bash <path>` / `sh <path>` command; null for any other
34
+ * shape (an inline command, a node script, a multi-token bare command) — we only auto-inject into a
35
+ * shell script we can unambiguously identify.
36
+ */
37
+ function resolveScriptPath(command, home) {
38
+ const parts = command.split(/\s+/).filter(Boolean);
39
+ let candidate = null;
40
+ if ((parts[0] === 'bash' || parts[0] === 'sh') && parts.length === 2) {
41
+ candidate = parts[1];
42
+ } else if (parts.length === 1) {
43
+ candidate = parts[0];
44
+ } else {
45
+ return null;
46
+ }
47
+ if (candidate.startsWith('~/')) candidate = path.join(home, candidate.slice(2));
48
+ return candidate;
49
+ }
50
+
51
+ /**
52
+ * Inspect `<home>/.claude/settings.json` for a configured statusline.
53
+ * @returns {{ configured: boolean, command: string|null, scriptPath: string|null }}
54
+ */
55
+ function detectStatusLine(home) {
56
+ let settings;
57
+ try {
58
+ settings = JSON.parse(fs.readFileSync(path.join(home, '.claude', 'settings.json'), 'utf8'));
59
+ } catch {
60
+ return { configured: false, command: null, scriptPath: null };
61
+ }
62
+ const sl = settings && settings.statusLine;
63
+ const command = sl && typeof sl.command === 'string' && sl.command.trim() ? sl.command.trim() : null;
64
+ if (!command) return { configured: false, command: null, scriptPath: null };
65
+ return { configured: true, command, scriptPath: resolveScriptPath(command, home) };
66
+ }
67
+
68
+ /**
69
+ * Append the sidecar block to an existing statusline script — APPEND-ONLY and idempotent.
70
+ * No-op if the marker is already present; never rewrites or reorders existing content. The script
71
+ * must already exist (we never conjure a statusline — that would risk shadowing the operator's own).
72
+ * @returns {{ injected: boolean, reason?: string, path: string }}
73
+ */
74
+ function injectSnippet(scriptPath, docPath) {
75
+ if (!scriptPath) throw new Error('injectSnippet requires a script path');
76
+ if (!fs.existsSync(scriptPath)) {
77
+ throw new Error(`statusline script not found: ${scriptPath} — create it (or configure a statusline) first`);
78
+ }
79
+ const current = fs.readFileSync(scriptPath, 'utf8');
80
+ if (current.includes(MARKER_START)) {
81
+ return { injected: false, reason: 'already-present', path: scriptPath };
82
+ }
83
+ const sep = current.length && !current.endsWith('\n') ? '\n' : '';
84
+ fs.appendFileSync(scriptPath, `${sep}\n${snippet(docPath)}`);
85
+ return { injected: true, path: scriptPath };
86
+ }
87
+
88
+ module.exports = {
89
+ snippet,
90
+ detectStatusLine,
91
+ injectSnippet,
92
+ resolveScriptPath,
93
+ MARKER_START,
94
+ MARKER_END,
95
+ STATUSLINE_HINT,
96
+ DOC_PATH,
97
+ };
package/lib/update.cjs CHANGED
@@ -4,7 +4,7 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
 
6
6
  const { loadManifest, inProfile } = require('./manifest.cjs');
7
- const { RECEIPT, packageVersion } = require('./install.cjs');
7
+ const { RECEIPT, MCP_PATH, packageVersion, mergeMcpServer } = require('./install.cjs');
8
8
  const { compareVersions } = require('./semver.cjs');
9
9
  const { runMigrations } = require('./migrate.cjs');
10
10
 
@@ -49,8 +49,20 @@ function update(opts) {
49
49
  }
50
50
 
51
51
  if (entry.class === 'managed') {
52
- lay(src, dest);
53
- updated.push({ path: entry.path, class: entry.class });
52
+ // .mcp.json is managed but operator-shared: an update MUST NOT clobber the operator's other MCP
53
+ // servers (finding N2). When the dest already exists, MERGE just the recon-wrxn key in (same
54
+ // contract as init). A malformed operator file can't be merged → preserve it untouched (never
55
+ // crash, never clobber a hand-written config); an absent dest falls through to a plain lay.
56
+ if (entry.path === MCP_PATH && fs.existsSync(dest)) {
57
+ if (mergeMcpServer(src, dest)) {
58
+ updated.push({ path: entry.path, class: entry.class, merged: true });
59
+ } else {
60
+ preserved.push({ path: entry.path, class: entry.class, reason: 'unparseable-preserved' });
61
+ }
62
+ } else {
63
+ lay(src, dest);
64
+ updated.push({ path: entry.path, class: entry.class });
65
+ }
54
66
  } else if (!fs.existsSync(dest)) {
55
67
  // seeded/state that did not exist in the prior version → lay it once now
56
68
  lay(src, dest);
package/manifest.json CHANGED
@@ -359,7 +359,12 @@
359
359
  "profile": "workspace"
360
360
  },
361
361
  {
362
- "path": ".recon.json",
362
+ "path": ".mcp.json",
363
+ "class": "managed",
364
+ "profile": "project"
365
+ },
366
+ {
367
+ "path": ".recon-wrxn.json",
363
368
  "class": "seeded",
364
369
  "profile": "project"
365
370
  },
@@ -438,6 +443,11 @@
438
443
  "class": "seeded",
439
444
  "profile": "project"
440
445
  },
446
+ {
447
+ "path": "docs/statusline-sidecar.sh",
448
+ "class": "managed",
449
+ "profile": "project"
450
+ },
441
451
  {
442
452
  "path": "docs/workspace/operator-layer.md",
443
453
  "class": "seeded",
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * R4 — rebrand an existing install from the legacy `recon` to `recon-wrxn`, in place, preserving the
8
+ * costly index (recon-wrxn-04). The ENTIRE migration is gated on the legacy `.recon/` index dir: a
9
+ * fresh (or already-migrated) install has no `.recon/`, so this is a complete no-op — which also makes
10
+ * it idempotent (a second run sees `.recon/` already renamed away). Runs via `wrxn update` once the
11
+ * install reaches 0.2.0; the file-class update has already laid the rebranded payload before this runs.
12
+ */
13
+ module.exports = {
14
+ id: '001',
15
+ version: '0.2.0',
16
+ up(ctx) {
17
+ const target = ctx.target;
18
+ const legacyDir = path.join(target, '.recon');
19
+ if (!fs.existsSync(legacyDir)) return; // gate: no legacy index → nothing to migrate
20
+
21
+ // 1. Rename the index dir, preserving the index (storage format unchanged per R1). If the new dir
22
+ // already exists (operator ran recon-wrxn before updating), it is authoritative — discard the
23
+ // legacy `.recon/` rather than leave it behind: it is a disposable cache that would otherwise
24
+ // linger no-longer-gitignored, and this run-once migration would never reclaim it.
25
+ const newDir = path.join(target, '.recon-wrxn');
26
+ if (!fs.existsSync(newDir)) {
27
+ fs.renameSync(legacyDir, newDir);
28
+ } else {
29
+ fs.rmSync(legacyDir, { recursive: true, force: true });
30
+ }
31
+
32
+ // 2. Rename the config (content unchanged — the operator's recon config carries over). The seeded
33
+ // `.recon-wrxn.json` template laid by `wrxn update` is overwritten so the operator's content wins.
34
+ const legacyCfg = path.join(target, '.recon.json');
35
+ if (fs.existsSync(legacyCfg)) {
36
+ fs.renameSync(legacyCfg, path.join(target, '.recon-wrxn.json'));
37
+ }
38
+
39
+ // 3. Drop the stale `recon` server key from .mcp.json (the `recon-wrxn` server was already merged in
40
+ // by `wrxn update`). Only touch a file that exists and parses; never crash on a malformed config,
41
+ // never remove any other server key.
42
+ const mcpPath = path.join(target, '.mcp.json');
43
+ if (fs.existsSync(mcpPath)) {
44
+ let mcp = null;
45
+ try {
46
+ mcp = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
47
+ } catch {
48
+ mcp = null; // malformed operator config → leave it alone
49
+ }
50
+ if (mcp && mcp.mcpServers && Object.prototype.hasOwnProperty.call(mcp.mcpServers, 'recon')) {
51
+ delete mcp.mcpServers.recon;
52
+ fs.writeFileSync(mcpPath, JSON.stringify(mcp, null, 2) + '\n');
53
+ }
54
+ }
55
+
56
+ // 4. .gitignore: replace the stale `.recon/` line(s) with a single `.recon-wrxn/`; drop any extra
57
+ // stale lines and never emit a duplicate (whether `.recon-wrxn/` pre-existed or `.recon/` repeats).
58
+ const giPath = path.join(target, '.gitignore');
59
+ if (fs.existsSync(giPath)) {
60
+ const lines = fs.readFileSync(giPath, 'utf8').split('\n');
61
+ let haveNew = lines.some((l) => l.trim() === '.recon-wrxn/');
62
+ const out = [];
63
+ for (const l of lines) {
64
+ if (l.trim() === '.recon/') {
65
+ if (!haveNew) { out.push('.recon-wrxn/'); haveNew = true; }
66
+ } else {
67
+ out.push(l);
68
+ }
69
+ }
70
+ fs.writeFileSync(giPath, out.join('\n'));
71
+ }
72
+
73
+ // 5. Remove the vendored legacy recon (superseded by the npm dependency; only WRXN-OS has it).
74
+ const vendor = path.join(target, 'vendor', 'recon-aiox');
75
+ if (fs.existsSync(vendor)) {
76
+ fs.rmSync(vendor, { recursive: true, force: true });
77
+ }
78
+ },
79
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gcunharodrigues/wrxn",
3
- "version": "0.1.0",
3
+ "version": "0.2.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"
@@ -15,6 +15,9 @@
15
15
  "scripts": {
16
16
  "test": "node --test"
17
17
  },
18
+ "dependencies": {
19
+ "recon-wrxn": "6.0.0-wrxn.1"
20
+ },
18
21
  "engines": {
19
22
  "node": ">=20"
20
23
  },
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- // WRXN code-intel-push hook — first-touch code-intel / recon-freshness nudge (wrxn-kernel-11).
4
+ // WRXN code-intel-push hook — first-touch code-intel / recon-wrxn-freshness nudge (wrxn-kernel-11).
5
5
  // PostToolUse (Edit|Write). On the FIRST touch of a code file this session it injects a <code-intel>
6
- // nudge: where a recon graph exists it notes freshness (commit lag) and points at recon; absent a
7
- // graph it nudges to build one. First-touch-GATED per session+file (a touched-list under
6
+ // nudge: where a recon-wrxn index exists it points at the mcp__recon-wrxn__* tools; absent an index
7
+ // it nudges to prime one. First-touch-GATED per session+file (a touched-list under
8
8
  // .wrxn/history/<sid>.touched) so a repeat edit of the same file is silent — no per-edit spam.
9
9
  //
10
10
  // Self-contained: ships into installs, MUST NOT import the kernel lib (node stdlib only).
@@ -60,13 +60,14 @@ function isFirstTouch(root, sid, relPath) {
60
60
  }
61
61
  }
62
62
 
63
- // A short freshness note: stale (or unknown) when the recon graph's commit doesn't prefix-match HEAD.
63
+ // A short freshness note pointing at the recon-wrxn MCP server (mcp__recon-wrxn__* tools), whose
64
+ // index lives in the fixed `.recon-wrxn/` dir and auto-builds lazily on first query.
64
65
  function freshnessNote(root) {
65
- const graph = path.join(root, '.recon', 'graph.json');
66
- if (!fs.existsSync(graph)) {
67
- return 'No recon graph (.recon/graph.json absent) — build it for code-connection enrichment, then reindex.';
66
+ const indexDir = path.join(root, '.recon-wrxn');
67
+ if (!fs.existsSync(indexDir)) {
68
+ return 'No recon-wrxn index (.recon-wrxn/ absent) — query via the mcp__recon-wrxn__* tools to auto-index, or run `recon-wrxn index` to prime it now.';
68
69
  }
69
- return 'recon graph present — reindex if it lags your edits (the graph is rebuilt at session close).';
70
+ return 'recon-wrxn index present (.recon-wrxn/) query code connections via the mcp__recon-wrxn__* tools; it re-indexes live as you edit.';
70
71
  }
71
72
 
72
73
  function main() {
@@ -179,26 +179,49 @@ function readResidentTokens(transcriptPath) {
179
179
  }
180
180
  }
181
181
 
182
- // Model context window, resolved by an explicit precedence (issue 29). On [1m] sessions
183
- // lastModelUsage is often EMPTY and the transcript model id lacks the [1m] tag there is no
184
- // reliable auto-signal — so the window must be settable explicitly rather than guessed:
185
- // 1. env WRXN_CONTEXT_WINDOW a positive finite number wins unconditionally.
186
- // 2. manifest CONTEXT_WINDOW a positive finite value (when manifestText is supplied).
187
- // 3. ~/.claude.json lastModelUsage KEYS — a [1m] tag ⇒ 1,000,000 (auto-detect, when present).
188
- // 4. fallback 200,000.
189
- // homeDir/manifestText overrides keep it testable.
190
- function modelWindow(cwd, homeDir, manifestText) {
182
+ // The LIVE window for a session, published by the statusline. UserPromptSubmit hooks receive no
183
+ // model/context-window data, but the statusline payload carries context_window.context_window_size
184
+ // (resolved by Claude Code, refreshed every render — so it tracks a mid-session /model switch). The
185
+ // statusline writes it to a session-scoped /tmp sidecar; we read it back here by session_id.
186
+ // See statusline.sh and memory `handoff-window-defaults-200k`. Returns a positive number or null.
187
+ function readStatuslineWindow(sessionId) {
188
+ if (!sessionId) return null;
189
+ try {
190
+ const p = path.join(os.tmpdir(), `claude-statusline-ctx-${sessionId}.json`);
191
+ const o = JSON.parse(fs.readFileSync(p, 'utf8'));
192
+ const w = Number(o && o.context_window_size);
193
+ return Number.isFinite(w) && w > 0 ? w : null;
194
+ } catch {
195
+ return null;
196
+ }
197
+ }
198
+
199
+ // Model context window, resolved by an explicit precedence (issue 29 + dynamic statusline bridge).
200
+ // On [1m] sessions lastModelUsage is often EMPTY and the transcript model id lacks the [1m] tag, and
201
+ // the hook payload carries no model — so we lean on the statusline (which DOES know the live window):
202
+ // 1. env WRXN_CONTEXT_WINDOW — a positive finite number wins unconditionally (manual force).
203
+ // 2. statusline sidecar — the live per-session window; tracks mid-session model switches.
204
+ // 3. manifest CONTEXT_WINDOW — a positive finite value (when manifestText is supplied).
205
+ // 4. ~/.claude.json lastModelUsage KEYS — a [1m] tag ⇒ 1,000,000 (auto-detect, when present).
206
+ // 5. self-correcting net — resident already past the 200k default ⇒ window is necessarily larger.
207
+ // 6. fallback 200,000.
208
+ // homeDir/manifestText/sessionId/resident overrides keep it testable.
209
+ function modelWindow(cwd, homeDir, manifestText, sessionId, resident) {
191
210
  // 1. explicit env override.
192
211
  const envWin = Number(process.env.WRXN_CONTEXT_WINDOW);
193
212
  if (Number.isFinite(envWin) && envWin > 0) return envWin;
194
213
 
195
- // 2. manifest CONTEXT_WINDOW (the engine already reads scalar manifest values).
214
+ // 2. statusline sidecar the live, authoritative window (dynamic across /model switches).
215
+ const scWin = readStatuslineWindow(sessionId);
216
+ if (scWin) return scWin;
217
+
218
+ // 3. manifest CONTEXT_WINDOW (the engine already reads scalar manifest values).
196
219
  if (manifestText != null) {
197
220
  const manWin = Number(manifestValue(manifestText, 'CONTEXT_WINDOW'));
198
221
  if (Number.isFinite(manWin) && manWin > 0) return manWin;
199
222
  }
200
223
 
201
- // 3. lastModelUsage [1m] auto-detect.
224
+ // 4. lastModelUsage [1m] auto-detect.
202
225
  try {
203
226
  const home = homeDir || process.env.HOME || os.homedir();
204
227
  const cfg = JSON.parse(fs.readFileSync(path.join(home, '.claude.json'), 'utf8'));
@@ -206,10 +229,13 @@ function modelWindow(cwd, homeDir, manifestText) {
206
229
  const keys = Object.keys(proj.lastModelUsage || {});
207
230
  if (keys.some((k) => /\[1m\]/i.test(k))) return 1000000;
208
231
  } catch {
209
- // fall through to the default.
232
+ // fall through.
210
233
  }
211
234
 
212
- // 4. fallback.
235
+ // 5. self-correcting net: resident past the 200k default ⇒ a larger (1M) window.
236
+ if (Number.isFinite(resident) && resident > 200000) return 1000000;
237
+
238
+ // 6. fallback.
213
239
  return 200000;
214
240
  }
215
241
 
@@ -295,7 +321,7 @@ function compose(root, event) {
295
321
  if (ev.transcript_path) {
296
322
  const resident = readResidentTokens(ev.transcript_path);
297
323
  if (resident != null) {
298
- const window = modelWindow(ev.cwd || root, process.env.HOME || os.homedir(), manifestText);
324
+ const window = modelWindow(ev.cwd || root, process.env.HOME || os.homedir(), manifestText, ev.session_id, resident);
299
325
  const consumed = resident / window;
300
326
  const pct = resolveHandoffPct(manifestText);
301
327
  if (consumed >= pct) out.push(handoffDirective(consumed, pct));
@@ -345,6 +371,7 @@ module.exports = {
345
371
  compose,
346
372
  findInstallRoot,
347
373
  readResidentTokens,
374
+ readStatuslineWindow,
348
375
  modelWindow,
349
376
  resolveHandoffPct,
350
377
  handoffDirective,
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "recon-wrxn": {
4
+ "command": "npx",
5
+ "args": ["-y", "recon-wrxn@6.0.0-wrxn.1", "serve"]
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "projects": [],
3
+ "embeddings": false,
4
+ "watch": true,
5
+ "ignore": []
6
+ }
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env bash
2
+ # WRXN SYNAPSE statusline sidecar — the live context-window writer.
3
+ #
4
+ # Why: UserPromptSubmit hooks (where SYNAPSE runs) receive no model/context-window data, so the
5
+ # handoff math cannot tell a 200k session from a 1M one. The statusline IS handed the live window
6
+ # (context_window.context_window_size) on stdin every render — so we publish it to a session-scoped
7
+ # temp file that the synapse-engine hook reads back (readStatuslineWindow → .context_window_size).
8
+ # The temp dir MUST match the reader's os.tmpdir() — both honor $TMPDIR, falling back to /tmp.
9
+ #
10
+ # How to enable: paste the marker-bounded block below into your Claude Code statusline script, OR run
11
+ # wrxn statusline --inject [--path <your-statusline-script>]
12
+ # which appends it idempotently. It NEVER overwrites your statusline — append-only, marker-guarded.
13
+ #
14
+ # Assumptions the block makes about the host statusline:
15
+ # - $input the raw statusline stdin JSON (most statuslines do `input=$(cat)` at the top).
16
+ # - $session_id the session id (e.g. `session_id=$(echo "$input" | jq -r '.session_id')`).
17
+ # If your statusline lacks these, set them above the block. The block fails safe regardless: the write
18
+ # is guarded by `2>/dev/null || true`, so a missing tool or var can never break your statusline render.
19
+
20
+ # >>> wrxn sidecar >>>
21
+ # UserPromptSubmit hooks receive NO model/context-window data, so SYNAPSE's handoff math can't tell
22
+ # a 200k session from a 1M one. Publish the live window to a session-scoped file the hook reads.
23
+ # Refreshed every render → tracks a mid-session /model switch.
24
+ if [[ -n "$session_id" ]]; then
25
+ cw_size=$(echo "$input" | jq -r '.context_window.context_window_size // empty')
26
+ [[ -z "$cw_size" || "$cw_size" == "null" ]] && { [[ "$(echo "$input" | jq -r '.model.id // ""')" == *"[1m]"* ]] && cw_size=1000000 || cw_size=200000; }
27
+ printf '{"context_window_size":%s,"model_id":"%s"}\n' "$cw_size" "$(echo "$input" | jq -r '.model.id // ""')" \
28
+ > "${TMPDIR:-/tmp}/claude-statusline-ctx-${session_id}.json" 2>/dev/null || true
29
+ fi
30
+ # <<< wrxn sidecar <<<
@@ -1,3 +0,0 @@
1
- {
2
- "ignore": ["node_modules", ".git", ".wrxn/wiki", "vendor"]
3
- }