aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/wf.py
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-07 16:57:32 +0200
committergodosa <godosa@godosa.eu>2026-10-07 16:57:32 +0200
commit61a98e767d7c45fb00b17f969c6fb42d2cbaca48 (patch)
tree284e1c453f28a35ce12ce9f84f2053031b7c1020 /wf.py
parentf23d7d865eb042512ebabad87e89b35b11cc1c64 (diff)
downloadworkflow-61a98e767d7c45fb00b17f969c6fb42d2cbaca48.tar.gz
workflow-61a98e767d7c45fb00b17f969c6fb42d2cbaca48.zip
orch: one stuck task parks itself, lane keeps picking
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEAjUkQRrCYX5MZhWxdtj2
Diffstat (limited to 'wf.py')
-rwxr-xr-xwf.py58
1 files changed, 49 insertions, 9 deletions
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 \"<why>\", 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)")