#!/usr/bin/env python3 """Run one command, retain its complete log, and print only a bounded result.""" from __future__ import annotations import argparse import contextlib import io import json import os import re import shlex import subprocess import sys import tempfile import time import tomllib from collections import deque from datetime import datetime, timezone from pathlib import Path from typing import Any, Iterable, Sequence WORKFLOW_PATH = Path(".codex/workflow.toml") ANSI_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]") ACTIONABLE_PATTERNS = ( re.compile(r"^\s*error(?:\[[A-Z0-9]+\])?:", re.IGNORECASE), re.compile(r"^\s*error\b", re.IGNORECASE), re.compile(r"\bpanicked at\b", re.IGNORECASE), re.compile(r"^\s*thread .+ panicked", re.IGNORECASE), re.compile(r"^\s*Traceback \(most recent call last\):"), re.compile(r"^\s*(?:AssertionError|RuntimeError|ValueError|TypeError):"), re.compile(r"^\s*failures:\s*$", re.IGNORECASE), re.compile(r"^\s*test result: FAILED", re.IGNORECASE), re.compile(r"^\s*FAILED(?:\s|$)", re.IGNORECASE), re.compile(r"^\s*FAIL(?:\s|$)", re.IGNORECASE), re.compile(r"^\s*Caused by:\s*\S", re.IGNORECASE), ) TEST_RESULT_RE = re.compile( r"test result: ok\.\s*(\d+) passed;\s*(\d+) failed;\s*(\d+) ignored", re.IGNORECASE, ) SAFE_NAME_RE = re.compile(r"[^a-zA-Z0-9_.-]+") class SummaryError(RuntimeError): """The wrapper cannot locate its configuration or output paths.""" def find_repository(start: Path | None = None) -> Path: override = os.environ.get("BLACKSITE_WORKSPACE_ROOT") configured = None config_path = (start or Path.cwd()) / ".codex/config.toml" if not override and config_path.is_file(): with config_path.open("rb") as handle: configured = tomllib.load(handle).get("workspace_root") candidates = [Path(override or configured)] if (override or configured) else [start or Path.cwd()] if start is None and not override: candidates.append(Path(__file__).parent) visited: set[Path] = set() for candidate in candidates: candidate = candidate.expanduser().absolute() for current in (candidate, *candidate.parents): canonical = current.resolve(strict=False) if canonical in visited: continue visited.add(canonical) if (current / WORKFLOW_PATH).is_file(): return current searched = ", ".join(str(path) for path in candidates) raise SummaryError(f"could not locate {WORKFLOW_PATH} from {searched}") def load_workflow(root: Path) -> dict[str, Any]: path = root / WORKFLOW_PATH try: with path.open("rb") as handle: workflow = tomllib.load(handle) except OSError as error: raise SummaryError(f"cannot read workflow configuration {path}: {error}") from error except tomllib.TOMLDecodeError as error: raise SummaryError(f"invalid workflow configuration {path}: {error}") from error if workflow.get("version") != 1: raise SummaryError(f"unsupported or missing workflow version in {path}") return workflow def output_settings(workflow: dict[str, Any]) -> tuple[str, int]: output = workflow.get("output", {}) if not isinstance(output, dict): raise SummaryError("[output] must be a TOML table") log_dir = output.get("log_dir", ".codex/logs") max_lines = output.get("max_failure_lines", 120) if not isinstance(log_dir, str) or not log_dir: raise SummaryError("output.log_dir must be a non-empty path string") if not isinstance(max_lines, int) or not 1 <= max_lines <= 1000: raise SummaryError("output.max_failure_lines must be an integer from 1 to 1000") return log_dir, max_lines def resolve_path(root: Path, raw: str) -> Path: path = Path(os.path.expandvars(os.path.expanduser(raw))) if not path.is_absolute(): path = root / path return path.absolute() def relative_display(root: Path, path: Path) -> str: try: return path.relative_to(root.resolve(strict=False)).as_posix() except ValueError: return str(path) def safe_name(value: str) -> str: cleaned = SAFE_NAME_RE.sub("-", value.strip()).strip("-._") return cleaned[:80] or "command" def default_log_path(root: Path, log_dir: str, gate: str) -> Path: timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S.%fZ") return resolve_path(root, str(Path(log_dir) / f"{timestamp}-{safe_name(gate)}.log")) def strip_ansi(value: str) -> str: return ANSI_RE.sub("", value).rstrip("\r\n") def duration_text(seconds: float) -> str: if seconds < 10: return f"{seconds:.2f}s" if seconds < 60: return f"{seconds:.1f}s" minutes, remainder = divmod(seconds, 60) return f"{int(minutes)}m {remainder:.0f}s" def is_actionable(line: str) -> bool: plain = strip_ansi(line) return any(pattern.search(plain) for pattern in ACTIONABLE_PATTERNS) def failure_excerpt(path: Path, limit: int) -> tuple[list[str], str | None]: """Return bounded context around the first actionable failure. The log is streamed rather than loaded wholesale, keeping the wrapper useful for very large compiler output. """ before_count = min(12, max(0, limit // 4)) before: deque[str] = deque(maxlen=before_count) excerpt: list[str] = [] first_actionable: str | None = None matched = False with path.open("r", encoding="utf-8", errors="replace") as handle: for raw_line in handle: line = strip_ansi(raw_line) if not matched: if is_actionable(line): matched = True first_actionable = line.strip() or None excerpt.extend(before) excerpt.append(line) else: before.append(line) elif len(excerpt) < limit: excerpt.append(line) else: break if not matched: # With no recognizable error, the command's tail is normally the most # useful bounded evidence (signal termination, tool-specific errors). tail: deque[str] = deque(maxlen=limit) with path.open("r", encoding="utf-8", errors="replace") as handle: for raw_line in handle: tail.append(strip_ansi(raw_line)) excerpt = list(tail) first_actionable = next((line.strip() for line in excerpt if line.strip()), None) while excerpt and not excerpt[0].strip(): excerpt.pop(0) while excerpt and not excerpt[-1].strip(): excerpt.pop() return excerpt[:limit], first_actionable def success_summary(path: Path) -> str: last_nonempty = "" test_result: tuple[str, str, str] | None = None explicit_pass = "" with path.open("r", encoding="utf-8", errors="replace") as handle: for raw_line in handle: line = strip_ansi(raw_line).strip() if not line: continue last_nonempty = line match = TEST_RESULT_RE.search(line) if match: test_result = match.groups() if line.startswith("PASS "): explicit_pass = line if test_result is not None: passed, failed, ignored = test_result return f"{passed} passed, {failed} failed, {ignored} ignored" if explicit_pass: return explicit_pass[:240] if last_nonempty.startswith("Finished "): return last_nonempty[:240] return "exit 0" def write_result(path: Path, result: dict[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) temporary: Path | None = None try: with tempfile.NamedTemporaryFile( "w", encoding="utf-8", dir=path.parent, prefix=f".{path.name}.", suffix=".tmp", delete=False, ) as handle: json.dump(result, handle, indent=2, sort_keys=True) handle.write("\n") handle.flush() os.fsync(handle.fileno()) temporary = Path(handle.name) os.replace(temporary, path) finally: if temporary is not None and temporary.exists(): temporary.unlink() def normalized_exit_code(returncode: int) -> int: return 128 + abs(returncode) if returncode < 0 else returncode def execute( *, root: Path, workflow: dict[str, Any], gate: str, command: Sequence[str], log: Path | None = None, json_result: Path | None = None, cwd: Path | None = None, ) -> int: if not command: raise SummaryError("a command is required after --") configured_log_dir, max_lines = output_settings(workflow) log_path = log or default_log_path(root, configured_log_dir, gate) result_path = json_result or log_path.with_suffix(log_path.suffix + ".json") if result_path == log_path: raise SummaryError("the JSON result path must differ from the full log path") command_cwd = cwd or Path.cwd() if not command_cwd.is_dir(): raise SummaryError(f"command working directory does not exist: {command_cwd}") log_path.parent.mkdir(parents=True, exist_ok=True) started_wall = datetime.now(timezone.utc) started = time.monotonic() returncode = 127 launch_error: str | None = None interrupted = False with log_path.open("wb") as log_handle: try: process = subprocess.Popen( list(command), cwd=command_cwd, stdout=log_handle, stderr=subprocess.STDOUT, shell=False, ) except OSError as error: launch_error = f"error: unable to execute {command[0]!r}: {error}" log_handle.write((launch_error + "\n").encode("utf-8", errors="replace")) else: try: returncode = normalized_exit_code(process.wait()) except KeyboardInterrupt: interrupted = True process.terminate() try: process.wait(timeout=5) except subprocess.TimeoutExpired: process.kill() process.wait() returncode = 130 log_handle.write(b"\nerror: command interrupted by user\n") duration = time.monotonic() - started status = "PASS" if returncode == 0 else "FAIL" summary = success_summary(log_path) if returncode == 0 else "command failed" excerpt: list[str] = [] actionable: str | None = launch_error if returncode != 0: excerpt, detected = failure_excerpt(log_path, max_lines) actionable = actionable or detected result = { "schema_version": 1, "gate": gate, "status": status, "exit_code": returncode, "duration_seconds": round(duration, 6), "log": relative_display(root, log_path), "result": relative_display(root, result_path), "command": list(command), "command_display": shlex.join(command), "cwd": str(command_cwd.resolve(strict=False)), "started_at": started_wall.isoformat(timespec="seconds"), "finished_at": datetime.now(timezone.utc).isoformat(timespec="seconds"), "summary": summary, "first_actionable_failure": actionable, "interrupted": interrupted, } write_result(result_path, result) if returncode == 0: print(f"PASS {gate} — {duration_text(duration)} — {summary}") else: print(f"FAIL {gate} — {duration_text(duration)}") for line in excerpt: print(line) print(f"Full log: {relative_display(root, log_path)}") return returncode def parse_arguments(argv: Sequence[str]) -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--gate", required=True, help="short verification gate name") parser.add_argument("--json-result", help="machine-readable result path") parser.add_argument("--log", help="complete combined-output log path") parser.add_argument("--cwd", help="command working directory; defaults to the invocation directory") parser.add_argument("command", nargs=argparse.REMAINDER) args = parser.parse_args(argv) if args.command and args.command[0] == "--": args.command = args.command[1:] if not args.command: parser.error("a command is required after --") return args def self_test() -> int: with tempfile.TemporaryDirectory(prefix="blacksite-summary-self-test-") as temporary: root = Path(temporary) (root / ".codex").mkdir() (root / ".codex/workflow.toml").write_text( """version = 1 [output] log_dir = ".codex/logs" max_failure_lines = 4 """, encoding="utf-8", ) nested = root / "nested" nested.mkdir() found = find_repository(nested) workflow = load_workflow(found) parsed = parse_arguments( [ "--gate", "parser-fixture", "--json-result", ".codex/logs/parser.json", "--", sys.executable, "--version", ] ) if parsed.command != [sys.executable, "--version"]: raise AssertionError("command arguments after -- were not preserved") success_log = root / ".codex/logs/success.log" success_json = root / ".codex/logs/success.json" captured = io.StringIO() with contextlib.redirect_stdout(captured): success = execute( root=root, workflow=workflow, gate="self-test-pass", command=[ sys.executable, "-c", "print('test result: ok. 3 passed; 0 failed; 1 ignored')", ], log=success_log, json_result=success_json, cwd=root, ) if success != 0: raise AssertionError("successful fixture returned a failure") if not captured.getvalue().startswith("PASS self-test-pass"): raise AssertionError("successful fixture did not emit a compact PASS summary") success_data = json.loads(success_json.read_text(encoding="utf-8")) for key in ("status", "exit_code", "duration_seconds", "log"): if key not in success_data: raise AssertionError(f"result JSON omitted required key {key}") if success_data["status"] != "PASS" or success_data["exit_code"] != 0: raise AssertionError("successful result JSON is incorrect") failure_log = root / ".codex/logs/failure.log" failure_json = root / ".codex/logs/failure.json" captured = io.StringIO() with contextlib.redirect_stdout(captured): failure = execute( root=root, workflow=workflow, gate="self-test-fail", command=[ sys.executable, "-c", "print('noise'); print('error[E0001]: actionable'); print('detail'); raise SystemExit(7)", ], log=failure_log, json_result=failure_json, cwd=root, ) if failure != 7: raise AssertionError("failing fixture did not preserve exit code 7") visible_lines = captured.getvalue().splitlines() if not visible_lines or not visible_lines[0].startswith("FAIL self-test-fail"): raise AssertionError("failing fixture did not emit a compact FAIL summary") if len(visible_lines[1:-1]) > 4: raise AssertionError("failure excerpt exceeded output.max_failure_lines") failure_data = json.loads(failure_json.read_text(encoding="utf-8")) if failure_data["status"] != "FAIL" or failure_data["exit_code"] != 7: raise AssertionError("failure result JSON is incorrect") if "error[E0001]" not in failure_log.read_text(encoding="utf-8"): raise AssertionError("full failure output was not captured") print("PASS summarize-command-self-test — logs, bounded failure, JSON, exit preservation") return 0 def main(argv: Sequence[str] | None = None) -> int: arguments = list(sys.argv[1:] if argv is None else argv) if arguments in (["--self-test"], ["self-test"]): try: return self_test() except (AssertionError, OSError, SummaryError, subprocess.SubprocessError, json.JSONDecodeError) as error: print(f"FAIL summarize-command-self-test — {error}", file=sys.stderr) return 1 try: args = parse_arguments(arguments) root = find_repository() workflow = load_workflow(root) log = resolve_path(root, args.log) if args.log else None result = resolve_path(root, args.json_result) if args.json_result else None cwd = resolve_path(root, args.cwd) if args.cwd else Path.cwd() return execute( root=root, workflow=workflow, gate=args.gate, command=args.command, log=log, json_result=result, cwd=cwd, ) except (OSError, SummaryError) as error: print(f"FAIL summarize-command — {error}", file=sys.stderr) return 2 if __name__ == "__main__": raise SystemExit(main())