How Career-Ops Update Checker Works: Safe Version Migration and Rollback

Career-Ops uses a self-contained Node.js script (update-system.mjs) that checks upstream GitHub releases against a local VERSION file, then applies updates transactionally while strictly isolating system files from user data like CVs and profiles.

The santifer/career-ops repository implements a robust update checker mechanism that keeps the system layer synchronized with upstream releases without risking user customizations. This version migration system operates through update-system.mjs, a command-line tool that handles everything from availability checks to atomic rollbacks while enforcing a strict boundary between system-owned scripts and user-owned content.

Update Check Workflow

The update checker follows a deterministic seven-step process to determine if a version migration is available. This workflow is implemented in update-system.mjs and consumed by both the CLI and the doctor.mjs health-check script.

Dismiss Flag and Local Version Detection

The process begins by checking for a .update-dismissed hidden file in the project root. If present, the script immediately returns {"status":"dismissed"} and skips all network operations, allowing users to silence update notifications temporarily.

If not dismissed, the script reads the current installation state via localVersion(), which parses the project-root VERSION file to extract the local semantic version. This file serves as the ground truth for the installed system layer.

Remote Version Fetching

To determine if a newer release exists, the script queries two upstream sources in parallel using Promise.all and curl:

  • RAW_VERSION_URL: The raw VERSION file from the upstream repository
  • RELEASES_API: GitHub's latest release API endpoint returning JSON metadata

Both sources are fetched simultaneously to minimize network latency. This dual-source approach ensures the update checker remains functional even if one endpoint is temporarily unavailable.

Semantic Version Parsing and Comparison

The raw response from RAW_VERSION_URL is processed by parseVersionFile(), which applies the SEMVER_RE regex (/v?(\d+\.\d+\.\d+)/) to extract valid version strings. Similarly, the release API response is parsed to extract release.tag_name using the same regex pattern.

The system then determines the authoritative remote version using a fallback strategy: if one source fails, the other is used; if both succeed, the higher semantic version wins. This handles cases where the raw VERSION file lags behind an official GitHub release. Finally, compareVersions() evaluates whether the remote version exceeds the local installation, returning JSON statuses including "up-to-date", "update-available" (with changelog), "offline", or "no-remote-version".

Transactional Update Application

When users run node update-system.mjs apply, the version migration executes as an atomic, multi-stage transaction with built-in recovery mechanisms.

Backup and Safety Staging

Before modifying any files, the updater creates a temporary backup branch (update-backup-<date>) and a WIP stash reference. This ensures any uncommitted work can be recovered even if the process interrupts unexpectedly. The backup is created before any file overwrites occur, following the principle that recovery assets must exist prior to risk exposure.

Self-Re-execution Pattern

The updater implements a defensive self-re-execution strategy to handle module changes across versions. Initially, it checks out only the files necessary for the updater itself to run via resolveReexecCheckout(), then spawns a second Node process with CAREER_OPS_UPDATE_REEXEC=1 set. This isolation prevents crashes that would occur if the current updater tried to import modules that exist only in the target version.

System File Checkout

Once re-executed, the updater reads the target version's SYSTEM_PATHS array by calling extractArrayFromSource() on the new update-system.mjs. It then checks out every path in this array from FETCH_HEAD, merging historic BOOTSTRAP_PATHS for backward compatibility with very old installations. This selective checkout ensures only system-layer files (scripts, modes, templates) are modified, while user-layer files remain untouched.

Materializing Skill Entrypoints

For filesystems that cannot store symlinks (common in certain sandboxed environments), the updater calls ensureSkillEntrypoints() to write full skill files (e.g., .claude/skills/…) directly to disk. This guarantees CLI functionality continues regardless of underlying filesystem limitations.

Safety Validation

After checkout, the script performs a strict safety validation by diffing git status against the initial clean state. Any modification detected outside SYSTEM_PATHS but within USER_PATHS prefixes triggers a SAFETY VIOLATION error, causing immediate abort. This boundary enforcement protects user files like cv.md, config/profile.yml, and modes/_profile.md from accidental overwrites.

Rollback and Recovery

If any stage fails or the user runs node update-system.mjs rollback, the system executes a complete reversal:

  1. Checks out the backup branch created at the start of apply
  2. Removes any files that did not exist in the backup (cleaning up newly added system files)
  3. Restores user files from the backup if the safety violation check detected unintentional touches

This atomic rollback guarantees the repository returns to the exact pre-update state, making version migrations fully reversible.

System vs. User Data Boundaries

The update checker mechanism respects a strict data contract defined in DATA_CONTRACT.md and enforced through path constants:

  • System Paths: Includes update-system.mjs itself, scripts, modes, templates, and dashboards
  • User Paths: Includes CV files, profile configurations, and custom modes

The updater itself is part of SYSTEM_PATHS, allowing future releases to safely add, rename, or remove system files without breaking older installations. The semantic version parser tolerates both plain semver (1.2.3) and prefixed tags (career-ops-v1.2.3), ensuring flexibility in release naming conventions.

Summary

  • Availability Detection: Compares local VERSION file against GitHub's raw content and releases API using parallel curl requests
  • Transactional Safety: Creates git backups and stashes before any modifications, with atomic rollback capability
  • Self-Re-execution: Isolates version jumps by re-spawning the updater process to handle new module imports safely
  • Path Enforcement: Strictly validates that changes remain within SYSTEM_PATHS and never touch USER_PATHS like CVs or profiles
  • Symlink Handling: Automatically materializes skill entrypoints on filesystems without symlink support

Frequently Asked Questions

How does Career-Ops handle network outages during update checks?

If both curl calls to RAW_VERSION_URL and RELEASES_API fail, the update checker returns {"status":"offline"} and preserves the current installation state. The system treats this as a non-fatal condition, allowing the user to continue working with the existing version until connectivity resumes.

Can I skip update notifications without applying the update?

Yes. Running node update-system.mjs dismiss creates a .update-dismissed file that suppresses future check notifications. The system will remain silent across all sessions until you manually remove this flag or apply the update, at which point the dismissal is automatically cleared.

What happens if the updater modifies my personal CV or profile data?

The updater strictly enforces a safety boundary between system and user data. If git status detects any modification to files within USER_PATHS (such as cv.md or config/profile.yml) during the update process, the script immediately aborts with a SAFETY VIOLATION error and triggers a rollback to the pre-update backup. This design ensures your personal content is never overwritten during version migrations.

Why does the updater restart itself during the apply process?

The self-re-execution pattern prevents import errors when upgrading across major versions. When the target version contains new dependencies or renamed modules that the current updater doesn't recognize, the script first checks out its own new source code, then spawns a fresh Node process with CAREER_OPS_UPDATE_REEXEC=1. This ensures the migration logic runs with the target version's code rather than the outdated local version.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →