ruvnet-brain 4.3.20 → 4.3.21
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/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Created: 2026-06-29 22:36:38 EDT
|
|
|
7
7
|
|
|
8
8
|
# 🧠 RuvNet Brain
|
|
9
9
|
|
|
10
|
-
### 🧠 RuvNet Brain — [](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
|
|
11
11
|
|
|
12
12
|
**A portable, source-grounded brain over Reuven Cohen's (rUv's) RuvNet stack — delivered as a Claude Code plugin that makes Claude _use_ the stack instead of fighting it.**
|
|
13
13
|
|
package/bin/install.mjs
CHANGED
|
@@ -67,6 +67,7 @@ const versionSatisfies = (installed, expected) => {
|
|
|
67
67
|
import {
|
|
68
68
|
CONSOLE_RUNTIME_SURFACE, CONSOLE_RUNTIME_IDENTITY_FILE, consoleRuntimeDigest,
|
|
69
69
|
} from '../scripts/console-runtime-identity.mjs';
|
|
70
|
+
import { shellDiff as pluginShellDiff } from '../plugin/scripts/host-shell-boundary.mjs';
|
|
70
71
|
|
|
71
72
|
// SEC-0010 #6 — the Ed25519 PUBLIC key is EMBEDDED here (not a separate file) so the installer's
|
|
72
73
|
// trust root travels with the installer code itself: an attacker who swaps the downloaded bundle
|
|
@@ -1182,6 +1183,55 @@ export function claudePluginStatus({ home = os.homedir() } = {}) {
|
|
|
1182
1183
|
}
|
|
1183
1184
|
}
|
|
1184
1185
|
|
|
1186
|
+
// A plugin version can change without changing anything a running host freezes at boot. Compare
|
|
1187
|
+
// the actual installed payload with this candidate before updating it so body-only releases do not
|
|
1188
|
+
// manufacture a restart requirement. Unknown paths fail closed: if we cannot prove the boundary,
|
|
1189
|
+
// the caller must retain the restart notice rather than silently promise hot loading.
|
|
1190
|
+
function inspectPluginShellBoundary(installedRoot, candidateRoot = path.join(REPO_ROOT, 'plugin')) {
|
|
1191
|
+
if (!installedRoot || !candidateRoot) {
|
|
1192
|
+
return { known: false, changed: true, paths: [], restartRequired: true, reason: 'plugin boot surface could not be located' };
|
|
1193
|
+
}
|
|
1194
|
+
try {
|
|
1195
|
+
const paths = pluginShellDiff(installedRoot, candidateRoot);
|
|
1196
|
+
return {
|
|
1197
|
+
known: true,
|
|
1198
|
+
changed: paths.length > 0,
|
|
1199
|
+
paths,
|
|
1200
|
+
restartRequired: paths.length > 0,
|
|
1201
|
+
reason: paths.length ? `boot-level declarations changed: ${paths.join(', ')}` : 'body-only plugin update',
|
|
1202
|
+
};
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
return { known: false, changed: true, paths: [], restartRequired: true,
|
|
1205
|
+
reason: `plugin boot surface comparison failed: ${error.message}` };
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function codexInstalledPluginRoot({ codexHome = codexHomeDir(), status } = {}) {
|
|
1210
|
+
const row = status?.row || {};
|
|
1211
|
+
for (const value of [status?.installPath, status?.path, row.installPath, row.path]) {
|
|
1212
|
+
if (typeof value === 'string' && value) {
|
|
1213
|
+
try {
|
|
1214
|
+
const real = fs.realpathSync(value);
|
|
1215
|
+
if (fs.statSync(real).isDirectory()) return real;
|
|
1216
|
+
} catch { /* try the cache scan below */ }
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
const cacheRoot = path.join(codexHome, 'plugins', 'cache', 'ruvnet-brain', 'ruvnet-brain');
|
|
1220
|
+
let candidates = [];
|
|
1221
|
+
try {
|
|
1222
|
+
candidates = fs.readdirSync(cacheRoot, { withFileTypes: true })
|
|
1223
|
+
.filter((entry) => entry.isDirectory())
|
|
1224
|
+
.map((entry) => path.join(cacheRoot, entry.name))
|
|
1225
|
+
.filter((dir) => {
|
|
1226
|
+
try {
|
|
1227
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(dir, '.codex-plugin', 'plugin.json'), 'utf8'));
|
|
1228
|
+
return manifest?.name === 'ruvnet-brain' && (!status?.version || manifest.version === status.version);
|
|
1229
|
+
} catch { return false; }
|
|
1230
|
+
});
|
|
1231
|
+
} catch { /* absent cache is handled as unknown below */ }
|
|
1232
|
+
return candidates.sort().at(-1) || null;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1185
1235
|
function wirePlugin({ expectedVersion = PACKAGE_VERSION, requireManaged = false } = {}) {
|
|
1186
1236
|
step(
|
|
1187
1237
|
'Wiring the Claude Code plugin',
|
|
@@ -1204,6 +1254,9 @@ function wirePlugin({ expectedVersion = PACKAGE_VERSION, requireManaged = false
|
|
|
1204
1254
|
|
|
1205
1255
|
const before = claudePluginStatus();
|
|
1206
1256
|
if (requireManaged && !before.managed) return { host: false, wired: false, action: 'unmanaged' };
|
|
1257
|
+
const shellBoundary = before.installed
|
|
1258
|
+
? inspectPluginShellBoundary(before.installPath)
|
|
1259
|
+
: { known: true, changed: false, paths: [], restartRequired: false, reason: 'new host installation' };
|
|
1207
1260
|
const addedMarket = before.managed
|
|
1208
1261
|
? tryRun('claude', ['plugin', 'marketplace', 'update', 'ruvnet-brain'])
|
|
1209
1262
|
: tryRun('claude', ['plugin', 'marketplace', 'add', marketplaceSource]);
|
|
@@ -1224,8 +1277,18 @@ function wirePlugin({ expectedVersion = PACKAGE_VERSION, requireManaged = false
|
|
|
1224
1277
|
const installedHookRetirement = claudeInstalledHookRetirementStatus({ plugin: installed });
|
|
1225
1278
|
if (installed.installed && versionSatisfies(installed.version, expectedVersion) && installedHookRetirement.ok) {
|
|
1226
1279
|
ok(`plugin installed at user scope (global, alongside Ruflo / RuVector) — exact version ${installed.version}`);
|
|
1227
|
-
|
|
1228
|
-
|
|
1280
|
+
if (shellBoundary.restartRequired) {
|
|
1281
|
+
warn(`boot-level plugin declarations changed; restart Claude Code once to load them (${shellBoundary.paths.join(', ') || shellBoundary.reason}).`);
|
|
1282
|
+
} else if (before.installed && before.version !== installed.version) {
|
|
1283
|
+
info(' body-only update: the Stable Spine is live on the next hook/MCP call; no restart is required.');
|
|
1284
|
+
}
|
|
1285
|
+
info(` commands available${shellBoundary.restartRequired ? ' after a restart' : ' immediately'}: ${c.bold('/rvbc')}, ${c.bold('/ruvnet-brain:configure')}`);
|
|
1286
|
+
return {
|
|
1287
|
+
host: true, wired: true, version: installed.version, manualMarketplace, manualInstall,
|
|
1288
|
+
shellChanged: shellBoundary.changed, shellChangedPaths: shellBoundary.paths,
|
|
1289
|
+
restartRequired: shellBoundary.restartRequired,
|
|
1290
|
+
...(shellBoundary.restartRequired ? { sessionSafety: 'restart-required', sessionSafetyReason: shellBoundary.reason } : {}),
|
|
1291
|
+
};
|
|
1229
1292
|
}
|
|
1230
1293
|
|
|
1231
1294
|
// The honest failure. The brain still WORKS — this is the difference between a broken install and
|
|
@@ -1654,6 +1717,7 @@ export function codexPluginStatus(options = {}) {
|
|
|
1654
1717
|
installed: Boolean(row?.installed),
|
|
1655
1718
|
enabled: Boolean(row?.enabled),
|
|
1656
1719
|
version: row?.version || null,
|
|
1720
|
+
installPath: row?.installPath || row?.path || null,
|
|
1657
1721
|
row: row || null,
|
|
1658
1722
|
};
|
|
1659
1723
|
}
|
|
@@ -1691,9 +1755,22 @@ export function wireCodexPlugin({
|
|
|
1691
1755
|
if (announce) warn(`Codex Brain plugin is installed but disabled by user or policy — left disabled (${CODEX_PLUGIN_ID}).`);
|
|
1692
1756
|
return { host: true, action: 'disabled', ...before };
|
|
1693
1757
|
}
|
|
1758
|
+
// An existing Codex session can keep the plugin generation it loaded at boot. Compare the
|
|
1759
|
+
// installed bytes with the source candidate before mutating the marketplace so body-only updates
|
|
1760
|
+
// remain live while a changed/unknown boot surface gets one explicit restart request.
|
|
1761
|
+
const shellBoundary = before.installed
|
|
1762
|
+
? inspectPluginShellBoundary(
|
|
1763
|
+
codexInstalledPluginRoot({ codexHome, status: before }),
|
|
1764
|
+
path.join(REPO_ROOT, 'plugin'),
|
|
1765
|
+
)
|
|
1766
|
+
: { known: true, changed: false, paths: [], restartRequired: false, reason: 'new host installation' };
|
|
1694
1767
|
if (before.installed && before.enabled && versionSatisfies(before.version, expectedVersion)) {
|
|
1695
1768
|
if (announce) ok(`Codex Brain plugin already installed and enabled (${before.version || 'version unknown'}) — no changes.`);
|
|
1696
|
-
return {
|
|
1769
|
+
return {
|
|
1770
|
+
host: true, action: 'unchanged', ...before,
|
|
1771
|
+
shellChanged: shellBoundary.changed, shellChangedPaths: shellBoundary.paths,
|
|
1772
|
+
restartRequired: false,
|
|
1773
|
+
};
|
|
1697
1774
|
}
|
|
1698
1775
|
|
|
1699
1776
|
if (runJson === runCodexJson && !localMarketplace) {
|
|
@@ -1735,17 +1812,23 @@ export function wireCodexPlugin({
|
|
|
1735
1812
|
}
|
|
1736
1813
|
if (announce) {
|
|
1737
1814
|
ok(`Codex Brain plugin installed and enabled (${after.version || 'version unknown'}).`);
|
|
1738
|
-
|
|
1815
|
+
if (shellBoundary.restartRequired) {
|
|
1816
|
+
warn(`boot-level plugin declarations changed; restart Codex once to load them (${shellBoundary.paths.join(', ') || shellBoundary.reason}).`);
|
|
1817
|
+
} else if (before.installed && before.version !== after.version) {
|
|
1818
|
+
info(' body-only update: the Stable Spine is live on the next hook/MCP call; no restart is required.');
|
|
1819
|
+
}
|
|
1739
1820
|
}
|
|
1740
1821
|
return {
|
|
1741
1822
|
host: true,
|
|
1742
1823
|
action: before.installed ? 'updated' : 'installed',
|
|
1743
1824
|
...after,
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
restartRequired
|
|
1748
|
-
|
|
1825
|
+
shellChanged: shellBoundary.changed,
|
|
1826
|
+
shellChangedPaths: shellBoundary.paths,
|
|
1827
|
+
restartRequired: shellBoundary.restartRequired,
|
|
1828
|
+
...(shellBoundary.restartRequired ? {
|
|
1829
|
+
sessionSafety: 'restart-required',
|
|
1830
|
+
sessionSafetyReason: shellBoundary.reason,
|
|
1831
|
+
} : {}),
|
|
1749
1832
|
};
|
|
1750
1833
|
}
|
|
1751
1834
|
|
|
@@ -2802,7 +2885,7 @@ function reportVersionDrift(cacheDir) {
|
|
|
2802
2885
|
if (!state.drift) return state;
|
|
2803
2886
|
warn(`the brain (${c.bold(state.kb)}) and the Claude Code plugin (${c.bold(state.wrapper)}) have drifted apart —`);
|
|
2804
2887
|
info(`that's normal (they update on separate schedules) and neither one is broken. To bring the`);
|
|
2805
|
-
info(`plugin up to date: ${c.bold('claude plugin marketplace update ruvnet-brain')} ${c.dim('(
|
|
2888
|
+
info(`plugin up to date: ${c.bold('claude plugin marketplace update ruvnet-brain')} ${c.dim('(body updates go live without a restart; boot-surface changes are called out)')}`);
|
|
2806
2889
|
return state;
|
|
2807
2890
|
}
|
|
2808
2891
|
|
|
@@ -3061,6 +3144,15 @@ export function syncHostsAfterUpdate(cacheDir = resolvedKbDir(), {
|
|
|
3061
3144
|
codexReceipt.sessionSafety = results.codex.sessionSafety || null;
|
|
3062
3145
|
codexReceipt.sessionSafetyReason = results.codex.sessionSafetyReason || null;
|
|
3063
3146
|
}
|
|
3147
|
+
const claudeReceipt = {
|
|
3148
|
+
state: results.claude.host ? 'ready' : 'absent',
|
|
3149
|
+
version: results.claude.version || null,
|
|
3150
|
+
};
|
|
3151
|
+
if (results.claude?.restartRequired === true) {
|
|
3152
|
+
claudeReceipt.restartRequired = true;
|
|
3153
|
+
claudeReceipt.sessionSafety = results.claude.sessionSafety || null;
|
|
3154
|
+
claudeReceipt.sessionSafetyReason = results.claude.sessionSafetyReason || null;
|
|
3155
|
+
}
|
|
3064
3156
|
if (okApplied) {
|
|
3065
3157
|
// ISSUE #153 — a running host may freeze an old plugin root. Reclaim only generations whose
|
|
3066
3158
|
// modern leases prove no live consumer; legacy roots without that proof stay on disk.
|
|
@@ -3093,7 +3185,7 @@ export function syncHostsAfterUpdate(cacheDir = resolvedKbDir(), {
|
|
|
3093
3185
|
desiredVersion: PACKAGE_VERSION,
|
|
3094
3186
|
verifiedAt: new Date().toISOString(),
|
|
3095
3187
|
hosts: {
|
|
3096
|
-
claude:
|
|
3188
|
+
claude: claudeReceipt,
|
|
3097
3189
|
codex: codexReceipt,
|
|
3098
3190
|
},
|
|
3099
3191
|
consoleRuntime: results.consoleRuntime,
|
|
@@ -3108,7 +3200,7 @@ export function syncHostsAfterUpdate(cacheDir = resolvedKbDir(), {
|
|
|
3108
3200
|
const convergence = classifyHostConvergence({
|
|
3109
3201
|
desiredVersion: PACKAGE_VERSION,
|
|
3110
3202
|
hosts: {
|
|
3111
|
-
claude:
|
|
3203
|
+
claude: claudeReceipt,
|
|
3112
3204
|
codex: codexReceipt,
|
|
3113
3205
|
},
|
|
3114
3206
|
consoleRuntime: results.consoleRuntime,
|
|
@@ -4871,10 +4963,8 @@ function success({ cacheDir, isCustom, plugin, codexHost, codexPlugin, env, nigh
|
|
|
4871
4963
|
console.log(`\n ${c.bold('Keep it fresh:')} re-run ${c.bold('npx ruvnet-brain')} any time — the brain itself always pulls the latest`);
|
|
4872
4964
|
console.log(` Release regardless (that part isn't cached). For the bleeding-edge installer too, use ${c.bold('npx github:stuinfla/ruvnet-brain')}.`);
|
|
4873
4965
|
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
console.log(`\n ${c.yellow(c.bold('One required cleanup step:'))} restart any ${hostLabel} window that was open before this update.`);
|
|
4877
|
-
console.log(` ${c.dim('That unloads the old in-memory hook registry; new sessions load the intentional empty registry.')}`);
|
|
4966
|
+
console.log(`\n ${c.bold('Updates take effect:')} body-only releases go live on the next hook/MCP call without a restart.`);
|
|
4967
|
+
console.log(` ${c.dim('If boot-level declarations change, the installer names the affected host and requests one restart.')}`);
|
|
4878
4968
|
|
|
4879
4969
|
// ── what to do now ──
|
|
4880
4970
|
console.log(`\n ${c.bold('What to do now:')}`);
|
|
@@ -4982,8 +5072,8 @@ Env:
|
|
|
4982
5072
|
download or an air-gapped machine is not a broken install). Only ever set
|
|
4983
5073
|
this for a locked-down environment where you want to know immediately.
|
|
4984
5074
|
|
|
4985
|
-
It is safe to re-run at any time.
|
|
4986
|
-
|
|
5075
|
+
It is safe to re-run at any time. Body updates go live on the next hook/MCP call. Restart only when
|
|
5076
|
+
the installer reports that boot-level declarations changed.
|
|
4987
5077
|
`);
|
|
4988
5078
|
}
|
|
4989
5079
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.21",
|
|
4
4
|
"description": "One-command installer for RuvNet Brain — a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
3
|
"description": "RuvNet brain transplant for Claude Code — grounds every RuvNet decision in real source across 77 rUv repositories, prefers Ruflo / RuVector-RVF / AgentDB over training-prior defaults (pgvector, Pinecone, hand-rolled cosine), and can pull in any RuvNet repo on demand. Ships an enforced UserPromptSubmit retrieve-and-inject grounding hook that sharply reduces drift.",
|
|
4
|
-
"version": "4.3.
|
|
4
|
+
"version": "4.3.21",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stuart Kerr"
|
|
7
7
|
},
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// host-shell-boundary.mjs — one definition of the files a running host freezes at boot.
|
|
2
|
+
//
|
|
3
|
+
// Body files are reached through the Stable Spine and can change in place. These paths are the
|
|
4
|
+
// host declarations or boot-loaded surfaces whose change must be reported honestly to a running
|
|
5
|
+
// Claude/Codex session. Both the updater and the installer consume this one list so they cannot
|
|
6
|
+
// disagree about whether a release needs a restart.
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
export const SHELL_PATHS = Object.freeze([
|
|
12
|
+
'hooks/hooks.json',
|
|
13
|
+
'scripts/hook-shim.mjs',
|
|
14
|
+
'scripts/hook-shim-bash.mjs',
|
|
15
|
+
'scripts/development-maintenance.mjs',
|
|
16
|
+
'mcp/server.mjs',
|
|
17
|
+
'.mcp.json',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
function treeDigest(root, relative) {
|
|
21
|
+
const target = path.join(root, relative);
|
|
22
|
+
let stat;
|
|
23
|
+
try { stat = fs.lstatSync(target); } catch { return '<missing>'; }
|
|
24
|
+
if (stat.isSymbolicLink()) return '<symlink>';
|
|
25
|
+
if (stat.isFile()) return `file:${crypto.createHash('sha256').update(fs.readFileSync(target)).digest('hex')}`;
|
|
26
|
+
if (!stat.isDirectory()) return `<special:${stat.mode}>`;
|
|
27
|
+
const entries = fs.readdirSync(target, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
28
|
+
return `dir:${entries.map((entry) => `${entry.name}:${treeDigest(target, entry.name)}`).join('|')}`;
|
|
29
|
+
}
|
|
30
|
+
/** Return the boot-loaded paths that differ between two plugin payload roots. */
|
|
31
|
+
export function shellDiff(prevRootAbs, nextRootAbs) {
|
|
32
|
+
if (!prevRootAbs || !nextRootAbs) throw new Error('both plugin roots are required for shell comparison');
|
|
33
|
+
const changed = [];
|
|
34
|
+
for (const rel of SHELL_PATHS) {
|
|
35
|
+
if (treeDigest(prevRootAbs, rel) !== treeDigest(nextRootAbs, rel)) changed.push(rel);
|
|
36
|
+
}
|
|
37
|
+
// skills/ and commands/ are markdown surfaces loaded at host boot. Any file-set or content
|
|
38
|
+
// difference is a boot-surface change, even though they are not executable hook declarations.
|
|
39
|
+
for (const dir of ['skills', 'commands']) {
|
|
40
|
+
if (treeDigest(prevRootAbs, dir) !== treeDigest(nextRootAbs, dir)) changed.push(`${dir}/`);
|
|
41
|
+
}
|
|
42
|
+
return changed;
|
|
43
|
+
}
|
|
@@ -41,6 +41,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
41
41
|
// check on every .sh it was about to install — the platform most likely to receive a broken hook
|
|
42
42
|
// was the one platform that never checked. Git-for-Windows bash can run `bash -n` perfectly well.
|
|
43
43
|
import { resolveBash } from './hook-shim-bash.mjs';
|
|
44
|
+
import { SHELL_PATHS, shellDiff } from './host-shell-boundary.mjs';
|
|
45
|
+
export { SHELL_PATHS, shellDiff } from './host-shell-boundary.mjs';
|
|
44
46
|
|
|
45
47
|
const BRAIN_HOME = process.env.RUVNET_BRAIN_HOME || path.join(os.homedir(), '.cache', 'ruvnet-brain');
|
|
46
48
|
const ACTIVE = path.join(BRAIN_HOME, 'active.json');
|
|
@@ -212,38 +214,6 @@ function promote(staging, version) {
|
|
|
212
214
|
// the update is fully live with no restart and session-start stays silent; if any did, the ONE
|
|
213
215
|
// honest nag fires (the release classifier computes the same thing publish-side; this is the
|
|
214
216
|
// client-side truth for locally-applied generations). Red-team finding 18's client half.
|
|
215
|
-
export const SHELL_PATHS = [
|
|
216
|
-
'hooks/hooks.json',
|
|
217
|
-
'scripts/hook-shim.mjs',
|
|
218
|
-
'scripts/hook-shim-bash.mjs',
|
|
219
|
-
'scripts/development-maintenance.mjs',
|
|
220
|
-
'mcp/server.mjs',
|
|
221
|
-
'.mcp.json',
|
|
222
|
-
];
|
|
223
|
-
|
|
224
|
-
function treeDigest(root, relative) {
|
|
225
|
-
const target = path.join(root, relative);
|
|
226
|
-
let stat;
|
|
227
|
-
try { stat = fs.lstatSync(target); } catch { return '<missing>'; }
|
|
228
|
-
if (stat.isSymbolicLink()) return '<symlink>';
|
|
229
|
-
if (stat.isFile()) return `file:${crypto.createHash('sha256').update(fs.readFileSync(target)).digest('hex')}`;
|
|
230
|
-
if (!stat.isDirectory()) return `<special:${stat.mode}>`;
|
|
231
|
-
const entries = fs.readdirSync(target, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
232
|
-
return `dir:${entries.map((entry) => `${entry.name}:${treeDigest(target, entry.name)}`).join('|')}`;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export function shellDiff(prevRootAbs, nextRootAbs) {
|
|
236
|
-
const changed = [];
|
|
237
|
-
for (const rel of SHELL_PATHS) {
|
|
238
|
-
if (treeDigest(prevRootAbs, rel) !== treeDigest(nextRootAbs, rel)) changed.push(rel);
|
|
239
|
-
}
|
|
240
|
-
// skills/ and commands/ are boot-loaded markdown: any file-set or content difference counts.
|
|
241
|
-
for (const dir of ['skills', 'commands']) {
|
|
242
|
-
if (treeDigest(prevRootAbs, dir) !== treeDigest(nextRootAbs, dir)) changed.push(`${dir}/`);
|
|
243
|
-
}
|
|
244
|
-
return changed;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
217
|
function flip(version, codeRootAbs, why) {
|
|
248
218
|
const prev = readJSON(ACTIVE);
|
|
249
219
|
const prevRootAbs = prev?.codeRoot ? (path.isAbsolute(prev.codeRoot) ? prev.codeRoot : path.join(BRAIN_HOME, prev.codeRoot)) : null;
|