An unhandled daemon crash skips teardown: the durable reviewer lease
comment survives, the in-memory session lease dies, and the
auto-reconnected daemon inherits a stale GITEA_ACTIVE_WORKTREE from its
parent environment (PR #701 / branches/review-pr-654 incident).
AC2 — safe clear/re-bind of stale bindings:
- new stale_binding_recovery.py: classifies the active env binding
(corroborated / unverified_inherited / provably_stale_missing_path /
superseded_by_session_lease) and produces a fail-closed recovery plan;
only provably stale bindings are clear-eligible
- gitea_resolve_task_capability and daemon boot run the sanctioned
recovery (durable audit via session state); runtime context surfaces
the classification read-only
- namespace_workspace_binding.resolve_namespace_workspace(verify_paths=…)
demotes env-bound worktrees whose path no longer exists; mutation and
runtime-context resolution always verify
AC3 — recoverable lease cleanup after unexpected exit:
- durable session-lease shadow (KIND_REVIEWER_SESSION_LEASE) written on
sanctioned record/heartbeat, removed on sanctioned clear; a crash
leaves provable orphan evidence (owner pid + session id)
- assess_crashed_session_lease_orphan derives tri-state
owner_process_alive: only a dead recorded owner pid proves exit; PID
liveness is never ownership proof
- diagnose/cleanup wrappers consume the shadow instead of passing
owner_process_alive=None
- new classification orphaned_expired_superseded_head: an expired
superseded-head lease with no terminal review stops being a permanent
ambiguous/wait; post-expiry it unlocks fresh acquisition, and guarded
cleanup becomes eligible only with owner-exit evidence + clean/absent
worktree + controller authorization + confirmation. Pre-expiry
behavior is unchanged (fail-closed wait, no steal).
Validation: tests/test_issue_702_stale_binding_lease_recovery.py (29
tests covering lost in-session leases, old-head leases, runtime/worktree
mismatch, managed reconnect, safe expiry); full suite 2665 passed,
6 skipped, 161 subtests.
Co-Authored-By: Claude Fable 5 <[email protected]>
Prevention hardening for the #670 incident (bare direct-to-master commit
2fa97c26 and the PR #654 merger stable-branch push attempt). Worker sessions
must never publish stable branches directly; stable updates land only through
sanctioned Gitea merge tooling or an authorized reconciler path.
New pure module stable_branch_push_guard.py:
- classify_push_command: detects git push <remote> <stable-ref> equivalents
including refspecs (HEAD:<ref>, +refs/heads/x:refs/heads/<ref>), --force,
--delete/:<ref>, and --dry-run/-n no-op probes (dry-run still proves intent).
Fetch/pull and feature-branch pushes are never flagged; sanctioned
gitea_merge_pr / API merge is not a push.
- assess_root_checkout_local_commit: detects control-checkout commits not on an
issue feature branch (branches/ worktrees exempt).
- redact_command: strips URL userinfo/token assignments before logging.
- build_contamination_record + assess_contamination_gate: durable marker shape
and fail-closed gate (reconciler-exempt; comment/lock stay allowed for handoff).
Server wiring (gitea_mcp_server.py, mcp_session_state.py):
- KIND_STABLE_BRANCH_CONTAMINATION durable session marker (per profile identity).
- _enforce_stable_branch_contamination_gate wired into verify_preflight_purity
so review/merge/close/completion mutations fail closed while contaminated.
- gitea_record_stable_branch_push_attempt: classify + mark on detection.
- gitea_audit_stable_branch_contamination: reconciler-only inspect/clear; a
worker session can never self-clear.
Tests (AC5): no-op dry-run push, real direct push, sanctioned Gitea merge,
fetch-only, root-checkout local commit, feature-branch push allowed, gate
block/reconciler-exempt, redaction. 51 new tests; full suite 2596 passed.
Docs: llm-project-workflow SKILL.md — universal rule, blocker class, and a
dedicated "Stable Branch Push Protection" section (worker sessions must never
push stable branches directly).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Share review-workflow-load and review-decision-lock state across MCP
daemon process pools via a user-private durable store keyed by profile
identity (not host-global /tmp), with TTL and fail-closed profile checks.