scripts.agent_check
Reference › scripts › agent_check
The host-side quality gate for work done by an agent inside an Incus container. It pairs with agent_notify, which the agent runs inside the container when it finishes. A human or host automation runs agent_check; no module or test imports it. It uses resolve_redis_host and resolve_redis_port from redis_factory.
Invocation: --task is required and names the task. --container selects the Incus container (default claude-orchestrata). --listen waits for the done notification on Redis before checking, and --pull skips the wait and checks immediately. --staging (default /tmp/staging) is the host directory that receives the code, and --review-script (default scripts/code-review.py) is the review tool to run. --redis-host, --redis-port and --redis-db override the connection; the database defaults to the REDIS_DB environment variable or 0.
Flow: get_redis_client connects, wait_for_done blocks (default timeout 3600 seconds) until the agent's notification arrives, pull_code copies the container's work into the staging directory, and run_checks runs four checks on it: ruff, mypy, the review script and pytest. generate_report formats the results as Markdown, and main writes it to <staging>/<task>-report.md and prints the path.
At a glance
| Source | scripts/agent_check.py |
| Lines of code | 389 |
| Classes | 0 |
| Functions | 6 |
| Imports (internal) | orchestrata_ultimate.core.redis_factory |
| Imported by | none |
| Tests that import it | none |
Class diagram
No classes; see Functions.
Dependency diagram
API
Functions
| Function | Signature | Summary |
|---|---|---|
get_redis_client |
get_redis_client(host: str, port: int, db: int) |
|
wait_for_done |
wait_for_done(redis_client, task: str, timeout: int = 3600) -> dict |
Block until agent sends done notification. |
pull_code |
pull_code(container: str, task: str, staging: Path) -> Path |
Pull code from Incus container to staging directory. |
run_checks |
run_checks(staged_path: Path, review_script: Path) -> dict |
Run the full quality gate suite against staged code. |
generate_report |
generate_report(results: dict, task: str) -> str |
Generate a Claude-facing failure report. |
main |
main() -> None |
Source
Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/scripts/agent_check.py
#!/usr/bin/env python3
"""Agent check script — run ON THE HOST.
Listens for Redis notification from the Incus container, pulls the code,
runs the full quality gate suite, and produces a Claude-facing report.
Usage:
python agent_check.py --listen --task TASK_ID --container claude-orchestrata
python agent_check.py --pull --task TASK_ID --container claude-orchestrata
"""
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
import time
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from orchestrata_ultimate.core.redis_factory import (
resolve_redis_host,
resolve_redis_port,
)
# Children's output is machine-parsed (regex/substring greps): force color
# OFF for every subprocess this tool spawns. An ambient FORCE_COLOR (e.g.
# exported by the launching terminal/agent harness) made mypy/ruff/pytest
# emit ANSI into captured pipes and silently broke the parsers (gate
# 2026-08-05: mypy "0 issues" on a dirty tree, ruff int(line) crash,
# FAILED-line greps missing their lines).
os.environ["NO_COLOR"] = "1" # gate-ok: process-bootstrap env routing
os.environ.pop("FORCE_COLOR", None) # gate-ok: process-bootstrap env routing
def get_redis_client(host: str, port: int, db: int):
try:
import redis
except ImportError:
print("ERROR: redis package not installed.", file=sys.stderr)
sys.exit(1)
return redis.Redis(host=host, port=port, db=db, decode_responses=True)
def wait_for_done(redis_client, task: str, timeout: int = 3600) -> dict:
"""Block until agent sends done notification."""
channel = f"agent:done:{task}"
pubsub = redis_client.pubsub()
pubsub.subscribe(channel)
print(f"Listening on Redis channel '{channel}'...")
start = time.time()
# Cap each poll by the time remaining, not a flat 5s: pubsub.get_message()
# blocks for its *entire* timeout waiting for a message, so a caller-supplied
# small `timeout` (e.g. tests exercising fast expiry) was previously stuck
# inside one 5s poll regardless of the outer deadline.
while True:
remaining = timeout - (time.time() - start)
if remaining <= 0:
break
message = pubsub.get_message(timeout=min(5, remaining))
if message and message["type"] == "message":
payload = json.loads(message["data"])
print(f"Received: {payload}")
return payload
print("TIMEOUT: No notification received.", file=sys.stderr)
sys.exit(1)
def pull_code(container: str, task: str, staging: Path) -> Path:
"""Pull code from Incus container to staging directory."""
src_in_container = f"/home/mohamed/workspace/{task}"
dest = staging / task
if dest.exists():
shutil.rmtree(dest)
dest.mkdir(parents=True)
# Use incus file pull to recursively copy
cmd = [
"incus",
"exec",
container,
"--",
"bash",
"-c",
f"cd {src_in_container} && tar czf - .",
]
print(f"Pulling code from {container}:{src_in_container} -> {dest}")
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
assert proc.stdout is not None # guaranteed by stdout=subprocess.PIPE above
tar = subprocess.run(["tar", "xzf", "-", "-C", str(dest)], stdin=proc.stdout)
proc.stdout.close()
proc.wait()
if proc.returncode != 0 or tar.returncode != 0:
print("ERROR: Failed to pull code from container.", file=sys.stderr)
sys.exit(1)
print(f"Code staged at {dest}")
return dest
def run_checks(staged_path: Path, review_script: Path) -> dict:
"""Run the full quality gate suite against staged code."""
results = {
"ruff": {},
"mypy": {},
"review": {},
"pytest": {},
}
src_path = staged_path / "src"
tests_path = staged_path / "tests"
def _ruff() -> None:
ruff = subprocess.run(
[
sys.executable,
"-m",
"ruff",
"check",
str(src_path),
str(tests_path),
"--output-format=json",
],
capture_output=True,
text=True,
)
try:
results["ruff"]["issues"] = json.loads(ruff.stdout) if ruff.stdout else []
except json.JSONDecodeError:
results["ruff"]["issues"] = []
results["ruff"]["returncode"] = ruff.returncode
def _mypy() -> None:
# NOTE: mypy has no built-in JSON output mode (no --output-format flag),
# unlike ruff. Parse its plain-text "file:line: severity: message" lines
# instead (a prior version passed --output-format=json, which mypy
# rejects as an unrecognized argument — it silently produced an empty
# stdout, so real mypy errors were never surfaced here).
# Invoke mypy as `python -m mypy` (not the bare "mypy" binary name): the
# caller's PATH is not guaranteed to include the venv's bin/ (e.g. when
# this script is spawned as a subprocess whose PATH was inherited from a
# shell that never activated the venv) — sys.executable always resolves
# to the interpreter this script itself is running under.
mypy = subprocess.run(
[
sys.executable,
"-m",
"mypy",
str(src_path),
"--no-error-summary",
"--no-color-output",
],
capture_output=True,
text=True,
)
mypy_issue_re = re.compile(
r"^(?P<file>[^:]+):(?P<line>\d+):\s*(?P<severity>\w+):\s*(?P<message>.*)$"
)
mypy_issues = []
for line in mypy.stdout.splitlines():
match = mypy_issue_re.match(line)
if match and match.group("severity") == "error":
mypy_issues.append(
{
"file": match.group("file"),
"line": int(match.group("line")),
"message": match.group("message"),
}
)
results["mypy"]["issues"] = mypy_issues
results["mypy"]["returncode"] = mypy.returncode
def _review() -> None:
# Unified review script (from HOST, not container)
report_path = staged_path / "review-report.json"
subprocess.run(
[
sys.executable,
str(review_script),
str(src_path),
str(tests_path),
"--format",
"json",
"--output",
str(report_path),
],
capture_output=True,
text=True,
)
try:
with open(report_path) as f:
review_data = json.load(f)
results["review"] = review_data
except FileNotFoundError, json.JSONDecodeError:
results["review"] = {"total_issues": 0, "issues": []}
def _pytest() -> None:
# Tests — via the sanctioned threadpool runner. pytest-xdist (-n auto)
# is BANNED (deadlocks the suite ~78%); run_tests_threadpool.py gives
# per-module parallelism instead. Use the staged checkout's own copy of
# the runner so the review reflects that tree.
# Use THIS host's runner (not the untrusted staged copy) against the staged
# tree via cwd=staged_path.
runner = Path(__file__).resolve().parent / "run_tests_threadpool.py"
pytest = subprocess.run(
[
sys.executable,
str(runner),
str(tests_path),
"--pytest-arg=--timeout=120",
"--pytest-arg=-q",
],
capture_output=True,
text=True,
cwd=staged_path,
)
results["pytest"]["returncode"] = pytest.returncode
results["pytest"]["stdout"] = pytest.stdout
results["pytest"]["stderr"] = pytest.stderr
with ThreadPoolExecutor(max_workers=4) as pool:
futures = [
pool.submit(_ruff),
pool.submit(_mypy),
pool.submit(_review),
pool.submit(_pytest),
]
for fut in futures:
fut.result()
print("\n=== Running ruff ===")
print(f"Ruff: {len(results['ruff']['issues'])} issues")
print("\n=== Running mypy ===")
print(f"MyPy: {len(results['mypy']['issues'])} issues")
print("\n=== Running unified review ===")
print(f"Review: {results['review'].get('total_issues', 0)} issues")
print("\n=== Running tests (threadpool runner, no xdist) ===")
print(f"Pytest: returncode={results['pytest']['returncode']}")
return results
def generate_report(results: dict, task: str) -> str:
"""Generate a Claude-facing failure report."""
lines = [f"# Quality Gate Report for Task: {task}", ""]
# Count errors
review_issues = results["review"].get("issues", [])
errors = [i for i in review_issues if i.get("severity") == "ERROR"]
warnings = [i for i in review_issues if i.get("severity") == "WARNING"]
ruff_count = len(results["ruff"]["issues"])
mypy_count = len(results["mypy"]["issues"])
pytest_rc = results["pytest"]["returncode"]
lines.append("## Summary")
lines.append(f"- Review ERRORs: {len(errors)}")
lines.append(f"- Review WARNINGs: {len(warnings)}")
lines.append(f"- Ruff issues: {ruff_count}")
lines.append(f"- MyPy issues: {mypy_count}")
lines.append(f"- Pytest: {'PASS' if pytest_rc == 0 else 'FAIL'} (rc={pytest_rc})")
lines.append("")
if errors:
lines.append("## CLAUDE_FIX_REQUIRED (ERRORs)")
for issue in errors[:50]:
lines.append(
f"- {issue.get('file', '?')}:{issue.get('line', '?')} "
f"[{issue.get('code', '?')}] {issue.get('message', '?')}"
)
if len(errors) > 50:
lines.append(f"- ... and {len(errors) - 50} more ERRORs")
lines.append("")
if ruff_count:
lines.append("## RUFF_ISSUES")
for issue in results["ruff"]["issues"][:20]:
lines.append(
f"- {issue.get('filename', '?')}:{issue.get('location', {}).get('row', '?')} "
f"[{issue.get('code', '?')}] {issue.get('message', '?')}"
)
lines.append("")
if mypy_count:
lines.append("## MYPY_ISSUES")
for issue in results["mypy"]["issues"][:20]:
line = issue.get("line", "?")
msg = issue.get("message", "?")
lines.append(f"- {issue.get('file', '?')}:{line} {msg}")
lines.append("")
if pytest_rc != 0:
lines.append("## PYTEST_FAILURES")
lines.append("```")
lines.append(
results["pytest"]["stdout"][-2000:]
if len(results["pytest"]["stdout"]) > 2000
else results["pytest"]["stdout"]
)
lines.append("```")
lines.append("")
# Leaked issues check
lines.append("## LEAK_CHECK")
lines.append(
"Verify no test files were deleted, no # noqa added, no review script modified."
)
lines.append("")
return "\n".join(lines)
def main() -> None:
parser = argparse.ArgumentParser(description="Agent quality gate checker")
parser.add_argument("--task", required=True, help="Task identifier")
parser.add_argument(
"--container", default="claude-orchestrata", help="Incus container name"
)
parser.add_argument(
"--listen",
action="store_true",
help="Wait for Redis notification before checking",
)
parser.add_argument(
"--pull", action="store_true", help="Pull code and run checks immediately"
)
parser.add_argument(
"--staging", default="/tmp/staging", help="Staging directory on host"
)
parser.add_argument(
"--review-script",
default="scripts/code-review.py",
help="Path to review script on host",
)
parser.add_argument("--redis-host", default=None)
parser.add_argument("--redis-port", type=int, default=None)
parser.add_argument(
"--redis-db", type=int, default=int(os.environ.get("REDIS_DB", "0"))
)
args = parser.parse_args()
redis_host = args.redis_host or resolve_redis_host()
redis_port = args.redis_port or resolve_redis_port()
redis_client = get_redis_client(redis_host, redis_port, args.redis_db)
if args.listen:
wait_for_done(redis_client, args.task)
if args.listen or args.pull:
staged = pull_code(args.container, args.task, Path(args.staging))
results = run_checks(staged, Path(args.review_script))
report = generate_report(results, args.task)
report_path = Path(args.staging) / f"{args.task}-report.md"
report_path.write_text(report)
print(f"\nReport written to {report_path}")
print(report)
# Exit non-zero if there are errors
total_errors = len(
[
i
for i in results["review"].get("issues", [])
if i.get("severity") == "ERROR"
]
)
total_errors += len(results["ruff"]["issues"])
total_errors += len(results["mypy"]["issues"])
if results["pytest"]["returncode"] != 0:
total_errors += 1
if total_errors > 0:
print(f"\n❌ QUALITY GATE FAILED: {total_errors} total issues found.")
sys.exit(1)
else:
print("\n✅ QUALITY GATE PASSED: Zero issues.")
if __name__ == "__main__":
main()