diff --git a/CLAUDE.md b/CLAUDE.md index a1e7cb772..c801aa540 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -480,9 +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 `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 +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 55c98f28c..f5c1e65a7 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, @@ -84,6 +86,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 2a96327e6..3d3b0714b 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 = '', @@ -35,6 +36,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 817faecfe..6cc0e213a 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 499830a48..aaa0d4c39 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 f0eb3c5f7..bc3f09d27 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 45c1e8ee8..df2675136 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 @@ -49,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") @@ -62,6 +70,7 @@ def _template(tmp_path_factory: pytest.TempPathFactory) -> Path: _git(r, "config", "gc.auto", "0") 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") @@ -99,21 +108,25 @@ def _run(repo: Path, script: str, *args: str, cloud: bool) -> subprocess.Complet @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("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" @@ -130,6 +143,82 @@ 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( + tmp_path: Path, ledger: Path, *args: str, remote: str | None +) -> subprocess.CompletedProcess[str]: + """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 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( + ("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( + 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(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(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(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") + + @needs_pwsh @needs_windows def test_seat_record_is_not_refused_in_the_cloud(repo: Path) -> None: @@ -147,6 +236,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( @@ -157,6 +248,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: @@ -188,11 +311,81 @@ 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 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", + "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" + + +# 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 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 + 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"