#!/usr/bin/env python3 """Run the repository's Blender/WBV2 unattended scheduler. It is the project-local wrapper for copied launcher scripts. It does not start an interactive agent, use a second ledger, or emulate Blender behavior. The authoritative Node scheduler owns queue validation, guardian/supervisor execution, evidence handoff, commit and push rules. """ from __future__ import annotations import argparse import json import os import signal import subprocess from datetime import datetime, timezone from wbv2_runtime import ROOT, scheduler_args STATUS_PATH = ROOT / "tests/golden/WBV2/my-tools-scheduler.json" PID_PATH = ROOT / "tests/golden/WBV2/my-tools-scheduler.pid" def write_status(**values: object) -> None: STATUS_PATH.parent.mkdir(parents=True, exist_ok=True) payload = {"schemaVersion": 1, "updatedAt": datetime.now(timezone.utc).isoformat(), **values} temporary = STATUS_PATH.with_suffix(f".tmp-{os.getpid()}") temporary.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") temporary.replace(STATUS_PATH) def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--max-iterations", "--max-cycles", type=int, default=0) parser.add_argument("--poll-ms", type=int, default=1000) parser.add_argument("--push-retries", type=int, default=6) parser.add_argument( "--commit", action="store_true", default=os.getenv("WBV2_AUTO_COMMIT", "1") != "0", help="commit the exact handoff files after a successful task (default: on)", ) parser.add_argument( "--push", action="store_true", default=os.getenv("WBV2_AUTO_PUSH", "1") != "0", help="push and verify origin/main after committing (default: on)", ) parser.add_argument("--preflight-only", action="store_true") args = parser.parse_args() if args.max_iterations < 0 or args.poll_ms < 0 or args.push_retries < 0: parser.error("iteration, poll and retry values must be non-negative") if args.push and not args.commit: parser.error("--push requires --commit") command = scheduler_args( commit=args.commit, push=args.push, max_iterations=args.max_iterations, preflight_only=args.preflight_only, ) command.extend(["--poll-ms", str(args.poll_ms), "--push-retries", str(args.push_retries)]) print("WBV2 scheduler:", " ".join(command), flush=True) PID_PATH.parent.mkdir(parents=True, exist_ok=True) if PID_PATH.exists(): try: existing = int(PID_PATH.read_text(encoding="ascii").strip()) os.kill(existing, 0) except (OSError, ValueError): PID_PATH.unlink(missing_ok=True) else: parser.error(f"scheduler wrapper already running: pid={existing}") PID_PATH.write_text(f"{os.getpid()}\n", encoding="ascii") write_status(status="starting", pid=os.getpid(), command=command) process = subprocess.Popen(command, cwd=ROOT, start_new_session=True) def forward(signum: int, _frame: object) -> None: try: os.killpg(process.pid, signum) except ProcessLookupError: pass signal.signal(signal.SIGINT, forward) signal.signal(signal.SIGTERM, forward) try: code = process.wait() write_status(status="complete" if code == 0 else "blocked", pid=os.getpid(), childPid=process.pid, exitCode=code) return code finally: PID_PATH.unlink(missing_ok=True) if __name__ == "__main__": raise SystemExit(main())