Files
gbrain/src/commands/self-upgrade.ts
Garry Tan a57d98b813 v0.42.12.0 feat: self-upgrading gbrain — invocation-riding update check + opt-in auto-upgrade (#1798)
* feat(self-upgrade): decision/cache/snooze foundation + atomic binary self-update

Pure decideSelfUpgrade (invocation + autopilot channels), atomic untrusted
cache + escalating snooze + shared marker grammar (forged-marker rejection),
semver helpers, and real darwin-arm64/linux-x64 binary self-update
(download -> fsync -> smoke -> atomic rename; failure leaves old binary intact).
Tests incl. real-HTTP-server swap E2E.

* feat(self-upgrade): check-update cache/markers, self-upgrade command, CLI heartbeat hook

check-update gains gstack-style cache/snooze/markers + refreshUpdateCache +
exported fetchLatestRelease. New 'gbrain self-upgrade' command. cli.ts emits the
update marker on every invocation (cache-read-only hot path, detached
single-flight refresh, skip-set + recursion guard + NODE_ENV=test gate).

* feat(self-upgrade): autopilot silent channel, doctor check, runPostUpgrade setup, config + identity marker

autopilot opt-in silent channel (auto+quiet+idle, swap-only+breadcrumb+exit-relaunch)
+ installSystemd Restart=always + migrateSystemdUnitToRestartAlways. doctor
self_upgrade_health. runPostUpgrade applySelfUpgradeSetup (one-time consent +
systemd rewrite). init defaults mode=notify. config self_upgrade plane +
KNOWN_CONFIG_KEYS. get_brain_identity carries update marker.

* docs(self-upgrade): gbrain-upgrade agent skill, RESOLVER/manifest, auto-update doc reversal, HEARTBEAT

New skills/gbrain-upgrade agent flow (mirror gstack-upgrade) wired into RESOLVER +
manifest. upgrades-auto-update.md reversed to document opt-in auto + conservative
gates. HEARTBEAT self-upgrade --check-only line. llms-full regenerated.

* chore: bump version and changelog (v0.42.12.0)

Self-upgrading gbrain: invocation-riding update marker + opt-in autopilot
silent channel + real atomic binary self-update. Mirrors gstack's mechanism.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(self-upgrade): write just-upgraded-from breadcrumb + clear stale cache after upgrade

Codex ship-review P3: the CLI startup hook reads just-upgraded-from to print the
one-time JUST_UPGRADED confirmation, but nothing wrote it — dead path. runUpgrade
now writes the breadcrumb (covers full + --swap-only) and clears the update-check
cache + snooze so a now-applied 'upgrade available' marker stops nudging.

* feat(self-upgrade): surface what's-new in notify + wire agent integration (AGENTS.md, HEARTBEAT) + e2e

- self-upgrade --check-only --json now includes changelog_diff + release_url
  (export fetchChangelog); the gbrain-upgrade skill shows 3-5 what's-new bullets
  before the 4-option prompt instead of just version numbers.
- setup injects a self-upgrade marker protocol into AGENTS.md so interactive
  agents (Claude Code, Codex) act on the UPGRADE_AVAILABLE stderr marker — the
  piece that makes notify actually fire for them.
- HEARTBEAT daily beat routes through the gbrain-upgrade skill (OpenClaw/Hermes
  cron cadence); auto-mode daemons ride the autopilot tick.
- e2e: real subprocess invocation proves the marker fires (notify emits;
  off/snooze/up-to-date silent; JUST_UPGRADED fires+clears; --quiet suppresses).
  Serial test: --check-only surfaces the changelog.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 06:20:03 -07:00

103 lines
3.7 KiB
TypeScript

import { VERSION } from '../version.ts';
import { isMinorOrMajorBump, isValidVersionString } from '../core/semver.ts';
import { fetchChangelog, fetchLatestRelease } from './check-update.ts';
import { detectInstallMethod, runUpgrade } from './upgrade.ts';
import { writeUpdateCache } from '../core/self-upgrade.ts';
/**
* `gbrain self-upgrade [--check-only] [--force] [--json]`
*
* The universal substrate every agent ecosystem (Codex / Claude Code / Hermes /
* OpenClaw / Perplexity-server) can call to stay current. The CLI startup hook
* emits a marker; the agent skill / autopilot daemon act on it by running THIS
* command. The action is always the hardcoded `gbrain upgrade` — never
* parameterized by any marker content (forged-marker guard).
*
* --check-only Report whether an upgrade is available; never apply.
* --force Apply even if not behind (re-run the install-method swap).
* --json Machine-readable output for the check.
*/
export async function runSelfUpgrade(args: string[]): Promise<void> {
if (args.includes('--help') || args.includes('-h')) {
console.log(
'Usage: gbrain self-upgrade [--check-only] [--force] [--json]\n\n' +
'Check for and apply gbrain updates. The shared entry point used by the\n' +
'CLI startup marker, the gbrain-upgrade agent skill, and the autopilot\n' +
'silent channel.\n\n' +
' --check-only Report whether an upgrade is available; do not apply.\n' +
' --force Apply even when not behind.\n' +
' --json Machine-readable output (with --check-only).',
);
return;
}
const checkOnly = args.includes('--check-only');
const force = args.includes('--force');
const json = args.includes('--json');
const release = await fetchLatestRelease();
const latest = release ? release.tag.replace(/^v/, '') : null;
const behind = !!latest && isValidVersionString(latest) && isMinorOrMajorBump(VERSION, latest);
// Warm the cache so the next invocation's startup hook can emit without a fetch.
try {
if (latest && isValidVersionString(latest)) {
writeUpdateCache(
behind
? { kind: 'upgrade_available', current: VERSION, latest }
: { kind: 'up_to_date', current: VERSION },
);
}
} catch {
/* best-effort */
}
if (checkOnly) {
// Tell the operator WHAT they'd get: fetch the changelog only when actually
// behind (so an up-to-date check stays a single release fetch). The agent
// skill surfaces these "what's new" bullets in the notify prompt.
let changelogDiff = '';
if (behind && latest) {
try {
changelogDiff = await fetchChangelog(VERSION, latest);
} catch {
/* best-effort: an unavailable changelog must not block the check */
}
}
if (json) {
console.log(
JSON.stringify(
{
current_version: VERSION,
latest_version: latest ?? '',
update_available: behind,
install_method: detectInstallMethod(),
release_url: release?.url ?? '',
changelog_diff: changelogDiff,
},
null,
2,
),
);
} else if (behind) {
console.log(`Update available: ${VERSION} -> ${latest}. Run: gbrain self-upgrade`);
if (changelogDiff) {
console.log('\nWhat changed:\n');
console.log(changelogDiff);
}
if (release?.url) console.log(`\nRelease: ${release.url}`);
} else {
console.log(`gbrain ${VERSION} is up to date.`);
}
return;
}
if (!behind && !force) {
console.log(`gbrain ${VERSION} is up to date.`);
return;
}
// Apply: delegate to the hardcoded upgrade path (full swap + post-upgrade).
await runUpgrade([]);
}