From 177877bbbe3959349da175118bed10206806ce4d Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Mon, 25 May 2026 19:42:37 +0200 Subject: [PATCH] Add Makefile and implementor logging/rate-limit wiring - Makefile default target runs build.sh - implementor mirrors stdout/stderr to implementor.log - implementor reads per-runner per-window caps from implementor-rate-limits.json each iteration; pauses when both runners are over cap - gitignore implementor.log and implementor-rate-limits.json Co-Authored-By: Claude Opus 4.7 --- .gitignore | 2 + Makefile | 6 ++ implementor | 170 ++++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 139 insertions(+), 39 deletions(-) create mode 100644 Makefile diff --git a/.gitignore b/.gitignore index e4b8960..33d8a88 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ /.env /crates/e2e/artifacts/ __pycache__/ +/implementor.log +/implementor-rate-limits.json diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5c7acbc --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +.PHONY: all build + +all: build + +build: + ./build.sh diff --git a/implementor b/implementor index 91dd72e..b7509ee 100755 --- a/implementor +++ b/implementor @@ -18,7 +18,35 @@ import time from datetime import datetime, timezone GIT_ROOT = "/Users/piefev/misc/we" -DEFAULT_CAP_PERCENT = 70.0 +LOG_PATH = "/Users/piefev/misc/we/implementor.log" +RATE_LIMITS_PATH = "/Users/piefev/misc/we/implementor-rate-limits.json" +PAUSE_SECONDS_WHEN_OVER_CAPS = 300 +DEFAULT_CAPS = { + "claude": {"five_hour": 100, "weekly": 100}, + "codex": {"five_hour": 100, "weekly": 100}, +} + +_log_file = None + + +class Tee: + """Write to multiple streams; used to mirror stdout/stderr into a log file.""" + + def __init__(self, *streams): + self.streams = streams + + def write(self, data): + for stream in self.streams: + stream.write(data) + stream.flush() + return len(data) + + def flush(self): + for stream in self.streams: + stream.flush() + + def isatty(self): + return False PROMPT = r""" You are an autonomous implementation agent for the `we` browser engine project. @@ -263,6 +291,17 @@ def run_iteration(iteration: int, runner: str) -> tuple[str, float]: elapsed = time.monotonic() - start output = result.stdout + result.stderr + if _log_file is not None: + _log_file.write( + f"\n----- Iteration {iteration} ({runner}) full output begin -----\n" + ) + _log_file.write(output) + if not output.endswith("\n"): + _log_file.write("\n") + _log_file.write( + f"----- Iteration {iteration} ({runner}) full output end -----\n" + ) + _log_file.flush() print(f"\n{'=' * 60}") print( f"Iteration {iteration} ({runner}) completed in {elapsed:.1f}s " @@ -355,69 +394,122 @@ def print_usage_report( print(usage_line("Codex", label, block.get("used_percent"), block.get("resets_at"))) -def claude_has_capacity( - cap_percent: float, - claude_data: dict | None, - claude_error: str | None, +def load_rate_limits() -> dict: + """Load per-runner per-window usage caps (percent) from RATE_LIMITS_PATH. + + Missing or malformed → warn and fall back to DEFAULT_CAPS (100% = no cap). + """ + try: + with open(RATE_LIMITS_PATH) as f: + data = json.load(f) + except FileNotFoundError: + print(f" rate-limits file {RATE_LIMITS_PATH} not found; using defaults") + return {k: dict(v) for k, v in DEFAULT_CAPS.items()} + except (OSError, json.JSONDecodeError) as exc: + print(f" rate-limits file unreadable ({exc}); using defaults") + return {k: dict(v) for k, v in DEFAULT_CAPS.items()} + + caps = {k: dict(v) for k, v in DEFAULT_CAPS.items()} + for runner in ("claude", "codex"): + block = data.get(runner) or {} + for window in ("five_hour", "weekly"): + value = block.get(window) + if isinstance(value, (int, float)) and 0 < value <= 100: + caps[runner][window] = float(value) + return caps + + +def runner_windows(runner: str, data: dict) -> list[tuple[str, str, object, object]]: + """Return (cap_key, label, utilization_percent, resets_at) for each window.""" + if runner == "claude": + five = data.get("five_hour") or {} + seven = data.get("seven_day") or {} + return [ + ("five_hour", "5h", five.get("utilization"), five.get("resets_at")), + ("weekly", "weekly", seven.get("utilization"), seven.get("resets_at")), + ] + rate_limits = data.get("rate_limits") or {} + primary = rate_limits.get("primary") or {} + secondary = rate_limits.get("secondary") or {} + return [ + ("five_hour", "5h", primary.get("used_percent"), primary.get("resets_at")), + ("weekly", "weekly", secondary.get("used_percent"), secondary.get("resets_at")), + ] + + +def runner_has_capacity( + runner: str, + caps: dict, + data: dict | None, + error: str | None, ) -> bool: - """Return whether both 5h and weekly Claude usage are under cap_percent.""" - if claude_data is None: - print(f" Claude usage unavailable ({claude_error}); using codex") + """Return whether all of `runner`'s usage windows are under their caps.""" + if data is None: + print(f" {runner} usage unavailable ({error})") return False + runner_caps = caps.get(runner, {}) over = [] - for key, label in (("five_hour", "5h session"), ("seven_day", "weekly")): - block = claude_data.get(key) or {} - util = block.get("utilization") - if util is not None and util >= cap_percent: - over.append((label, util, block.get("resets_at"))) + for cap_key, label, util, reset_at in runner_windows(runner, data): + cap = runner_caps.get(cap_key) + if cap is None or util is None: + continue + try: + util_f = float(util) + except (TypeError, ValueError): + continue + if util_f >= float(cap): + over.append((label, util_f, float(cap), reset_at)) if not over: return True - for label, util, reset_at in over: + for label, util_f, cap_f, reset_at in over: print( - f" Claude {label}: {util:.0f}% >= cap {cap_percent:.0f}% " + f" {runner} {label}: {util_f:.0f}% >= cap {cap_f:.0f}% " f"(resets {format_reset(reset_at)})" ) - - print(" Claude usage over cap; using codex") return False -def parse_cap(argv: list[str]) -> float: - if len(argv) <= 1: - return DEFAULT_CAP_PERCENT - try: - cap = float(argv[1]) - except ValueError: - print(f"error: expected a number for max usage %, got {argv[1]!r}", file=sys.stderr) - sys.exit(2) - if not (0 < cap <= 100): - print(f"error: max usage % must be in (0, 100], got {cap}", file=sys.stderr) - sys.exit(2) - return cap - - def main() -> None: - cap_percent = parse_cap(sys.argv) + global _log_file + _log_file = open(LOG_PATH, "a", buffering=1) + sys.stdout = Tee(sys.__stdout__, _log_file) + sys.stderr = Tee(sys.__stderr__, _log_file) + print( + f"\n========== implementor started {datetime.now(timezone.utc).isoformat()} ==========" + ) print("we - autonomous implementation loop") - print(f"Usage cap: {cap_percent:.0f}% for both 5h and weekly Claude usage") + print(f"Rate limits: {RATE_LIMITS_PATH}") + print(f"Logging to {LOG_PATH}") print("Press Ctrl+C to stop\n") iteration = 0 try: while True: - print(f"\n--- Checking usage (Claude cap {cap_percent:.0f}%) ---") + caps = load_rate_limits() + print( + f"\n--- Checking usage (caps " + f"claude 5h={caps['claude']['five_hour']:.0f}% / weekly={caps['claude']['weekly']:.0f}%, " + f"codex 5h={caps['codex']['five_hour']:.0f}% / weekly={caps['codex']['weekly']:.0f}%) ---" + ) claude_data, claude_error = load_usage_json("claude-usage") codex_data, codex_error = load_usage_json("codex-usage") print_usage_report(claude_data, claude_error, codex_data, codex_error) - runner = ( - "claude" - if claude_has_capacity(cap_percent, claude_data, claude_error) - else "codex" - ) + + if runner_has_capacity("claude", caps, claude_data, claude_error): + runner = "claude" + elif runner_has_capacity("codex", caps, codex_data, codex_error): + runner = "codex" + else: + print( + f"\nBoth claude and codex are over their caps; " + f"sleeping {PAUSE_SECONDS_WHEN_OVER_CAPS}s before re-checking." + ) + time.sleep(PAUSE_SECONDS_WHEN_OVER_CAPS) + continue iteration += 1 print( -- 2.51.2