mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-28 06:23:01 +00:00
* 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>
71 lines
2.9 KiB
TypeScript
71 lines
2.9 KiB
TypeScript
/**
|
|
* Tiny semver comparison helpers, extracted from `check-update.ts` so both
|
|
* the update-check path and the new self-upgrade decision module
|
|
* (`src/core/self-upgrade.ts`) can depend on them without an import cycle
|
|
* (self-upgrade ← check-update would cycle once check-update imports the
|
|
* cache helpers back from self-upgrade). `check-update.ts` re-exports
|
|
* `parseSemver` / `isMinorOrMajorBump` for back-compat with existing importers.
|
|
*
|
|
* Supports both 3-segment (`0.41.38`) and 4-segment (`0.42.3.0`) gbrain
|
|
* version strings. The 4th `.MICRO` segment is gbrain's dot-suffix
|
|
* follow-up channel; comparisons use it as a 4th ordering key.
|
|
*/
|
|
|
|
/** A parsed version tuple (major, minor, patch). The 4th `.MICRO` segment is
|
|
* deliberately NOT compared — micro bumps collapse to "equal" with the patch,
|
|
* which is the desired "ignored" behavior for the self-upgrade decision (we
|
|
* only ever act on minor/major bumps). Kept 3-wide for back-compat with
|
|
* existing `parseSemver` callers/tests. */
|
|
export type SemverTuple = [number, number, number];
|
|
|
|
/** Strict shape gate for a remote version string before it reaches the agent.
|
|
* Accepts both 3-segment (`0.41.38`) and 4-segment (`0.42.3.0`) gbrain versions. */
|
|
export const VERSION_RE = /^\d+\.\d+(?:\.\d+){0,2}$/;
|
|
|
|
/** True iff `v` (optionally `v`-prefixed) is a plain numeric dotted version. */
|
|
export function isValidVersionString(v: string): boolean {
|
|
return VERSION_RE.test(v.replace(/^v/, ''));
|
|
}
|
|
|
|
/**
|
|
* Parse a version string into a (major, minor, patch) tuple. Returns null on
|
|
* any non-numeric or too-short input. Accepts a leading `v`. A 4th `.MICRO`
|
|
* segment is accepted by the shape gate but truncated here.
|
|
*/
|
|
export function parseSemver(v: string): SemverTuple | null {
|
|
const clean = v.replace(/^v/, '');
|
|
const parts = clean.split('.');
|
|
if (parts.length < 3) return null;
|
|
const nums = parts.slice(0, 3).map(Number);
|
|
if (nums.some((n) => !Number.isFinite(n))) return null;
|
|
return [nums[0], nums[1], nums[2]];
|
|
}
|
|
|
|
/** Strict greater-than over the tuple. */
|
|
export function semverGt(a: SemverTuple, b: SemverTuple): boolean {
|
|
for (let i = 0; i < 3; i++) {
|
|
if (a[i] !== b[i]) return a[i] > b[i];
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** a <= b. */
|
|
export function semverLte(a: SemverTuple, b: SemverTuple): boolean {
|
|
return !semverGt(a, b);
|
|
}
|
|
|
|
/**
|
|
* True when `latest` is a minor or major bump over `current` (patch / micro
|
|
* bumps are deliberately ignored, matching `gbrain check-update`'s
|
|
* established posture — patch noise should not nag every invocation).
|
|
* Unparseable inputs are treated as "not a bump" (fail-open to up-to-date).
|
|
*/
|
|
export function isMinorOrMajorBump(current: string, latest: string): boolean {
|
|
const cur = parseSemver(current);
|
|
const lat = parseSemver(latest);
|
|
if (!cur || !lat) return false;
|
|
if (lat[0] > cur[0]) return true;
|
|
if (lat[0] === cur[0] && lat[1] > cur[1]) return true;
|
|
return false;
|
|
}
|