The launch-time repair (hermes_bootstrap -> _early_recovery under the checkout lock) lives in
the tree a killed git rewrites. A merge SIGKILLed while writing hermes_bootstrap.py,
_early_recovery.py, a prefix of hermes_cli/__init__.py or a custody module left every launch
exiting 1 with the marker kept and the tree never restored.
arm_tree_move now publishes the recovery/lock/custody modules, as committed at the move's
pre commit (git cat-file, never the working tree), to <git dir>/hermes-update-recovery/<pre>/
by an atomic directory rename before it writes the marker. The minted launcher catches a
failing checkout import; with the marker present and its pre's closure published it purges
checkout modules, runs restore_interrupted_pull from that copy (same claim and checkout lock:
a live writer still refuses) and relaunches from the restored tree.
(cherry picked from commit 2bc12d9283d90a54f970a354a52f0a95d41bbfb0)
(cherry picked from commit 924b2a9a2d4dde6bc2a9074ff3ee0cd8f87c8684)
A merge killed while writing hermes_constants.py now reaches the repair (the launcher
imports hermes_bootstrap first), but on a Windows install whose only git is PM's store
copy the repair looked it up through pm, which imports hermes_constants: the lookup
failed, a bare 'git' died with WinError 2 and the tree stayed torn (native mid_git,
run 37198020181). arm_tree_move records the absolute git this run resolved (PATH or
expose_pm_git) and _git_executable prefers it.
(cherry picked from commit 59a744696d3805fdc9fb3f0bf5d5f3b20161cbce)
(cherry picked from commit 8bc8502e336d847a88fc4fd7c408f18d6950377f)
update_cmd_commit's diff/show/rev-parse/status probes run through
update_custody.run_git like every other updater git call: -c gc.autoDetach=false
-c maintenance.auto=false, no lock fd for readers, the owner's kill-on-close job
on Windows (R2).
(cherry picked from commit ee8726ef171b9dd355aed80014c5feb3f0ac3256)
(cherry picked from commit cd2ca1f31296f9de2b3f14f2196e8fe67653bd5a)
R1. The launch-time replay of a syntax rollback ran `git reset -q pre` before any
dead-lock reclaim and ignored its result; HEAD still on the broken release then
read as "git finished" and the marker was deleted, leaving the broken tree and a
stale index.lock with nothing left to restore it. In-process, the same path made
settle_failed_tree_move report the rollback complete.
Now: the killed step's index.lock is reclaimed first and only once its git is
proven gone (/proc fds + live git with cwd in the checkout on Linux, lsof on
macOS/BSD, the unlink itself on Windows; unknowable = keep); update-ref, reset
and the HEAD readback are each checked; a rollback marker is removed only when
HEAD == pre, no index.lock and no tracked change. The in-process rollback checks
drop_added_files and the same whole-tree oracle before removing the marker, and
says the next launch finishes it when it could not.
Was (reviewer probe, real git SIGKILLed at index.lock): restore returned False,
HEAD broken, lock left, marker gone. Now: restore returns True, HEAD pre, clean,
lock and marker gone; a live lock holder keeps the marker and the lock.
(cherry picked from commit 087ae72d38be89892a70e97a935db532a9e8284f)
(cherry picked from commit 5984c98726cb29b1123552f875f99d5020f01b71)
The takeover's new fleet-obligation arm imported `update_cmd_fleet`'s writer,
which imports `update_cmd` -> `hermes_cli.config` -> ruamel. `prepare()` runs
in the HISTORICAL interpreter before PM syncs the new dependencies, so every
update from a release whose venv has no ruamel (the installer-script and
e2e-upgrade core/handoff/pm legs) exited 1 with
`Update preparation failed: No module named 'ruamel'`. The arm now writes the
host obligation through the stdlib-only `update_host_obligation`, with the same
per-home breadcrumb fallback.
Also:
- The syntax rollback's mixed reset un-tracked the files the broken commit
ADDED, and `reset --hard` never touches untracked files, so they stayed in
the tree (`?? hermes_cli/`). They are now removed by name after the reset.
- A git that EXITED on another git's `index.lock` no longer has that lock
deleted by the in-process settle: the lock is foreign by construction, the
scan only reads, and a tree with writes to put back keeps its marker.
- `_early_recovery`'s git runs carry `cwd` as well as `-C`.
- `_early_recovery._git_executable` gets a reasoned resolution-allowlist row
(stdlib-only launch repair; cannot import `hermes_platform`).
Tests rewritten to the new contract:
- test_source_release_channels zip fallback: the download stub takes the
target SHA the ZIP swap now journals, and asserts it is passed through.
- test_update_target_identity: the fault injection keys the upstream sync by
its remote (`upstream`/`upstream/main`), since the sync now merges the
fetched ref instead of a second `pull`; the origin-target syntax case
expects the pre-move preflight refusal.
- test_hostile_commit ZIP cell kills right after a tracked file landed, not
at the N-th swap (listing order made it land on unchanged entries only).
(cherry picked from commit b6adf96ca6988262fe7c408ea652d3b390c7be8d)
(cherry picked from commit 028cb933271626d21003c255bb397deb8998b7da)
The syntax rollback's first step was `reset --soft` (or `update-ref` for a
parked/detached checkout), which leaves the index on the broken commit. A
kill during the following `reset --hard`, after it had written a good file,
left that file staged as the broken version, so the next launch's restore
landed on the pre-update commit with a dirty index (`MM`).
The first step is now a mixed reset (`reset -q <pre>`), after `update-ref
--no-deref HEAD` for the detach mode. HEAD and the index are back on the
pre-update commit before any file is written. The marker's `rollback=` field
is `branch`/`detach`, and recovery redoes the same steps when the kill came
before them. A failed rollback step now goes to the in-process restore every
time. Before this, a failure in the first step dropped the marker, even
after `update-ref` had already moved HEAD.
Also: the old branch-switch hostile cell triggers on `checkout [--no-guess]
main`, since CP0 now passes `--no-guess`. The fork cell makes origin read as
a fork through the git shim, because the sandbox otherwise reports the
official URL and the upstream sync never runs.
(cherry picked from commit 23f9658868b9c1f639ed972e59251fe154372929)
(cherry picked from commit 5fd6de2e62152076d6703e5ff70250ec1ece71b9)
Review round 2 on the LP-COMMIT commit point:
- A failed upstream fork fast-forward after a committed origin pull no
longer hands back the obligations the origin pull armed.
`disarm_commit_obligations` now refuses unless HEAD is the commit the
run STARTED from (`record_run_start`), so every caller is covered; the
restore of the snapshot is an atomic replace.
- The ZIP swap journal counts a leftover `<entry>.hermes-update-old` as
"existed" (`_stage_replacement` puts it back before the swap), and the
`swapping` recovery treats any backup as proof the entry existed, so a
kill mid-swap can no longer delete both copies. A pre-run journal the
start-of-run settle could not clear refuses the swap instead of being
overwritten.
- CP0 runs `git checkout --no-guess <branch>` only when the local branch
exists; otherwise only the marked `checkout -B <branch> origin/<branch>`
runs. git's checkout guess used to create the branch and rewrite the
tree while the code believed the checkout could not move.
- The syntax rollback moves HEAD back to the pre-update commit first
(`reset --soft`, or `update-ref --no-deref` for a parked/detached
checkout), then `reset --hard`. The marker names pre = the good commit
and `rollback=<mode>`, so a kill anywhere lands the next launch on the
pre-update commit; recovery redoes the HEAD step if the kill came first.
- The syntax guards (preflight `git show`, ZIP compile gate, post-pull
backstop) skip a target whose `requires-python` excludes the running
interpreter: its syntax may be a newer Python's. Unknown specs keep
checking.
- The no-op exit in `_verify_head_after_pull` disarms (self-verified).
(cherry picked from commit c787a8c3f9823be8639f6f38efbbdff2b78a97cd)
(cherry picked from commit d785ad1c7e919bd7d64d17f2e9d664b84a7e8982)
Arm the source-completion tail and the fleet obligation in the parent before
any tree move; cover the CP0 branch switch, the upstream fork ff and the syntax
rollback with the interrupted-pull marker; settle (never drop) the marker on a
non-kill git failure; refuse foreign-owned venvs and uncompilable targets before
HEAD moves; push the fork only after a validated update; ZIP fallback only from
an untouched checkout; journal the ZIP swap so start-of-run recovery finishes or
rolls it back and no staging/backup sibling wedges the next run.
(cherry picked from commit 584acad44f3d6c552b7c2767d12c612de0436498)
(cherry picked from commit c6567ac2d28c421f384d95e1a6fc28912cb30f86)