total-recall-brain 3.47.0 → 3.47.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/bin/total-recall.mjs +5 -0
- package/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +30 -5
- package/scaffold/.agent/skills/total-recall/plugins/auth/skills/auth/SKILL.md +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/home.js +494 -79
- package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +33 -8
- package/scaffold/.agent/skills/total-recall/plugins/automations/skills/automations/SKILL.md +29 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/dream-consolidation.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +31 -4
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/skills/code-quality/SKILL.md +27 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +17 -3
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +24 -4
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/skills/embeddings/SKILL.md +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/git/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +293 -58
- package/scaffold/.agent/skills/total-recall/plugins/git/skills/git/SKILL.md +36 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +47 -14
- package/scaffold/.agent/skills/total-recall/plugins/mesh/skills/mesh/SKILL.md +36 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +6 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +4 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +28 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +8 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/included-usage.mjs +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +7 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/model-selection.mjs +59 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +11 -5
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +50 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/session-controls.md +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +56 -7
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +199 -36
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/skills/openwiki/SKILL.md +32 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +34 -8
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +6 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +15 -7
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +4 -3
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +22 -5
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/ssss/plugin.json +169 -37
- package/scaffold/.agent/skills/total-recall/plugins/ssss/skills/ssss/SKILL.md +30 -0
- package/src/cli/plugin/create.mjs +2 -1
- package/src/cli/plugin/monetization.mjs +66 -32
- package/src/cli/plugin/search.mjs +25 -3
- package/src/cli/preflight.mjs +40 -6
- package/src/cli/update.mjs +20 -1
- package/src/core/automatic-brain-updates.mjs +8 -4
- package/src/core/brain-updates.mjs +25 -6
- package/src/core/gate-mode.mjs +13 -24
- package/src/core/plugin-distribution.mjs +35 -1
- package/src/core/preflight-gate.mjs +28 -8
- package/src/core/preflight-receipt.mjs +11 -0
- package/src/core/release-defaults-handoff.mjs +89 -0
package/src/cli/preflight.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { globalBrainDirFor, isBrainEnabled } from '../core/brain-registry.mjs';
|
|
|
7
7
|
import { GATE_MODES, gateConfigPath, projectBrainDir, readGateLayers, resolveGateMode, writeGateMode } from '../core/gate-mode.mjs';
|
|
8
8
|
import { installHarnessHooks, loadGateAdapters } from '../core/harness-hooks.mjs';
|
|
9
9
|
import { ackDirectory, capsuleVerb, decide, gatePackageBroken, isAckCommand, isCheckCommand, isRouteCommand, isSubagentPayload, nativeToolOf, sessionOf, SWEEP_CONSULT_MS, whenGateFails } from '../core/preflight-gate.mjs';
|
|
10
|
-
import { ACK_COMMAND, acknowledge, checkAgentReceipt, checkAnyReceipt, findRoot, forgetCapsuleDeliveries, lastArrivalMs, lastCapsuleDelivery, lastConsultMs, lastRoutedMs, lastRouteWarnedMs, lastSessionAck, markArrival, markDelegated, markRouted, markRouteWarned, markSessionAck, noteCaller, recordDecision, sessionHasStanding, sessionKey, readDecisions, takeCaller } from '../core/preflight-receipt.mjs';
|
|
10
|
+
import { ACK_COMMAND, acknowledge, checkAgentReceipt, checkAnyReceipt, findRoot, forgetCapsuleDeliveries, lastArrivalMs, lastCapsuleDelivery, lastConsultMs, lastRoutedMs, lastRouteSelection, lastRouteWarnedMs, lastSessionAck, markArrival, markDelegated, markRouted, markRouteSelection, markRouteWarned, markSessionAck, noteCaller, recordDecision, sessionHasStanding, sessionKey, readDecisions, takeCaller } from '../core/preflight-receipt.mjs';
|
|
11
11
|
|
|
12
12
|
const USAGE = `Usage: total-recall preflight <command>
|
|
13
13
|
ack --task "<task>" [--action read,edit,test] [--json] Record a receipt after context ready:true and rules verify, and print the capsule.
|
|
@@ -19,9 +19,9 @@ const USAGE = `Usage: total-recall preflight <command>
|
|
|
19
19
|
remind [--json] Prompt-hook entry: advisory text when the receipt is missing; always exits 0
|
|
20
20
|
session-start Session-hook entry: after compaction or clear, the next ack prints the capsule; always exits 0
|
|
21
21
|
install [--harness <id>|--all] [--dry-run] Install the gate for a harness, or for every harness here. Install and update do this. No off switch.
|
|
22
|
-
mode [
|
|
23
|
-
Show
|
|
24
|
-
|
|
22
|
+
mode [enforce] [--plugin <id>|--router] [--repo] [--json]
|
|
23
|
+
Show gate enforcement status. All plugin and routing gates are permanently
|
|
24
|
+
enforced; off, warn, and clear bypasses are disabled.
|
|
25
25
|
coverage [--dir <folder>] [--json] List plugins as declared or missing; exit 1 on a gap. Default: the shared plugin folder
|
|
26
26
|
verify | report Harness adapters, owned by the cli-agents plugin
|
|
27
27
|
Aliases: total-recall tr-preflight --ack | --check. A receipt lasts 30 minutes (config preflight_max_age_minutes).
|
|
@@ -397,6 +397,33 @@ function projectBrainRoot(dir) {
|
|
|
397
397
|
return brain && isBrainEnabled(brain.brainDir) ? brain.projectRoot : null;
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
+
/** How long a route's selection binds when the receipt names no window: the default receipt lifetime. */
|
|
401
|
+
const ROUTE_SELECTION_MS = 30 * 60000;
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Records which harness a router command selected, so the gate can deny writes from the others.
|
|
405
|
+
* Called after the router plugin's own command runs, with what it printed. Output that is not a
|
|
406
|
+
* route result records nothing. Bookkeeping only: it never throws.
|
|
407
|
+
* @returns {string|null|undefined} the selected harness, null when the route chose none, undefined when nothing was recorded
|
|
408
|
+
*/
|
|
409
|
+
export function noteRouteSelection({ manifest, args = [], stdout = '', cwd = process.cwd() } = {}) {
|
|
410
|
+
try {
|
|
411
|
+
const router = manifest?.gate?.router;
|
|
412
|
+
if (!router?.pattern) return undefined;
|
|
413
|
+
const command = `${manifest.cli?.command || manifest.id} ${args.map(String).join(' ')}`;
|
|
414
|
+
if (!new RegExp(`(?:^|\\s)(?:${router.pattern})(\\s|$)`).test(command)) return undefined;
|
|
415
|
+
const text = String(stdout || '').trim();
|
|
416
|
+
let result;
|
|
417
|
+
try { result = JSON.parse(text); } catch { result = JSON.parse(text.split('\n').pop()); }
|
|
418
|
+
if (!result || typeof result !== 'object' || !('selected' in result)) return undefined;
|
|
419
|
+
const selected = typeof result.selected === 'string' && result.selected ? result.selected : null;
|
|
420
|
+
const root = findRoot(cwd);
|
|
421
|
+
markRouted(root);
|
|
422
|
+
markRouteSelection(root, selected);
|
|
423
|
+
return selected;
|
|
424
|
+
} catch { return undefined; }
|
|
425
|
+
}
|
|
426
|
+
|
|
400
427
|
/** The gate's answer to a call. Nothing here may throw, because an answer is all that keeps its repair reachable. */
|
|
401
428
|
function evaluate(call, options = {}) {
|
|
402
429
|
try {
|
|
@@ -450,6 +477,13 @@ function judge(call, { harness, readablePaths, session = null, subagent = false,
|
|
|
450
477
|
ownerHealthy: (plugin, use) => ownerRuns(plugin, use, root),
|
|
451
478
|
// A subagent or a run the router started reports to whoever routed the work.
|
|
452
479
|
dispatched: runByRouter || subagent || Boolean(session && !ownAck),
|
|
480
|
+
// Only a named harness can be told apart from the one the router chose.
|
|
481
|
+
harness: harness && harness !== 'generic' ? harness : null,
|
|
482
|
+
routerRun: runByRouter,
|
|
483
|
+
selection: (receipt) => {
|
|
484
|
+
const chosen = lastRouteSelection(root);
|
|
485
|
+
return chosen && Date.now() - chosen.at < (limit ?? receipt?.limitMs ?? ROUTE_SELECTION_MS) ? chosen : null;
|
|
486
|
+
},
|
|
453
487
|
check,
|
|
454
488
|
recentConsult: () => {
|
|
455
489
|
const now = Date.now();
|
|
@@ -569,14 +603,14 @@ async function mode(args) {
|
|
|
569
603
|
const repo = args.includes('--repo');
|
|
570
604
|
const brain = repo ? projectBrainDir(root) : globalBrainDirFor();
|
|
571
605
|
if (value !== undefined) {
|
|
572
|
-
if (value !== '
|
|
606
|
+
if (value !== 'enforce') return fail(`✖ Plugin and routing gates are permanently enforced and have no "${value}" bypass.`);
|
|
573
607
|
const plugin = flag(args, '--plugin');
|
|
574
608
|
const router = args.includes('--router');
|
|
575
609
|
if (plugin && router) return fail('Choose --plugin or --router, not both.');
|
|
576
610
|
if (plugin && !plugins.includes(plugin)) return fail(`No plugin here declares raw commands under "${plugin}". Declared: ${plugins.join(', ') || 'none'}.`);
|
|
577
611
|
if (router && !declared.router) return fail('No plugin here declares a router.');
|
|
578
612
|
if (repo && !fs.existsSync(brain)) return fail(`${root} has no project brain to hold this setting. Run total-recall init there, or leave off --repo.`);
|
|
579
|
-
writeGateMode(brain, { mode:
|
|
613
|
+
writeGateMode(brain, { mode: 'enforce', plugin, router });
|
|
580
614
|
}
|
|
581
615
|
const layers = readGateLayers({ globalBrainDir: globalBrainDirFor(), root });
|
|
582
616
|
const { adapters } = await loadGateAdapters(root);
|
package/src/cli/update.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
} from '../core/package-auto-update.mjs';
|
|
18
18
|
import { brainDir } from '../core/config.mjs';
|
|
19
19
|
import { installAutoPull } from '../core/autopull-install.mjs';
|
|
20
|
-
import { updateAllBrains } from '../core/brain-updates.mjs';
|
|
20
|
+
import { updateAllBrains, ensureBrainDefaults } from '../core/brain-updates.mjs';
|
|
21
21
|
import { installHarnessHooks } from '../core/harness-hooks.mjs';
|
|
22
22
|
import { automaticUpdateStatus, runAutomaticBrainUpdates } from '../core/automatic-brain-updates.mjs';
|
|
23
23
|
import { runningServerStatus } from '../core/running-server.mjs';
|
|
@@ -38,6 +38,7 @@ export function parseArgs(args) {
|
|
|
38
38
|
automatic: false,
|
|
39
39
|
status: false,
|
|
40
40
|
localOnly: false,
|
|
41
|
+
defaults: false,
|
|
41
42
|
};
|
|
42
43
|
for (let i = 0; i < args.length; i++) {
|
|
43
44
|
const a = args[i];
|
|
@@ -55,6 +56,7 @@ export function parseArgs(args) {
|
|
|
55
56
|
else if (a === '--automatic') opts.automatic = true;
|
|
56
57
|
else if (a === '--status') opts.status = true;
|
|
57
58
|
else if (a === '--local-only') opts.localOnly = true;
|
|
59
|
+
else if (a === '--defaults') opts.defaults = true;
|
|
58
60
|
}
|
|
59
61
|
return opts;
|
|
60
62
|
}
|
|
@@ -85,6 +87,12 @@ function printHelp() {
|
|
|
85
87
|
installed plugin from its recorded source and refresh surfaces. Where a
|
|
86
88
|
per-user service (LaunchAgent, systemd user unit) runs the server from
|
|
87
89
|
the global install, also restart a stale server and verify /health.
|
|
90
|
+
When this run installed a release newer than the running code, that release
|
|
91
|
+
refreshes the defaults with its own code (update --defaults) in the same pass.
|
|
92
|
+
total-recall update --defaults [--repo <path>] [--json]
|
|
93
|
+
Refresh the shared default plugins from this release's bundled copies (a
|
|
94
|
+
linked owner checkout is left alone) and provision every registered brain.
|
|
95
|
+
Exit 1 when any brain fails.
|
|
88
96
|
total-recall update --status [--json]
|
|
89
97
|
Read the actual last automatic maintenance report without starting work.
|
|
90
98
|
Also reports what the server on this host runs against the release.
|
|
@@ -121,6 +129,17 @@ export default async function update(args = []) {
|
|
|
121
129
|
console.log(JSON.stringify(status,null,2));
|
|
122
130
|
return status;
|
|
123
131
|
}
|
|
132
|
+
if (opts.defaults) {
|
|
133
|
+
const report = ensureBrainDefaults({ brainDir, roots: opts.repos });
|
|
134
|
+
if (opts.json) console.log(JSON.stringify(report, null, 2));
|
|
135
|
+
else {
|
|
136
|
+
console.log(`\n Default plugins: ${report.results.length - report.failed} brain(s) ready, ${report.failed} failed`);
|
|
137
|
+
for (const row of report.results) console.log(` ${row.status === 'ready' ? '✓' : '✗'} ${row.root}${row.error ? `: ${row.error}` : ''}`);
|
|
138
|
+
console.log('');
|
|
139
|
+
}
|
|
140
|
+
if (report.failed) process.exitCode = 1;
|
|
141
|
+
return report;
|
|
142
|
+
}
|
|
124
143
|
if(opts.automatic){
|
|
125
144
|
const report=await runAutomaticBrainUpdates({brainDir,force:opts.force,fleet:!opts.localOnly});
|
|
126
145
|
console.log(JSON.stringify(report,null,2));
|
|
@@ -14,6 +14,7 @@ import { updateSourceBrainPackages } from './source-brain-updates.mjs';
|
|
|
14
14
|
import { updateInstalledPlugins } from './plugin-updates.mjs';
|
|
15
15
|
import { runningServerStatus } from './running-server.mjs';
|
|
16
16
|
import { replaceStaleServer } from './server-restart.mjs';
|
|
17
|
+
import { handOffDefaultsToRelease } from './release-defaults-handoff.mjs';
|
|
17
18
|
|
|
18
19
|
export function automaticUpdateStatus(brainDir) {
|
|
19
20
|
let last;
|
|
@@ -38,7 +39,7 @@ export function startAutomaticBrainUpdates({brainDir,intervalMs=60*60*1000,run=r
|
|
|
38
39
|
return {tick,stop(){stopped=true;clearInterval(timer);}};
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
export async function runAutomaticBrainUpdates({brainDir,fleet=false,force=false,skipThrottle=false,intervalMs=60*60*1000,update=updateAllBrains,compile=maintainCliSurfaces,globalUpdate=updateGlobalBrainPackages,meshUpdate=updateMeshBrainPackages,sourceUpdate=updateSourceBrainPackages,pluginUpdate=updateInstalledPlugins,skillMaintenance=maintainPortableSkills,serverCheck=runningServerStatus,replaceServer=replaceStaleServer,now=Date.now}={}) {
|
|
42
|
+
export async function runAutomaticBrainUpdates({brainDir,fleet=false,force=false,skipThrottle=false,intervalMs=60*60*1000,update=updateAllBrains,compile=maintainCliSurfaces,globalUpdate=updateGlobalBrainPackages,meshUpdate=updateMeshBrainPackages,sourceUpdate=updateSourceBrainPackages,pluginUpdate=updateInstalledPlugins,defaultsHandoff=handOffDefaultsToRelease,skillMaintenance=maintainPortableSkills,serverCheck=runningServerStatus,replaceServer=replaceStaleServer,now=Date.now}={}) {
|
|
42
43
|
if(!force && !isPackageAutoUpdateEnabled())return {skipped:true,reason:'disabled',results:[]};
|
|
43
44
|
const file=path.join(brainDir,'memory-derived/automatic-brain-updates.json');
|
|
44
45
|
let saved;try{saved=JSON.parse(fs.readFileSync(file,'utf8'));}catch{}
|
|
@@ -62,10 +63,13 @@ export async function runAutomaticBrainUpdates({brainDir,fleet=false,force=false
|
|
|
62
63
|
const summary=await update({brainDir,dryRun:false,ensureDefaults:true});
|
|
63
64
|
const global=summary.versions?await globalUpdate({versions:summary.versions}):{failed:1,results:[],error:'Cannot maintain global packages without verified published versions'};
|
|
64
65
|
const sources=summary.versions?await sourceUpdate({brainDir,versions:summary.versions}):{failed:1,results:[]};
|
|
66
|
+
// The defaults above came from the running code, which is the release being replaced. A newer
|
|
67
|
+
// release installed just now refreshes them with its own code and bundled copies, this pass.
|
|
68
|
+
const defaults=await defaultsHandoff({global,sources});
|
|
65
69
|
// The shared plugin folder follows the source each plugin was installed from (git, mesh peer, directory).
|
|
66
70
|
const plugins=await pluginUpdate({roots:[path.resolve(brainDir,'../../..')]});
|
|
67
71
|
const mesh=fleet && summary.versions?await meshUpdate({versions:summary.versions,version:summary.versions['total-recall-brain']}):{failed:0,results:[],scope:'local-only'};
|
|
68
|
-
const changed=summary.updated>0 || plugins.updated>0 || global.results.some(row=>row.status==='updated') || sources.results.some(row=>row.status==='updated') || JSON.stringify(saved?.versions)!==JSON.stringify(summary.versions);
|
|
72
|
+
const changed=summary.updated>0 || plugins.updated>0 || defaults.status==='refreshed' || global.results.some(row=>row.status==='updated') || sources.results.some(row=>row.status==='updated') || JSON.stringify(saved?.versions)!==JSON.stringify(summary.versions);
|
|
69
73
|
const skills=await skillMaintenance({brainDir});
|
|
70
74
|
const surfaces=await compile({brainDir,force:changed || skills.updated > 0});
|
|
71
75
|
// Last, so a replaced server starts on everything this run moved. Where maintenance cannot
|
|
@@ -79,8 +83,8 @@ export async function runAutomaticBrainUpdates({brainDir,fleet=false,force=false
|
|
|
79
83
|
// local_failed is this node's own maintenance. Peers judge a node by it, not by
|
|
80
84
|
// `failed`, which also counts the node's view of its peers: judging by that made
|
|
81
85
|
// every fleet pass mark every peer failed forever, each because the others had.
|
|
82
|
-
const local_failed=(summary.failed || 0)+global.failed+sources.failed+plugins.failed+(skills.failed || 0)+(surfaces.ok?0:surfaces.results.filter(row=>row.status==='failed').length)+serverFailed;
|
|
83
|
-
report={...summary,global,sources,plugins,mesh,server,skills,surfaces,local_failed,failed:local_failed+mesh.failed,checkedAt:now()};
|
|
86
|
+
const local_failed=(summary.failed || 0)+global.failed+sources.failed+(defaults.status==='failed'?1:0)+plugins.failed+(skills.failed || 0)+(surfaces.ok?0:surfaces.results.filter(row=>row.status==='failed').length)+serverFailed;
|
|
87
|
+
report={...summary,global,sources,defaults,plugins,mesh,server,skills,surfaces,local_failed,failed:local_failed+mesh.failed,checkedAt:now()};
|
|
84
88
|
}catch(error){report={failed:1,error:error.message,checkedAt:now(),results:[]};}
|
|
85
89
|
const temporary=`${file}.${process.pid}.tmp`;fs.writeFileSync(temporary,JSON.stringify(report,null,2));fs.renameSync(temporary,file);
|
|
86
90
|
return report;
|
|
@@ -217,18 +217,37 @@ export function installEnv(env = process.env) {
|
|
|
217
217
|
return kept;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
/** The brains maintenance provisions: each update root that has a brain, plus the selected brain. */
|
|
221
|
+
function maintainedBrains(roots, brainDir) {
|
|
222
|
+
const brains = roots.map(root => ({ root, brain: path.join(root, '.agent', 'skills', 'total-recall') }))
|
|
223
|
+
.filter(({ brain }) => fs.existsSync(brain));
|
|
224
|
+
if (brainDir) brains.push({ root: brainDir, brain: brainDir });
|
|
225
|
+
return brains;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Refresh the shared default plugins from this package's bundled copies and provision every
|
|
230
|
+
* registered brain, reporting each brain's outcome instead of throwing. `update --defaults` runs
|
|
231
|
+
* this, so a release maintenance has just installed can do it with its own code and its own
|
|
232
|
+
* bundled copies (release-defaults-handoff.mjs).
|
|
233
|
+
*/
|
|
234
|
+
export function ensureBrainDefaults(opts = {}, { ensure = ensureRequiredPlugins } = {}) {
|
|
235
|
+
const roots = listUpdateRoots(opts).map(row => row.root);
|
|
236
|
+
const results = maintainedBrains(roots, opts.brainDir).map(({ root, brain }) => {
|
|
237
|
+
try { ensure(brain); return { root, status: 'ready' }; }
|
|
238
|
+
catch (error) { return { root, status: 'failed', error: error.message }; }
|
|
239
|
+
});
|
|
240
|
+
return { failed: results.filter(row => row.status === 'failed').length, results };
|
|
241
|
+
}
|
|
242
|
+
|
|
220
243
|
export async function updateAllBrains(opts = {}) {
|
|
221
244
|
const dryRun = opts.dryRun !== false;
|
|
222
245
|
const installMissing = opts.installMissing === true;
|
|
223
246
|
const exec = opts.exec || spawnSync;
|
|
224
247
|
const roots = listUpdateRoots(opts).map(row => row.root);
|
|
225
248
|
const errors = [];
|
|
226
|
-
const brains = roots
|
|
227
|
-
|
|
228
|
-
if (opts.brainDir) {
|
|
229
|
-
brains.push({ root: opts.brainDir, brain: opts.brainDir });
|
|
230
|
-
roots.push(opts.brainDir);
|
|
231
|
-
}
|
|
249
|
+
const brains = maintainedBrains(roots, opts.brainDir);
|
|
250
|
+
if (opts.brainDir) roots.push(opts.brainDir);
|
|
232
251
|
for (const { root, brain } of brains) {
|
|
233
252
|
if (!dryRun && opts.ensureDefaults === true) {
|
|
234
253
|
try { ensureRequiredPlugins(brain); }
|
package/src/core/gate-mode.mjs
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Modes for the gate rules that steer work toward a plugin: raw commands a plugin owns, and
|
|
3
|
-
* routing.
|
|
4
|
-
* The preflight receipt is not one of these rules and has no mode.
|
|
3
|
+
* routing. Gates are permanently enforced (`enforce`): there is no `off` or `warn` bypass.
|
|
5
4
|
*
|
|
6
|
-
* Settings live in `config/gate.json` in a brain
|
|
7
|
-
*
|
|
8
|
-
* The repository's brain wins over the machine's. This runs on every tool call, so it reads the
|
|
9
|
-
* files directly, and an unreadable file or unknown value counts as unset: a bad settings file
|
|
10
|
-
* must never be what blocks work.
|
|
5
|
+
* Settings live in `config/gate.json` in a brain. Any legacy `off` or `warn` values in
|
|
6
|
+
* `gate.json` are ignored so enforcement cannot be disabled on disk.
|
|
11
7
|
*/
|
|
12
8
|
import fs from 'node:fs';
|
|
13
9
|
import path from 'node:path';
|
|
14
10
|
|
|
15
|
-
export const GATE_MODES = ['
|
|
16
|
-
export const DEFAULT_GATE_MODE = '
|
|
11
|
+
export const GATE_MODES = ['enforce'];
|
|
12
|
+
export const DEFAULT_GATE_MODE = 'enforce';
|
|
17
13
|
|
|
18
14
|
export const gateConfigPath = (brainDir) => path.join(brainDir, 'config', 'gate.json');
|
|
19
15
|
export const projectBrainDir = (root) => path.join(root, '.agent', 'skills', 'total-recall');
|
|
@@ -30,39 +26,32 @@ export function readGateLayers({ globalBrainDir, root }) {
|
|
|
30
26
|
return [readSettings(projectBrainDir(root)), readSettings(globalBrainDir)];
|
|
31
27
|
}
|
|
32
28
|
|
|
33
|
-
const known = (value) => (GATE_MODES.includes(value) ? value : undefined);
|
|
34
|
-
|
|
35
29
|
/**
|
|
36
30
|
* @param {object[]} layers from readGateLayers
|
|
37
31
|
* @param {'router'|'owned'} kind
|
|
38
32
|
* @param {string} [plugin] the owning plugin, for `owned`
|
|
39
33
|
*/
|
|
40
|
-
export function resolveGateMode(
|
|
41
|
-
for (const layer of layers) {
|
|
42
|
-
const specific = kind === 'router' ? known(layer.router) : known(layer.plugins?.[plugin]);
|
|
43
|
-
const mode = specific ?? known(layer.default);
|
|
44
|
-
if (mode) return mode;
|
|
45
|
-
}
|
|
34
|
+
export function resolveGateMode(_layers, _kind, _plugin) {
|
|
46
35
|
return DEFAULT_GATE_MODE;
|
|
47
36
|
}
|
|
48
37
|
|
|
49
38
|
/**
|
|
50
|
-
*
|
|
51
|
-
* that level follows the next layer again.
|
|
39
|
+
* Persist `enforce` mode. `off`, `warn`, and clearing are refused.
|
|
52
40
|
* @param {string} brainDir
|
|
53
41
|
* @param {{ mode: string|null, plugin?: string, router?: boolean }} change
|
|
54
42
|
*/
|
|
55
43
|
export function writeGateMode(brainDir, { mode, plugin, router }) {
|
|
44
|
+
if (mode !== 'enforce') {
|
|
45
|
+
throw new Error('Plugin and routing gates are permanently enforced and cannot be set to off, warn, or cleared.');
|
|
46
|
+
}
|
|
56
47
|
const settings = readSettings(brainDir);
|
|
57
48
|
if (router) {
|
|
58
|
-
|
|
49
|
+
settings.router = 'enforce';
|
|
59
50
|
} else if (plugin) {
|
|
60
51
|
settings.plugins = settings.plugins && typeof settings.plugins === 'object' ? settings.plugins : {};
|
|
61
|
-
|
|
62
|
-
} else if (mode === null) {
|
|
63
|
-
delete settings.default;
|
|
52
|
+
settings.plugins[plugin] = 'enforce';
|
|
64
53
|
} else {
|
|
65
|
-
settings.default =
|
|
54
|
+
settings.default = 'enforce';
|
|
66
55
|
}
|
|
67
56
|
const file = gateConfigPath(brainDir);
|
|
68
57
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -12,7 +12,41 @@ function describePluginWithShare(plugin, projectRoot) {
|
|
|
12
12
|
return result;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
async function
|
|
15
|
+
async function resolveStoreBundle(id, { storeUrl = process.env.TR_STORE_URL || 'http://127.0.0.1:3000', fetchImpl = fetch, licenseKey } = {}) {
|
|
16
|
+
const base = storeUrl.replace(/\/+$/, '');
|
|
17
|
+
const headers = {};
|
|
18
|
+
const resolvedKey = licenseKey || process.env[`${id.toUpperCase().replace(/-/g, '_')}_LICENSE_KEY`] || process.env.TR_LICENSE_KEY;
|
|
19
|
+
if (resolvedKey) headers['x-license-key'] = resolvedKey;
|
|
20
|
+
const listRes = await fetchImpl(`${base}/api/store/plugins?q=${encodeURIComponent(id)}`);
|
|
21
|
+
if (!listRes.ok) throw new Error(`Store catalog unavailable (HTTP ${listRes.status})`);
|
|
22
|
+
const listData = await listRes.json();
|
|
23
|
+
const entry = (listData.plugins || []).find((p) => p.id === id);
|
|
24
|
+
if (!entry) throw new Error(`Plugin '${id}' not found in Store catalog`);
|
|
25
|
+
const bundleRes = await fetchImpl(`${base}/api/store/plugins/${encodeURIComponent(id)}/bundle`, { headers });
|
|
26
|
+
if (!bundleRes.ok) {
|
|
27
|
+
let reason = `HTTP ${bundleRes.status}`;
|
|
28
|
+
try { const err = await bundleRes.json(); if (err?.error) reason = err.error; } catch {}
|
|
29
|
+
throw new Error(`Store install denied for '${id}': ${reason}`);
|
|
30
|
+
}
|
|
31
|
+
const bundle = Buffer.from(await bundleRes.arrayBuffer());
|
|
32
|
+
const sha256 = bundleRes.headers?.get?.('x-plugin-sha256') || entry.sha256;
|
|
33
|
+
return { bundle, sha256, sourceRecord: { kind: 'store', ref: `store:${id}`, store_url: base }, expectedId: id };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function resolveBundle(source, { peerDeps, publicDeps, storeDeps } = {}) {
|
|
37
|
+
if (typeof source === 'string' && source.startsWith('store:')) {
|
|
38
|
+
const id = source.slice('store:'.length).trim();
|
|
39
|
+
if (!id) throw new Error('Missing plugin id in store:<id>');
|
|
40
|
+
const { bundle, sha256, sourceRecord, expectedId } = await resolveStoreBundle(id, storeDeps);
|
|
41
|
+
const decoded = decodeBundle(bundle, sha256 ? { expectedSha256: sha256 } : {});
|
|
42
|
+
const entry = decoded.entries.find(e => e.path === 'plugin.json');
|
|
43
|
+
if (!entry) throw new Error('Store plugin bundle has no plugin.json');
|
|
44
|
+
const manifest = JSON.parse(entry.data.toString('utf8'));
|
|
45
|
+
const validation = validatePluginManifest(manifest);
|
|
46
|
+
if (!validation.valid) throw new Error(`Invalid plugin manifest: ${validation.errors.join('; ')}`);
|
|
47
|
+
if (manifest.id !== expectedId) throw new Error(`Requested '${expectedId}' but bundle contained '${manifest.id}'`);
|
|
48
|
+
return { manifest, entries: decoded.entries, sha256: decoded.sha256, source: sourceRecord };
|
|
49
|
+
}
|
|
16
50
|
const peerModule = typeof source === 'string' && source.startsWith('peer:')
|
|
17
51
|
? await import('./plugin-peers.mjs') : null;
|
|
18
52
|
const peer = peerModule?.parsePeerSource(source);
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
* (`gate.router`). Reading, searching and native subagents stay open, as does any
|
|
30
30
|
* plain Total Recall command, so routing can always run. A process the router
|
|
31
31
|
* started has already been routed. It warns once per receipt, not on every call.
|
|
32
|
+
* 8. With a receipt, once the router has selected a harness for this repository, a write from
|
|
33
|
+
* any other harness is denied, whatever the mode. Only a run the router started may write
|
|
34
|
+
* for it. A selection lasts as long as a receipt, and routing again replaces it. A call with
|
|
35
|
+
* no named harness is not judged by this rule, because it cannot be told apart.
|
|
32
36
|
* Rules 6 and 7 follow a mode (see gate-mode.mjs): `off`, `warn` (the default) or `enforce`.
|
|
33
37
|
* `enforce` blocks only while the owner passes its health check, so a broken plugin can
|
|
34
38
|
* never trap an agent. Warnings are statements of fact, because harnesses treat
|
|
@@ -154,6 +158,13 @@ export function ackDirectory(command, cwd) {
|
|
|
154
158
|
return target ? path.resolve(cwd || process.cwd(), target.replace(/^(["'])([\s\S]*)\1$/, '$2')) : cwd;
|
|
155
159
|
}
|
|
156
160
|
|
|
161
|
+
export function isBlockedGrep(command) {
|
|
162
|
+
const text = runnableText(command);
|
|
163
|
+
if (/(^|[\s;&|(])(grep|egrep|fgrep|rg|ripgrep|ag)(\s|$)/.test(text)) return true;
|
|
164
|
+
if (/(^|[\s;&|(])git\s+grep(\s|$)/.test(text)) return true;
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
157
168
|
/** A repository-wide search used to answer a question, as the DSH `tr-first.sh` rule defined it. */
|
|
158
169
|
export function isRepoSweep(command) {
|
|
159
170
|
const c = String(command || '');
|
|
@@ -235,10 +246,7 @@ const staleness = (receipt) => (receipt.ageMs > receipt.limitMs
|
|
|
235
246
|
* What a rule that steers work toward a plugin does with a call it matched: nothing (null),
|
|
236
247
|
* a denial, or a warning to carry.
|
|
237
248
|
*/
|
|
238
|
-
function steer(deps, {
|
|
239
|
-
const mode = deps.mode ? deps.mode(kind, plugin) : DEFAULT_GATE_MODE;
|
|
240
|
-
if (mode === 'off') return null;
|
|
241
|
-
if (mode !== 'enforce') return { name, warning: note };
|
|
249
|
+
function steer(deps, { plugin, name, reason, note, use }) {
|
|
242
250
|
if (!deps.ownerHealthy || deps.ownerHealthy(plugin, use)) return deny(name, reason, use);
|
|
243
251
|
return { name, warning: `${note} The ${plugin} plugin failed its health check, so this call was not blocked.` };
|
|
244
252
|
}
|
|
@@ -287,7 +295,7 @@ export function whenGateFails(call, err) {
|
|
|
287
295
|
|
|
288
296
|
/**
|
|
289
297
|
* @param {{ tool: string, command?: string, path?: string, cwd?: string }} call
|
|
290
|
-
* @param {{ check: () => { ok: boolean, problems: string[], ageMs?: number|null, limitMs?: number }, recentConsult?: () => boolean, readablePaths?: RegExp[], ownedCommands?: () => { plugin: string, pattern: string, use: string }[], router?: () => ({ plugin: string, pattern: string, use: string } | null), routed?: (receipt: object) => boolean, routeWarned?: (receipt: object) => boolean, sessionAcknowledged?: (receipt: object) => boolean, sessionLapsed?: () => boolean, arrival?: () => (string | null), dispatched?: boolean, mode?: (kind: 'router'|'owned', plugin: string) => string, ownerHealthy?: (plugin: string, use: string) => boolean }} deps
|
|
298
|
+
* @param {{ check: () => { ok: boolean, problems: string[], ageMs?: number|null, limitMs?: number }, recentConsult?: () => boolean, readablePaths?: RegExp[], ownedCommands?: () => { plugin: string, pattern: string, use: string }[], router?: () => ({ plugin: string, pattern: string, use: string } | null), routed?: (receipt: object) => boolean, routeWarned?: (receipt: object) => boolean, sessionAcknowledged?: (receipt: object) => boolean, sessionLapsed?: () => boolean, arrival?: () => (string | null), dispatched?: boolean, harness?: string|null, routerRun?: boolean, selection?: (receipt: object) => ({ harness: string|null, at: number } | null), mode?: (kind: 'router'|'owned', plugin: string) => string, ownerHealthy?: (plugin: string, use: string) => boolean }} deps
|
|
291
299
|
*/
|
|
292
300
|
export function decide(call, deps) {
|
|
293
301
|
try {
|
|
@@ -313,14 +321,26 @@ export function decide(call, deps) {
|
|
|
313
321
|
: `this session has not acknowledged (directory: ${call.cwd || 'unknown'}). A receipt from another agent does not count.`;
|
|
314
322
|
return deny('receipt:session', `TR-PREFLIGHT gate: ${state} ${nextStep(call)}`);
|
|
315
323
|
}
|
|
316
|
-
if (tool === 'shell' &&
|
|
317
|
-
return deny('
|
|
324
|
+
if (tool === 'shell' && isBlockedGrep(call.command)) {
|
|
325
|
+
return deny('grep', 'TR-PREFLIGHT gate: `grep`/`rg`/`ag` is blocked. Use Total Recall OpenWiki (`total-recall openwiki` / `openwiki/`) or `total-recall recall "<topic>" --local` instead.',
|
|
326
|
+
'total-recall openwiki hubs --json');
|
|
327
|
+
}
|
|
328
|
+
if ((tool === 'search' || (tool === 'shell' && isRepoSweep(call.command))) && !(deps.recentConsult ? deps.recentConsult() : false)) {
|
|
329
|
+
return deny('sweep', 'TR-PREFLIGHT gate: do not sweep or search the repository before consulting Total Recall / OpenWiki. Run `total-recall openwiki hubs --json`, `total-recall recall "<topic>" --local`, or `total-recall help "<task>"` first.',
|
|
318
330
|
'total-recall help "<task>"');
|
|
319
331
|
}
|
|
332
|
+
const router = deps.router ? deps.router() : null;
|
|
333
|
+
if (tool === 'write' && deps.harness && !deps.routerRun) {
|
|
334
|
+
const chosen = deps.selection ? deps.selection(receipt) : null;
|
|
335
|
+
if (chosen?.harness && chosen.harness !== deps.harness) {
|
|
336
|
+
const by = router?.plugin || 'the router';
|
|
337
|
+
return deny('route:selected', `TR-PREFLIGHT gate: ${by} selected ${chosen.harness} for this task, so ${deps.harness} may not write here. Only a run ${by} started may write: hand the task over with its --execute form, or route it again.`,
|
|
338
|
+
router?.use || 'route the task again');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
320
341
|
const notes = [];
|
|
321
342
|
const arrival = deps.arrival ? deps.arrival() : null;
|
|
322
343
|
if (arrival) notes.push({ name: 'arrival', warning: arrival });
|
|
323
|
-
const router = deps.router ? deps.router() : null;
|
|
324
344
|
if (router && !deps.dispatched) {
|
|
325
345
|
const open = ['read', 'search', 'web', 'agent'].includes(tool) || (tool === 'shell' && isPlainTotalRecall(call.command));
|
|
326
346
|
if (!open && !(deps.routed ? deps.routed(receipt.receipt) : false)) {
|
|
@@ -339,6 +339,17 @@ export function lastRouteWarnedMs(root, { home = os.homedir(), session = null }
|
|
|
339
339
|
return readStamp(routeWarnedPath(root, home, session));
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
const selectionPath = (root, home) => path.join(stateDir(home), scoped('route-selected', root));
|
|
343
|
+
|
|
344
|
+
/** The harness the router chose for work in this repository, or null when it chose none. */
|
|
345
|
+
export function markRouteSelection(root, harness, { home = os.homedir(), now = Date.now() } = {}) {
|
|
346
|
+
writeBookkeeping(selectionPath(root, home), (target) => fs.writeFileSync(target, JSON.stringify({ at: now, harness: harness || null, root }), { mode: 0o600 }));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function lastRouteSelection(root, { home = os.homedir() } = {}) {
|
|
350
|
+
return newestRecord(selectionPath(root, home));
|
|
351
|
+
}
|
|
352
|
+
|
|
342
353
|
const sessionAckPath = (session, home) => path.join(stateDir(home), `session-ack-${sessionKey(session)}.stamp`);
|
|
343
354
|
const delegatedPath = (root, home) => path.join(stateDir(home), scoped('delegated', root));
|
|
344
355
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic maintenance refreshes the default plugins before it installs a new release: the code
|
|
3
|
+
* that runs it is the old release, and so are its bundled copies. A default is replaced only by a
|
|
4
|
+
* higher bundled version, so that pass changed nothing and a release's defaults arrived one pass
|
|
5
|
+
* (about an hour) later. After the global and source updates, the newest installed release of this
|
|
6
|
+
* package refreshes the defaults itself, with its own code and its own bundled copies.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { PACKAGE_ROOT } from './plugin-loader.mjs';
|
|
11
|
+
import { comparePackageVersions } from './package-version.mjs';
|
|
12
|
+
import { findLinkedSourceCheckout } from './global-brain-updates.mjs';
|
|
13
|
+
import { runBounded } from './startup-health.mjs';
|
|
14
|
+
|
|
15
|
+
const PACKAGE = 'total-recall-brain';
|
|
16
|
+
|
|
17
|
+
function readVersion(dir) {
|
|
18
|
+
try { return JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).version || null; } catch { return null; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The release this process runs, read once at import. A source update can fast-forward this
|
|
23
|
+
* checkout during the run, so reading the file again later would report the new release.
|
|
24
|
+
*/
|
|
25
|
+
export const RUNNING_VERSION = readVersion(PACKAGE_ROOT);
|
|
26
|
+
|
|
27
|
+
/** Installed releases this run may have moved: the global install and source checkouts it updated. */
|
|
28
|
+
export function installedReleases({ global, sources } = {}) {
|
|
29
|
+
const dirs = new Set();
|
|
30
|
+
const add = dir => { try { dirs.add(fs.realpathSync(dir)); } catch {} };
|
|
31
|
+
if (global?.root) {
|
|
32
|
+
const direct = path.join(global.root, PACKAGE);
|
|
33
|
+
const dir = fs.existsSync(direct) ? direct : findLinkedSourceCheckout(global.root, PACKAGE);
|
|
34
|
+
if (dir) add(dir);
|
|
35
|
+
}
|
|
36
|
+
for (const row of sources?.results || []) {
|
|
37
|
+
if (row?.package === PACKAGE && row.status === 'updated' && row.root) add(row.root);
|
|
38
|
+
}
|
|
39
|
+
return [...dirs]
|
|
40
|
+
.map(dir => ({ dir, version: readVersion(dir) }))
|
|
41
|
+
.filter(release => release.version && fs.existsSync(path.join(release.dir, 'bin', 'total-recall.mjs')));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The JSON report `update --defaults --json` prints, even after other output on stdout. */
|
|
45
|
+
export function parseDefaultsReport(text) {
|
|
46
|
+
const trimmed = String(text || '').trim();
|
|
47
|
+
try { return JSON.parse(trimmed); } catch {}
|
|
48
|
+
const start = trimmed.lastIndexOf('\n{\n');
|
|
49
|
+
if (start >= 0) { try { return JSON.parse(trimmed.slice(start + 1)); } catch {} }
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* When an installed release is newer than the running code, run its `update --defaults` and report
|
|
55
|
+
* what it did. Never throws: the outcome is a row in the maintenance report.
|
|
56
|
+
* @returns {Promise<{status:'current'|'refreshed'|'failed', running?:string|null, installed?:string, version?:string, from?:string|null, results?:object[], error?:string}>}
|
|
57
|
+
*/
|
|
58
|
+
export async function handOffDefaultsToRelease({ global, sources, cwd = process.cwd(), running = RUNNING_VERSION,
|
|
59
|
+
execPath = process.execPath, run = runBounded, timeoutMs = 10 * 60 * 1000 } = {}) {
|
|
60
|
+
let newest = null;
|
|
61
|
+
try {
|
|
62
|
+
for (const release of installedReleases({ global, sources })) {
|
|
63
|
+
if (!newest || comparePackageVersions(release.version, newest.version) === 1) newest = release;
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return { status: 'failed', running, error: `Installed releases could not be read: ${error.message}` };
|
|
67
|
+
}
|
|
68
|
+
if (!newest || comparePackageVersions(newest.version, running) !== 1) {
|
|
69
|
+
return { status: 'current', running, ...(newest ? { installed: newest.version } : {}) };
|
|
70
|
+
}
|
|
71
|
+
const handoff = { version: newest.version, from: running };
|
|
72
|
+
let result;
|
|
73
|
+
try {
|
|
74
|
+
result = await run(execPath, [path.join(newest.dir, 'bin', 'total-recall.mjs'), 'update', '--defaults', '--json'],
|
|
75
|
+
{ cwd, timeoutMs, maxBytes: 1024 * 1024 });
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return { status: 'failed', ...handoff, error: `update --defaults could not run: ${error.message}` };
|
|
78
|
+
}
|
|
79
|
+
const report = parseDefaultsReport(result?.output);
|
|
80
|
+
if (result?.code === 0 && report && report.failed === 0) {
|
|
81
|
+
return { status: 'refreshed', ...handoff, results: report.results || [] };
|
|
82
|
+
}
|
|
83
|
+
const failures = (report?.results || []).filter(row => row.status === 'failed').map(row => `${row.root}: ${row.error}`);
|
|
84
|
+
const error = result?.reason ? `update --defaults stopped: ${result.reason}`
|
|
85
|
+
: failures.length ? failures.join('; ')
|
|
86
|
+
: !report ? `update --defaults printed no report (exit ${result?.code ?? 'unknown'}): ${String(result?.stderr || result?.output || '').trim().slice(-1000)}`
|
|
87
|
+
: `update --defaults exited ${result?.code}`;
|
|
88
|
+
return { status: 'failed', ...handoff, error, ...(report?.results ? { results: report.results } : {}) };
|
|
89
|
+
}
|