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 +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package-lock.json +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/ssss/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/ssss/runtime/installation.cjs +71 -30
- package/src/cli/release.mjs +16 -1
- package/src/core/automatic-brain-updates.mjs +3 -1
- package/src/core/brain-updates.mjs +26 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "total-recall-brain",
|
|
3
|
-
"version": "3.43.
|
|
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.
|
|
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.
|
|
9
|
+
"version": "0.1.2",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@gregiteen/ssss-cli": "0.14.4"
|
|
@@ -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.
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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(
|
|
175
|
-
if (stat(wrapper) &&
|
|
176
|
-
throw new Error('Existing skill directory belongs to another skill: ' +
|
|
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(
|
|
226
|
+
listFiles(target);
|
|
179
227
|
}
|
|
180
|
-
publishDirectory(
|
|
181
|
-
if (exists) fs.cpSync(
|
|
182
|
-
|
|
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
|
|
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
|
-
|
|
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 ? {
|
|
316
|
+
return { ...installed, ...(recoveredFrom ? { recoveredFrom } : {}), ...(replacedLink ? { replacedLink } : {}) };
|
|
276
317
|
}
|
|
277
318
|
|
|
278
319
|
async function maintain({ brainDir, entries, apply, run }) {
|
package/src/cli/release.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =>
|
|
249
|
-
|
|
250
|
-
|
|
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.
|