total-recall-brain 3.24.1 → 3.24.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/SKILL.md +13 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +2 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +2 -0
- package/scaffold/.agent/skills/total-recall/scripts/sync-repo.mjs +166 -61
- package/src/cli/backup.mjs +63 -0
- package/src/server/route-manifest.json +4 -0
package/package.json
CHANGED
|
@@ -597,6 +597,19 @@ Current baseline: **288 spec files / 1493 tests, ~160 s** on the Mac Mini.
|
|
|
597
597
|
>
|
|
598
598
|
> A non-login `ssh` shell has no Homebrew on PATH — export it or use `bash -lc`,
|
|
599
599
|
> otherwise the run dies with `command not found: npx` **and still exits 0**.
|
|
600
|
+
>
|
|
601
|
+
> **Never read a test result through a pipe.** `vitest run` exits 1 correctly on
|
|
602
|
+
> failure, but `npx vitest run | tail -20` reports *tail's* status — always 0 —
|
|
603
|
+
> so a red suite looks green. On 2026-08-19 this cost a full re-run and a wrong
|
|
604
|
+
> "the suite exits 0 on failure" claim about the repo; the defect was the
|
|
605
|
+
> pipeline, not vitest. Capture to a file and check the code separately:
|
|
606
|
+
>
|
|
607
|
+
> ```bash
|
|
608
|
+
> npx vitest run > /tmp/t.log 2>&1; echo "exit=$?"; tail -20 /tmp/t.log
|
|
609
|
+
> ```
|
|
610
|
+
>
|
|
611
|
+
> `check.mjs` is unaffected — it spawns without a shell and reads the real exit
|
|
612
|
+
> code — so prefer the gate over hand-rolled invocations.
|
|
600
613
|
|
|
601
614
|
**Never skip tests** to make a release look clean, and never use `--force` on a
|
|
602
615
|
checker. Audit and clean up any rows, files, or mock entries a suite leaves
|
package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md
CHANGED
|
@@ -3,6 +3,7 @@ type: memory
|
|
|
3
3
|
slug: always-websearch-gap
|
|
4
4
|
category: preferences
|
|
5
5
|
title: "Always Perform Web Search for Cutoff Gap"
|
|
6
|
+
description: "Web-search to confirm any fact that falls in or past the training-cutoff gap, rather than answering from weights."
|
|
6
7
|
status: active
|
|
7
8
|
confidence: 1
|
|
8
9
|
importance: 5
|
|
@@ -32,6 +33,7 @@ schema_version: 2
|
|
|
32
33
|
x_temporal_context: "2026-05-25T22:36:21.376Z"
|
|
33
34
|
priority: absolute
|
|
34
35
|
immutable: true
|
|
36
|
+
timestamp: "2026-05-25T22:36:21.376Z"
|
|
35
37
|
---
|
|
36
38
|
|
|
37
39
|
# Always Perform Web Search for Cutoff Gap
|
|
@@ -3,6 +3,7 @@ type: memory
|
|
|
3
3
|
slug: topic-research-sop
|
|
4
4
|
category: preferences
|
|
5
5
|
title: "Standard Operating Procedure for Topic Research"
|
|
6
|
+
description: "How the agent handles a research request: queue it to the background daemon, keep one living scratch document, and compile so it is searchable."
|
|
6
7
|
status: active
|
|
7
8
|
confidence: 1
|
|
8
9
|
importance: 5
|
|
@@ -32,6 +33,7 @@ schema_version: 2
|
|
|
32
33
|
x_temporal_context: "2026-05-25T22:36:21.376Z"
|
|
33
34
|
priority: absolute
|
|
34
35
|
immutable: true
|
|
36
|
+
timestamp: "2026-05-25T22:36:21.376Z"
|
|
35
37
|
---
|
|
36
38
|
|
|
37
39
|
# Standard Operating Procedure for Topic Research
|
|
@@ -1,83 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* sync-repo — refresh this brain's skill definitions from the upstream templates.
|
|
4
|
+
*
|
|
5
|
+
* Replaces a stub that printed "Synced and verified skill: <name>", "Merging core
|
|
6
|
+
* invariants non-destructively" and "Sync Completed Successfully!" without
|
|
7
|
+
* fetching, merging or writing anything — while the skill's Core Directive #4
|
|
8
|
+
* told agents to run it to keep skills current. It also probed
|
|
9
|
+
* <agent>/memory-vault, which is not where a brain lives, so on a correct
|
|
10
|
+
* install it aborted with a misleading "run init first".
|
|
11
|
+
*
|
|
12
|
+
* What it actually does now:
|
|
13
|
+
* - resolves a real template source (env override, installed package,
|
|
14
|
+
* running-from-source checkout, or a shallow clone of upstream)
|
|
15
|
+
* - copies skill files, PRESERVING each destination's compiled
|
|
16
|
+
* "<!-- BEGIN INJECTED MEMORY -->" block, which is per-repo and must not be
|
|
17
|
+
* overwritten by a template
|
|
18
|
+
* - never touches memory-vault/ — that is user memory, not a template
|
|
19
|
+
* - reports the files it really changed, and exits non-zero if it changed nothing
|
|
20
|
+
* because it could not find a source
|
|
21
|
+
*
|
|
22
|
+
* Usage: node sync-repo.mjs [--dry-run] [--source <dir>]
|
|
23
|
+
*/
|
|
1
24
|
import fs from 'node:fs';
|
|
2
25
|
import path from 'node:path';
|
|
3
26
|
import os from 'node:os';
|
|
27
|
+
import { execFileSync } from 'node:child_process';
|
|
4
28
|
import { fileURLToPath } from 'node:url';
|
|
5
29
|
|
|
6
|
-
|
|
30
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const DRY = process.argv.includes('--dry-run');
|
|
32
|
+
const srcFlag = process.argv.indexOf('--source');
|
|
33
|
+
const SRC_OVERRIDE = srcFlag !== -1 ? process.argv[srcFlag + 1] : process.env.TR_SCAFFOLD_DIR;
|
|
34
|
+
|
|
7
35
|
const home = os.homedir();
|
|
8
36
|
const AGENT_DIR = process.env.AGENT_DIR || path.join(home, '.agent');
|
|
9
|
-
|
|
37
|
+
// A brain is <agent>/skills/total-recall — memory-vault, config and the rest all
|
|
38
|
+
// live INSIDE it. The old code looked for <agent>/memory-vault and never found one.
|
|
39
|
+
const BRAIN_DIR = path.join(AGENT_DIR, 'skills', 'total-recall');
|
|
10
40
|
const SKILLS_DIR = path.join(AGENT_DIR, 'skills');
|
|
11
41
|
|
|
12
|
-
|
|
13
|
-
|
|
42
|
+
const INJECT_START = '<!-- BEGIN INJECTED MEMORY';
|
|
43
|
+
const INJECT_END = '<!-- END INJECTED MEMORY -->';
|
|
44
|
+
|
|
45
|
+
function log(m) { console.log(m); }
|
|
46
|
+
function fail(m) { console.error(`❌ ${m}`); process.exit(1); }
|
|
47
|
+
|
|
48
|
+
/** Locate real template files. Returns { dir, origin } or null. */
|
|
49
|
+
function resolveTemplateSource() {
|
|
50
|
+
if (SRC_OVERRIDE) {
|
|
51
|
+
if (!fs.existsSync(SRC_OVERRIDE)) fail(`--source path does not exist: ${SRC_OVERRIDE}`);
|
|
52
|
+
return { dir: SRC_OVERRIDE, origin: `explicit: ${SRC_OVERRIDE}` };
|
|
53
|
+
}
|
|
54
|
+
// Walk up looking for the package root rather than counting '..' segments:
|
|
55
|
+
// this script legitimately runs from two different depths (a brain at
|
|
56
|
+
// <agent>/skills/total-recall/scripts, and the template copy at
|
|
57
|
+
// <repo>/scaffold/.agent/skills/total-recall/scripts). A fixed relative path
|
|
58
|
+
// is right for one and silently wrong for the other — which resolved to the
|
|
59
|
+
// INSTALLED (older) package and would have reverted newer local templates.
|
|
60
|
+
const fromCheckout = (() => {
|
|
61
|
+
let dir = __dirname;
|
|
62
|
+
for (let i = 0; i < 8; i += 1) {
|
|
63
|
+
const pkg = path.join(dir, 'package.json');
|
|
64
|
+
const scaffold = path.join(dir, 'scaffold', '.agent', 'skills');
|
|
65
|
+
if (fs.existsSync(pkg) && fs.existsSync(scaffold)) {
|
|
66
|
+
try {
|
|
67
|
+
if (JSON.parse(fs.readFileSync(pkg, 'utf8')).name === 'total-recall-brain') return scaffold;
|
|
68
|
+
} catch { /* keep walking */ }
|
|
69
|
+
}
|
|
70
|
+
const up = path.dirname(dir);
|
|
71
|
+
if (up === dir) break;
|
|
72
|
+
dir = up;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
})();
|
|
14
76
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (
|
|
77
|
+
const candidates = [
|
|
78
|
+
[fromCheckout, 'source checkout'],
|
|
79
|
+
[path.join(process.cwd(), 'node_modules', 'total-recall-brain', 'scaffold', '.agent', 'skills'), 'installed package'],
|
|
80
|
+
[path.join(home, 'node_modules', 'total-recall-brain', 'scaffold', '.agent', 'skills'), 'installed package (home)'],
|
|
81
|
+
];
|
|
82
|
+
for (const [dir, origin] of candidates) {
|
|
83
|
+
if (dir && fs.existsSync(dir) && fs.statSync(dir).isDirectory()) return { dir, origin };
|
|
22
84
|
}
|
|
23
|
-
|
|
24
|
-
// Use default
|
|
85
|
+
return null;
|
|
25
86
|
}
|
|
26
|
-
console.log(`🌐 Upstream Repository URL: ${upstreamUrl}`);
|
|
27
87
|
|
|
28
|
-
|
|
29
|
-
|
|
88
|
+
/** Last resort: shallow-clone upstream into a temp dir. */
|
|
89
|
+
function cloneUpstream() {
|
|
90
|
+
let upstream = 'https://github.com/gregiteen/total-recall';
|
|
30
91
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
throw new Error('Total Recall directories are missing. Run "npx total-recall init" first.');
|
|
92
|
+
const cfg = path.join(BRAIN_DIR, 'config', 'brain.json');
|
|
93
|
+
if (fs.existsSync(cfg)) {
|
|
94
|
+
const parsed = JSON.parse(fs.readFileSync(cfg, 'utf8'));
|
|
95
|
+
if (parsed.upstream) upstream = parsed.upstream;
|
|
36
96
|
}
|
|
97
|
+
} catch { /* default */ }
|
|
98
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tr-sync-'));
|
|
99
|
+
log(`🌐 No local templates — shallow-cloning ${upstream}`);
|
|
100
|
+
try {
|
|
101
|
+
execFileSync('git', ['clone', '--depth', '1', '--quiet', upstream, tmp], { stdio: 'pipe' });
|
|
102
|
+
} catch (err) {
|
|
103
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
104
|
+
fail(`could not clone upstream (${err.message.split('\n')[0]}). Pass --source <dir> to sync offline.`);
|
|
105
|
+
}
|
|
106
|
+
const dir = path.join(tmp, 'scaffold', '.agent', 'skills');
|
|
107
|
+
if (!fs.existsSync(dir)) {
|
|
108
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
109
|
+
fail('upstream clone has no scaffold/.agent/skills — cannot sync.');
|
|
110
|
+
}
|
|
111
|
+
return { dir, origin: `clone of ${upstream}`, cleanup: tmp };
|
|
112
|
+
}
|
|
37
113
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
114
|
+
/** Copy src over dst, carrying dst's compiled injected-memory block across. */
|
|
115
|
+
function writePreservingInjectedBlock(srcFile, dstFile) {
|
|
116
|
+
let next = fs.readFileSync(srcFile, 'utf8');
|
|
117
|
+
if (fs.existsSync(dstFile)) {
|
|
118
|
+
const cur = fs.readFileSync(dstFile, 'utf8');
|
|
119
|
+
const s = cur.indexOf(INJECT_START);
|
|
120
|
+
const e = cur.indexOf(INJECT_END);
|
|
121
|
+
if (s !== -1 && e !== -1 && e > s) {
|
|
122
|
+
const block = cur.slice(s, e + INJECT_END.length);
|
|
123
|
+
const ns = next.indexOf(INJECT_START);
|
|
124
|
+
const ne = next.indexOf(INJECT_END);
|
|
125
|
+
next = (ns !== -1 && ne !== -1 && ne > ns)
|
|
126
|
+
? next.slice(0, ns) + block + next.slice(ne + INJECT_END.length)
|
|
127
|
+
: `${next.replace(/\s*$/, '')}\n\n${block}\n`;
|
|
52
128
|
}
|
|
129
|
+
if (cur === next) return 'identical';
|
|
130
|
+
}
|
|
131
|
+
if (!DRY) {
|
|
132
|
+
fs.mkdirSync(path.dirname(dstFile), { recursive: true });
|
|
133
|
+
fs.writeFileSync(dstFile, next);
|
|
134
|
+
}
|
|
135
|
+
return 'updated';
|
|
136
|
+
}
|
|
53
137
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
derivedDir,
|
|
68
|
-
instructionsFile
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
console.log(`🚀 Compilation Complete: Processed ${result.nodesProcessed} nodes, injected ${result.skillsInjected} skills.`);
|
|
72
|
-
} catch (err) {
|
|
73
|
-
console.warn(`⚠️ Compilation skipped or ran into error: ${err.message}`);
|
|
74
|
-
}
|
|
138
|
+
function walk(dir) {
|
|
139
|
+
const out = [];
|
|
140
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
141
|
+
const p = path.join(dir, e.name);
|
|
142
|
+
if (e.isDirectory()) {
|
|
143
|
+
// memory-vault is the user's own memory. A template must never overwrite it.
|
|
144
|
+
if (e.name === 'memory-vault' || e.name === 'memory-derived' || e.name === 'sessions') continue;
|
|
145
|
+
if (e.name === '.git' || e.name === 'node_modules') continue;
|
|
146
|
+
out.push(...walk(p));
|
|
147
|
+
} else out.push(p);
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
75
151
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
152
|
+
log('🔄 Total Recall — syncing skill definitions from upstream templates');
|
|
153
|
+
log(`📂 Brain: ${BRAIN_DIR}`);
|
|
154
|
+
if (DRY) log('🔍 DRY RUN — nothing will be written');
|
|
155
|
+
|
|
156
|
+
if (!fs.existsSync(BRAIN_DIR)) fail(`no brain at ${BRAIN_DIR}. Run "npx total-recall init" first.`);
|
|
157
|
+
|
|
158
|
+
let source = resolveTemplateSource();
|
|
159
|
+
if (!source) source = cloneUpstream();
|
|
160
|
+
log(`📦 Template source: ${source.origin}`);
|
|
161
|
+
|
|
162
|
+
const skills = fs.readdirSync(source.dir, { withFileTypes: true })
|
|
163
|
+
.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
164
|
+
if (skills.length === 0) fail(`template source has no skills: ${source.dir}`);
|
|
165
|
+
|
|
166
|
+
let updated = 0, identical = 0, created = 0;
|
|
167
|
+
const touched = [];
|
|
168
|
+
for (const skill of skills) {
|
|
169
|
+
const from = path.join(source.dir, skill);
|
|
170
|
+
const to = path.join(SKILLS_DIR, skill);
|
|
171
|
+
const isNew = !fs.existsSync(to);
|
|
172
|
+
for (const f of walk(from)) {
|
|
173
|
+
const rel = path.relative(from, f);
|
|
174
|
+
const res = writePreservingInjectedBlock(f, path.join(to, rel));
|
|
175
|
+
if (res === 'updated') { updated++; touched.push(`${skill}/${rel}`); } else identical++;
|
|
80
176
|
}
|
|
177
|
+
if (isNew) created++;
|
|
81
178
|
}
|
|
82
179
|
|
|
83
|
-
|
|
180
|
+
if (source.cleanup) fs.rmSync(source.cleanup, { recursive: true, force: true });
|
|
181
|
+
|
|
182
|
+
log('');
|
|
183
|
+
log(`${DRY ? '🔍' : '✅'} ${updated} file(s) ${DRY ? 'would change' : 'updated'}, ${identical} already current, ${created} new skill(s)`);
|
|
184
|
+
for (const t of touched.slice(0, 20)) log(` ${DRY ? '~' : '↳'} ${t}`);
|
|
185
|
+
if (touched.length > 20) log(` … and ${touched.length - 20} more`);
|
|
186
|
+
if (updated === 0) log(' Nothing to do — every skill file already matches the templates.');
|
|
187
|
+
log('');
|
|
188
|
+
log(DRY ? 'Re-run without --dry-run to apply.' : 'Run "npx total-recall compile" to rebuild the instruction surfaces.');
|
package/src/cli/backup.mjs
CHANGED
|
@@ -116,6 +116,33 @@ function printHelp() {
|
|
|
116
116
|
`);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
function ensureBrainGitignore(SKILL_DIR) {
|
|
120
|
+
const gitignorePath = path.join(SKILL_DIR, '.gitignore');
|
|
121
|
+
const entries = [
|
|
122
|
+
'memory-derived/', 'sessions/', '*.backup', 'logs/', 'node_modules/',
|
|
123
|
+
'memory-vault/.events/', 'browser-profile/', '.snapshots/', 'daemon.pid',
|
|
124
|
+
];
|
|
125
|
+
let existing = '';
|
|
126
|
+
if (fs.existsSync(gitignorePath)) existing = fs.readFileSync(gitignorePath, 'utf8');
|
|
127
|
+
const missing = entries.filter((e) => !existing.split('\n').some((l) => l.trim() === e));
|
|
128
|
+
if (missing.length === 0) return 0;
|
|
129
|
+
fs.appendFileSync(
|
|
130
|
+
gitignorePath,
|
|
131
|
+
'\n# Auto-added by total-recall backup — runtime/derived/sensitive, never versioned\n'
|
|
132
|
+
+ missing.join('\n') + '\n',
|
|
133
|
+
);
|
|
134
|
+
console.error(` \u2705 .gitignore: added ${missing.length} protected entr${missing.length === 1 ? 'y' : 'ies'}`);
|
|
135
|
+
// Anything already tracked must also be untracked, or the ignore is inert.
|
|
136
|
+
for (const e of missing) {
|
|
137
|
+
const tracked = git(SKILL_DIR, 'ls-files', e.replace(/\/$/, ''));
|
|
138
|
+
if (tracked.ok && tracked.stdout) {
|
|
139
|
+
git(SKILL_DIR, 'rm', '-r', '--cached', '-f', '-q', e.replace(/\/$/, ''));
|
|
140
|
+
console.error(` \u2705 untracked ${e} (kept on disk)`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return missing.length;
|
|
144
|
+
}
|
|
145
|
+
|
|
119
146
|
async function pushGitBackup(remote, layer = 'auto') {
|
|
120
147
|
const SKILL_DIR = getSkillDir(layer);
|
|
121
148
|
if (!commandExists('git')) {
|
|
@@ -129,6 +156,19 @@ async function pushGitBackup(remote, layer = 'auto') {
|
|
|
129
156
|
process.exit(1);
|
|
130
157
|
}
|
|
131
158
|
|
|
159
|
+
// A brain is memory, not runtime state. Three classes of file have each
|
|
160
|
+
// broken a real backup by being versioned:
|
|
161
|
+
// memory-vault/.events/ append-only logs; default.jsonl reached 306MB and
|
|
162
|
+
// GitHub hard-rejects >100MB, silently killing every
|
|
163
|
+
// global backup for 5 weeks (2026-07-14 -> 08-19).
|
|
164
|
+
// browser-profile/ the rotation Chromium profile — live Stripe/GitHub
|
|
165
|
+
// cookies and Login Data. Was tracked and one
|
|
166
|
+
// successful push from landing on GitHub.
|
|
167
|
+
// .snapshots/ ~46MB vault tarballs stored inside the vault.
|
|
168
|
+
// Enforced on every push, not just --install, so brains created before this
|
|
169
|
+
// rule are corrected on their next backup.
|
|
170
|
+
ensureBrainGitignore(SKILL_DIR);
|
|
171
|
+
|
|
132
172
|
// Initialise the skill folder as a git repo if needed
|
|
133
173
|
const gitDir = path.join(SKILL_DIR, '.git');
|
|
134
174
|
if (!fs.existsSync(gitDir)) {
|
|
@@ -170,6 +210,29 @@ async function pushGitBackup(remote, layer = 'auto') {
|
|
|
170
210
|
const stamp = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
171
211
|
|
|
172
212
|
if (!status.stdout) {
|
|
213
|
+
// A clean working tree does NOT mean the remote is current. Two scheduled
|
|
214
|
+
// jobs pointing the shared "backup" remote at different URLs is enough to
|
|
215
|
+
// break this: whichever ran second re-pointed the remote, found the tree
|
|
216
|
+
// clean, and returned "nothing to push" — so that destination silently
|
|
217
|
+
// froze for months while every run reported success. Only skip when the
|
|
218
|
+
// remote actually has our HEAD.
|
|
219
|
+
const head = git(SKILL_DIR, 'rev-parse', 'HEAD');
|
|
220
|
+
const localSha = head.ok ? (head.stdout || '').trim() : '';
|
|
221
|
+
const remoteHead = git(SKILL_DIR, 'ls-remote', remote, 'main');
|
|
222
|
+
const remoteSha = remoteHead.ok ? (remoteHead.stdout || '').split(/\s+/)[0] : '';
|
|
223
|
+
// No local commit to compare, or the remote already has it: genuinely
|
|
224
|
+
// nothing to do. Only a KNOWN mismatch justifies pushing a clean tree.
|
|
225
|
+
if (!localSha || remoteSha === localSha) {
|
|
226
|
+
console.error(` ✅ Brain unchanged — nothing to push (${stamp})`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (remoteSha) {
|
|
230
|
+
console.error(` 📤 Working tree clean but ${remote} is behind — pushing existing commits...`);
|
|
231
|
+
const p = git(SKILL_DIR, 'push', remote, 'HEAD:main');
|
|
232
|
+
if (p.ok) console.error(` ✅ Brain pushed to backup (${stamp})`);
|
|
233
|
+
else console.error(` ❌ Push failed: ${p.stderr || p.stdout}`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
173
236
|
console.error(` ✅ Brain unchanged — nothing to push (${stamp})`);
|
|
174
237
|
return;
|
|
175
238
|
}
|