@gcunharodrigues/wrxn 0.10.0 → 0.12.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.
Files changed (46) hide show
  1. package/bin/wrxn.cjs +155 -0
  2. package/lib/ci-checks.cjs +296 -0
  3. package/lib/executor.cjs +7 -4
  4. package/lib/install.cjs +3 -0
  5. package/lib/protect.cjs +195 -0
  6. package/lib/release.cjs +51 -0
  7. package/lib/ship.cjs +76 -0
  8. package/lib/update.cjs +15 -1
  9. package/manifest.json +17 -12
  10. package/migrations/005-protect-main-gate.cjs +32 -0
  11. package/migrations/006-refresh-routing-rule.cjs +56 -0
  12. package/migrations/007-auto-memory-transition.cjs +136 -0
  13. package/package.json +1 -1
  14. package/payload/.claude/agents/builder.md +2 -1
  15. package/payload/.claude/agents/devops.md +34 -29
  16. package/payload/.claude/agents/qa-walker.md +2 -1
  17. package/payload/.claude/agents/researcher.md +2 -1
  18. package/payload/.claude/agents/reviewer.md +2 -1
  19. package/payload/.claude/agents/security.md +2 -1
  20. package/payload/.claude/constitution.md +10 -6
  21. package/payload/.claude/hooks/enforce-managed-guard.cjs +14 -11
  22. package/payload/.claude/hooks/enforce-managed-precommit.cjs +11 -8
  23. package/payload/.claude/hooks/enforce-pipeline-adherence.cjs +116 -0
  24. package/payload/.claude/hooks/memory-synth-spawn.cjs +101 -0
  25. package/payload/.claude/hooks/session-start.cjs +86 -8
  26. package/payload/.claude/hooks/synapse-engine.cjs +2 -3
  27. package/payload/.claude/settings.json +14 -4
  28. package/payload/.claude/skills/compass/SKILL.md +6 -1
  29. package/payload/.claude/skills/dream/SKILL.md +2 -31
  30. package/payload/.claude/skills/synapse/SKILL.md +9 -9
  31. package/payload/.claude/skills/synapse/references/brackets.md +1 -2
  32. package/payload/.claude/skills/synapse/references/domains.md +2 -2
  33. package/payload/.claude/skills/synapse/references/layers.md +4 -5
  34. package/payload/.github/workflows/wrxn-ci.yml +54 -0
  35. package/payload/.synapse/global +2 -2
  36. package/payload/.synapse/manifest +4 -3
  37. package/payload/.synapse/pipeline +1 -0
  38. package/payload/.synapse/routing +1 -1
  39. package/payload/.wrxn/dream.cjs +112 -66
  40. package/payload/.wrxn/memory-synth.cjs +761 -0
  41. package/payload/.wrxn/memory.config.json +12 -0
  42. package/payload/.wrxn/wiki.cjs +7 -19
  43. package/payload/.claude/hooks/enforce-push-authority.cjs +0 -52
  44. package/payload/.claude/hooks/enforce-review-marker.cjs +0 -62
  45. package/payload/.claude/hooks/enforce-tests-on-push.cjs +0 -40
  46. package/payload/.claude/skills/handoff/SKILL.md +0 -22
@@ -0,0 +1,195 @@
1
+ 'use strict';
2
+
3
+ // WRXN protect — the server-side hard gate (gate-redesign gate-02).
4
+ //
5
+ // A client-side hook can never be hard enforcement (it gates one tool surface, not the repository).
6
+ // The only control that survives every bypass — human terminal, IDE, MCP, API, `--no-verify`, the
7
+ // 2026-06-19 settings.local.json disarm bug — is a SERVER-SIDE GitHub branch ruleset. This module
8
+ // builds and applies the `wrxn-main-gate` ruleset on a repo's `origin`: block direct push to the
9
+ // default branch, require a PR (0 approvals — a solo account auto-merges its own PR), require the
10
+ // `wrxn-ci` status check, require the branch up to date (race-safety), and NO bypass actor.
11
+ //
12
+ // Application is idempotent (create-or-update BY NAME — re-run = no-op) and fail-soft (no `gh`, not a
13
+ // repo admin, no remote, or any non-zero exit → a clear message and exit 0, never a throw, so it can
14
+ // never break `wrxn update` on a remote-less install). REUSE of lib/connect.cjs's injectable-invoker
15
+ // shape: an injected `invoker` makes unit tests deterministic; the real `spawnSync gh`/`git` runs only
16
+ // when no invoker is injected (the CLI layer) — that is what makes the application "validated by
17
+ // invocation". The ruleset is repo-agnostic via `~DEFAULT_BRANCH`, so the SAME spec protects any
18
+ // default-main repo (the kernel, every install, and the recon-wrxn sibling — gate-06).
19
+ //
20
+ // lib/protect.cjs is package code (invoked via bin/wrxn.cjs), NOT payload — no manifest entry,
21
+ // consistent with lib/connect.cjs / lib/ship.cjs / lib/executor.cjs / lib/onboard.cjs.
22
+
23
+ const { spawnSync } = require('child_process');
24
+
25
+ const RULESET_NAME = 'wrxn-main-gate';
26
+ const DEFAULT_CHECK = 'wrxn-ci';
27
+
28
+ /**
29
+ * Build the `wrxn-main-gate` GitHub repository-ruleset payload. PURE — a fresh, independent object
30
+ * each call, no side effects. The authoritative payload (gate-02 API contract): block direct push to
31
+ * the default branch, require a PR with 0 approvals, require `requiredCheck` strict (up-to-date), no
32
+ * bypass actor. `~DEFAULT_BRANCH` keeps it repo-agnostic (no hard-coded `main`).
33
+ * @param {{ requiredCheck?: string }} [opts]
34
+ * @returns {object} the ruleset payload for POST/PUT `/repos/{slug}/rulesets`
35
+ */
36
+ function buildRulesetSpec({ requiredCheck = DEFAULT_CHECK } = {}) {
37
+ return {
38
+ name: RULESET_NAME,
39
+ target: 'branch',
40
+ enforcement: 'active',
41
+ bypass_actors: [],
42
+ conditions: { ref_name: { include: ['~DEFAULT_BRANCH'], exclude: [] } },
43
+ rules: [
44
+ { type: 'deletion' },
45
+ { type: 'non_fast_forward' },
46
+ {
47
+ type: 'pull_request',
48
+ parameters: {
49
+ required_approving_review_count: 0,
50
+ dismiss_stale_reviews_on_push: false,
51
+ require_code_owner_review: false,
52
+ require_last_push_approval: false,
53
+ required_review_thread_resolution: false,
54
+ },
55
+ },
56
+ {
57
+ type: 'required_status_checks',
58
+ parameters: {
59
+ strict_required_status_checks_policy: true,
60
+ required_status_checks: [{ context: requiredCheck }],
61
+ },
62
+ },
63
+ ],
64
+ };
65
+ }
66
+
67
+ /**
68
+ * The real command invoker — a single spawnSync (mirrors lib/ship.cjs / lib/connect.cjs defaultInvoke).
69
+ * Captures stdout (needed to parse the ruleset list) and passes a body on stdin (the create/update).
70
+ * The CLI layer wires this implicitly (no injected invoker) — that is what makes the apply real.
71
+ * @returns {{ ok:boolean, status:number|null, stdout:string, stderr:string }}
72
+ */
73
+ function defaultInvoke({ cmd, args, input }) {
74
+ const r = spawnSync(cmd, args, { encoding: 'utf8', input: input || undefined });
75
+ // Judge a child by its EXIT STATUS, not by whether we finished writing its stdin. The real `gh api
76
+ // ... --input -` reads the body from stdin; if it (or a stub) exits before draining it, spawnSync's
77
+ // stdin write races the child's exit and surfaces an EPIPE in `r.error` — yet it still captures the
78
+ // real `status`/`stdout`. Only a spawn that NEVER ran (e.g. ENOENT — `status` stays null) is a true
79
+ // failure here; an EPIPE on a child that exited 0 is a success, not a "command not found" skip.
80
+ if (r.error && r.status == null) {
81
+ return { ok: false, status: null, stdout: '', stderr: r.error.code || r.error.message };
82
+ }
83
+ return { ok: r.status === 0, status: r.status, stdout: r.stdout || '', stderr: r.stderr || '' };
84
+ }
85
+
86
+ /** A fail-soft skip result — a logged-elsewhere reason, never a throw, so it never breaks `wrxn update`. */
87
+ function softSkip(reason) {
88
+ return { ok: false, action: 'skipped', reason };
89
+ }
90
+
91
+ /** One-line failure detail from an invoker result (status + first stderr line). */
92
+ function detailOf(r) {
93
+ const status = r.status == null ? 'no exit (command not found?)' : `exit ${r.status}`;
94
+ const err = String(r.stderr || '').trim().split('\n')[0];
95
+ return err ? `${status}: ${err}` : status;
96
+ }
97
+
98
+ /**
99
+ * Apply the `wrxn-main-gate` ruleset to `slug`'s repo via `gh api`. Idempotent (list → create-or-update
100
+ * BY NAME, so a re-run converges to a no-op) and fail-soft (no gh / not admin / no remote / any
101
+ * non-zero exit → a skipped result carrying a clear reason; NEVER throws). The invoker is injectable so
102
+ * unit tests are deterministic; the CLI layer uses the real `gh` spawn.
103
+ * @param {{ invoker?:Function, slug?:string, requiredCheck?:string }} [opts]
104
+ * @returns {{ ok:boolean, action:'created'|'updated'|'skipped', slug?:string, name?:string, detail?:string, reason?:string }}
105
+ */
106
+ function applyProtection({ invoker, slug, requiredCheck = DEFAULT_CHECK } = {}) {
107
+ const run = invoker || defaultInvoke;
108
+
109
+ if (!slug || typeof slug !== 'string' || slug.trim() === '') {
110
+ return softSkip('no origin remote — the wrxn-main-gate ruleset is not applied (a remote-less install is unprotected here; protection lands when it gains a GitHub origin)');
111
+ }
112
+
113
+ // 1. list existing rulesets — the idempotency lookup (find one named wrxn-main-gate).
114
+ const list = run({ cmd: 'gh', args: ['api', `/repos/${slug}/rulesets`] });
115
+ if (!list.ok) {
116
+ return softSkip(`could not list rulesets on ${slug} (${detailOf(list)}) — is gh installed, authenticated, and admin on the repo? skipping (exit 0)`);
117
+ }
118
+ let rulesets;
119
+ try {
120
+ rulesets = JSON.parse(list.stdout || '[]');
121
+ } catch {
122
+ return softSkip(`unexpected gh output listing rulesets on ${slug} — skipping`);
123
+ }
124
+ const existing = Array.isArray(rulesets) ? rulesets.find((r) => r && r.name === RULESET_NAME) : null;
125
+ const body = JSON.stringify(buildRulesetSpec({ requiredCheck }));
126
+
127
+ // 2. update in place (PUT to the existing id) if present, else create (POST) — re-run = no-op.
128
+ if (existing) {
129
+ const put = run({ cmd: 'gh', args: ['api', '--method', 'PUT', `/repos/${slug}/rulesets/${existing.id}`, '--input', '-'], input: body });
130
+ if (!put.ok) {
131
+ return softSkip(`could not update the ${RULESET_NAME} ruleset on ${slug} (${detailOf(put)}) — skipping`);
132
+ }
133
+ return { ok: true, action: 'updated', slug, name: RULESET_NAME, detail: `${RULESET_NAME} updated on ${slug}` };
134
+ }
135
+ const post = run({ cmd: 'gh', args: ['api', '--method', 'POST', `/repos/${slug}/rulesets`, '--input', '-'], input: body });
136
+ if (!post.ok) {
137
+ return softSkip(`could not create the ${RULESET_NAME} ruleset on ${slug} (${detailOf(post)}) — is the token a repo admin? skipping`);
138
+ }
139
+ return { ok: true, action: 'created', slug, name: RULESET_NAME, detail: `${RULESET_NAME} created on ${slug}` };
140
+ }
141
+
142
+ /** A well-formed GitHub `owner/repo` slug: owner starts alphanumeric, repo is alnum/`.`/`_`/`-`. */
143
+ const SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9._-]+$/;
144
+
145
+ /**
146
+ * Parse an `owner/repo` slug from a git remote URL (ssh `git@host:owner/repo.git`, https
147
+ * `https://host/owner/repo.git`, with or without the `.git` suffix / a trailing slash) or a bare
148
+ * `owner/repo`. The captured slug is then VALIDATED against the GitHub owner/repo grammar (gate-02
149
+ * LOW-1, defense-in-depth): `..` traversal, spaces, `;`, `$()`, backticks, and `--flag`-looking text
150
+ * are rejected → null, so a malformed remote fail-soft-skips instead of reaching `gh` as data. Returns
151
+ * null when no well-formed slug can be read — protection then fail-soft-skips.
152
+ */
153
+ function parseSlug(url) {
154
+ const s = String(url || '').trim();
155
+ if (!s) return null;
156
+ const m = s.match(/(?:^|[:/])([^/:]+\/[^/:]+?)(?:\.git)?\/?$/);
157
+ const slug = m ? m[1] : null;
158
+ return slug && SLUG_RE.test(slug) ? slug : null;
159
+ }
160
+
161
+ /**
162
+ * Derive the `owner/repo` slug of a repo's `origin` remote. Injectable git invoker for tests; the
163
+ * real `git` spawn runs at the CLI/update/migration layer. Returns null on no origin / parse failure
164
+ * (→ applyProtection soft-skips) — never throws.
165
+ */
166
+ function originSlug(root, { invoker } = {}) {
167
+ const run = invoker || defaultInvoke;
168
+ const r = run({ cmd: 'git', args: ['-C', root, 'remote', 'get-url', 'origin'] });
169
+ if (!r.ok) return null;
170
+ return parseSlug(r.stdout);
171
+ }
172
+
173
+ /**
174
+ * Derive the origin slug of the install at `root` and apply the ruleset to it. The single entry point
175
+ * the CLI (`wrxn protect`), `wrxn update`, and migration 005 share. Repo-agnostic (the slug is read
176
+ * from origin, so it works for the kernel, any install, and recon-wrxn) and fail-soft end-to-end (a
177
+ * remote-less root → applyProtection's no-remote skip; never throws). Both invokers are injectable.
178
+ * @param {string} root install/repo root
179
+ * @param {{ gitInvoker?:Function, ghInvoker?:Function, requiredCheck?:string }} [opts]
180
+ */
181
+ function protectOrigin(root, { gitInvoker, ghInvoker, requiredCheck } = {}) {
182
+ const slug = originSlug(root, { invoker: gitInvoker });
183
+ return applyProtection({ slug, invoker: ghInvoker, requiredCheck });
184
+ }
185
+
186
+ module.exports = {
187
+ RULESET_NAME,
188
+ DEFAULT_CHECK,
189
+ buildRulesetSpec,
190
+ defaultInvoke,
191
+ applyProtection,
192
+ parseSlug,
193
+ originSlug,
194
+ protectOrigin,
195
+ };
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The CD type-gate: decide whether a merge to main publishes, and at what bump, by conventional-commit
5
+ * type. Pure — no I/O. The release.yml workflow reads commit messages from the merged range and gates
6
+ * the OIDC publish on `release`.
7
+ */
8
+
9
+ // Bump precedence: a mixed set releases at the highest applicable bump.
10
+ const RANK = { major: 3, minor: 2, patch: 1 };
11
+
12
+ /** Classify one commit message → 'major' | 'minor' | 'patch' | null (no release). */
13
+ function classify(message) {
14
+ const text = String(message);
15
+ // A `BREAKING CHANGE:`/`BREAKING-CHANGE:` footer is a breaking change regardless of type — major.
16
+ // Spec-correct: it starts a footer line, so anchor to a line start to avoid prose false-positives.
17
+ if (/^BREAKING[ -]CHANGE:/m.test(text)) return 'major';
18
+ const subject = text.split('\n')[0];
19
+ const m = subject.match(/^([a-z]+)(\([^)]*\))?(!)?:/i);
20
+ if (!m) return null;
21
+ const type = m[1].toLowerCase();
22
+ // The `!` marker (feat!, fix(api)!, refactor!) is a breaking change on ANY type — major.
23
+ if (m[3] === '!') return 'major';
24
+ if (type === 'feat') return 'minor';
25
+ if (type === 'fix' || type === 'perf') return 'patch';
26
+ return null;
27
+ }
28
+
29
+ /**
30
+ * shouldRelease(commits) → { release, bump }. Given the merged commit messages, return whether a merge
31
+ * publishes and the highest applicable bump.
32
+ */
33
+ function shouldRelease(commits) {
34
+ const list = Array.isArray(commits) ? commits : [];
35
+ let best = null;
36
+ for (const c of list) {
37
+ const bump = classify(c);
38
+ if (bump && (best === null || RANK[bump] > RANK[best])) best = bump;
39
+ }
40
+ return { release: best !== null, bump: best };
41
+ }
42
+
43
+ /**
44
+ * Split NUL-delimited `git log --format=%B%x00 <range>` output into trimmed commit messages. Pure: the
45
+ * git read stays at the CLI layer; this just parses what it returns. Empty/blank entries are dropped.
46
+ */
47
+ function parseLog(raw) {
48
+ return String(raw).split('\0').map((s) => s.trim()).filter(Boolean);
49
+ }
50
+
51
+ module.exports = { shouldRelease, parseLog };
package/lib/ship.cjs ADDED
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ // WRXN ship — the autonomous promote path (gate-redesign gate-03).
4
+ //
5
+ // Replaces the WRXN_ACTIVE_AGENT / settings.local.json env-flag dance (a 2026-06-19 audit proved
6
+ // it a live no-op) with ONE command that pushes a reviewed branch, opens a PR, and arms
7
+ // auto-merge — GitHub then merges the instant the server-enforced CI gate is green. No env flag,
8
+ // no settings.local.json, no GitHub clicks.
9
+ //
10
+ // buildShipPlan() is PURE: branch/title in → the ordered git + gh command list out (no side
11
+ // effects). It mirrors lib/connect.cjs's split of a pure description from its invocation.
12
+ //
13
+ // lib/ship.cjs is package code (invoked via bin/wrxn.cjs), NOT payload — no manifest entry,
14
+ // consistent with lib/connect.cjs / lib/executor.cjs / lib/onboard.cjs.
15
+
16
+ const { spawnSync } = require('child_process');
17
+
18
+ const REMOTE = 'origin';
19
+ const DEFAULT_BASE = 'main';
20
+
21
+ /**
22
+ * Build the ordered promote command list for a reviewed branch. PURE — no side effects.
23
+ * branch → push (publish the branch to origin) → gh pr create → gh pr merge --auto --squash.
24
+ * @returns {{ label:string, cmd:string, args:string[] }[]}
25
+ * @throws when branch or title is missing/blank — a malformed promote is never runnable.
26
+ */
27
+ function buildShipPlan({ branch, title, base = DEFAULT_BASE, body = '' } = {}) {
28
+ const errors = [];
29
+ if (typeof branch !== 'string' || branch.trim() === '') errors.push('branch is required (the reviewed branch to promote)');
30
+ if (typeof title !== 'string' || title.trim() === '') errors.push('title is required (the PR title)');
31
+ if (errors.length) throw new Error(`cannot build ship plan: ${errors.join('; ')}`);
32
+ // CF-6 (SEC-LOW-1): the branch is a bare positional in `git push` and `gh pr merge`, so a
33
+ // dash-leading branch name could be misread as a flag. Fence it behind an end-of-options `--`.
34
+ // For `gh pr merge` the real flags (--auto/--squash) MUST precede `--` so they still parse as
35
+ // flags; everything after `--` is positional. (`gh pr create` carries the branch as the value of
36
+ // --head/--base, not a bare positional, so it needs no separator.)
37
+ return [
38
+ { label: 'push', cmd: 'git', args: ['push', '-u', REMOTE, '--', branch] },
39
+ { label: 'pr-create', cmd: 'gh', args: ['pr', 'create', '--base', base, '--head', branch, '--title', title, '--body', body || ''] },
40
+ { label: 'auto-merge', cmd: 'gh', args: ['pr', 'merge', '--auto', '--squash', '--', branch] },
41
+ ];
42
+ }
43
+
44
+ /**
45
+ * The real command invoker — a single spawnSync, mirroring lib/connect.cjs's defaultInvoke. The
46
+ * CLI layer wires this (a real git/gh invocation), which is what makes the promote "validated by
47
+ * invocation". A step succeeds iff it actually ran (not ENOENT) and exited 0.
48
+ * @returns {{ ok:boolean, detail:string, status?:number }}
49
+ */
50
+ function defaultInvoke(step) {
51
+ const r = spawnSync(step.cmd, step.args, { encoding: 'utf8' });
52
+ if (r.error) {
53
+ return { ok: false, detail: `${step.cmd} ${step.args.join(' ')} did not run: ${r.error.code || r.error.message}` };
54
+ }
55
+ const tail = r.stderr ? `: ${String(r.stderr).trim()}` : '';
56
+ return { ok: r.status === 0, status: r.status, detail: `${step.cmd} ${step.args[0]} exited ${r.status}${tail}` };
57
+ }
58
+
59
+ /**
60
+ * Run the promote plan through the injected invoker (defaults to the real defaultInvoke at the CLI
61
+ * layer). Reuses lib/connect.cjs's injectable-invoker shape so unit tests stay deterministic.
62
+ * @returns {{ ok:boolean, steps:{step:string,ok:boolean,detail:string}[], failed?:string }}
63
+ */
64
+ function ship({ invoker, branch, title, base, body } = {}) {
65
+ const plan = buildShipPlan({ branch, title, base, body });
66
+ const run = invoker || defaultInvoke;
67
+ const steps = [];
68
+ for (const step of plan) {
69
+ const r = run(step);
70
+ steps.push({ step: step.label, ok: !!r.ok, detail: r.detail });
71
+ if (!r.ok) return { ok: false, steps, failed: step.label };
72
+ }
73
+ return { ok: true, steps };
74
+ }
75
+
76
+ module.exports = { buildShipPlan, defaultInvoke, ship, REMOTE, DEFAULT_BASE };
package/lib/update.cjs CHANGED
@@ -7,6 +7,7 @@ const { loadManifest, inProfile } = require('./manifest.cjs');
7
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
+ const protect = require('./protect.cjs');
10
11
 
11
12
  /**
12
13
  * Pull-based update honoring the three file classes:
@@ -84,7 +85,20 @@ function update(opts) {
84
85
  // the next `wrxn update` resumes from the failed migration. See lib/migrate.cjs.
85
86
  const migrationsRan = runMigrations(pkgRoot, target, { fromVersion: from, toVersion: to });
86
87
 
87
- return { from, to, updated, preserved, migrationsRan };
88
+ // Apply the server-side hard gate (gate-redesign gate-02): the wrxn-main-gate ruleset on the
89
+ // install's origin. Idempotent (create-or-update by name → a re-update on a protected repo is a
90
+ // no-op) and fail-soft (no gh / not admin / no remote → a skipped result, never a throw), so it can
91
+ // never break an update on a remote-less install. The git/gh invokers are injectable for tests.
92
+ let protection;
93
+ try {
94
+ protection = protect.protectOrigin(target, { gitInvoker: opts.gitInvoker, ghInvoker: opts.ghInvoker });
95
+ } catch (err) {
96
+ // protection is fail-soft by construction; this is a belt-and-braces guard so a defect there can
97
+ // never break an `update`.
98
+ protection = { ok: false, action: 'skipped', reason: `protection error (skipped): ${err.message}` };
99
+ }
100
+
101
+ return { from, to, updated, preserved, migrationsRan, protection };
88
102
  }
89
103
 
90
104
  function readReceipt(target) {
package/manifest.json CHANGED
@@ -69,17 +69,12 @@
69
69
  "profile": "project"
70
70
  },
71
71
  {
72
- "path": ".claude/hooks/enforce-push-authority.cjs",
72
+ "path": ".claude/hooks/enforce-pipeline-adherence.cjs",
73
73
  "class": "managed",
74
74
  "profile": "project"
75
75
  },
76
76
  {
77
- "path": ".claude/hooks/enforce-review-marker.cjs",
78
- "class": "managed",
79
- "profile": "project"
80
- },
81
- {
82
- "path": ".claude/hooks/enforce-tests-on-push.cjs",
77
+ "path": ".claude/hooks/memory-synth-spawn.cjs",
83
78
  "class": "managed",
84
79
  "profile": "project"
85
80
  },
@@ -153,11 +148,6 @@
153
148
  "class": "managed",
154
149
  "profile": "project"
155
150
  },
156
- {
157
- "path": ".claude/skills/handoff/SKILL.md",
158
- "class": "managed",
159
- "profile": "project"
160
- },
161
151
  {
162
152
  "path": ".claude/skills/harvest/SKILL.md",
163
153
  "class": "managed",
@@ -438,6 +428,11 @@
438
428
  "class": "seeded",
439
429
  "profile": "workspace"
440
430
  },
431
+ {
432
+ "path": ".github/workflows/wrxn-ci.yml",
433
+ "class": "managed",
434
+ "profile": "project"
435
+ },
441
436
  {
442
437
  "path": ".mcp.json",
443
438
  "class": "managed",
@@ -493,6 +488,16 @@
493
488
  "class": "state",
494
489
  "profile": "project"
495
490
  },
491
+ {
492
+ "path": ".wrxn/memory-synth.cjs",
493
+ "class": "managed",
494
+ "profile": "project"
495
+ },
496
+ {
497
+ "path": ".wrxn/memory.config.json",
498
+ "class": "seeded",
499
+ "profile": "project"
500
+ },
496
501
  {
497
502
  "path": ".wrxn/sync.cjs",
498
503
  "class": "managed",
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ const protect = require('../lib/protect.cjs');
4
+
5
+ /**
6
+ * 005 — apply the wrxn-main-gate ruleset on existing installs (gate-redesign gate-02).
7
+ *
8
+ * The server-side hard gate (block direct push to the default branch, require a PR + the wrxn-ci check,
9
+ * require the branch up-to-date, no bypass actor) replaces the disarmable settings.local.json env-flag
10
+ * gate (ADR 0007). `wrxn update` now applies the ruleset to an install's origin as part of its run, but
11
+ * a pre-0.11.0 install never ran that step — so this migration performs the FIRST application on an
12
+ * existing install once it reaches the release that carries the gate.
13
+ *
14
+ * up() delegates to protect.protectOrigin(ctx.target): derive the install's origin slug and idempotently
15
+ * create-or-update the ruleset via `gh api`. Repo-agnostic (the slug comes from origin, so the SAME
16
+ * logic protects the kernel, every install, and the recon-wrxn sibling) and fail-soft by construction —
17
+ * no gh / not admin / no remote → a skipped result, never a throw. A remote-less install derives no slug
18
+ * and is a pure no-op (no `gh` call). Defensive like 003/004: the try/catch is belt-and-braces so the
19
+ * migration can never fail the runner even if protection somehow threw. `version` 0.11.0 = the release
20
+ * that carries the push-gate redesign (the same release whose `update` gained the protect step).
21
+ */
22
+ module.exports = {
23
+ id: '005',
24
+ version: '0.11.0',
25
+ up(ctx) {
26
+ try {
27
+ protect.protectOrigin(ctx.target);
28
+ } catch {
29
+ // protection is fail-soft by design — swallow defensively so the migration never breaks `update`.
30
+ }
31
+ },
32
+ };
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * 006 — seeded-routing refresh to the PR + CI + auto-merge gate (gate-redesign gate-04).
8
+ *
9
+ * `.synapse/routing` is a SEEDED file: `wrxn update` refreshes the managed `.synapse/global`, the
10
+ * constitution, and the synapse skill docs (all moved to the PR + CI + auto-merge model in gate-04),
11
+ * but it NEVER overwrites the operator-owned routing seed. So the existing installs keep their OLD
12
+ * ROUTING_RULE_0 — the retired WRXN_ACTIVE_AGENT=devops confirmation-flag dance, the exact text
13
+ * migration 002 last wrote — leaving one doctrine echo out of step with the rest of the install.
14
+ *
15
+ * up() rewrites ONLY a ROUTING_RULE_0 line that still carries the WRXN_ACTIVE_AGENT marker, replacing
16
+ * it with the frozen 0.11.0 rule below. The marker is the retired mechanism's own identifier: it is
17
+ * absent from the new rule (so a second run / an already-refreshed install is a no-op) and from any
18
+ * operator doctrine (a rule that never named the kernel-internal flag is left untouched). The
19
+ * startsWith('ROUTING_RULE_0=') clause scopes the rewrite to rule 0 alone — sibling rules, the comment
20
+ * header, and operator-added ROUTING_RULE_N lines are preserved verbatim, and the split/join
21
+ * round-trip keeps the trailing newline. A missing or unreadable routing is a clean no-op (the fs work
22
+ * is wrapped defensively, like 005, so a cosmetic doctrine refresh can never break `wrxn update`).
23
+ *
24
+ * The new rule is EMBEDDED as a frozen 0.11.0 constant: a migration is a historical transform of the
25
+ * release it ships with, not a re-read of the evolving seeded template (ctx carries no pkgRoot, by
26
+ * design). Idempotency falls out of the gate — after the rewrite the marker is gone. Runs via
27
+ * `wrxn update` once the install reaches 0.11.0.
28
+ */
29
+
30
+ // The PR + CI + auto-merge ROUTING_RULE_0 — mirrors the seeded `.synapse/routing` template at 0.11.0.
31
+ const NEW_ROUTING_RULE_0 =
32
+ 'ROUTING_RULE_0=git push, PR creation, and release tags promote through `wrxn ship` (push the branch → open a PR → arm auto-merge); a server-enforced GitHub ruleset is the gate — it blocks direct pushes to the trunk and merges only when CI is green, so never push directly to the trunk.';
33
+
34
+ module.exports = {
35
+ id: '006',
36
+ version: '0.11.0',
37
+ up(ctx) {
38
+ const routingPath = path.join(ctx.target, '.synapse', 'routing');
39
+ try {
40
+ if (!fs.existsSync(routingPath)) return;
41
+ const lines = fs.readFileSync(routingPath, 'utf8').split('\n');
42
+ let changed = false;
43
+ const out = lines.map((line) => {
44
+ if (line.startsWith('ROUTING_RULE_0=') && line.includes('WRXN_ACTIVE_AGENT')) {
45
+ changed = true;
46
+ return NEW_ROUTING_RULE_0;
47
+ }
48
+ return line;
49
+ });
50
+ if (changed) fs.writeFileSync(routingPath, out.join('\n'));
51
+ } catch {
52
+ // Defensive (belt-and-braces, like 005): an unreadable/odd routing is a clean no-op — a cosmetic
53
+ // doctrine refresh must never fail `wrxn update`.
54
+ }
55
+ },
56
+ };
@@ -0,0 +1,136 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * 007 — transition existing installs onto auto-memory (auto-memory-05).
8
+ *
9
+ * The auto-memory release makes memory automatic: a SessionEnd hook (memory-synth-spawn.cjs) spawns a
10
+ * background synth that writes the continuity baton and consolidates dream pages — so the manual
11
+ * `handoff` skill is removed (the synth is the sole baton writer) and the stale `_slots/current-focus`
12
+ * slot + its `set-focus` op are dropped. The new payload stops shipping the handoff skill and now wires
13
+ * SessionEnd + ships a seeded `memory.config.json`. But `wrxn update` overwrites managed files in place —
14
+ * it never PRUNES a removed one — and a seeded file already present is preserved, so a pre-0.12.0 install
15
+ * still carries the old `handoff` skill files and the stale focus slot, and may lack the new wiring/seed
16
+ * (e.g. a hand-edited settings.json the managed overwrite left alone, or an install that never had a
17
+ * config). up() transitions it.
18
+ *
19
+ * Steps: (1) remove the install's `handoff` skill dir; (2) wire SessionEnd → memory-synth-spawn.cjs into
20
+ * the install settings.json IDEMPOTENTLY (add only if absent — if update's managed overwrite already laid
21
+ * it, this is a safe no-op); a corrupt settings.json is left untouched; (3) seed `memory.config.json` if
22
+ * absent (the slice-02 default shape); (4) remove the stale `_slots/current-focus.md` focus slot;
23
+ * (5) backfill the install `.gitignore` for the `.env` secret and the continuity runtime temps the synth/
24
+ * dream now write (`.wrxn/continuity/.pending`, `.pending-handoff`, the baton `.tmp`, `.dream.*.tmp`) —
25
+ * slice-02 added `.env` for NEW installs; this closes the gap for OLD ones (slice-04 reviewer F1).
26
+ *
27
+ * Defensive like 004: every step is existence-guarded and best-effort (force-rm ignores a missing file),
28
+ * a missing/clean target is a no-op, a corrupt settings.json is left untouched (never clobber a hand-
29
+ * edited file — the other steps still run), and the gitignore backfill adds each line at most once.
30
+ * Idempotent (a second run finds nothing to do) and never throws on an already-clean install. `version`
31
+ * 0.12.0 = the auto-memory release that carries the transition (the same release whose payload stops
32
+ * shipping the handoff skill and starts wiring the SessionEnd synth).
33
+ */
34
+
35
+ // The .gitignore lines auto-memory needs an install to carry: the `.env` secret (slice-02 added it for
36
+ // NEW installs — backfilled here for OLD ones) + the continuity runtime markers/temps the synth and dream
37
+ // write and clean in a finally, which a SIGKILL could leave behind UNTRACKED (slice-04 reviewer F1). The
38
+ // tracked baton `latest.md` is deliberately NOT ignored — only its dot-prefixed `.tmp` is.
39
+ const GITIGNORE_LINES = [
40
+ '.env',
41
+ '.wrxn/continuity/.pending*', // the .pending + .pending-handoff synth markers
42
+ '.wrxn/continuity/.dream.*.tmp', // dream's per-call blob/batch/stage/approved temps
43
+ '.wrxn/continuity/.latest.md.*.tmp', // the atomic baton-write temp (rename-over target)
44
+ ];
45
+
46
+ /** Append `line` to `<target>/.gitignore` (create if absent) exactly once — mirrors install.cjs. */
47
+ function ensureGitignoreLine(target, line) {
48
+ const giPath = path.join(target, '.gitignore');
49
+ let body = '';
50
+ try {
51
+ body = fs.readFileSync(giPath, 'utf8');
52
+ } catch {
53
+ body = '';
54
+ }
55
+ if (body.split('\n').some((l) => l.trim() === line)) return; // already ignored
56
+ const prefix = body.length && !body.endsWith('\n') ? '\n' : '';
57
+ fs.writeFileSync(giPath, body + prefix + line + '\n');
58
+ }
59
+
60
+ // The SessionEnd spawn hook the auto-memory payload wires (must match payload/.claude/settings.json).
61
+ const SPAWN_HOOK_BASENAME = 'memory-synth-spawn.cjs';
62
+ const SPAWN_HOOK_COMMAND = 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/memory-synth-spawn.cjs"';
63
+
64
+ // Idempotently wire SessionEnd → the spawn hook into a parsed settings config. Adds the hook ONLY if no
65
+ // existing hook command across any event already references it — so a config the managed overwrite already
66
+ // laid (or a prior run) is a safe no-op. Returns true iff the config changed. Preserves every other event.
67
+ function wireSessionEndSpawn(cfg) {
68
+ if (!cfg || typeof cfg !== 'object') return false;
69
+ cfg.hooks = cfg.hooks && typeof cfg.hooks === 'object' ? cfg.hooks : {};
70
+ // already wired anywhere? (mirror unwireHook's whole-config scan) → no-op.
71
+ for (const groups of Object.values(cfg.hooks)) {
72
+ if (!Array.isArray(groups)) continue;
73
+ for (const group of groups) {
74
+ if (!group || !Array.isArray(group.hooks)) continue;
75
+ if (group.hooks.some((h) => h && typeof h.command === 'string' && h.command.includes(SPAWN_HOOK_BASENAME))) {
76
+ return false;
77
+ }
78
+ }
79
+ }
80
+ const event = Array.isArray(cfg.hooks.SessionEnd) ? cfg.hooks.SessionEnd : [];
81
+ event.push({ hooks: [{ type: 'command', command: SPAWN_HOOK_COMMAND }] });
82
+ cfg.hooks.SessionEnd = event;
83
+ return true;
84
+ }
85
+
86
+ module.exports = {
87
+ id: '007',
88
+ version: '0.12.0',
89
+ up(ctx) {
90
+ const target = ctx.target;
91
+
92
+ // 1. remove the retired `handoff` skill dir (the synth is the sole baton writer now). recursive +
93
+ // force = an absent dir is a no-op.
94
+ fs.rmSync(path.join(target, '.claude', 'skills', 'handoff'), { recursive: true, force: true });
95
+
96
+ // 2. wire SessionEnd → memory-synth-spawn.cjs into the install settings.json, idempotently. A corrupt
97
+ // file is left untouched (never clobber a hand-edited config); an absent file is left absent (the
98
+ // managed overwrite lays the wired settings — the migration only backfills a hand-edited one).
99
+ const settingsPath = path.join(target, '.claude', 'settings.json');
100
+ if (fs.existsSync(settingsPath)) {
101
+ let cfg = null;
102
+ try {
103
+ cfg = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
104
+ } catch {
105
+ cfg = null; // hand-corrupted operator file → never clobber, never crash
106
+ }
107
+ if (cfg && typeof cfg === 'object' && wireSessionEndSpawn(cfg)) {
108
+ fs.writeFileSync(settingsPath, JSON.stringify(cfg, null, 2) + '\n');
109
+ }
110
+ }
111
+
112
+ // 3. seed memory.config.json if absent (the slice-02 default). Copied from THIS package's payload so
113
+ // the seeded shape can never drift from what a fresh install ships. Already present ⇒ preserved
114
+ // (it is a seeded, operator-owned file). A missing payload source is swallowed (best-effort).
115
+ const cfgPath = path.join(target, '.wrxn', 'memory.config.json');
116
+ if (!fs.existsSync(cfgPath)) {
117
+ const seedSrc = path.join(__dirname, '..', 'payload', '.wrxn', 'memory.config.json');
118
+ try {
119
+ const seed = fs.readFileSync(seedSrc, 'utf8');
120
+ fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
121
+ fs.writeFileSync(cfgPath, seed);
122
+ } catch {
123
+ // no payload seed reachable → skip (the managed/seeded update path lays it anyway)
124
+ }
125
+ }
126
+
127
+ // 4. remove the stale `_slots/current-focus.md` focus slot (the slot + its set-focus op are dropped).
128
+ // Only the slot PAGE goes — the empty `_slots` tier dir + its gitkeep stay (the tier is retained).
129
+ // force = an absent slot is a no-op.
130
+ fs.rmSync(path.join(target, '.wrxn', 'wiki', '_slots', 'current-focus.md'), { force: true });
131
+
132
+ // 5. gitignore backfill — add the `.env` secret + continuity runtime-temp lines (each at most once).
133
+ // Closes the gap for installs created before slice-02 added `.env` / before the synth wrote temps.
134
+ for (const line of GITIGNORE_LINES) ensureGitignoreLine(target, line);
135
+ },
136
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gcunharodrigues/wrxn",
3
- "version": "0.10.0",
3
+ "version": "0.12.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"
@@ -26,7 +26,8 @@ you add no behavior the harness does not already define.
26
26
 
27
27
  ## Constraints (hard)
28
28
  - **Never `git push`** — only the devops executor may. Integration happens downstream.
29
- - Do **not** edit managed (kernel-owned) files without the managed-confirm token.
29
+ - Edit managed (kernel-owned) files only as a deliberate kernel change that lands through the PR + CI
30
+ gate — a local edit raises a non-blocking advisory; the server-side CI managed-integrity check is the teeth.
30
31
  - A review marker (`review-<id>.md`) is required downstream before this work is pushed — do not
31
32
  fabricate it.
32
33
  - Stay inside the one slice. No scope creep, no speculative features.