* 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>
103 lines
3.7 KiB
TypeScript
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([]);
|
|
}
|