From 61a98e767d7c45fb00b17f969c6fb42d2cbaca48 Mon Sep 17 00:00:00 2001 From: godosa Date: Wed, 7 Oct 2026 16:57:32 +0200 Subject: orch: one stuck task parks itself, lane keeps picking Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JEAjUkQRrCYX5MZhWxdtj2 --- wf.py | 58 +++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 9 deletions(-) (limited to 'wf.py') diff --git a/wf.py b/wf.py index 108f3da..122b7db 100755 --- a/wf.py +++ b/wf.py @@ -1597,6 +1597,7 @@ def cmd_gate(args) -> int: # ------------------------------------------------------------------ orchestrator GO_ON = ("done", "done+gate-red", "sliced") # outcomes after which the lane picks its next task +PARK = ("handback", "awaiting", "post-check-red") # block only that task (park_task), the lane keeps picking def orch_main(args) -> tuple[Path, Project]: @@ -1778,6 +1779,38 @@ def archived_meta(cfg: config.Config, id: str) -> tuple[str | None, str | None]: return (it.model, it.effort) if it else (None, None) +ARCHIVED = "archived, nothing to mark" + + +def park_task(root: Path, id: str, final: str, words: list[str]) -> str: + """Block only this task after a handback/awaiting/post-check-red: blocked on its open a-id, else + Sessions: owner (+ status cleared on a handback) and a note. Returns what it did (alert text).""" + p = load_project(argparse.Namespace(project=str(root)), write=True) + if id not in p.doc.ids(): + return ARCHIVED + item = p.doc.item(id) + why = " ".join(words[1:]) or "-" + open_a = {i.id for s in p.doc.sections if s.key == "awaiting" for i in s.items} + a = next((w for w in words[1:] if w.strip("[]") in open_a), None) if final == "awaiting" else None + if (item.status or "").startswith("blocked:"): + done = f"already {item.status}" + elif a: + tasks.set_status(p.doc, id, f"blocked: [[{a.strip('[]')}]]") + done = f"blocked on {a.strip('[]')}" + else: + tasks.set_fields(p.doc, id, sessions="owner") + done = "Sessions: owner" + if final == "handback" and item.status: + tasks.set_status(p.doc, id, None) + done += ", status cleared" + tasks.add_note(p.doc, id, f"orch {datetime.date.today().isoformat()}: {final} ({why}) → {done}; " + "lane kept picking, owner decides") + p.save(False) + if done.endswith("status cleared"): + unclaim(p.cfg, [id]) + return done + + def orch_post(main: Path, p: Project, args) -> int: cfg, id, lane = p.cfg, args.id, args.lane rec = orch_record(cfg, id) @@ -1803,7 +1836,9 @@ def orch_post(main: Path, p: Project, args) -> int: out.append(f"commit {c} not in code repo {repo} (a private sha?): using its {master} {fixed}") c = fixed return c - commit = None + commit, parked = None, None + raised = bool(item and rec.get("model") and not outcome.startswith("done") + and tasks.MODELS.index(item.model) > tasks.MODELS.index(rec["model"])) with project_lock(cfg.root): if outcome.startswith("done"): if id not in p.archived: @@ -1832,12 +1867,15 @@ def orch_post(main: Path, p: Project, args) -> int: r = git_run(main, "commit", "-q", "-m", msg, "--", *files) out.append("committed leftover " + " ".join(files) if not r.returncode else f"leftover commit failed: {(r.stderr.strip() or 'git error').splitlines()[-1]}") - elif git_run(main, "status", "--porcelain", "--", *files).stdout.strip(): - r = git_run(main, "commit", "-q", "-m", f"{id} {outcome} (orchestrator)", "--", *files) - out.append("committed leftover " + " ".join(files) if not r.returncode - else f"leftover commit failed: {(r.stderr.strip() or 'git error').splitlines()[-1]}") - raised = bool(item and rec.get("model") and not outcome.startswith("done") - and tasks.MODELS.index(item.model) > tasks.MODELS.index(rec["model"])) + if problems or not outcome.startswith("done"): + if lane != CLOUD and (problems or outcome in PARK and not raised): # cloud: pull already re-queued + parked = park_task(cfg.root, id, "post-check-red" if problems else outcome, words) + if (not problems or parked and parked != ARCHIVED) \ + and git_run(main, "status", "--porcelain", "--", *files).stdout.strip(): + r = git_run(main, "commit", "-q", "-m", f"{id} {'post-check-red' if problems else outcome}" + " (orchestrator)", "--", *files) + out.append("committed leftover " + " ".join(files) if not r.returncode + else f"leftover commit failed: {(r.stderr.strip() or 'git error').splitlines()[-1]}") final = "post-check-red" if problems else ("model-raised" if raised else outcome) if outcome.startswith("done") and not problems and (cfg.root / ".wf" / "push-failed").is_file(): final = "push-failed" @@ -1867,7 +1905,9 @@ def orch_post(main: Path, p: Project, args) -> int: print(f"fix {fix.id} not runner-ready: add its Done/Model (wf set {fix.id} --done … --model …), " f"then wf orch pick {lane} --id {fix.id}") return 0 - if final not in GO_ON and final != "model-raised": + if final in PARK and lane != CLOUD: + print(f"alert: {id} {final} → tell the owner ({parked}); lane {lane} keeps picking") + elif final not in GO_ON and final != "model-raised": print(f"stop lane {lane}: {final} → tell the owner" + (f" (wf push in {cfg.root})" if final == "push-failed" else "") + (f" (crash/no report: one fresh worker: wf orch pick {lane} --id {id} --recovery \"\", then stop)" @@ -2306,7 +2346,7 @@ def parser() -> argparse.ArgumentParser: op.add_argument("--recovery", metavar="WHY", help="prompt gets 'Recovery: WHY' (crashed worker, one retry)") op = osub.add_parser("post", help="after the worker's report: post-check (done: archive line, branch gone, " "worktree clean and in master, else wf merge; wf check), leftover commit otherwise, " - "out/wf-orch.log line, out/wf-cost.log line (--agent), then the next pick or 'stop lane'") + "out/wf-orch.log line, out/wf-cost.log line (--agent); awaiting/handback/post-check-red park only the task (blocked / Sessions: owner + note, 'alert:' line); then the next pick or 'stop lane'") op.add_argument("id") op.add_argument("lane") op.add_argument("--result", metavar="LINE", help="the report's result line (default: done if archived, else no-report)") -- cgit