@gcunharodrigues/wrxn 0.1.0 → 0.2.1

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/executor.cjs CHANGED
@@ -80,7 +80,7 @@ const EXECUTORS = {
80
80
  instructions: [
81
81
  'You are the devops integration executor — the ONLY executor authorized to push. Integrate the',
82
82
  'reviewed + security-passed + qa-walked track to the trunk: verify the review marker (review-<id>.md)',
83
- '+ a green suite exist, THEN push (AIOX_ACTIVE_AGENT=devops). This is the single path through the push gate.',
83
+ '+ a green suite exist, then authorize the push by setting WRXN_ACTIVE_AGENT to devops under the `env` key of .claude/settings.local.json (an inline command-scoped assignment never reaches the gate hook), push, then REMOVE WRXN_ACTIVE_AGENT from .claude/settings.local.json — a persistent flag defeats the anti-accidental-push gate. This is the single path through the push gate.',
84
84
  ],
85
85
  artifact: 'authorized-push',
86
86
  canPush: true,
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
+ };
@@ -0,0 +1,100 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * 002 — seeded-file honesty migration (foundation-honesty-06).
8
+ *
9
+ * Managed payload reaches existing installs on `wrxn update`, but SEEDED files are never overwritten
10
+ * (operator-owned) — so two artifacts seeded before 0.2.1 keep their stale wording forever unless a
11
+ * migration corrects them:
12
+ * - .synapse/routing's ROUTING_RULE_0 still asserts a fictional "devops role" authority.
13
+ * - docs/agents/domain.md still points at the deleted CONTEXT-MAP.md context.
14
+ * Each file is rewritten in place ONLY while it still carries its known-stale marker, so an operator
15
+ * who already customized it (or whose install is already honest) is never clobbered. The two branches
16
+ * are independent and neither ever crashes on a missing file.
17
+ *
18
+ * The honest content is EMBEDDED below as frozen 0.2.1 constants: a migration is a historical
19
+ * transform of the 0.2.1 release, not a re-read of the evolving template (ctx carries no pkgRoot, by
20
+ * design). Idempotency falls out of the gate — after the rewrite the stale markers ("devops role",
21
+ * "CONTEXT-MAP.md") are gone, so a second run is a no-op. Runs via `wrxn update` once the install
22
+ * reaches 0.2.1.
23
+ */
24
+
25
+ // The honest ROUTING_RULE_0 — mirrors the seeded `.synapse/routing` template (issue 04 confirmation-
26
+ // gate wording, minus the constitution citation the managed `global` GLOBAL_RULE_0 carries).
27
+ const HONEST_ROUTING_RULE_0 =
28
+ 'ROUTING_RULE_0=git push, PR creation, and release tags are deliberate acts held behind a confirmation flag (anti-accidental-push) — they run only once the session sets WRXN_ACTIVE_AGENT=devops in .claude/settings.local.json; `devops` is a dispatch-phase label, not an authority.';
29
+
30
+ // The honest domain glossary — frozen verbatim from the post-issue-05 payload docs/agents/domain.md.
31
+ const HONEST_DOMAIN_MD = `# Domain Docs
32
+
33
+ How the engineering skills should consume this repo's domain documentation when exploring the codebase.
34
+
35
+ ## Before exploring, read these
36
+
37
+ - **\`CONTEXT.md\`** at the repo root — the domain glossary, the canonical vocabulary for this project.
38
+ - **\`docs/adr/\`** — Architecture Decision Records. Read the ADRs that touch the area you're about to work in.
39
+
40
+ If either doesn't exist yet, **proceed silently**. Don't flag its absence; don't suggest creating it upfront. The producer skill (\`grill-with-docs\`) creates them lazily — \`CONTEXT.md\` when the first term is resolved, an ADR when a hard-to-reverse decision is actually made.
41
+
42
+ ## File structure
43
+
44
+ A fresh install ships neither file. They appear at the repo root as the project's language and decisions accumulate:
45
+
46
+ \`\`\`
47
+ /
48
+ ├── CONTEXT.md ← domain glossary (created lazily by grill-with-docs)
49
+ └── docs/
50
+ └── adr/ ← one file per decision, named NNNN-<slug>.md
51
+ \`\`\`
52
+
53
+ ## Use the glossary's vocabulary
54
+
55
+ When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in \`CONTEXT.md\`. Don't drift to synonyms the glossary explicitly avoids.
56
+
57
+ If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for \`grill-with-docs\`).
58
+
59
+ ## Flag ADR conflicts
60
+
61
+ If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
62
+
63
+ > _Contradicts ADR-0007 — but worth reopening because…_
64
+ `;
65
+
66
+ module.exports = {
67
+ id: '002',
68
+ version: '0.2.1',
69
+ up(ctx) {
70
+ const target = ctx.target;
71
+
72
+ // 1. routing: replace ONLY a ROUTING_RULE_0 line still carrying the stale "devops role" authority
73
+ // wording. Comments and any operator-added ROUTING_RULE_N lines are preserved verbatim; the
74
+ // split/join round-trip keeps the trailing newline. No stale line → routing left untouched.
75
+ const routingPath = path.join(target, '.synapse', 'routing');
76
+ if (fs.existsSync(routingPath)) {
77
+ const lines = fs.readFileSync(routingPath, 'utf8').split('\n');
78
+ let changed = false;
79
+ const out = lines.map((line) => {
80
+ if (line.startsWith('ROUTING_RULE_0=') && line.includes('devops role')) {
81
+ changed = true;
82
+ return HONEST_ROUTING_RULE_0;
83
+ }
84
+ return line;
85
+ });
86
+ if (changed) fs.writeFileSync(routingPath, out.join('\n'));
87
+ }
88
+
89
+ // 2. domain.md: overwrite the whole glossary with the honest content ONLY while it still names the
90
+ // deleted CONTEXT-MAP.md context. An honest or operator-customized file (marker absent) is left
91
+ // untouched. Missing file → nothing to do.
92
+ const domainPath = path.join(target, 'docs', 'agents', 'domain.md');
93
+ if (fs.existsSync(domainPath)) {
94
+ const body = fs.readFileSync(domainPath, 'utf8');
95
+ if (body.includes('CONTEXT-MAP.md')) {
96
+ fs.writeFileSync(domainPath, HONEST_DOMAIN_MD);
97
+ }
98
+ }
99
+ },
100
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gcunharodrigues/wrxn",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
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
  },
@@ -5,7 +5,10 @@ Project-local preferences live in the seeded `constitution.local.md` addendum, n
5
5
 
6
6
  ## Article I — Agent Authority (NON-NEGOTIABLE)
7
7
 
8
- - `git push`, PR creation, and release tags are EXCLUSIVE to the devops role.
8
+ - `git push`, PR creation, and release tags are deliberate acts, held behind a
9
+ confirmation flag to prevent an accidental push: the op proceeds only once the session
10
+ confirms intent by setting `WRXN_ACTIVE_AGENT=devops` in the machine-local
11
+ `.claude/settings.local.json`. `devops` here is a dispatch-phase label, not an authority grant.
9
12
  - An agent acts only within its scope; it delegates when out of scope and never assumes
10
13
  another agent's authority.
11
14
 
@@ -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() {