diff --git a/agents.py b/agents.py index c1751eb..64808c8 100755 --- a/agents.py +++ b/agents.py @@ -977,6 +977,25 @@ def _pipeline_marker(cfg, stage): p = Path(stage["done"]) return p if p.is_absolute() else Path(cfg["project_dir"]) / stage["done"] +def _pipeline_push_on_retire(agent): + """Best-effort `git push` in a completing stage's dir before it's retired, so its final commits + aren't stranded locally if it wrote its completion marker before its last push landed (a race: + the pipeline retires a stage the instant its marker appears). Never raises; logs the outcome.""" + d = agent.get("dir") + if not d or not (Path(d) / ".git").exists(): + return + try: + r = subprocess.run(["git", "-C", d, "push", "origin", "HEAD:main"], + env={**os.environ, "GIT_SSH_COMMAND": "ssh -o BatchMode=yes"}, + capture_output=True, text=True, timeout=90) + if r.returncode == 0: + log(f"pipeline: pushed {agent['name']!r} before retiring (no stranded commits)") + elif "up-to-date" not in (r.stderr + r.stdout).lower(): + log(f"pipeline: push-on-retire for {agent['name']!r} nonzero (non-fatal): " + f"{(r.stderr or r.stdout).strip().splitlines()[-1:] }") + except Exception as e: + log(f"pipeline: push-on-retire for {agent['name']!r} failed (non-fatal): {e}") + def pipeline_check(cfg): """Reconcile the [pipeline]: run the first stage whose completion marker is absent, retire every other pipeline agent. Returns the ACTIVE stage's agent dict (so the watchdog stall/heal-watches it), @@ -1000,6 +1019,10 @@ def pipeline_check(cfg): log(f"pipeline: advancing → start stage {ag['name']!r}") start_agent(cfg, ag, force=True) elif alive: + # Retiring a non-active stage. If it's a COMPLETED stage (its marker exists), push its repo + # first so final commits written just before the marker aren't stranded locally. + if _pipeline_marker(cfg, st).exists(): + _pipeline_push_on_retire(ag) log(f"pipeline: retire stage {ag['name']!r} (complete or not yet active)") kill_session(ag["session"]) if active is None: