From 6aff0affd95447311e258aeece2631c77b577baf Mon Sep 17 00:00:00 2001 From: Wendy Date: Mon, 20 Jul 2026 00:31:02 +0800 Subject: [PATCH] Pull operating system before start-day --- README.md | 2 +- loop-engine/bin/loopctl.py | 26 +++++++++++++++++++++++++ loop-engine/tests/test_daily_routine.py | 21 ++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bc6a088..bb30ede 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ loop approve --medium-envelope primary-surface \ --verification-command 'git diff --check' # 7) Start the approved day loop -loop start-day # first cycle now, then hourly until stop/pause/budget +loop start-day # pull Park Operating System, then first cycle now and hourly # 8) Evening recap loop evening # pauses all active registered loops, writes scorecards + daily report diff --git a/loop-engine/bin/loopctl.py b/loop-engine/bin/loopctl.py index b3a03b4..439e6f0 100755 --- a/loop-engine/bin/loopctl.py +++ b/loop-engine/bin/loopctl.py @@ -88,6 +88,7 @@ CONFIG_DIR = Path(os.environ.get("LOOP_CONFIG_DIR", str(Path.home() / ".config" / "loop"))).expanduser() CONFIG_PATH = CONFIG_DIR / "config.json" PORTFOLIO_MODES = {"loop", "plan-only", "read-only", "hold"} +PARK_OPERATING_SYSTEM_DIR = Path.home() / "work" / "park-operating-system" class LoopBlocked(RuntimeError): @@ -5591,12 +5592,34 @@ def activate_without_immediate_tick(project: str) -> None: schedule_next_cycle(project) +def sync_park_operating_system() -> None: + if not (PARK_OPERATING_SYSTEM_DIR / ".git").is_dir(): + raise LoopBlocked( + "park_operating_system_missing", + f"Park Operating System checkout is missing: {PARK_OPERATING_SYSTEM_DIR}", + {"next_action": f"clone zinan92/park-operating-system to {PARK_OPERATING_SYSTEM_DIR}"}, + ) + result = run( + ["git", "-C", str(PARK_OPERATING_SYSTEM_DIR), "pull"], + cwd=ENGINE_ROOT, + check=False, + ) + if result.returncode != 0: + raise LoopBlocked( + "park_operating_system_sync_failed", + "Could not refresh Park Operating System before start-day.", + {"stderr": result.stderr.strip()[-1000:]}, + ) + print(f"PARK_OPERATING_SYSTEM_SYNCED path={PARK_OPERATING_SYSTEM_DIR}") + + def start_day_command(projects: list[str] | None) -> None: approvals = load_latest_approvals() approved = approvals.get("approved") or {} selected = projects or sorted(approved.keys()) if not selected: raise LoopBlocked("no_daily_approvals", "No approved projects found for today. Run loop morning, then loop approve .") + operating_system_synced = False for project in selected: if project not in approved: raise LoopBlocked("not_approved_today", f"Project {project!r} is not approved for today's loop start.") @@ -5606,6 +5629,9 @@ def start_day_command(projects: list[str] | None) -> None: f"Project {project!r} was approved only for loop initialization; run loop morning and approve execution before start-day.", {"project": project, "next_actions": ["loop morning", f"loop approve {project}"]}, ) + if not operating_system_synced: + sync_park_operating_system() + operating_system_synced = True registry_project(project) medium = approved[project].get("medium_envelope") if medium and not approved[project].get("medium_first_supervised_at"): diff --git a/loop-engine/tests/test_daily_routine.py b/loop-engine/tests/test_daily_routine.py index 4d6fee2..00a9f30 100644 --- a/loop-engine/tests/test_daily_routine.py +++ b/loop-engine/tests/test_daily_routine.py @@ -40,6 +40,7 @@ def patch_engine(monkeypatch, tmp_path, repo): monkeypatch.setattr(loopctl, "LOCK_DIR", engine / "locks") monkeypatch.setattr(loopctl, "CONFIG_DIR", tmp_path / "config") monkeypatch.setattr(loopctl, "CONFIG_PATH", tmp_path / "config" / "config.json") + monkeypatch.setattr(loopctl, "sync_park_operating_system", lambda: None) monkeypatch.setattr(loopctl, "scheduler_status_payload", lambda project: { "label": f"com.agent-loop.{project}", "plist": str(tmp_path / "noop.plist"), @@ -885,15 +886,35 @@ def test_start_day_runs_medium_first_cycle_supervised(monkeypatch, tmp_path): }) monkeypatch.setattr(loopctl, "load_scheduler", lambda project: calls.append(("load", project))) monkeypatch.setattr(loopctl, "send_notification", lambda *args, **kwargs: False) + monkeypatch.setattr(loopctl, "sync_park_operating_system", lambda: calls.append(("sync", "park-operating-system"))) loopctl.start_day_command(["demo"]) + assert calls[0] == ("sync", "park-operating-system") assert ("demo", True) in calls assert ("load", "demo") in calls state = loopctl.load_state()["projects"]["demo"] assert state["loop_job"]["state"] == "active" +def test_sync_park_operating_system_pulls_expected_checkout(monkeypatch, tmp_path, capsys): + checkout = tmp_path / "park-operating-system" + (checkout / ".git").mkdir(parents=True) + calls = [] + + def fake_run(cmd, cwd, check=True, log_path=None): + calls.append((cmd, cwd, check)) + return subprocess.CompletedProcess(cmd, 0, stdout="Already up to date.\n", stderr="") + + monkeypatch.setattr(loopctl, "PARK_OPERATING_SYSTEM_DIR", checkout) + monkeypatch.setattr(loopctl, "run", fake_run) + + loopctl.sync_park_operating_system() + + assert calls == [(["git", "-C", str(checkout), "pull"], loopctl.ENGINE_ROOT, False)] + assert f"PARK_OPERATING_SYSTEM_SYNCED path={checkout}" in capsys.readouterr().out + + def test_evening_writes_scorecard_and_daily_report(monkeypatch, tmp_path): repo = make_repo(tmp_path) engine = patch_engine(monkeypatch, tmp_path, repo)