From 7a97239f17aea09b05bb925b183be33d42e0a3ef Mon Sep 17 00:00:00 2001 From: Scott Hall Date: Mon, 19 Oct 2026 14:16:11 -0500 Subject: [PATCH 1/7] Coordination scripts refuse in a Claude Code cloud session In a cloud VM --git-common-dir resolves to the clone's own .git, so seat.ps1, mail.ps1, alloc.ps1 and claim.ps1 would "succeed" into a mefor-coord directory no other session reads. Each now prints one stderr line and exits 3 when CLAUDE_CODE_REMOTE=true. seat.ps1 exempts its two hook paths, -Record and -Prompt, which must exit 0. The SessionStart declare-prompt hook prints a one-line cloud-Builder notice instead of the prompt, stamps nothing, and still exits 0. tests/conftest.py drops an inherited CLAUDE_CODE_REMOTE so the existing pwsh suites keep testing the local path inside a cloud VM. New tests/test_coord_cloud_refusal.py pairs each refusal with a control that takes the normal path. --- scripts/coord/alloc.ps1 | 8 ++ scripts/coord/claim.ps1 | 8 ++ scripts/coord/mail.ps1 | 8 ++ scripts/coord/seat.ps1 | 11 ++ scripts/hooks/seat-declare-prompt.ps1 | 9 ++ tests/conftest.py | 7 + tests/test_coord_cloud_refusal.py | 193 ++++++++++++++++++++++++++ tests/tooling_manifest.txt | 1 + 8 files changed, 245 insertions(+) create mode 100644 tests/test_coord_cloud_refusal.py diff --git a/scripts/coord/alloc.ps1 b/scripts/coord/alloc.ps1 index 2500cb847d..192b804666 100644 --- a/scripts/coord/alloc.ps1 +++ b/scripts/coord/alloc.ps1 @@ -125,6 +125,14 @@ param( $ErrorActionPreference = "Stop" +# A CLAUDE CODE CLOUD SESSION REACHES NO OTHER SESSION THROUGH THIS SCRIPT: its --git-common-dir is +# the VM's own clone, so a write would "succeed" where nobody reads. CLAUDE.md section 5, "A cloud +# session is a Builder in its own session"; tests/test_coord_cloud_refusal.py holds the copies equal. +if ($env:CLAUDE_CODE_REMOTE -eq 'true') { + [Console]::Error.WriteLine('alloc.ps1: refused in a Claude Code cloud session (CLAUDE_CODE_REMOTE=true). This VM''s .git/mefor-coord reaches no other session, so nothing was read or recorded.') + exit 3 +} + # ANCHOR ON THE SCRIPT, NOT ON THE CURRENT DIRECTORY (BACKLOG #1060). Unanchored, every `git` call below # resolved against wherever the shell happened to be standing, so an absolute `-File` invocation from # worktree A while intending to commit from worktree B recorded the claim to A. The ledger gate then diff --git a/scripts/coord/claim.ps1 b/scripts/coord/claim.ps1 index 7f395f126e..c5ce6cd22e 100644 --- a/scripts/coord/claim.ps1 +++ b/scripts/coord/claim.ps1 @@ -82,6 +82,14 @@ param( $ErrorActionPreference = "Stop" +# A CLAUDE CODE CLOUD SESSION REACHES NO OTHER SESSION THROUGH THIS SCRIPT: its --git-common-dir is +# the VM's own clone, so a write would "succeed" where nobody reads. CLAUDE.md section 5, "A cloud +# session is a Builder in its own session"; tests/test_coord_cloud_refusal.py holds the copies equal. +if ($env:CLAUDE_CODE_REMOTE -eq 'true') { + [Console]::Error.WriteLine('claim.ps1: refused in a Claude Code cloud session (CLAUDE_CODE_REMOTE=true). This VM''s .git/mefor-coord reaches no other session, so nothing was read or recorded.') + exit 3 +} + # Anchored on the SCRIPT, not the current directory -- the reasoning is written out once, at the head of # alloc.ps1 (BACKLOG #1060). It applies here identically and was found here by inspection rather than by # a second reproduction: this file recorded `worktree = $repo` from the same unanchored call, so an diff --git a/scripts/coord/mail.ps1 b/scripts/coord/mail.ps1 index 6dc84036fd..9d6a6cae31 100644 --- a/scripts/coord/mail.ps1 +++ b/scripts/coord/mail.ps1 @@ -134,6 +134,14 @@ param( $ErrorActionPreference = 'Stop' +# A CLAUDE CODE CLOUD SESSION REACHES NO OTHER SESSION THROUGH THIS SCRIPT: its --git-common-dir is +# the VM's own clone, so a write would "succeed" where nobody reads. CLAUDE.md section 5, "A cloud +# session is a Builder in its own session"; tests/test_coord_cloud_refusal.py holds the copies equal. +if ($env:CLAUDE_CODE_REMOTE -eq 'true') { + [Console]::Error.WriteLine('mail.ps1: refused in a Claude Code cloud session (CLAUDE_CODE_REMOTE=true). This VM''s .git/mefor-coord reaches no other session, so nothing was read or recorded.') + exit 3 +} + # Send-set arguments without -Send: refuse by name, before anything is read or written. See $Send. if ($PSCmdlet.ParameterSetName -eq 'Send' -and -not $Send) { throw "Send arguments were given without -Send. Nothing was sent. Add -Send and re-run." diff --git a/scripts/coord/seat.ps1 b/scripts/coord/seat.ps1 index b0ed32f519..b8ccbfa40a 100644 --- a/scripts/coord/seat.ps1 +++ b/scripts/coord/seat.ps1 @@ -113,6 +113,17 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +# A CLAUDE CODE CLOUD SESSION REACHES NO OTHER SESSION THROUGH THIS SCRIPT: its --git-common-dir is +# the VM's own clone, so a write would "succeed" where nobody reads. CLAUDE.md section 5, "A cloud +# session is a Builder in its own session"; tests/test_coord_cloud_refusal.py holds the copies equal. +# Every write verb is refused except the two hook paths, -Record and -Prompt, which must exit 0 on +# every route; the user-scope Stop hook that drives -Record never loads in a cloud session, and the +# SessionStart hook that drives -Prompt skips it there. +if ($env:CLAUDE_CODE_REMOTE -eq 'true' -and $PSCmdlet.ParameterSetName -notin 'Record', 'Prompt') { + [Console]::Error.WriteLine('seat.ps1: refused in a Claude Code cloud session (CLAUDE_CODE_REMOTE=true). This VM''s .git/mefor-coord reaches no other session, so nothing was read or recorded.') + exit 3 +} + $SCHEMA = 1 $WRITER = 'seat.ps1/1.0.0' diff --git a/scripts/hooks/seat-declare-prompt.ps1 b/scripts/hooks/seat-declare-prompt.ps1 index 582b8484c9..21d6404201 100644 --- a/scripts/hooks/seat-declare-prompt.ps1 +++ b/scripts/hooks/seat-declare-prompt.ps1 @@ -61,6 +61,15 @@ param() $ErrorActionPreference = 'Continue' try { + # A CLAUDE CODE CLOUD SESSION HAS NO SEAT REGISTRY TO DECLARE INTO. In a cloud VM --git-common-dir + # resolves to the clone's own .git, so the prompt would send the session to a seat.ps1 -Declare that + # writes where nobody reads -- which is why seat.ps1 -Declare now refuses there. Nothing is stamped + # either, for the same reason. One line instead of the prompt, and still exit 0 (see the header). + if ($env:CLAUDE_CODE_REMOTE -eq 'true') { + Write-Output "[seat] This is a Claude Code cloud session (CLAUDE_CODE_REMOTE=true): a cloud Builder with no seat registry, so do not declare. CLAUDE.md section 5, 'A cloud session is a Builder in its own session', says how it hands its PR over." + exit 0 + } + # Read stdin if a payload is there. SessionStart delivers session_id, and without it seat.ps1 # keys the record by a fallback rather than by the session that is actually starting. It also # delivers cwd, the session's own directory, which this process may not be in. diff --git a/tests/conftest.py b/tests/conftest.py index 0e9296ea6b..d8934b1094 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -157,6 +157,13 @@ def _publish_slot_var(name: str, value: str) -> None: _publish_slot_var("MEFOR_TEST_PORT_BASE", str(20000 + _PORTS_PER_SLOT * _SLOT)) _publish_slot_var("MEFOR_TEST_QSETTINGS_ORG", f"MEFOR-Test-{_SLOT}") +# THE SUITE TESTS THE LOCAL MACHINE, SO IT DOES NOT INHERIT A CLOUD SESSION'S MARKER. A Claude Code +# cloud VM exports CLAUDE_CODE_REMOTE=true, and the coordination scripts refuse every call under it +# (tests/test_coord_cloud_refusal.py). Inherited, that refusal would fail every pwsh-driven test of +# those scripts in a cloud Builder's run for a reason unrelated to the change under test. The tests +# of the refusal set the variable themselves, per subprocess. +os.environ.pop("CLAUDE_CODE_REMOTE", None) + @pytest.fixture(scope="session", autouse=True) def _force_aad_bind_when_requested() -> Iterator[None]: diff --git a/tests/test_coord_cloud_refusal.py b/tests/test_coord_cloud_refusal.py new file mode 100644 index 0000000000..6e2c81ac8a --- /dev/null +++ b/tests/test_coord_cloud_refusal.py @@ -0,0 +1,193 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 MessageFoundry Foundation, LLC and contributors +"""The coordination scripts refuse in a Claude Code cloud session instead of writing where nobody reads. + +A cloud VM is its own clone, so ``git rev-parse --git-common-dir`` resolves to that clone's ``.git`` +and ``/mefor-coord`` is a valid directory that no other session ever reads. Before the +guard, ``seat.ps1``, ``mail.ps1``, ``alloc.ps1`` and ``claim.ps1`` would all have "succeeded" into it: +a declaration nobody sees, mail nobody drains, an ADR number the fleet never learns about. + +Each refusal is paired with a control that runs the same call without ``CLAUDE_CODE_REMOTE`` and +must take the normal path, because a refusal test alone also passes against a script that refuses +every call. tests/conftest.py drops the variable from the suite's own environment, so the controls +stay meaningful when the suite itself runs in a cloud VM. +""" + +from __future__ import annotations + +import os +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +COORD = ROOT / "scripts" / "coord" +HOOKS = ROOT / "scripts" / "hooks" +TIMEOUT = 120 +REFUSAL_EXIT = 3 +REFUSAL_TAIL = ( + ": refused in a Claude Code cloud session (CLAUDE_CODE_REMOTE=true). This VM's .git/mefor-coord" + " reaches no other session, so nothing was read or recorded." +) +SEAT_DECLARE = ("-Declare", "-Seat", "builder", "-Goal", "cloud refusal test") + +needs_pwsh = pytest.mark.skipif(shutil.which("pwsh") is None, reason="the scripts run under pwsh") +# The normal paths of these scripts are exercised on Windows by their own suites; the controls here +# follow the same platform gate. The refusals fire before anything platform-specific. +needs_windows = pytest.mark.skipif( + os.name != "nt", reason="the scripts' normal paths run on Windows" +) + + +def _git(repo: Path, *args: str) -> None: + subprocess.run( + ["git", "-C", str(repo), *args], capture_output=True, text=True, timeout=TIMEOUT, check=True + ) + + +@pytest.fixture(scope="module") +def _template(tmp_path_factory: pytest.TempPathFactory) -> Path: + """One clone carrying its OWN copy of scripts/coord and scripts/hooks, built once per module.""" + r = tmp_path_factory.mktemp("template") / "repo" + r.mkdir() + _git(r, "init", "-q", "-b", "main") + _git(r, "config", "user.email", "t@example.invalid") + _git(r, "config", "user.name", "t") + shutil.copytree(COORD, r / "scripts" / "coord") + shutil.copytree(HOOKS, r / "scripts" / "hooks") + (r / "f.txt").write_text("x", encoding="utf-8") + _git(r, "add", "f.txt") + _git(r, "commit", "-qm", "base") + return r + + +@pytest.fixture +def repo(tmp_path: Path, _template: Path) -> Path: + """A private copy of the template per test. These scripts anchor on where they live, not on the + cwd, so the copy is what keeps every write out of the real registry and out of a sibling test.""" + r = tmp_path / "repo" + shutil.copytree(_template, r, symlinks=True) + return r + + +def _coord_dir(repo: Path) -> Path: + return repo / ".git" / "mefor-coord" + + +def _run(repo: Path, script: str, *args: str, cloud: bool) -> subprocess.CompletedProcess[str]: + env = dict(os.environ) + if cloud: + env["CLAUDE_CODE_REMOTE"] = "true" + return subprocess.run( + ["pwsh", "-NoProfile", "-NonInteractive", "-File", str(repo / "scripts" / script), *args], + cwd=str(repo), + env=env, + input="", + capture_output=True, + text=True, + timeout=TIMEOUT, + check=False, + ) + + +@needs_pwsh +@pytest.mark.parametrize( + ("label", "script", "args"), + [ + pytest.param("seat.ps1", "coord/seat.ps1", SEAT_DECLARE, id="seat-declare"), + pytest.param("seat.ps1", "coord/seat.ps1", ("-Close",), id="seat-close"), + pytest.param("mail.ps1", "coord/mail.ps1", ("-Status",), id="mail-status"), + pytest.param("alloc.ps1", "coord/alloc.ps1", ("-List",), id="alloc-list"), + pytest.param("claim.ps1", "coord/claim.ps1", ("-List",), id="claim-list"), + ], +) +def test_each_coordination_script_refuses_in_a_cloud_session( + repo: Path, label: str, script: str, args: tuple[str, ...] +) -> None: + proc = _run(repo, script, *args, cloud=True) + assert proc.returncode == REFUSAL_EXIT, (proc.stdout, proc.stderr) + assert proc.stderr.strip() == label + REFUSAL_TAIL, proc.stderr + assert proc.stdout == "", proc.stdout + assert not _coord_dir(repo).exists(), "a refused call still created the coordination directory" + + +@needs_pwsh +def test_mail_send_refuses_before_writing_to_an_explicit_mail_root( + repo: Path, tmp_path: Path +) -> None: + """-MailRoot bypasses the git lookup, so this proves the refusal comes before the write itself.""" + root = tmp_path / "mail" + args = ("-Send", "-To", str(repo), "-Body", "x", "-MailRoot", str(root)) + proc = _run(repo, "coord/mail.ps1", *args, cloud=True) + assert proc.returncode == REFUSAL_EXIT, (proc.stdout, proc.stderr) + assert not root.exists() + + +@needs_pwsh +@needs_windows +def test_seat_record_is_not_refused_in_the_cloud(repo: Path) -> None: + """-Record is a Stop-hook path that must exit 0 on every route, so it is exempt.""" + proc = _run(repo, "coord/seat.ps1", "-Record", cloud=True) + assert "refused in a Claude Code cloud session" not in proc.stdout + proc.stderr + assert proc.returncode == 0, (proc.stdout, proc.stderr) + + +@needs_pwsh +@needs_windows +@pytest.mark.parametrize( + ("script", "args"), + [ + pytest.param("coord/mail.ps1", ("-Status",), id="mail-status"), + pytest.param("coord/alloc.ps1", ("-List",), id="alloc-list"), + pytest.param("coord/claim.ps1", ("-List",), id="claim-list"), + ], +) +def test_each_coordination_script_takes_its_normal_path_outside_the_cloud( + repo: Path, script: str, args: tuple[str, ...] +) -> None: + proc = _run(repo, script, *args, cloud=False) + assert proc.returncode == 0, (proc.stdout, proc.stderr) + assert "refused in a Claude Code cloud session" not in proc.stdout + proc.stderr + + +@needs_pwsh +@needs_windows +def test_seat_declare_outside_the_cloud_writes_its_record(repo: Path) -> None: + """The control for the declaration: the record the refusal withholds is really written here.""" + proc = _run(repo, "coord/seat.ps1", *SEAT_DECLARE, cloud=False) + assert proc.returncode == 0, (proc.stdout, proc.stderr) + assert "refused in a Claude Code cloud session" not in proc.stdout + proc.stderr + records = list((_coord_dir(repo) / "seats").rglob("*.json")) + assert records, "seat.ps1 -Declare wrote no record outside the cloud" + + +@needs_pwsh +def test_the_declare_prompt_hook_prints_one_cloud_line_and_exits_zero(repo: Path) -> None: + proc = _run(repo, "hooks/seat-declare-prompt.ps1", cloud=True) + assert proc.returncode == 0, (proc.stdout, proc.stderr) + lines = [ln for ln in proc.stdout.splitlines() if ln.strip()] + assert len(lines) == 1, proc.stdout + assert "cloud Builder with no seat registry" in lines[0] + assert "seat.ps1 -Declare -Seat" not in proc.stdout + assert not _coord_dir(repo).exists(), "the cloud hook stamped a seat record nobody reads" + + +@needs_pwsh +@needs_windows +def test_the_declare_prompt_hook_prints_the_prompt_outside_the_cloud(repo: Path) -> None: + proc = _run(repo, "hooks/seat-declare-prompt.ps1", cloud=False) + assert proc.returncode == 0, (proc.stdout, proc.stderr) + assert "seat.ps1 -Declare -Seat" in proc.stdout + assert "cloud Builder" not in proc.stdout + + +@pytest.mark.parametrize("name", ["seat", "mail", "alloc", "claim"]) +def test_each_script_carries_the_one_refusal_line(name: str) -> None: + """The guard is inlined: alloc.ps1 and claim.ps1 dot-source no helper the other two share. This + holds the four copies to one line without pwsh, so it also runs where the refusal tests skip.""" + text = (COORD / f"{name}.ps1").read_text(encoding="utf-8") + line = f"[Console]::Error.WriteLine('{name}.ps1{REFUSAL_TAIL}')".replace("VM's", "VM''s") + guard = f" {line}\n exit {REFUSAL_EXIT}\n" + assert text.count(guard) == 1, f"{name}.ps1 lost or changed its cloud refusal line" diff --git a/tests/tooling_manifest.txt b/tests/tooling_manifest.txt index 04ac31cf49..23c8eb8dbc 100644 --- a/tests/tooling_manifest.txt +++ b/tests/tooling_manifest.txt @@ -93,6 +93,7 @@ tests/test_coord_claim_liveness.py tests/test_coord_claim_reconcile.py tests/test_coord_claim_refresh.py tests/test_coord_claim_release_history.py +tests/test_coord_cloud_refusal.py tests/test_coord_handoff_clear.py tests/test_coord_handoff_pointer.py tests/test_coord_handoff_report.py From ef75a363338faaacc0742ad68795be9b5d891912 Mon Sep 17 00:00:00 2001 From: Scott Hall Date: Mon, 19 Oct 2026 14:17:50 -0500 Subject: [PATCH 2/7] ensure-venv.ps1 finds the venv interpreter on Linux It hardcoded .venv\Scripts\python.exe, so it failed on a Claude Code cloud VM even with pwsh installed. It now uses .venv/bin/python off Windows, judged by [Environment]::OSVersion so Windows PowerShell 5.1 (no $IsWindows) still reads as Windows, and defaults -Python to python3 there. The install line is unchanged, so both parity tests hold. tests/test_worktree_ensure_venv.py drops its Windows-only gate and builds the fixture's site-packages path per platform, so the ubuntu legs, which carry pwsh, drive the bin/python branch. --- scripts/worktree/ensure-venv.ps1 | 16 ++++++++++++++-- tests/test_worktree_ensure_venv.py | 15 +++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/scripts/worktree/ensure-venv.ps1 b/scripts/worktree/ensure-venv.ps1 index ef7165bc45..4170ce60f9 100644 --- a/scripts/worktree/ensure-venv.ps1 +++ b/scripts/worktree/ensure-venv.ps1 @@ -40,12 +40,22 @@ param( # session runs this in its own worktree. Resolved to the checkout ROOT via git, so running it # from a subdirectory does the right thing instead of creating a stray .venv there. [string]$Path = $PWD.Path, - [string]$Python = "python", + # The interpreter that creates the venv. Defaults to `python` on Windows. Elsewhere it prefers + # `python3.14`, because the distro `python3` on Ubuntu, which a Claude Code cloud VM runs, is older + # than the >=3.14 pyproject requires and has no `python` beside it; then `python3`. + [string]$Python, [switch]$Sqlserver # also install the [sqlserver] extra ) $ErrorActionPreference = "Stop" +# [Environment]::OSVersion rather than $IsWindows, which Windows PowerShell 5.1 does not define, so that +# host would have read as Linux. +$onWindows = [Environment]::OSVersion.Platform -eq 'Win32NT' +if (-not $Python) { + $Python = if ($onWindows) { "python" } elseif (Get-Command python3.14 -ErrorAction SilentlyContinue) { "python3.14" } else { "python3" } +} + if (-not (Test-Path -LiteralPath $Path)) { throw "No such path: $Path" } # Ask GIT for the checkout root rather than walking parents ourselves. A worktree's root is not @@ -63,7 +73,9 @@ if (-not (Test-Path -LiteralPath (Join-Path $root 'pyproject.toml'))) { } $venv = Join-Path $root ".venv" -$venvPy = Join-Path $venv "Scripts\python.exe" +# A venv puts its interpreter at Scripts\python.exe on Windows and at bin/python everywhere else. The +# Linux layout is what a Claude Code cloud VM gets. +$venvPy = if ($onWindows) { Join-Path $venv "Scripts\python.exe" } else { Join-Path $venv "bin/python" } # Where the venv imports the two packages from, judged by the venv's own interpreter (BACKLOG #2857). # One line per package: " OK " or " BAD ". diff --git a/tests/test_worktree_ensure_venv.py b/tests/test_worktree_ensure_venv.py index 1c2c994107..f6183dae46 100644 --- a/tests/test_worktree_ensure_venv.py +++ b/tests/test_worktree_ensure_venv.py @@ -31,10 +31,10 @@ from __future__ import annotations -import os import shutil import subprocess import sys +import sysconfig from pathlib import Path import pytest @@ -43,9 +43,11 @@ _SCRIPT = _REPO / "scripts" / "worktree" / "ensure-venv.ps1" _NEW_PS1 = _REPO / "scripts" / "worktree" / "new.ps1" +# Not Windows-only: a Claude Code cloud Builder runs this script on Linux, so the ubuntu legs, which +# carry pwsh, drive the same paths against a bin/python venv. pytestmark = pytest.mark.skipif( - shutil.which("pwsh") is None or os.name != "nt", - reason="ensure-venv.ps1 is a PowerShell script driven through pwsh on Windows", + shutil.which("pwsh") is None, + reason="ensure-venv.ps1 is a PowerShell script driven through pwsh", ) @@ -95,8 +97,9 @@ def test_it_refuses_a_checkout_that_is_not_this_project(tmp_path: Path) -> None: def _template_venv(tmp_path_factory: pytest.TempPathFactory) -> Path: """One REAL venv with no pip, built once per session; each test gets its own copy of it. - Building one takes seconds. A Windows venv's launcher finds the ``pyvenv.cfg`` beside it, so a - copied tree is a working venv of its own, and no test can change another's.""" + Building one takes seconds. A venv's interpreter finds the ``pyvenv.cfg`` beside it (on Linux + through the copied symlink's own path), so a copied tree is a working venv of its own, and no + test can change another's.""" venv = tmp_path_factory.mktemp("template") / ".venv" subprocess.run( [sys.executable, "-m", "venv", "--without-pip", str(venv)], @@ -122,7 +125,7 @@ def _venv_checkout(tmp_path: Path, template: Path) -> tuple[Path, Path]: (root / name / "__init__.py").write_text(f"# {name} source\n", encoding="utf-8") venv = root / ".venv" shutil.copytree(template, venv, symlinks=True) - site = venv / "Lib" / "site-packages" + site = Path(sysconfig.get_path("purelib", vars={"base": str(venv), "platbase": str(venv)})) (site / "_editable_impl_messagefoundry.pth").write_text(f"{root}\n", encoding="utf-8") return root, site From 77219a0882f116ebfadd634330ab856371bee3e3 Mon Sep 17 00:00:00 2001 From: Scott Hall Date: Mon, 19 Oct 2026 14:27:37 -0500 Subject: [PATCH 3/7] CLAUDE.md: a cloud session is a Builder in its own session Section 5 gains one rule: a Claude Code cloud session is a Builder in its own session, on MEFORORG/MessageFoundry alone and never with a second repository attached, because a multi-repo session loads no project hooks. It opens its PR over REST, since the proxy refuses GraphQL, and hands it to the Lander with the qa label and a PR comment, not mail. Evidence: probe PR 2432 and wiki note 20261018T215116801Z-5lqiqo. --- CLAUDE.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 44bdfc076f..a1e7cb7720 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -475,6 +475,25 @@ gates a merge**, and no seat has to clear one. drift is a reason to re-check the rest. An item already answered is a good outcome: record it and stop. BACKLOG #1448; the evidence is in [`docs/METHOD.md`](docs/METHOD.md). +### A cloud session is a Builder in its own session + +A Claude Code cloud session (`CLAUDE_CODE_REMOTE=true`) holds one seat only: a **Builder in its own +session, on `MEFORORG/MessageFoundry` alone**. **Never attach a second repository**: a session with +several repositories loads hooks from none of them. Its `.git/mefor-coord` belongs to its own clone +and reaches no other session, so `seat.ps1 -Declare`, `mail.ps1`, `alloc.ps1` and `claim.ps1` +refuse there, and the declare rule above does not apply. A brief that needs an ADR number is not a +cloud brief. Cite the brief's item in the commit body, not the subject: the commit-msg claim gate +refuses a code commit whose subject cites a ledger number nobody claimed, and no claim can be taken +there. Commit under the allowlisted git identity the cloud environment sets, or `cla` blocks the PR. + +The GitHub proxy refuses GraphQL, so no `gh pr` and no `gh issue`. Open the PR over REST with +`gh api -X POST repos/MEFORORG/MessageFoundry/pulls -f title=... -f head= -f base=main -F +body=@`, then hand it to the Lander with the `qa` label +(`gh api -X POST repos/MEFORORG/MessageFoundry/issues//labels -f "labels[]=qa"`) and a PR comment, +never with mail. The evidence is probe PR +[2432](https://github.com/MEFORORG/MessageFoundry/pull/2432) (2026-10-19) and fleet wiki note +`20261018T215116801Z-5lqiqo`. + ### The Manager plans, dispatches, and holds the owner's attention - **Plan first, then dispatch.** For anything past a trivial change the Manager produces a plan and From f9e7726e66babb0f173a7722317becb76f5971ae Mon Sep 17 00:00:00 2001 From: Scott Hall Date: Mon, 19 Oct 2026 15:39:50 -0500 Subject: [PATCH 4/7] test_coord_cloud_refusal: copy the template without git maintenance The ubuntu tooling leg errored in the fixture: the template's commit detached background maintenance, whose .git/objects/maintenance.lock vanished while copytree walked it. The template now sets maintenance.auto=false and gc.auto=0, and the per-test copy skips *.lock files. --- tests/test_coord_cloud_refusal.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_coord_cloud_refusal.py b/tests/test_coord_cloud_refusal.py index 6e2c81ac8a..45c1e8ee8a 100644 --- a/tests/test_coord_cloud_refusal.py +++ b/tests/test_coord_cloud_refusal.py @@ -55,6 +55,11 @@ def _template(tmp_path_factory: pytest.TempPathFactory) -> Path: _git(r, "init", "-q", "-b", "main") _git(r, "config", "user.email", "t@example.invalid") _git(r, "config", "user.name", "t") + # No background maintenance: the commit below can detach one that holds + # .git/objects/maintenance.lock while a test copies the tree, and the copy then fails on a lock + # that vanished mid-walk. Measured on the ubuntu tooling leg, PR 2434. + _git(r, "config", "maintenance.auto", "false") + _git(r, "config", "gc.auto", "0") shutil.copytree(COORD, r / "scripts" / "coord") shutil.copytree(HOOKS, r / "scripts" / "hooks") (r / "f.txt").write_text("x", encoding="utf-8") @@ -68,7 +73,7 @@ def repo(tmp_path: Path, _template: Path) -> Path: """A private copy of the template per test. These scripts anchor on where they live, not on the cwd, so the copy is what keeps every write out of the real registry and out of a sibling test.""" r = tmp_path / "repo" - shutil.copytree(_template, r, symlinks=True) + shutil.copytree(_template, r, symlinks=True, ignore=shutil.ignore_patterns("*.lock")) return r From b541aaff49c8010c031583f5ad95448836f469a3 Mon Sep 17 00:00:00 2001 From: Scott Hall Date: Mon, 19 Oct 2026 15:53:13 -0500 Subject: [PATCH 5/7] The remaining mefor-coord writers refuse in a Claude Code cloud session handoff.ps1, lane.ps1, owner_log.py, tombstone-retired-backlog-allocator.ps1 and hygiene-report.ps1 now print the same one stderr line as PR 2434's four scripts and exit 3 under CLAUDE_CODE_REMOTE=true, path overrides included. Inline copies, not a shared helper: the set spans PowerShell and Python, fixtures copy scripts file by file, and an override bypasses a resolver. tests/test_coord_cloud_refusal.py adds a refusal and a control per script, and a census that fails until every script naming mefor-coord is guarded or classified (user-scope hook, clone-local, reader, mention). --- CLAUDE.md | 5 +- scripts/coord/handoff.ps1 | 9 + scripts/coord/lane.ps1 | 8 + scripts/coord/owner_log.py | 11 + .../tombstone-retired-backlog-allocator.ps1 | 9 + scripts/worktree/hygiene-report.ps1 | 11 + tests/test_coord_cloud_refusal.py | 231 +++++++++++++++++- 7 files changed, 272 insertions(+), 12 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a1e7cb7720..e5d9f4ed94 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -480,8 +480,9 @@ gates a merge**, and no seat has to clear one. A Claude Code cloud session (`CLAUDE_CODE_REMOTE=true`) holds one seat only: a **Builder in its own session, on `MEFORORG/MessageFoundry` alone**. **Never attach a second repository**: a session with several repositories loads hooks from none of them. Its `.git/mefor-coord` belongs to its own clone -and reaches no other session, so `seat.ps1 -Declare`, `mail.ps1`, `alloc.ps1` and `claim.ps1` -refuse there, and the declare rule above does not apply. A brief that needs an ADR number is not a +and reaches no other session, so the scripts that write into it refuse there, at least `seat.ps1 +-Declare`, `mail.ps1`, `alloc.ps1`, `claim.ps1`, `handoff.ps1`, `lane.ps1` and `owner_log.py` +(`tests/test_coord_cloud_refusal.py` holds the census), and the declare rule above does not apply. A brief that needs an ADR number is not a cloud brief. Cite the brief's item in the commit body, not the subject: the commit-msg claim gate refuses a code commit whose subject cites a ledger number nobody claimed, and no claim can be taken there. Commit under the allowlisted git identity the cloud environment sets, or `cla` blocks the PR. diff --git a/scripts/coord/handoff.ps1 b/scripts/coord/handoff.ps1 index 55c98f28ce..e808d4c0b2 100644 --- a/scripts/coord/handoff.ps1 +++ b/scripts/coord/handoff.ps1 @@ -84,6 +84,15 @@ param( $ErrorActionPreference = 'Stop' +# A CLAUDE CODE CLOUD SESSION REACHES NO OTHER SESSION THROUGH THIS SCRIPT: its --git-common-dir is +# the VM's own clone, so a write would "succeed" where nobody reads. CLAUDE.md section 5, "A cloud +# session is a Builder in its own session"; tests/test_coord_cloud_refusal.py holds the copies equal. +# Refused even with -CoordDir, as mail.ps1 refuses with -MailRoot. Exit 3 sits outside the 0/1/2 above. +if ($env:CLAUDE_CODE_REMOTE -eq 'true') { + [Console]::Error.WriteLine('handoff.ps1: refused in a Claude Code cloud session (CLAUDE_CODE_REMOTE=true). This VM''s .git/mefor-coord reaches no other session, so nothing was read or recorded.') + exit 3 +} + # A partial -Retire or -Write call: refuse by name, before anything is read or written. See $Retire. $missing = switch ($PSCmdlet.ParameterSetName) { 'Retire' { if (-not $Retire) { '-Retire ' } } diff --git a/scripts/coord/lane.ps1 b/scripts/coord/lane.ps1 index 2a96327e66..7b9676998e 100644 --- a/scripts/coord/lane.ps1 +++ b/scripts/coord/lane.ps1 @@ -35,6 +35,14 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +# A CLAUDE CODE CLOUD SESSION REACHES NO OTHER SESSION THROUGH THIS SCRIPT: its --git-common-dir is +# the VM's own clone, so a write would "succeed" where nobody reads. CLAUDE.md section 5, "A cloud +# session is a Builder in its own session"; tests/test_coord_cloud_refusal.py holds the copies equal. +if ($env:CLAUDE_CODE_REMOTE -eq 'true') { + [Console]::Error.WriteLine('lane.ps1: refused in a Claude Code cloud session (CLAUDE_CODE_REMOTE=true). This VM''s .git/mefor-coord reaches no other session, so nothing was read or recorded.') + exit 3 +} + # A level needs both numbers: refuse by name, before anything is read or written. See $Free. Bound, # not truthy, because 0 is a real answer for either. if ($PSCmdlet.ParameterSetName -eq 'State' -and diff --git a/scripts/coord/owner_log.py b/scripts/coord/owner_log.py index 817faecfe1..6cc0e213a3 100644 --- a/scripts/coord/owner_log.py +++ b/scripts/coord/owner_log.py @@ -394,6 +394,17 @@ def main() -> int: st = sub.add_parser("status", help="every item and its legs") st.set_defaults(func=cmd_status) a = p.parse_args() + # A CLAUDE CODE CLOUD SESSION REACHES NO OTHER SESSION THROUGH THIS SCRIPT: its --git-common-dir + # is the VM's own clone, so a write would "succeed" where nobody reads. CLAUDE.md section 5, "A + # cloud session is a Builder in its own session"; tests/test_coord_cloud_refusal.py holds the + # copies equal. Refused even with MEFOR_OWNER_LOG set, as mail.ps1 refuses with -MailRoot. + # Case-folded because the PowerShell copies compare with -eq, which ignores case. + if os.environ.get("CLAUDE_CODE_REMOTE", "").lower() == "true": + sys.stderr.write( + "owner_log.py: refused in a Claude Code cloud session (CLAUDE_CODE_REMOTE=true). This" + " VM's .git/mefor-coord reaches no other session, so nothing was read or recorded.\n" + ) + return 3 result: int = a.func(a) return result diff --git a/scripts/coord/tombstone-retired-backlog-allocator.ps1 b/scripts/coord/tombstone-retired-backlog-allocator.ps1 index 499830a483..aaa0d4c39c 100644 --- a/scripts/coord/tombstone-retired-backlog-allocator.ps1 +++ b/scripts/coord/tombstone-retired-backlog-allocator.ps1 @@ -70,6 +70,15 @@ param( $ErrorActionPreference = "Stop" +# A CLAUDE CODE CLOUD SESSION REACHES NO OTHER SESSION THROUGH THIS SCRIPT: its --git-common-dir is +# the VM's own clone, so a write would "succeed" where nobody reads. CLAUDE.md section 5, "A cloud +# session is a Builder in its own session"; tests/test_coord_cloud_refusal.py holds the copies equal. +# Refused even with -GitCommonDir, as mail.ps1 refuses with -MailRoot. +if ($env:CLAUDE_CODE_REMOTE -eq 'true') { + [Console]::Error.WriteLine('tombstone-retired-backlog-allocator.ps1: refused in a Claude Code cloud session (CLAUDE_CODE_REMOTE=true). This VM''s .git/mefor-coord reaches no other session, so nothing was read or recorded.') + exit 3 +} + # Far above any real ledger number, and a round one so it reads as deliberate. The vault ledger was # under #1900 when this was written; it would take a thousand years at the current rate to reach this. $TombstoneNumber = 1000000 diff --git a/scripts/worktree/hygiene-report.ps1 b/scripts/worktree/hygiene-report.ps1 index f0eb3c5f7e..bc3f09d273 100644 --- a/scripts/worktree/hygiene-report.ps1 +++ b/scripts/worktree/hygiene-report.ps1 @@ -130,6 +130,17 @@ param( ) $ErrorActionPreference = 'Stop' + +# A CLAUDE CODE CLOUD SESSION REACHES NO OTHER SESSION THROUGH THIS SCRIPT: its --git-common-dir is +# the VM's own clone, so a write would "succeed" where nobody reads. CLAUDE.md section 5, "A cloud +# session is a Builder in its own session"; tests/test_coord_cloud_refusal.py holds the copies equal. +# Refused even with -OutDir, as mail.ps1 refuses with -MailRoot: the report describes this VM's +# worktrees, which no session outside it can act on. +if ($env:CLAUDE_CODE_REMOTE -eq 'true') { + [Console]::Error.WriteLine('hygiene-report.ps1: refused in a Claude Code cloud session (CLAUDE_CODE_REMOTE=true). This VM''s .git/mefor-coord reaches no other session, so nothing was read or recorded.') + exit 3 +} + # A non-zero git or gh exit is DATA here (a missing remote, a failed fetch), never a throw. $PSNativeCommandUseErrorActionPreference = $false # A scheduled task has nobody to answer a credential prompt, in a terminal or in Git Credential diff --git a/tests/test_coord_cloud_refusal.py b/tests/test_coord_cloud_refusal.py index 6e2c81ac8a..20eec19457 100644 --- a/tests/test_coord_cloud_refusal.py +++ b/tests/test_coord_cloud_refusal.py @@ -5,7 +5,10 @@ A cloud VM is its own clone, so ``git rev-parse --git-common-dir`` resolves to that clone's ``.git`` and ``/mefor-coord`` is a valid directory that no other session ever reads. Before the guard, ``seat.ps1``, ``mail.ps1``, ``alloc.ps1`` and ``claim.ps1`` would all have "succeeded" into it: -a declaration nobody sees, mail nobody drains, an ADR number the fleet never learns about. +a declaration nobody sees, mail nobody drains, an ADR number the fleet never learns about. The same +refusal now also guards ``handoff.ps1``, ``lane.ps1``, ``owner_log.py``, +``tombstone-retired-backlog-allocator.ps1`` and ``hygiene-report.ps1``, and a census below holds every +script that names the directory to a decision about it. Each refusal is paired with a control that runs the same call without ``CLAUDE_CODE_REMOTE`` and must take the normal path, because a refusal test alone also passes against a script that refuses @@ -18,6 +21,7 @@ import os import shutil import subprocess +import sys from pathlib import Path import pytest @@ -25,6 +29,7 @@ ROOT = Path(__file__).resolve().parents[1] COORD = ROOT / "scripts" / "coord" HOOKS = ROOT / "scripts" / "hooks" +WORKTREE = ROOT / "scripts" / "worktree" TIMEOUT = 120 REFUSAL_EXIT = 3 REFUSAL_TAIL = ( @@ -32,6 +37,8 @@ " reaches no other session, so nothing was read or recorded." ) SEAT_DECLARE = ("-Declare", "-Seat", "builder", "-Goal", "cloud refusal test") +# The report's offline shape: no gh, no fetch, no roots beyond the sandbox clone. +HYGIENE_OFFLINE = ("-SkipGh", "-SkipFetch", "-NoExtraRoots") needs_pwsh = pytest.mark.skipif(shutil.which("pwsh") is None, reason="the scripts run under pwsh") # The normal paths of these scripts are exercised on Windows by their own suites; the controls here @@ -57,6 +64,7 @@ def _template(tmp_path_factory: pytest.TempPathFactory) -> Path: _git(r, "config", "user.name", "t") shutil.copytree(COORD, r / "scripts" / "coord") shutil.copytree(HOOKS, r / "scripts" / "hooks") + shutil.copytree(WORKTREE, r / "scripts" / "worktree") (r / "f.txt").write_text("x", encoding="utf-8") _git(r, "add", "f.txt") _git(r, "commit", "-qm", "base") @@ -76,12 +84,21 @@ def _coord_dir(repo: Path) -> Path: return repo / ".git" / "mefor-coord" -def _run(repo: Path, script: str, *args: str, cloud: bool) -> subprocess.CompletedProcess[str]: +def _run( + repo: Path, script: str, *args: str, cloud: bool, extra_env: dict[str, str] | None = None +) -> subprocess.CompletedProcess[str]: env = dict(os.environ) if cloud: env["CLAUDE_CODE_REMOTE"] = "true" + env.update(extra_env or {}) + path = str(repo / "scripts" / script) + argv = ( + [sys.executable, path] + if script.endswith(".py") + else ["pwsh", "-NoProfile", "-NonInteractive", "-File", path] + ) return subprocess.run( - ["pwsh", "-NoProfile", "-NonInteractive", "-File", str(repo / "scripts" / script), *args], + [*argv, *args], cwd=str(repo), env=env, input="", @@ -101,6 +118,24 @@ def _run(repo: Path, script: str, *args: str, cloud: bool) -> subprocess.Complet pytest.param("mail.ps1", "coord/mail.ps1", ("-Status",), id="mail-status"), pytest.param("alloc.ps1", "coord/alloc.ps1", ("-List",), id="alloc-list"), pytest.param("claim.ps1", "coord/claim.ps1", ("-List",), id="claim-list"), + pytest.param("handoff.ps1", "coord/handoff.ps1", ("-Where",), id="handoff-where"), + pytest.param("handoff.ps1", "coord/handoff.ps1", ("-Report",), id="handoff-report"), + pytest.param("lane.ps1", "coord/lane.ps1", ("-Show",), id="lane-show"), + pytest.param( + "lane.ps1", "coord/lane.ps1", ("-Free", "1", "-InFlight", "0"), id="lane-state" + ), + pytest.param( + "tombstone-retired-backlog-allocator.ps1", + "coord/tombstone-retired-backlog-allocator.ps1", + (), + id="tombstone-write", + ), + pytest.param( + "hygiene-report.ps1", + "worktree/hygiene-report.ps1", + HYGIENE_OFFLINE, + id="hygiene-report", + ), ], ) def test_each_coordination_script_refuses_in_a_cloud_session( @@ -125,6 +160,71 @@ def test_mail_send_refuses_before_writing_to_an_explicit_mail_root( assert not root.exists() +@needs_pwsh +@pytest.mark.parametrize( + ("script", "args", "override"), + [ + pytest.param("coord/handoff.ps1", ("-Report",), "-CoordDir", id="handoff-coorddir"), + pytest.param( + "coord/tombstone-retired-backlog-allocator.ps1", (), "-GitCommonDir", id="tombstone" + ), + pytest.param( + "worktree/hygiene-report.ps1", HYGIENE_OFFLINE, "-OutDir", id="hygiene-outdir" + ), + ], +) +def test_an_explicit_path_override_is_refused_too( + repo: Path, tmp_path: Path, script: str, args: tuple[str, ...], override: str +) -> None: + """Each override bypasses the git lookup, so this proves the refusal comes before the write.""" + target = tmp_path / "override" + proc = _run(repo, script, *args, override, str(target), cloud=True) + assert proc.returncode == REFUSAL_EXIT, (proc.stdout, proc.stderr) + assert not target.exists() + assert not _coord_dir(repo).exists() + + +OWNER_SENT = ("sent", "--item", "t1", "--actor", "builder", "--summary", "cloud refusal test") + + +def _owner_log( + repo: Path, ledger: Path, *args: str, cloud: bool, remote: str = "true" +) -> subprocess.CompletedProcess[str]: + env = {"MEFOR_OWNER_LOG": str(ledger)} + if cloud: + env["CLAUDE_CODE_REMOTE"] = remote + return _run(repo, "coord/owner_log.py", *args, cloud=False, extra_env=env) + + +@pytest.mark.parametrize( + ("args", "remote"), + [ + pytest.param(OWNER_SENT, "true", id="sent"), + pytest.param(("status",), "true", id="status"), + # The PowerShell copies compare with -eq, which ignores case; the Python copy must agree. + pytest.param(("status",), "TRUE", id="status-upper-case"), + ], +) +def test_owner_log_refuses_in_a_cloud_session_even_with_its_override( + repo: Path, tmp_path: Path, args: tuple[str, ...], remote: str +) -> None: + """MEFOR_OWNER_LOG bypasses the git lookup, so the refusal is proven to come before the write.""" + ledger = tmp_path / "owner-log" / "OWNER-ITEMS.md" + proc = _owner_log(repo, ledger, *args, cloud=True, remote=remote) + assert proc.returncode == REFUSAL_EXIT, (proc.stdout, proc.stderr) + assert proc.stderr.strip() == "owner_log.py" + REFUSAL_TAIL, proc.stderr + assert proc.stdout == "", proc.stdout + assert not ledger.parent.exists() + + +def test_owner_log_writes_its_row_outside_the_cloud(repo: Path, tmp_path: Path) -> None: + """The control: the row the refusal withholds is really written here.""" + ledger = tmp_path / "owner-log" / "OWNER-ITEMS.md" + proc = _owner_log(repo, ledger, *OWNER_SENT, cloud=False) + assert proc.returncode == 0, (proc.stdout, proc.stderr) + assert "| t1 | request_sent |" in ledger.read_text(encoding="utf-8") + + @needs_pwsh @needs_windows def test_seat_record_is_not_refused_in_the_cloud(repo: Path) -> None: @@ -142,6 +242,8 @@ def test_seat_record_is_not_refused_in_the_cloud(repo: Path) -> None: pytest.param("coord/mail.ps1", ("-Status",), id="mail-status"), pytest.param("coord/alloc.ps1", ("-List",), id="alloc-list"), pytest.param("coord/claim.ps1", ("-List",), id="claim-list"), + pytest.param("coord/handoff.ps1", ("-Report",), id="handoff-report"), + pytest.param("coord/lane.ps1", ("-Show",), id="lane-show"), ], ) def test_each_coordination_script_takes_its_normal_path_outside_the_cloud( @@ -152,6 +254,38 @@ def test_each_coordination_script_takes_its_normal_path_outside_the_cloud( assert "refused in a Claude Code cloud session" not in proc.stdout + proc.stderr +@needs_pwsh +@needs_windows +@pytest.mark.parametrize( + ("script", "args", "written"), + [ + pytest.param("coord/handoff.ps1", ("-Where",), "handoffs", id="handoff-where"), + pytest.param("coord/lane.ps1", ("-Free", "1", "-InFlight", "0"), "lanes", id="lane-state"), + pytest.param( + "worktree/hygiene-report.ps1", HYGIENE_OFFLINE, "hygiene", id="hygiene-report" + ), + ], +) +def test_each_writer_writes_outside_the_cloud( + repo: Path, script: str, args: tuple[str, ...], written: str +) -> None: + """The control for each new refusal: the directory the refusal withholds is written here.""" + proc = _run(repo, script, *args, cloud=False) + assert proc.returncode == 0, (proc.stdout, proc.stderr) + out = _coord_dir(repo) / written + assert out.is_dir() and any(out.iterdir()), f"{script} wrote nothing under {written}/" + + +@needs_pwsh +@needs_windows +def test_the_tombstone_check_takes_its_normal_path_outside_the_cloud(repo: Path) -> None: + """Exit 1 and ABSENT is the normal answer in a clone with no tombstone. The write path needs an + origin/main the sandbox does not have, so the check is the control.""" + proc = _run(repo, "coord/tombstone-retired-backlog-allocator.ps1", "-Check", cloud=False) + assert proc.returncode == 1, (proc.stdout, proc.stderr) + assert "ABSENT:" in proc.stdout + + @needs_pwsh @needs_windows def test_seat_declare_outside_the_cloud_writes_its_record(repo: Path) -> None: @@ -183,11 +317,88 @@ def test_the_declare_prompt_hook_prints_the_prompt_outside_the_cloud(repo: Path) assert "cloud Builder" not in proc.stdout -@pytest.mark.parametrize("name", ["seat", "mail", "alloc", "claim"]) -def test_each_script_carries_the_one_refusal_line(name: str) -> None: - """The guard is inlined: alloc.ps1 and claim.ps1 dot-source no helper the other two share. This - holds the four copies to one line without pwsh, so it also runs where the refusal tests skip.""" - text = (COORD / f"{name}.ps1").read_text(encoding="utf-8") - line = f"[Console]::Error.WriteLine('{name}.ps1{REFUSAL_TAIL}')".replace("VM's", "VM''s") +# Every inline copy of the refusal, by the path that carries it. A shared helper would need a new +# dot-source in scripts that test fixtures copy file by file, an override such as -CoordDir would +# bypass a check placed in a root resolver, and the Python copy could not share it anyway. +GUARDED = ( + "coord/seat.ps1", + "coord/mail.ps1", + "coord/alloc.ps1", + "coord/claim.ps1", + "coord/handoff.ps1", + "coord/lane.ps1", + "coord/tombstone-retired-backlog-allocator.ps1", + "worktree/hygiene-report.ps1", +) +PY_GUARDED = "coord/owner_log.py" + + +@pytest.mark.parametrize("rel", GUARDED) +def test_each_script_carries_the_one_refusal_line(rel: str) -> None: + """The guard is inlined, so this holds every copy to one line without pwsh, which also runs + where the refusal tests skip.""" + name = rel.rsplit("/", 1)[-1] + text = (ROOT / "scripts" / rel).read_text(encoding="utf-8") + line = f"[Console]::Error.WriteLine('{name}{REFUSAL_TAIL}')".replace("VM's", "VM''s") guard = f" {line}\n exit {REFUSAL_EXIT}\n" - assert text.count(guard) == 1, f"{name}.ps1 lost or changed its cloud refusal line" + assert text.count(guard) == 1, f"{rel} lost or changed its cloud refusal line" + + +def test_the_python_copy_carries_the_one_refusal_line() -> None: + text = (ROOT / "scripts" / PY_GUARDED).read_text(encoding="utf-8") + call = text.split("sys.stderr.write(\n", 1)[1].split("\n )\n", 1)[0] + joined = "".join(piece.strip()[1:-1] for piece in call.splitlines()) + assert joined == "owner_log.py" + REFUSAL_TAIL + "\\n", joined + assert text.count('if os.environ.get("CLAUDE_CODE_REMOTE", "").lower() == "true":') == 1 + assert f" return {REFUSAL_EXIT}\n result: int = a.func(a)" in text + + +# Every script under scripts/coord, scripts/hooks and scripts/worktree that names mefor-coord, and +# why it carries no refusal of its own. A new one fails the census until someone decides: a writer a +# cloud session can reach gets the refusal and joins GUARDED, and anything else is classified here. +# The census reads the literal directory name, so a script that reaches it only through another one +# (worktree/new.ps1 through coord/lock.ps1) is covered by that one's entry. +NOT_GUARDED = { + # Run only by a user-scope hook, which install-coordination.ps1 writes into a config root's + # settings.json and which a cloud session never loads. + "hooks/announce-session.ps1": "user-scope hook", + "hooks/collision_gate.ps1": "user-scope hook", + "hooks/mail-drain.ps1": "user-scope hook", + "hooks/mail-watch.ps1": "user-scope hook, retired from the wiring", + "hooks/wiki-write-prompt.ps1": "user-scope hook", + "worktree/session-context.ps1": "user-scope hook", + # Writes state that is correct for the clone it runs in, so it is no dead letter in a VM. + "coord/lock.ps1": "clone-local mutex around worktree/new.ps1", + "coord/overlap.ps1": "a cache of its own answer; run by the user-scope hooks above", + # Reads mefor-coord and writes nothing there, or writes only through a guarded script. + "coord/claim-adjudicate.ps1": "reader", + "coord/claim-reconcile.ps1": "reader; -Apply writes only through claim.ps1", + "coord/fleet.ps1": "reader", + "hooks/claim_check.py": "reader, commit-msg hook", + "hooks/ledger_check.py": "reader, pre-commit hook", + "hooks/lane-level.ps1": "reader", + "hooks/precompact-reprime.ps1": "reader, project hook", + "hooks/worktree_gate.ps1": "reader; writes only its own log, outside mefor-coord", + "worktree/prune-merged.ps1": "reader", + "worktree/remove.ps1": "reader", + # Names the directory in prose or prints its path, and writes nothing there. + "coord/install-coordination.ps1": "mention", + "coord/install-git-hooks.ps1": "mention", + "coord/rescue.ps1": "mention", + "coord/unbacked_check.ps1": "mention", + "worktree/register-hygiene-task.ps1": "mention", +} + + +def test_every_script_naming_the_coordination_directory_is_classified() -> None: + named = { + p.relative_to(ROOT / "scripts").as_posix() + for d in (COORD, HOOKS, WORKTREE) + for p in d.rglob("*") + if p.suffix in {".ps1", ".py", ".sh"} and "mefor-coord" in p.read_text(encoding="utf-8") + } + # A positive control: a census that found nothing would pass the subset check below. + assert PY_GUARDED in named + classified = {*GUARDED, PY_GUARDED, *NOT_GUARDED} + assert sorted(named - classified) == [], "classify these; guard a writer a cloud VM can reach" + assert sorted(classified - named) == [], "stale census entries" From b827f5eeb131b32e184a00bef0bdd807db566528 Mon Sep 17 00:00:00 2001 From: Scott Hall Date: Mon, 19 Oct 2026 15:56:41 -0500 Subject: [PATCH 6/7] Cloud refusal tests: derive the label, run owner_log in place, drop repeats The /simplify pass: the script label is derived from its path, owner_log.py runs from a cwd with no clone and one remote flag, the text check of the Python copy goes (the behavioural tests already run without pwsh), and the GUARDED comment states the reasons that hold. --- tests/test_coord_cloud_refusal.py | 95 +++++++++++++------------------ 1 file changed, 38 insertions(+), 57 deletions(-) diff --git a/tests/test_coord_cloud_refusal.py b/tests/test_coord_cloud_refusal.py index 20eec19457..5f7657086c 100644 --- a/tests/test_coord_cloud_refusal.py +++ b/tests/test_coord_cloud_refusal.py @@ -84,21 +84,12 @@ def _coord_dir(repo: Path) -> Path: return repo / ".git" / "mefor-coord" -def _run( - repo: Path, script: str, *args: str, cloud: bool, extra_env: dict[str, str] | None = None -) -> subprocess.CompletedProcess[str]: +def _run(repo: Path, script: str, *args: str, cloud: bool) -> subprocess.CompletedProcess[str]: env = dict(os.environ) if cloud: env["CLAUDE_CODE_REMOTE"] = "true" - env.update(extra_env or {}) - path = str(repo / "scripts" / script) - argv = ( - [sys.executable, path] - if script.endswith(".py") - else ["pwsh", "-NoProfile", "-NonInteractive", "-File", path] - ) return subprocess.run( - [*argv, *args], + ["pwsh", "-NoProfile", "-NonInteractive", "-File", str(repo / "scripts" / script), *args], cwd=str(repo), env=env, input="", @@ -111,39 +102,25 @@ def _run( @needs_pwsh @pytest.mark.parametrize( - ("label", "script", "args"), + ("script", "args"), [ - pytest.param("seat.ps1", "coord/seat.ps1", SEAT_DECLARE, id="seat-declare"), - pytest.param("seat.ps1", "coord/seat.ps1", ("-Close",), id="seat-close"), - pytest.param("mail.ps1", "coord/mail.ps1", ("-Status",), id="mail-status"), - pytest.param("alloc.ps1", "coord/alloc.ps1", ("-List",), id="alloc-list"), - pytest.param("claim.ps1", "coord/claim.ps1", ("-List",), id="claim-list"), - pytest.param("handoff.ps1", "coord/handoff.ps1", ("-Where",), id="handoff-where"), - pytest.param("handoff.ps1", "coord/handoff.ps1", ("-Report",), id="handoff-report"), - pytest.param("lane.ps1", "coord/lane.ps1", ("-Show",), id="lane-show"), - pytest.param( - "lane.ps1", "coord/lane.ps1", ("-Free", "1", "-InFlight", "0"), id="lane-state" - ), - pytest.param( - "tombstone-retired-backlog-allocator.ps1", - "coord/tombstone-retired-backlog-allocator.ps1", - (), - id="tombstone-write", - ), - pytest.param( - "hygiene-report.ps1", - "worktree/hygiene-report.ps1", - HYGIENE_OFFLINE, - id="hygiene-report", - ), + pytest.param("coord/seat.ps1", SEAT_DECLARE, id="seat-declare"), + pytest.param("coord/seat.ps1", ("-Close",), id="seat-close"), + pytest.param("coord/mail.ps1", ("-Status",), id="mail-status"), + pytest.param("coord/alloc.ps1", ("-List",), id="alloc-list"), + pytest.param("coord/claim.ps1", ("-List",), id="claim-list"), + pytest.param("coord/handoff.ps1", ("-Where",), id="handoff-where"), + pytest.param("coord/lane.ps1", ("-Free", "1", "-InFlight", "0"), id="lane-state"), + pytest.param("coord/tombstone-retired-backlog-allocator.ps1", (), id="tombstone-write"), + pytest.param("worktree/hygiene-report.ps1", HYGIENE_OFFLINE, id="hygiene-report"), ], ) def test_each_coordination_script_refuses_in_a_cloud_session( - repo: Path, label: str, script: str, args: tuple[str, ...] + repo: Path, script: str, args: tuple[str, ...] ) -> None: proc = _run(repo, script, *args, cloud=True) assert proc.returncode == REFUSAL_EXIT, (proc.stdout, proc.stderr) - assert proc.stderr.strip() == label + REFUSAL_TAIL, proc.stderr + assert proc.stderr.strip() == script.rsplit("/", 1)[-1] + REFUSAL_TAIL, proc.stderr assert proc.stdout == "", proc.stdout assert not _coord_dir(repo).exists(), "a refused call still created the coordination directory" @@ -188,12 +165,23 @@ def test_an_explicit_path_override_is_refused_too( def _owner_log( - repo: Path, ledger: Path, *args: str, cloud: bool, remote: str = "true" + tmp_path: Path, ledger: Path, *args: str, remote: str | None ) -> subprocess.CompletedProcess[str]: - env = {"MEFOR_OWNER_LOG": str(ledger)} - if cloud: + """MEFOR_OWNER_LOG skips the git lookup, so this runs the real script in place, from a cwd that + is no repository at all.""" + env = {k: v for k, v in os.environ.items() if k != "CLAUDE_CODE_REMOTE"} + env["MEFOR_OWNER_LOG"] = str(ledger) + if remote is not None: env["CLAUDE_CODE_REMOTE"] = remote - return _run(repo, "coord/owner_log.py", *args, cloud=False, extra_env=env) + return subprocess.run( + [sys.executable, str(COORD / "owner_log.py"), *args], + cwd=str(tmp_path), + env=env, + capture_output=True, + text=True, + timeout=TIMEOUT, + check=False, + ) @pytest.mark.parametrize( @@ -206,21 +194,21 @@ def _owner_log( ], ) def test_owner_log_refuses_in_a_cloud_session_even_with_its_override( - repo: Path, tmp_path: Path, args: tuple[str, ...], remote: str + tmp_path: Path, args: tuple[str, ...], remote: str ) -> None: """MEFOR_OWNER_LOG bypasses the git lookup, so the refusal is proven to come before the write.""" ledger = tmp_path / "owner-log" / "OWNER-ITEMS.md" - proc = _owner_log(repo, ledger, *args, cloud=True, remote=remote) + proc = _owner_log(tmp_path, ledger, *args, remote=remote) assert proc.returncode == REFUSAL_EXIT, (proc.stdout, proc.stderr) assert proc.stderr.strip() == "owner_log.py" + REFUSAL_TAIL, proc.stderr assert proc.stdout == "", proc.stdout assert not ledger.parent.exists() -def test_owner_log_writes_its_row_outside_the_cloud(repo: Path, tmp_path: Path) -> None: +def test_owner_log_writes_its_row_outside_the_cloud(tmp_path: Path) -> None: """The control: the row the refusal withholds is really written here.""" ledger = tmp_path / "owner-log" / "OWNER-ITEMS.md" - proc = _owner_log(repo, ledger, *OWNER_SENT, cloud=False) + proc = _owner_log(tmp_path, ledger, *OWNER_SENT, remote=None) assert proc.returncode == 0, (proc.stdout, proc.stderr) assert "| t1 | request_sent |" in ledger.read_text(encoding="utf-8") @@ -317,9 +305,11 @@ def test_the_declare_prompt_hook_prints_the_prompt_outside_the_cloud(repo: Path) assert "cloud Builder" not in proc.stdout -# Every inline copy of the refusal, by the path that carries it. A shared helper would need a new -# dot-source in scripts that test fixtures copy file by file, an override such as -CoordDir would -# bypass a check placed in a root resolver, and the Python copy could not share it anyway. +# Every PowerShell copy of the refusal, inlined rather than shared. No helper is loaded by all of +# them: alloc.ps1, claim.ps1 and the tombstone script dot-source nothing, and about ten fixtures copy +# alloc.ps1 or claim.ps1 alone, so a new dot-source would break them. There is no shared coord-root +# resolver either, and one would be the wrong home: -CoordDir, -MailRoot and MEFOR_OWNER_LOG bypass +# it. The Python copy is held by the behavioural owner_log tests, which need no pwsh. GUARDED = ( "coord/seat.ps1", "coord/mail.ps1", @@ -344,15 +334,6 @@ def test_each_script_carries_the_one_refusal_line(rel: str) -> None: assert text.count(guard) == 1, f"{rel} lost or changed its cloud refusal line" -def test_the_python_copy_carries_the_one_refusal_line() -> None: - text = (ROOT / "scripts" / PY_GUARDED).read_text(encoding="utf-8") - call = text.split("sys.stderr.write(\n", 1)[1].split("\n )\n", 1)[0] - joined = "".join(piece.strip()[1:-1] for piece in call.splitlines()) - assert joined == "owner_log.py" + REFUSAL_TAIL + "\\n", joined - assert text.count('if os.environ.get("CLAUDE_CODE_REMOTE", "").lower() == "true":') == 1 - assert f" return {REFUSAL_EXIT}\n result: int = a.func(a)" in text - - # Every script under scripts/coord, scripts/hooks and scripts/worktree that names mefor-coord, and # why it carries no refusal of its own. A new one fails the census until someone decides: a writer a # cloud session can reach gets the refusal and joins GUARDED, and anything else is classified here. From 277913f2fd5db25b0d04f5fd4d34f97d691e4b3d Mon Sep 17 00:00:00 2001 From: Scott Hall Date: Mon, 19 Oct 2026 16:01:30 -0500 Subject: [PATCH 7/7] Cloud refusal: review fixes to prose, exit-code docs and the census read CLAUDE.md no longer says every writer refuses: a clone-local lock or cache is still written, and the census says which. handoff.ps1 documents exit 3, the param comments in handoff.ps1 and lane.ps1 name the refusal they mean, and the census tests bytes, so a non-UTF-8 script cannot crash it. --- CLAUDE.md | 9 +++++---- scripts/coord/handoff.ps1 | 6 ++++-- scripts/coord/lane.ps1 | 3 ++- tests/test_coord_cloud_refusal.py | 5 +++-- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e5d9f4ed94..c801aa540e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -480,10 +480,11 @@ gates a merge**, and no seat has to clear one. A Claude Code cloud session (`CLAUDE_CODE_REMOTE=true`) holds one seat only: a **Builder in its own session, on `MEFORORG/MessageFoundry` alone**. **Never attach a second repository**: a session with several repositories loads hooks from none of them. Its `.git/mefor-coord` belongs to its own clone -and reaches no other session, so the scripts that write into it refuse there, at least `seat.ps1 --Declare`, `mail.ps1`, `alloc.ps1`, `claim.ps1`, `handoff.ps1`, `lane.ps1` and `owner_log.py` -(`tests/test_coord_cloud_refusal.py` holds the census), and the declare rule above does not apply. A brief that needs an ADR number is not a -cloud brief. Cite the brief's item in the commit body, not the subject: the commit-msg claim gate +and reaches no other session. So the scripts that write state for other sessions to read refuse +there, at least `seat.ps1 -Declare`, `mail.ps1`, `alloc.ps1`, `claim.ps1`, `handoff.ps1`, +`lane.ps1` and `owner_log.py`, and the declare rule above does not apply. A clone-local lock or +cache is still written. `tests/test_coord_cloud_refusal.py` holds the census of which is which. A +brief that needs an ADR number is not a cloud brief. Cite the brief's item in the commit body, not the subject: the commit-msg claim gate refuses a code commit whose subject cites a ledger number nobody claimed, and no claim can be taken there. Commit under the allowlisted git identity the cloud environment sets, or `cla` blocks the PR. diff --git a/scripts/coord/handoff.ps1 b/scripts/coord/handoff.ps1 index e808d4c0b2..f5c1e65a74 100644 --- a/scripts/coord/handoff.ps1 +++ b/scripts/coord/handoff.ps1 @@ -43,7 +43,8 @@ nobody looks. The app's suggested remedy, `\.git\mefor-coord\...`, cannot work: a worktree's `.git` is a file. So a session drafts the document inside its own worktree, where the edit tools work, and this script copies it into the shared directory. Exit 0 is written, 1 is a - refusal that names its reason, and 2 is a failure while writing. + refusal that names its reason, 2 is a failure while writing, and 3 is the refusal in a Claude + Code cloud session. .EXAMPLE pwsh -NoProfile -File scripts\coord\handoff.ps1 -Report @@ -65,7 +66,8 @@ param( # Move ONE entry to _retired-/. Refuses on any hold unless -Force. # -Retire, -Write and -From are NOT Mandatory, ON PURPOSE: a partial call binds the Retire or # Write set without them (-Force alone, for one), and a Mandatory prompt there hangs a caller with - # stdin open (tests/_no_prompt.py, vault BACKLOG #3627). The refusal is right after this param block. + # stdin open (tests/_no_prompt.py, vault BACKLOG #3627). The partial-call refusal follows this + # param block, after the cloud refusal. [Parameter(ParameterSetName = 'Retire')][string]$Retire, [Parameter(ParameterSetName = 'Retire')][switch]$Force, diff --git a/scripts/coord/lane.ps1 b/scripts/coord/lane.ps1 index 7b9676998e..3d3b0714bc 100644 --- a/scripts/coord/lane.ps1 +++ b/scripts/coord/lane.ps1 @@ -22,7 +22,8 @@ param( # NEITHER IS Mandatory, ON PURPOSE: a bare call, or one giving a single number, binds this default # set without both, and a Mandatory prompt there hangs a caller with stdin open - # (tests/_no_prompt.py, vault BACKLOG #3627). The refusal is right after this param block. + # (tests/_no_prompt.py, vault BACKLOG #3627). The refusal follows this param block, after the + # cloud refusal. [Parameter(ParameterSetName = 'State')][ValidateRange(0, 99)][int]$Free, [Parameter(ParameterSetName = 'State')][ValidateRange(0, 99)][int]$InFlight, [Parameter(ParameterSetName = 'State')][string]$Note = '', diff --git a/tests/test_coord_cloud_refusal.py b/tests/test_coord_cloud_refusal.py index 5f7657086c..1f958aafe5 100644 --- a/tests/test_coord_cloud_refusal.py +++ b/tests/test_coord_cloud_refusal.py @@ -56,7 +56,8 @@ def _git(repo: Path, *args: str) -> None: @pytest.fixture(scope="module") def _template(tmp_path_factory: pytest.TempPathFactory) -> Path: - """One clone carrying its OWN copy of scripts/coord and scripts/hooks, built once per module.""" + """One clone carrying its OWN copy of scripts/coord, scripts/hooks and scripts/worktree, built + once per module.""" r = tmp_path_factory.mktemp("template") / "repo" r.mkdir() _git(r, "init", "-q", "-b", "main") @@ -376,7 +377,7 @@ def test_every_script_naming_the_coordination_directory_is_classified() -> None: p.relative_to(ROOT / "scripts").as_posix() for d in (COORD, HOOKS, WORKTREE) for p in d.rglob("*") - if p.suffix in {".ps1", ".py", ".sh"} and "mefor-coord" in p.read_text(encoding="utf-8") + if p.suffix in {".ps1", ".py", ".sh"} and b"mefor-coord" in p.read_bytes() } # A positive control: a census that found nothing would pass the subset check below. assert PY_GUARDED in named