The MCP server derived the canonical repository root from the install
checkout the server script lives in (PROJECT_ROOT), so any namespace that
ran the server against an external repository (e.g. eagenda-author, or the
mcp-control-plane reviewer/merger namespaces) failed every mutation: the
branches-only / worktree-membership guards (#274) compared the task
workspace against the Gitea-Tools .git directory it can never belong to.
Separate the two concepts:
- PROJECT_ROOT stays the immutable code/install root.
- A new, optional, namespace-scoped canonical_repository_root binds the
session to the working root of the target repository.
Implementation:
- canonical_repository_root.py: resolve the binding from profile/env
(GITEA_CANONICAL_REPOSITORY_ROOT overrides the profile field), validate
existence + git identity, fail closed on missing/conflicting/forged
bindings. Preserves the single-repo default when unconfigured.
- session_context_binding.py: pin canonical_repository_root into the
immutable #714 session context; drift fails closed.
- namespace_workspace_binding.py: route the configured target root through
resolve_namespace_mutation_context so #274 / membership guards evaluate
against the target repository.
- gitea_mcp_server.py: seed + enforce the binding in the mutation preflight
(both purity-order and FORCE_PRODUCTION_GUARDS paths); validate repository
identity and git common-directory membership.
- gitea_config.py: validate the optional absolute-path profile field.
- gitea_auth.py: surface the config-sourced field through get_profile.
No weakening of root-checkout, remote/repository, or anti-stomp guards; the
single-repo Gitea-Tools path is unchanged.
Tests: two distinct real git repositories/worktrees prove cross-repository
bindings resolve, enforce membership, and fail closed on forged/conflicting
identity and simultaneous prgs/mdcps isolation.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
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]>
Namespace-scoped workspace resolution prevents GITEA_AUTHOR_WORKTREE from
poisoning reviewer, merger, and reconciler purity checks. Each role uses
its own worktree env var with actionable binding-source errors and safe
reconnect guidance. Preserves #274/#475 author and control-checkout guards.