fix(kvb-check-gate): exclude operator-listed targets from every gate

gate_excluded_targets in .claude/agents-council/config.json of the passwd home (never $HOME or the environment, so no caller swaps in its own list) names targets no gate requires or runs. status shows them excluded and keeps the gate green without them, naming the exclusion and the config in the green line; excluding every target keeps the gate open and skips the hotfix check. run refuses an excluded target with exit 2. Operator order 2026-11-11: no oracle tests in any gate.

🎯 Fix: 93 %
💥 Break: 4 %
💸 Cost: 100 %
⏱️ Time: 100 %
🛡️ Security: 100 %
🏆 Score: 89
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-11 22:34:47 +00:00
1 parent e805b07c27
commit bcbcd9f9a2
3 files changed
+98 -13

No files matched your search

+5 -3
View File
@@ -11,9 +11,11 @@ confidence ≥ 90 %, break risk ≤ 5 %, cost and time ≤ 100 % in
the cause) before every `make test*` target of the project, each Makefile target named `test` or starting with
`test-`, ran green on the exact head with a clean tracked tree. Until then
the numbers are provisional and every target not green is a residual.
Targets in the operator's `gate_excluded_targets` are excluded, not
residuals.
`${CLAUDE_SKILL_DIR}/scripts/gate.py` reads the targets from the Makefile and
its includes without running make, and keeps results per commit in
its includes and keeps results per commit in
`kvb-gate.json` in the git common dir: linked worktrees share them, a new
commit starts from zero.
@@ -30,8 +32,8 @@ commit starts from zero.
3. **Status**: `gate.py status --repo <root>`. `gate: green` ends here; a
`missing hotfix` reason means back to step 1.
4. **Admit**: every gate target is heavy and runs only inside
`${CLAUDE_SKILL_DIR}/../kvb-org-queue/scripts/queue.py hold` (step 5),
which grants by rule and releases on exit. Only a single test module
`${CLAUDE_SKILL_DIR}/../kvb-org-queue/scripts/queue.py hold` (step 5).
Only a single test module
self-admits (`${CLAUDE_SKILL_DIR}/../kvb-org-organize/scripts/watch.py
admit --kind test-nostack`), and it never turns a gate target green. A
hotfix gate adds `--hotfix <slug>` and takes the next free gate slot.
+46 -10
View File
@@ -6,10 +6,14 @@ Args:
parsed statically from the Makefile make reads in <dir> and every file
it includes; make is never run.
status --repo <dir> [--json]: per listed target green, red or missing for
HEAD, from `kvb-gate.json` in the git common dir; `hotfix.py pending
--target <t> ...` of the kvb-git-hotfix skill, given the listed
targets, keeps the gate open while a merged hotfix that turns one of
them green is missing from HEAD.
HEAD, from `kvb-gate.json` in the git common dir, or excluded when the
list gate_excluded_targets in CONFIG names it; an excluded target never
keeps the gate open, excluding every target does. CONFIG is
.claude/agents-council/config.json in the passwd home of the account
running gate.py, never $HOME. `hotfix.py pending --target <t> ...`
of the kvb-git-hotfix skill, given the other listed targets, keeps the
gate open while a merged hotfix that turns one of them green is
missing from HEAD.
run --repo <dir> --target <t> [--target ...] --log-dir <dir>
[--fail-regex <re> ...]: under the worktree lock, run each target as
`make -C <dir> <t>` with stdout and stderr in
@@ -44,7 +48,8 @@ Args:
Output:
list: one `<target>\t<file>:<line>` per target.
status: one `<status>\t<target>\t<log>\t<finished>\t<reason>` per target,
then `gate: green`, or `gate: open (...)` naming what is missing,
then `gate: green`, `gate: green (excluded: <target>, ... by
<CONFIG>)`, or `gate: open (...)` naming what is missing,
`missing hotfix: <slug>, ...` included.
run: one `<status>\t<target>\t<log>\t<reason>` per target; on a void
`void: merge main for hotfix <slug>, then rerun`.
@@ -54,8 +59,8 @@ Output:
Exit codes:
list: 0 done, 2 usage error.
status: 0 gate green, 1 gate open, 2 usage error.
run: 0 every target green, 1 a target red, 2 usage error, dirty tracked
files or HEAD moved during a run, 3 void: the remaining targets are
run: 0 every target green, 1 a target red, 2 usage error, an excluded
target, dirty tracked files or HEAD moved during a run, 3 void: the remaining targets are
neither run nor recorded, 75 (EX_TEMPFAIL) busy: another run or hold
holds the worktree, or a sync still holds it after SYNC_WAIT_SECONDS.
hold: the command's exit code unchanged, 128+N when signal N ended it;
@@ -71,6 +76,7 @@ import fcntl
import glob
import json
import os
import pwd
import re
import shlex
import signal
@@ -103,6 +109,8 @@ POLL_SECONDS = 0.1
BUSY = os.EX_TEMPFAIL
FORWARDED = (signal.SIGINT, signal.SIGTERM, signal.SIGHUP)
HOTFIX = Path(__file__).resolve().parents[2] / "kvb-git-hotfix" / "scripts" / "hotfix.py"
CONFIG = Path(pwd.getpwuid(os.getuid()).pw_dir) / ".claude" / "agents-council" / "config.json"
EXCLUDED = "gate_excluded_targets"
def fail(message: str) -> NoReturn:
@@ -380,6 +388,18 @@ def repository(value: str) -> Path:
return repo
def excluded() -> set[str]:
if not CONFIG.is_file():
return set()
try:
listed = json.loads(CONFIG.read_text(encoding="utf-8")).get(EXCLUDED, [])
except (OSError, ValueError, AttributeError) as error:
fail(f"cannot read {EXCLUDED} from {CONFIG}: {error}")
if not isinstance(listed, list) or not all(isinstance(target, str) for target in listed):
fail(f"{EXCLUDED} in {CONFIG} must be a list of target names")
return set(listed)
def cmd_list(args: argparse.Namespace) -> int:
found = targets(repository(args.repo))
if args.json:
@@ -400,22 +420,30 @@ def cmd_status(args: argparse.Namespace) -> int:
sha = head(repo)
state = load(state_path(repo))
dirty, untracked = tree(repo)
skip = excluded()
required = [target for target in found if target not in skip]
skipped = [target for target in found if target in skip]
rows = []
for target in found:
entry = state[sha].get(target) if sha in state else None
rows.append({"target": target, "status": "missing"} if entry is None else {"target": target, **entry})
if target in skip:
rows.append({"target": target, "status": "excluded", "reason": f"{EXCLUDED} in {CONFIG}"})
else:
rows.append({"target": target, "status": "missing"} if entry is None else {"target": target, **entry})
missing = [r["target"] for r in rows if r["status"] == "missing"]
red = [r["target"] for r in rows if r["status"] == "red"]
reasons = []
if not found:
reasons.append("no make test* target found")
elif not required:
reasons.append("every make test* target is excluded")
if missing:
reasons.append(f"missing: {', '.join(missing)}")
if red:
reasons.append(f"red: {', '.join(red)}")
if dirty:
reasons.append(f"dirty: {len(dirty)} tracked file(s)")
hotfixes = missing_hotfixes(repo, list(found)) if hotfix_checks() else []
hotfixes = missing_hotfixes(repo, required) if required and hotfix_checks() else []
if hotfixes:
reasons.append(f"missing hotfix: {', '.join(hotfixes)}")
gate = "open" if reasons else "green"
@@ -431,7 +459,12 @@ def cmd_status(args: argparse.Namespace) -> int:
print("\t".join([r["status"], r["target"], *(cell(r.get(k)) for k in ("log", "finished", "reason"))]))
if untracked:
print(f"untracked\t{len(untracked)} file(s), not part of the gate")
print("gate: green" if gate == "green" else f"gate: open ({'; '.join(reasons)})")
if reasons:
print(f"gate: open ({'; '.join(reasons)})")
elif skipped:
print(f"gate: green (excluded: {', '.join(skipped)} by {CONFIG})")
else:
print("gate: green")
return 0 if gate == "green" else 1
@@ -447,6 +480,9 @@ def cmd_run(args: argparse.Namespace) -> int:
unknown = [t for t in args.target if t not in found]
if unknown:
fail(f"not a make test* target of {repo}: {', '.join(unknown)}")
barred = sorted(set(args.target) & excluded())
if barred:
fail(f"excluded from every gate by {EXCLUDED} in {CONFIG}: {', '.join(barred)}")
with worktree_lock(repo, ["gate.py", "run", "--repo", str(repo), *target_flags(args.target)], []) as lock:
sha = head(repo)
dirty, untracked = tree(repo)
+47
View File
@@ -2,12 +2,14 @@
from __future__ import annotations
import argparse
import contextlib
import fcntl
import importlib.util
import io
import json
import os
import pwd
import shutil
import signal
import subprocess
@@ -17,6 +19,7 @@ import time
import unittest
from pathlib import Path
from typing import ClassVar
from unittest import mock
REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPT = REPO_ROOT / "skills" / "kvb-check-gate" / "scripts" / "gate.py"
@@ -68,6 +71,7 @@ RED = {
}
USAGE = {"Makefile": "test:\n\t@true\nbuild:\n\t@true\n"}
VOID = {"Makefile": ".PHONY: test test-a test-b\ntest:\n\t@echo all\ntest-a:\n\t@echo a\ntest-b:\n\t@echo b\n"}
ORACLE = {"Makefile": ".PHONY: test test-oracle\ntest:\n\t@echo all\ntest-oracle:\n\t@echo oracle\n"}
FAILED = ("--fail-regex", r"FAILED TARGETS: \S")
SYNC_HOLDER = "4242\thotfix.py sync --task t\n"
COUNTING_CHILD = (
@@ -142,6 +146,49 @@ class GateCase(unittest.TestCase):
return self.gate("status", "--repo", str(self.repo)).stdout.splitlines()[-1]
class TestExcluded(GateCase):
files = ORACLE
def call(self, gate: object, command: str, **fields: object) -> tuple[int, str, str]:
out, err = io.StringIO(), io.StringIO()
with mock.patch.dict(os.environ, self.env, clear=True), contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
try:
code = getattr(gate, command)(argparse.Namespace(repo=str(self.repo), **fields))
except SystemExit as stop:
code = stop.code
return code, out.getvalue(), err.getvalue()
def exclude(self, gate: object, listed: object) -> None:
gate.CONFIG.write_text(json.dumps({"gate_excluded_targets": listed}), encoding="utf-8")
def test_an_excluded_target_is_shown_excluded_never_required_and_refused_by_run(self):
gate = load_gate()
gate.CONFIG = self.root / "config.json"
run = {"log_dir": str(self.logs), "fail_regex": None}
self.assertEqual(self.call(gate, "cmd_run", target=["test"], **run)[0], 0)
code, out, _ = self.call(gate, "cmd_status", json=True)
self.assertEqual((code, json.loads(out)["open"]), (1, ["missing: test-oracle"]))
self.exclude(gate, ["test-oracle"])
code, out, _ = self.call(gate, "cmd_status", json=True)
after = json.loads(out)
self.assertEqual((code, after["gate"]), (0, "green"))
self.assertEqual({r["target"]: r["status"] for r in after["targets"]}, {"test": "green", "test-oracle": "excluded"})
self.assertEqual(self.call(gate, "cmd_status", json=False)[1].splitlines()[-1], f"gate: green (excluded: test-oracle by {gate.CONFIG})")
code, _, err = self.call(gate, "cmd_run", target=["test-oracle"], **run)
self.assertEqual(code, 2)
self.assertIn("excluded from every gate by gate_excluded_targets", err)
self.exclude(gate, ["test", "test-oracle"])
code, out, _ = self.call(gate, "cmd_status", json=True)
self.assertEqual((code, json.loads(out)["open"]), (1, ["every make test* target is excluded"]))
self.exclude(gate, "test-oracle")
self.assertEqual(self.call(gate, "cmd_status", json=False)[0], 2)
def test_the_exclusion_list_ignores_home_and_environment_overrides(self):
with mock.patch.dict(os.environ, {"HOME": str(self.root), "AGENTS_COUNCIL_CONFIG": str(self.root / "config.json")}):
gate = load_gate()
self.assertEqual(gate.CONFIG, Path(pwd.getpwuid(os.getuid()).pw_dir) / ".claude" / "agents-council" / "config.json")
class TestGreenPath(GateCase):
files = GREEN