total-recall-brain 3.43.0 → 3.43.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.43.0",
3
+ "version": "3.43.2",
4
4
  "description": "The Vibe Coding Brain & Autonomous Software Factory: memory, rules, secrets, and 33 visual plugins automatically managed across all your devices.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "tr-plugin-ssss",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "tr-plugin-ssss",
9
- "version": "0.1.1",
9
+ "version": "0.1.2",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@gregiteen/ssss-cli": "0.14.4"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tr-plugin-ssss",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Canonical SSSS distribution, scaffolding and validation for Total Recall installations.",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://github.com/gregiteen/total-recall/blob/main/metadata.plugin.schema.json",
3
3
  "id": "ssss",
4
4
  "name": "SSSS",
5
- "version": "0.1.1",
5
+ "version": "0.1.2",
6
6
  "description": "Canonical SSSS distribution, scaffolding and validation for Total Recall installations.",
7
7
  "license": "MIT",
8
8
  "use_cases": ["software-development", "state-conformance"],
@@ -99,16 +99,25 @@ function inspectScaffold(dir, source) {
99
99
  return { ok: true, path: dir, files: Object.keys(expected.files).length };
100
100
  }
101
101
 
102
- function skillRootFor(target, sharedBrainDir) {
103
- if (!stat(target)?.isSymbolicLink()) return target;
104
- const sharedSkill = sharedBrainDir && path.join(path.dirname(sharedBrainDir), 'ssss');
105
- const resolved = fs.realpathSync(target);
106
- if (!sharedSkill || resolved !== sharedSkill) throw new Error('SSSS skill points outside the shared skill installation');
107
- return resolved;
102
+ const realOrNull = file => { try { return fs.realpathSync(file); } catch { return null; } };
103
+ /** A path with its folders resolved and its last part kept, so a link is compared as itself. */
104
+ const entryOf = file => path.join(realOrNull(path.dirname(file)) ?? path.dirname(file), path.basename(file));
105
+
106
+ /**
107
+ * True for a project's link to the machine's shared skill. A link at the shared location itself
108
+ * is not one: that location is what has to hold the skill.
109
+ */
110
+ function linksToSharedSkill(target, sharedBrainDir) {
111
+ if (!sharedBrainDir) return false;
112
+ const shared = path.join(path.dirname(sharedBrainDir), 'ssss');
113
+ if (entryOf(shared) === entryOf(target)) return false;
114
+ const resolved = realOrNull(target);
115
+ return resolved !== null && resolved === realOrNull(shared);
108
116
  }
109
117
 
110
- function inspectSkill(target, source, sharedBrainDir) {
111
- const dir = skillRootFor(target, sharedBrainDir);
118
+ /** A link that reaches a complete, current skill is left alone, like a linked plugin. */
119
+ function inspectSkill(target, source) {
120
+ const dir = stat(target)?.isSymbolicLink() ? fs.realpathSync(target) : target;
112
121
  requireDirectory(dir);
113
122
  const wrapper = fs.readFileSync(regularPath(dir, 'SKILL.md'), 'utf8');
114
123
  if (!/^name:\s*['"]?ssss['"]?\s*$/m.test(wrapper) || !wrapper.includes('core/SKILL.md')) {
@@ -164,31 +173,63 @@ function writeAsset(root, relative, content) {
164
173
  fs.writeFileSync(file, content, { flag: 'wx' });
165
174
  }
166
175
 
176
+ const NAMES_THIS_SKILL = /^name:\s*['"]?ssss['"]?\s*$/m;
177
+
178
+ /** Fill a staged skill: any files already there stay, and the portable core is always replaced. */
179
+ function completeSkill(stage, source) {
180
+ fs.rmSync(path.join(stage, 'core'), { recursive: true, force: true });
181
+ for (const relative of Object.keys(source.skill)) {
182
+ if (!stat(regularPath(stage, relative))) {
183
+ writeAsset(stage, relative, fs.readFileSync(regularPath(source.skillRoot, relative)));
184
+ }
185
+ }
186
+ const wrapper = path.join(stage, 'SKILL.md');
187
+ if (!fs.readFileSync(wrapper, 'utf8').includes('core/SKILL.md')) fs.appendFileSync(wrapper, ROUTE);
188
+ inspectSkill(stage, source);
189
+ }
190
+
191
+ /**
192
+ * An older layout linked the skill in, and the link is now dangling or stale. Only the link is
193
+ * replaced: nothing is written through it, so whatever it pointed at stays exactly as it was.
194
+ * A wrapper with this skill's name carries over, because it can hold repository guidance.
195
+ */
196
+ function replaceLink(target, source) {
197
+ const pointsAt = fs.readlinkSync(target);
198
+ let wrapper = null;
199
+ try {
200
+ const text = fs.readFileSync(path.join(target, 'SKILL.md'), 'utf8');
201
+ if (NAMES_THIS_SKILL.test(text)) wrapper = text;
202
+ } catch { /* a dangling link or a folder with no entry has no wrapper to keep */ }
203
+ publishDirectory(target, stage => {
204
+ if (wrapper) writeAsset(stage, 'SKILL.md', wrapper);
205
+ completeSkill(stage, source);
206
+ }, true);
207
+ return { path: target, target: pointsAt };
208
+ }
209
+
210
+ /** @returns {{ path: string, target: string } | undefined} the link it replaced, if it replaced one */
167
211
  function installSkill(target, source, sharedBrainDir) {
168
- const dir = skillRootFor(target, sharedBrainDir);
169
- if (dir !== target) return inspectSkill(target, source, sharedBrainDir);
170
- checkRoot(dir);
171
- const exists = Boolean(stat(dir));
212
+ if (stat(target)?.isSymbolicLink()) {
213
+ if (!linksToSharedSkill(target, sharedBrainDir)) return replaceLink(target, source);
214
+ // The shared skill is repaired where it lives. A project does not take a private copy.
215
+ inspectSkill(target, source);
216
+ return undefined;
217
+ }
218
+ checkRoot(target);
219
+ const exists = Boolean(stat(target));
172
220
  // Stage the complete skill so a failed source read cannot leave a partial skill.
173
221
  if (exists) {
174
- const wrapper = regularPath(dir, 'SKILL.md');
175
- if (stat(wrapper) && !/^name:\s*['"]?ssss['"]?\s*$/m.test(fs.readFileSync(wrapper, 'utf8'))) {
176
- throw new Error('Existing skill directory belongs to another skill: ' + dir);
222
+ const wrapper = regularPath(target, 'SKILL.md');
223
+ if (stat(wrapper) && !NAMES_THIS_SKILL.test(fs.readFileSync(wrapper, 'utf8'))) {
224
+ throw new Error('Existing skill directory belongs to another skill: ' + target);
177
225
  }
178
- listFiles(dir);
226
+ listFiles(target);
179
227
  }
180
- publishDirectory(dir, stage => {
181
- if (exists) fs.cpSync(dir, stage, { recursive: true });
182
- fs.rmSync(path.join(stage, 'core'), { recursive: true, force: true });
183
- for (const relative of Object.keys(source.skill)) {
184
- if (!stat(regularPath(stage, relative))) {
185
- writeAsset(stage, relative, fs.readFileSync(regularPath(source.skillRoot, relative)));
186
- }
187
- }
188
- const wrapper = path.join(stage, 'SKILL.md');
189
- if (!fs.readFileSync(wrapper, 'utf8').includes('core/SKILL.md')) fs.appendFileSync(wrapper, ROUTE);
190
- inspectSkill(stage, source);
228
+ publishDirectory(target, stage => {
229
+ if (exists) fs.cpSync(target, stage, { recursive: true });
230
+ completeSkill(stage, source);
191
231
  }, exists);
232
+ return undefined;
192
233
  }
193
234
 
194
235
  function hasUpdaterDrift(dir, source, receipt) {
@@ -247,7 +288,7 @@ function inspectInstalled(options, source) {
247
288
  catch (error) { return { ok: false, path: target, error: error.message }; }
248
289
  };
249
290
  const observed = {
250
- skill: check(skill, () => inspectSkill(skill, source, options.sharedBrainDir)),
291
+ skill: check(skill, () => inspectSkill(skill, source)),
251
292
  scaffold: check(scaffold, () => {
252
293
  checkRoot(options.brainDir);
253
294
  checkRoot(path.dirname(scaffold));
@@ -269,10 +310,10 @@ function ensureInstallation(options) {
269
310
  checkRoot(options.brainDir);
270
311
  const current = inspectInstalled(options, source);
271
312
  const recoveredFrom = current.scaffold.ok ? undefined : installScaffold(scaffold, source, options.repair === true);
272
- if (!current.skill.ok) installSkill(skill, source, options.sharedBrainDir);
313
+ const replacedLink = current.skill.ok ? undefined : installSkill(skill, source, options.sharedBrainDir);
273
314
  const installed = inspectInstalled(options, source);
274
315
  if (!installed.ok) throw new Error([installed.skill.error, installed.scaffold.error].filter(Boolean).join('; '));
275
- return recoveredFrom ? { ...installed, recoveredFrom } : installed;
316
+ return { ...installed, ...(recoveredFrom ? { recoveredFrom } : {}), ...(replacedLink ? { replacedLink } : {}) };
276
317
  }
277
318
 
278
319
  async function maintain({ brainDir, entries, apply, run }) {
@@ -197,11 +197,13 @@ export function consumerState(root, pkg, version = null) {
197
197
  const declaredFiles = [];
198
198
  const pkgFile = path.join(root, 'package.json');
199
199
  let declared = null;
200
+ let devOnly = false;
200
201
  if (fs.existsSync(pkgFile)) {
201
202
  try {
202
203
  const j = JSON.parse(fs.readFileSync(pkgFile, 'utf8'));
203
204
  const all = { ...(j.dependencies || {}), ...(j.devDependencies || {}), ...(j.optionalDependencies || {}) };
204
205
  declared = all[pkg] ?? null;
206
+ devOnly = Boolean(j.devDependencies?.[pkg]) && !j.dependencies?.[pkg] && !j.optionalDependencies?.[pkg];
205
207
  if (declared) declaredFiles.push(pkgFile);
206
208
  } catch {
207
209
  /* unreadable package.json is reported as unknown, not as satisfied */
@@ -243,7 +245,15 @@ export function consumerState(root, pkg, version = null) {
243
245
  /* keep looking */
244
246
  }
245
247
  }
246
- const satisfied = range ? Boolean(installed) && installed === version : Boolean(want && installed === want);
248
+ // A checkout with no copy of a dev dependency runs under the shared install. Nothing there is stale.
249
+ if (!installed && devOnly) {
250
+ return { root, declared, want, range, installed: null, satisfied: true, pinBehind: false, devNotInstalled: true, reason: 'dev-not-installed' };
251
+ }
252
+ // What a consumer runs is what counts, and the version named is the question. A pin can lag on
253
+ // purpose, because `update` installs plugin copies with --no-save. Comparing against the pin
254
+ // would fail those and pass a consumer whose pin and install both lag.
255
+ const expected = version || want;
256
+ const satisfied = Boolean(expected && installed === expected);
247
257
  return {
248
258
  root,
249
259
  declared,
@@ -251,6 +261,7 @@ export function consumerState(root, pkg, version = null) {
251
261
  range,
252
262
  installed,
253
263
  satisfied,
264
+ pinBehind: Boolean(satisfied && want && want !== installed),
254
265
  reason: !installed ? 'not-installed' : satisfied ? 'ok' : 'installed-mismatch',
255
266
  };
256
267
  }
@@ -279,6 +290,8 @@ export function verifyConsumers(pkg, version, opts = {}) {
279
290
  consumers: relevant.length,
280
291
  failing: failing.length,
281
292
  failures: failing.map((s) => ({ root: s.root, declared: s.declared, installed: s.installed, reason: s.reason })),
293
+ pin_behind: relevant.filter((s) => s.pinBehind).map((s) => ({ root: s.root, declared: s.declared, installed: s.installed })),
294
+ dev_not_installed: relevant.filter((s) => s.devNotInstalled).map((s) => s.root),
282
295
  };
283
296
  }
284
297
 
@@ -362,6 +375,8 @@ export async function run(argv = process.argv, deps = {}) {
362
375
  return fail(`${pkg}@${version}: ${consumers.failing}/${consumers.consumers} consumers not satisfied. First: ${consumers.failures[0].root} (${consumers.failures[0].installed ?? 'not installed'}).`);
363
376
  }
364
377
  log(` ✓ ${pkg}@${version} verified: registry + ${consumers.consumers}/${consumers.consumers} consumers`);
378
+ if (consumers.pin_behind.length) log(` ${consumers.pin_behind.length} run it with an older pin in package.json, such as ${consumers.pin_behind[0].root}`);
379
+ if (consumers.dev_not_installed.length) log(` ${consumers.dev_not_installed.length} keep no dev copy of it and run under the shared install`);
365
380
  continue;
366
381
  }
367
382
 
@@ -40,7 +40,9 @@ export async function runAutomaticBrainUpdates({brainDir,fleet=false,force=false
40
40
  if(!force && !isPackageAutoUpdateEnabled())return {skipped:true,reason:'disabled',results:[]};
41
41
  const file=path.join(brainDir,'memory-derived/automatic-brain-updates.json');
42
42
  let saved;try{saved=JSON.parse(fs.readFileSync(file,'utf8'));}catch{}
43
- const delay=saved?.failed?Math.min(intervalMs,5*60*1000):intervalMs;
43
+ // Fast retry is for this machine's own failures. A peer with no update transport fails every
44
+ // run, and counting it made maintenance run back to back forever.
45
+ const delay=(saved?.local_failed??saved?.failed)?Math.min(intervalMs,5*60*1000):intervalMs;
44
46
  if(!force && !skipThrottle && saved?.checkedAt && now()>=saved.checkedAt && now()-saved.checkedAt<delay)return {skipped:true,reason:'throttled',results:[]};
45
47
  fs.mkdirSync(path.dirname(file),{recursive:true});
46
48
  const lock=`${file}.lock`;
@@ -185,6 +185,22 @@ function targetSpec(key, version) {
185
185
  }
186
186
 
187
187
  /** Dry by default. Resolve releases once; install independently; verify actual resolution. */
188
+ /**
189
+ * A checkout that keeps no copy of a dev dependency runs under the shared install, so it has no
190
+ * old version to replace. Its pin stays current, and its dev tree is its owner's to install: one
191
+ * plugin's tree is about 460 MB, so restoring them all automatically could fill a disk.
192
+ */
193
+ const absentDevDep = (dep) => dep.section === 'devDependencies' && !dep.file;
194
+
195
+ /**
196
+ * The daemon runs with NODE_ENV=production, and under that npm and pnpm remove dev dependencies
197
+ * from whatever they install in. An install that wants a runtime tree says so with a flag.
198
+ */
199
+ function installEnv(env = process.env) {
200
+ const { NODE_ENV, npm_config_omit, NPM_CONFIG_OMIT, npm_config_production, NPM_CONFIG_PRODUCTION, ...kept } = env;
201
+ return kept;
202
+ }
203
+
188
204
  export async function updateAllBrains(opts = {}) {
189
205
  const dryRun = opts.dryRun !== false;
190
206
  const exec = opts.exec || spawnSync;
@@ -245,9 +261,12 @@ export async function updateAllBrains(opts = {}) {
245
261
  row.status = 'failed'; row.error = 'Newer local/preview package; refusing an implicit downgrade'; continue;
246
262
  }
247
263
  const shared = isLink(path.join(owner.owner, 'node_modules')) || isLink(path.join(consumer.root, 'node_modules'));
248
- const needsChange = shared || consumer.deps.some(dep => dep.version !== versions[dep.package]
249
- || (!consumer.runtimeOnly && dep.declared !== targetSpec(dep.key, versions[dep.package]))
250
- || !dep.file?.startsWith(path.join(owner.owner, 'node_modules') + path.sep));
264
+ const needsChange = shared || consumer.deps.some(dep => {
265
+ const pinLags = !consumer.runtimeOnly && dep.declared !== targetSpec(dep.key, versions[dep.package]);
266
+ if (absentDevDep(dep)) return pinLags;
267
+ return pinLags || dep.version !== versions[dep.package]
268
+ || !dep.file?.startsWith(path.join(owner.owner, 'node_modules') + path.sep);
269
+ });
251
270
  if (!needsChange) continue;
252
271
  row.status = dryRun ? 'would_update' : 'pending';
253
272
  if (!groups.has(owner.owner)) groups.set(owner.owner, { ...owner, consumers: [] });
@@ -264,8 +283,8 @@ export async function updateAllBrains(opts = {}) {
264
283
  const runtimeOnly = group.consumers.every(({ consumer }) => consumer.runtimeOnly);
265
284
  const unpinned = runtimeOnly && group.manager === 'npm';
266
285
  const needsInstall = group.consumers.some(({ consumer }) =>
267
- isLink(path.join(consumer.root, 'node_modules')) || consumer.deps.some(dep =>
268
- dep.version !== versions[dep.package] || !dep.file?.startsWith(path.join(group.owner, 'node_modules') + path.sep)));
286
+ isLink(path.join(consumer.root, 'node_modules')) || consumer.deps.some(dep => !absentDevDep(dep)
287
+ && (dep.version !== versions[dep.package] || !dep.file?.startsWith(path.join(group.owner, 'node_modules') + path.sep))));
269
288
  try {
270
289
  lockFd = acquireBrainUpdateLock(lock, opts.lockWaitMs === undefined ? {} : { waitMs: opts.lockWaitMs });
271
290
  for (const name of ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml']) {
@@ -294,7 +313,7 @@ export async function updateAllBrains(opts = {}) {
294
313
  if (unpinned) args.push('--no-save', ...new Set(group.consumers.flatMap(({ consumer }) =>
295
314
  consumer.deps.map(dep => `${dep.key}@${targetSpec(dep.key, versions[dep.package])}`))));
296
315
  else if (!needsInstall) args.push(group.manager === 'pnpm' ? '--lockfile-only' : '--package-lock-only');
297
- const install = exec(group.manager, args, { cwd: group.owner, encoding: 'utf8',
316
+ const install = exec(group.manager, args, { cwd: group.owner, env: installEnv(), encoding: 'utf8',
298
317
  timeout: 600000, maxBuffer: 8 * 1024 * 1024 });
299
318
  if (install.status !== 0) throw new Error(install.error?.message || install.stderr?.trim().slice(-1500) || `Install exited ${install.status}`);
300
319
  for (const { consumer, row } of group.consumers) {
@@ -309,6 +328,7 @@ export async function updateAllBrains(opts = {}) {
309
328
  const placed = (file) => Boolean(file) && allowedRoots.some((root) => file.startsWith(root));
310
329
  const bad = consumer.deps.find(dep => {
311
330
  const actual = installedBrainPackage(consumer.root, dep.key);
331
+ if (absentDevDep(dep) && !actual.file) return false;
312
332
  return actual.version !== versions[dep.package] || !placed(actual.file);
313
333
  });
314
334
  // A link is only a fault when it escapes every legitimate root; a self-contained one is fine.