@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 +59 -1
- package/lib/executor.cjs +1 -1
- package/lib/install.cjs +76 -5
- package/lib/statusline.cjs +97 -0
- package/lib/update.cjs +15 -3
- package/manifest.json +11 -1
- package/migrations/001-recon-to-recon-wrxn.cjs +79 -0
- package/migrations/002-seeded-honesty.cjs +100 -0
- package/package.json +4 -1
- package/payload/.claude/constitution.md +4 -1
- package/payload/.claude/hooks/code-intel-push.cjs +9 -8
- package/payload/.claude/hooks/enforce-push-authority.cjs +6 -5
- package/payload/.claude/hooks/session-end.cjs +86 -46
- package/payload/.claude/hooks/synapse-engine.cjs +41 -14
- package/payload/.claude/skills/synapse/SKILL.md +94 -93
- package/payload/.claude/skills/synapse/assets/README.md +18 -34
- package/payload/.claude/skills/synapse/references/brackets.md +50 -76
- package/payload/.claude/skills/synapse/references/commands.md +43 -100
- package/payload/.claude/skills/synapse/references/domains.md +41 -105
- package/payload/.claude/skills/synapse/references/layers.md +74 -152
- package/payload/.claude/skills/synapse/references/manifest.md +58 -108
- package/payload/.mcp.json +8 -0
- package/payload/.recon-wrxn.json +6 -0
- package/payload/.synapse/global +1 -1
- package/payload/.synapse/routing +1 -1
- package/payload/docs/agents/domain.md +8 -13
- package/payload/docs/statusline-sidecar.sh +30 -0
- package/payload/.recon.json +0 -3
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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": ".
|
|
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
|
|
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
|
|
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
|
|
7
|
-
//
|
|
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
|
|
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
|
|
66
|
-
if (!fs.existsSync(
|
|
67
|
-
return 'No recon
|
|
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
|
|
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() {
|