fix: correct runtime-gate alignment and per-call state for #615 (Closes #615)

Remediates reviewer findings F1, F2, and F3 from review 483 on PR #770.

F1 (blocking): _current_runtime_mode_report derived the runtime-gate alignment
input as realpath(workspace_root) == process_project_root, redefining
workspace_roots_aligned. Its established meaning is ctx["roots_aligned"]
(canonical_repo_root == process_project_root) — a repository-level question.
Because the global worktree rule requires all task work to live in a branches/
worktree, the old derivation reported aligned False for exactly the sessions
that are configured correctly, raising unsafe_process_root_workspace_alignment
and refusing every non-read operation once an author, reviewer, or merger
namespace held a worktree binding. The gate is now fed ctx["roots_aligned"].

F2 (blocking): _current_runtime_mode_report cached its first result into
_STARTUP_RUNTIME_MODE, which was initialised to None rather than captured at
import, and the read-only refresh=True path seeded it too. Session-scoped
fields (active_task_workspace and its derived alignment) and mutable fields
(dirty_files) were frozen for the process lifetime, so the acceptance
criterion 7 dirty-runtime blocker stopped applying after the snapshot and one
session's binding decided alignment for every later session.

Immutable process facts (process root, branch, head, checkout-ness) are now
captured at import as _STARTUP_RUNTIME_FACTS, matching the #420 parity
baseline. Dirty state, workspace binding, and alignment are recomputed on every
call. A new stable_control_runtime.observe_dirty_files() splits out the one
runtime fact that legitimately changes during a process lifetime;
observe_runtime() delegates to it so the parsing lives in one place. refresh is
retained for the read-only reporting path but no longer selects a cache, so a
read-only call can neither seed nor weaken a later mutation decision.

F3 (major): no test exercised the real derivation — every server-wiring test
asserting a healthy runtime permits mutations patched
_current_runtime_mode_report with a fixture whose workspace_roots_aligned was
True. New TestServerWiringRealDerivation patches only the derivation's inputs
and lets the real function build the report:

- a clean stable control checkout plus a correctly bound branches/ worktree
  permits an otherwise authorized mutation;
- misaligned process/canonical roots fail closed;
- newly dirty task state is detected after an earlier clean read;
- a read-only refresh cannot freeze a permissive mutation result;
- an unresolvable binding reports unknown alignment, never alignment proof.

Acceptance criteria 6, 8, 10, and 11 are unchanged and untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
2026-07-20 12:28:07 -04:00
co-authored by Claude Opus 4.8
parent 9bf3acfef6
commit ab34280f90
3 changed files with 189 additions and 30 deletions
+43 -28
View File
@@ -2010,12 +2010,21 @@ import stable_control_runtime # noqa: E402
# Read-only operations are never blocked by staleness.
_STARTUP_PARITY = master_parity_gate.capture_startup_parity(PROJECT_ROOT)
# Stable-control runtime snapshot (#615): which runtime this process serves
# from. Captured lazily on first use and then reused, so the mutation gate never
# shells out per call. Whether the gate enforces at all is decided from how the
# process was loaded -- a suite running from a branches/ worktree is dev-test by
# design, and per-test production simulation must not switch this gate on.
_STARTUP_RUNTIME_MODE: dict | None = None
# Stable-control runtime facts (#615): which runtime this process serves from.
# These are the *immutable* facts -- process root, branch, head, checkout-ness --
# and they are captured at import exactly like the #420 parity baseline, because
# the runtime a process serves from is fixed when it loads its code.
#
# Dirty state and the session's workspace binding are deliberately NOT captured
# here. Both change during a process lifetime, so freezing them would retire the
# acceptance-criterion-7 dirty-runtime blocker after the first snapshot and let
# whichever session happened to call first decide alignment for every session
# after it. _current_runtime_mode_report() re-evaluates them per call.
#
# Whether the gate enforces at all is decided from how the process was loaded --
# a suite running from a branches/ worktree is dev-test by design, and per-test
# production simulation must not switch this gate on.
_STARTUP_RUNTIME_FACTS = stable_control_runtime.observe_runtime(PROJECT_ROOT)
_RUNTIME_MODE_GATE_UNDER_TEST = "pytest" in sys.modules or "unittest" in sys.modules
import worktree_cleanup_audit # noqa: E402
import canonical_comment_validator as ccv # noqa: E402
@@ -11372,18 +11381,22 @@ def _current_runtime_mode_report(refresh: bool = False) -> dict:
binding this session resolved, so the report answers both "what code is the
control plane running" and "does it agree with the task workspace".
The mutation gate uses the **startup snapshot** (``refresh=False``), for the
same reason the #420 parity gate compares against a startup baseline: the
runtime a process serves from is fixed when it loads its code, and editing
files afterwards does not change what is already in memory. That also keeps
the mutation path free of per-call subprocess reads. ``refresh=True`` re-reads
live state for the read-only reporting path.
"""
global _STARTUP_RUNTIME_MODE
The immutable facts -- process root, branch, head, checkout-ness -- come from
the import-time snapshot, for the same reason the #420 parity gate compares
against a startup baseline: the runtime a process serves from is fixed when
it loads its code, and editing files afterwards does not change what is
already in memory.
if not refresh and _STARTUP_RUNTIME_MODE is not None:
return _STARTUP_RUNTIME_MODE
observed = stable_control_runtime.observe_runtime(PROJECT_ROOT)
Everything mutation-sensitive is recomputed on **every** call: the dirty-file
list, the session's workspace binding, and the alignment derived from it.
Caching those would mean a checkout that goes dirty after the first call is
never caught again, and that one session's binding decides alignment for the
whole process. ``refresh`` is retained for the read-only reporting path; it
no longer selects between a cache and live state, so a read-only call can
neither seed nor weaken a later mutation decision.
"""
facts = _STARTUP_RUNTIME_FACTS
dirty_files = stable_control_runtime.observe_dirty_files(PROJECT_ROOT)
workspace_root = None
aligned = None
canonical_root = None
@@ -11391,9 +11404,14 @@ def _current_runtime_mode_report(refresh: bool = False) -> dict:
ctx = _resolve_namespace_mutation_context(None)
workspace_root = ctx.get("workspace_path")
canonical_root = ctx.get("canonical_repo_root")
aligned = os.path.realpath(workspace_root or "") == (
ctx.get("process_project_root") or ""
)
# Alignment keeps its established repository-level meaning (#615 F1):
# does this namespace target the repository the process is installed in,
# i.e. canonical_repo_root == process_project_root. It is deliberately
# NOT path equality between the task workspace and the process root --
# the global worktree rule requires task work to live in a branches/
# worktree, so that comparison would classify every correctly bound
# author, reviewer, and merger session as unsafe.
aligned = ctx.get("roots_aligned")
except Exception:
# An unresolvable binding is reported as unknown alignment, never as
# proof of alignment.
@@ -11402,21 +11420,18 @@ def _current_runtime_mode_report(refresh: bool = False) -> dict:
profile_name = get_profile()["profile_name"]
except Exception:
profile_name = None
report = stable_control_runtime.build_runtime_report(
return stable_control_runtime.build_runtime_report(
process_root=PROJECT_ROOT,
checkout_branch=observed["checkout_branch"],
runtime_head=observed["runtime_head"],
is_git_checkout=observed["is_git_checkout"],
dirty_files=observed["dirty_files"],
checkout_branch=facts["checkout_branch"],
runtime_head=facts["runtime_head"],
is_git_checkout=facts["is_git_checkout"],
dirty_files=dirty_files,
active_task_workspace=workspace_root,
canonical_repository_root=canonical_root,
workspace_roots_aligned=aligned,
profile=profile_name,
declared_mode=stable_control_runtime.declared_runtime_mode(),
)
if _STARTUP_RUNTIME_MODE is None:
_STARTUP_RUNTIME_MODE = report
return report
def _runtime_mode_block(op: str) -> list[str]: