Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<branch> -f base=main -F
body=@<file>`, then hand it to the Lander with the `qa` label
(`gh api -X POST repos/MEFORORG/MessageFoundry/issues/<N>/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
Expand Down
8 changes: 8 additions & 0 deletions scripts/coord/alloc.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions scripts/coord/claim.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions scripts/coord/mail.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
11 changes: 11 additions & 0 deletions scripts/coord/seat.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
9 changes: 9 additions & 0 deletions scripts/hooks/seat-declare-prompt.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 14 additions & 2 deletions scripts/worktree/ensure-venv.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: "<name> OK <how> <dir>" or "<name> BAD <why>".
Expand Down
7 changes: 7 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
198 changes: 198 additions & 0 deletions tests/test_coord_cloud_refusal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# 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 ``<git-common-dir>/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")
# 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")
_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, ignore=shutil.ignore_patterns("*.lock"))
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"
15 changes: 9 additions & 6 deletions tests/test_worktree_ensure_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@

from __future__ import annotations

import os
import shutil
import subprocess
import sys
import sysconfig
from pathlib import Path

import pytest
Expand All @@ -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",
)


Expand Down Expand Up @@ -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)],
Expand All @@ -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

Expand Down
Loading
Loading