From 3c6ca02f3dca6874175c15c1554a553b13cd3185 Mon Sep 17 00:00:00 2001 From: HackingGate Date: Sun, 25 Oct 2026 00:54:23 +0900 Subject: [PATCH 1/2] uphold holds its own agent sessions to a [stop] gate: the reconcile and the Python suite policy/principles.toml declares `[stop] run` with `uphold check` and the Python suite, and .claude/settings.json configures `uphold hook claude-code` for SessionStart (async) and Stop, so a Claude Code session here cannot end a turn with either newly red. `cargo test` is left out on purpose: the gate runs at every turn's end, and the Rust suite adds a minute or more there. It stays at pre-push and in CI. Run by hand through the hook on this tree, the gate is green and takes about ten seconds. Needs uphold 1.36.0 or later on PATH; 1.35.2 refuses a policy carrying `[stop]`. Claude-Session: https://claude.ai/code/session_017EkhA3KMqw95DzAEYPkDn7 --- .claude/settings.json | 10 ++++++++++ policy/principles.toml | 13 +++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..d1d2e30 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,10 @@ +{ + "hooks": { + "SessionStart": [ + {"hooks": [{"type": "command", "command": "uphold hook claude-code", "async": true}]} + ], + "Stop": [ + {"hooks": [{"type": "command", "command": "uphold hook claude-code", "timeout": 600}]} + ] + } +} diff --git a/policy/principles.toml b/policy/principles.toml index 86180f9..493dc05 100644 --- a/policy/principles.toml +++ b/policy/principles.toml @@ -638,3 +638,16 @@ files.exclude = ["**/tests/**", "**/test/**", "**/*_test.go", "policy/**", "READ [override.no-pinned-versioned-fetch] files.exclude = ["**/tests/**", "**/test/**", "**/*_test.go", "policy/**", "README.md", "third_party/**", "vendor/**", ".github/workflows/release.yml"] + +# What an agent's turn may not end red. `uphold hook claude-code`, configured +# for SessionStart and Stop in .claude/settings.json, runs these at session +# start and again when the agent stops, and blocks once on a command that was +# green when the session started. The reconcile and the Python suite, and not +# `cargo test`: these run at the end of every turn, and a gate that adds a +# minute or more to each one is a gate people turn off. The Rust suite stays at +# pre-push and in CI. +[stop] +run = [ + ["uphold", "check"], + ["uv", "run", "--no-project", "python", "-m", "unittest", "discover", "-s", "tests"], +] From 6e36fa78cf16ffd802a8e1e92373f04d003279f6 Mon Sep 17 00:00:00 2001 From: HackingGate Date: Sun, 25 Oct 2026 01:36:03 +0900 Subject: [PATCH 2/2] The preflight test plants its impossible rustc floor in a copy, not in the tree's toolchain.toml test_a_version_the_host_cannot_satisfy_is_refused wrote the repository's own toolchain.toml and restored the saved bytes afterwards. The Python suite now runs as the [stop] gate, in the background while an agent works, so an edit made to that file during the run would have been reverted. It now copies scripts/deps.sh into a temporary directory beside the edited manifest; deps.sh reads only the toolchain.toml next to its own scripts/ directory, so the comparison under test is the same. Claude-Session: https://claude.ai/code/session_017EkhA3KMqw95DzAEYPkDn7 --- tests/test_toolchain.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/test_toolchain.py b/tests/test_toolchain.py index 2afd17f..00f11fd 100644 --- a/tests/test_toolchain.py +++ b/tests/test_toolchain.py @@ -16,6 +16,7 @@ import shutil import subprocess +import tempfile import tomllib import unittest from pathlib import Path @@ -60,10 +61,10 @@ class Preflight(unittest.TestCase): reason "not installed" and would pass while proving nothing. """ - def _check(self) -> subprocess.CompletedProcess[str]: + def _check(self, root: Path) -> subprocess.CompletedProcess[str]: return subprocess.run( ["./scripts/deps.sh", "check"], - cwd=REPO, + cwd=root, capture_output=True, text=True, check=False, @@ -75,12 +76,16 @@ def test_a_version_the_host_cannot_satisfy_is_refused(self) -> None: impossible = manifest.replace(f'want = "{want}"', 'want = ">=999.0"', 1) self.assertNotEqual(manifest, impossible, "the rustc `want` line moved") - original = (REPO / "toolchain.toml").read_bytes() - try: - (REPO / "toolchain.toml").write_text(impossible, encoding="utf-8") - done = self._check() - finally: - (REPO / "toolchain.toml").write_bytes(original) + # In a copy, never the tree: this suite runs as the [stop] gate while + # an agent works, and writing the real toolchain.toml and restoring it + # afterwards would revert any edit made to it in between. deps.sh reads + # only the toolchain.toml beside its own scripts/ directory. + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "scripts").mkdir() + shutil.copy2(REPO / "scripts" / "deps.sh", root / "scripts" / "deps.sh") + (root / "toolchain.toml").write_text(impossible, encoding="utf-8") + done = self._check(root) report = done.stdout + done.stderr self.assertNotEqual(done.returncode, 0, report)