System Upgrade
Upgrade an ALIVE world from any prior version to the current target. The skill is a thin operator-facing surface; the work happens in the system_upgrade/ Python package shipped with the plugin (orchestrator + 13 locked phases). Stdlib-only, no PyYAML/ruamel.
If you are reading this on the old monolithic skill (476 lines of inline upgrade logic), that file has been retired in favour of the orchestrator. This file documents what the operator and skill agent need to know to drive it.
When It Fires
- The human runs
/alive:system-upgrade(any version of the world — v1, v2, v3.0, v3.1, v3.2). - The session-new hook detects a legacy structure and surfaces the upgrade prompt.
- The human says "upgrade my world", "migrate to the new version", "update alive".
- The human asks to inspect or restore an earlier upgrade tarball (
--rollback).
Tool version vs world version
Two distinct version concepts; the orchestrator never confuses them.
- Tool version — read from
plugins/alive/.claude-plugin/plugin.jsonversion. The version of the migrator currently installed. Used for--resumeplugin-version-skew validation and the upgrade record'stool_version_at_runfield. Never feeds world-version inference; never feeds the no-op short-circuit. - World version — derived from world content fingerprints only: path/file existence, bundle schema fingerprints, hook/script content patterns. Three signals, not four. Lowest-version-wins. When zero signals fire, refuse with
--assume-empty-worldas the explicit override. - TARGET_WORLD_VERSION — hardcoded constant in
system_upgrade/__init__.py(currently"3.2.0"). The version the redesign migrates worlds TO. The no-op short-circuit compares the world version (and every per-walnut version) against this constant — never against the tool version. Bumped in lockstep with each plugin minor that introduces world-format changes.
Phases (locked 13)
The orchestrator runs the same 13 steps every invocation. Numbers below are stable contract numbers — they appear in resume markers, runstate logs, and the phase_reached envelope field.
For higher-level grouping in operator briefings the orchestrator uses the labels Setup / Detection block (steps 1–5) and Mutation / Verify block (steps 6–13) — labels are not "macro phases" and never re-use the digit 1 to mean step 1 of a sub-grouping; the locked numbers below are the only phase numbers that matter.
1. Preflight — resolve world_root, .alive symlink + containment check, then UpgradeLock
acquire (lock-meta written ONLY after containment validation), dirty stash,
Syncthing, half-sync, submodule guards
2. Snapshot — FileSnapshot pass: both world files + required plugin files. Inputs frozen
for the rest of the run.
3. Detect — consume snapshot; produce DetectionReport (world_version + per-walnut
versions + all_signals_raw + tool_version_at_run + walkthrough_eligible_matches).
The retired-pattern PRE-SCAN runs here as a read-only pass against the
snapshot — phase 7 only renders prompts for matches found here.
4. Probe surfaces — each surface's --version --json; collect state_paths for sweep exclusion.
NO migrator dispatch yet. --surfaces=none skips per-surface probe + dispatch
but does NOT skip the prior-record load — load_prior_final_record runs
unconditionally so pending retries from a prior run still reach the no-op gate.
5. NO-OP short-circuit — gate predicate: world_version == TARGET_WORLD_VERSION AND every
per-walnut version == TARGET AND walkthrough_eligible_matches is empty AND
surface_retry_map is empty AND probe_results contains no hard-fail. On pass:
write a no-op upgrade record, release lock, exit 0. On fail: continue.
--force-run bypasses; --dry-run logs the would-be no-op without writing.
6. Backup — write .alive/upgrades/pre-upgrade-<iso-ts>.tar.gz (atomic stage → fsync →
rename). Stages selected paths into a temp staging dir; excludes
.alive/upgrades/, the lock files, and any .alive/.rollback-* dirs to prevent
recursive self-inclusion.
7. Walkthrough decide — render prompts for the walkthrough_eligible_matches collected in phase 3;
collect y/n decisions. Pure presentation + decision capture; NO writes
(dry-run-safe). Phase 7 NEVER re-scans the catalog.
8. Plugin cleanup — world-root sweep + per-walnut audit. Operates ONLY on retired-pattern
catalog entries with cleanup_action == "cleanup". migrate_input entries
(`_core/`, `_capsules/`, `now.md`, `tasks.md`, `observations.md`,
`_kernel/_generated/`, `03_Inputs/`, `companion.md`) are NOT deleted here
— phase 9 consumes them.
9. Plugin migrate — per-version migrations (v2→v3.0, v3.0→v3.1, v3.1→v3.2). Consumes the
walkthrough decisions from phase 7 to apply extension rewrites; consumes
cleanup_action=="migrate_input" catalog entries (reads, transforms into
the v3 layout, then removes the source paths atomically).
10. Surface dispatch — run each surface's migrator (alive-mcp, Hermes, Codex). Soft-fail per the
four-class probe contract. Also consumes the carried-forward needs_retry[]
from phase 4's prior-record load.
11. Verify — live-read verification against post-migration state. Reads disk fresh, NOT
the start-of-run snapshot (which would miss failed cleanup/migrate effects).
Under --dry-run, verify reads through the virtual post-state overlay.
12. Record — atomic write .alive/upgrades/<iso-ts>.yaml (final upgrade record).
13. Release lock — flock release + lock-meta cleanup. Always runs (`finally` block in the CLI
handler) even when an earlier phase refused.
Skill-invocation pattern
Every ALIVE skill that drives a Python orchestrator follows the same six steps (per conventions.md § Skill-invocation convention). System-upgrade is no exception; the steps are spelled out here so future skill authors and the agents reading this skill know exactly how the surface must behave.
- Resolve world root. The skill calls
target_resolver.resolve_target_world(cwd=...)(the legacy-aware resolver shipped in T1) — it walks up from cwd looking for a high-confidence world marker (.alive/, two canonical numbered domain dirs,.walnut/,_core/+companion.md,_core/+now.md, or thecompanion+now+taskstriple). For destructive operations against un-numbered legacy domains the resolver refuses to guess; the operator must pass--world-root(or the positional path) explicitly. Other ALIVE skills use_common.find_world_root_with_strategy(); system-upgrade uses the legacy-aware path because it is the one command that operates on pre-v3 worlds. - Validate flags + read TTY confirmations. The skill validates flag combinations (mutex on
<world-path>vs--world-root,--dry-runrequires--plan-outputor--json, etc.), prompts the operator for type-back when the path-policy gate flags a home/cloud target as confirm-required, and gates--unsafe-confirm-targeton a real TTY (or--non-interactiveto bypass). - Invoke the orchestrator via subprocess.run. The skill calls
python -m system_upgrade.cli <args>(or, in-process, dispatches through thebin/alive_SUBCOMMANDSregistry) withsubprocess.run(..., shell=False, capture_output=True). Nevershell=True; never process substitu