aboutsummaryrefslogtreecommitdiffziptar.gz
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-07 17:15:33 +0200
committergodosa <godosa@godosa.eu>2026-10-07 17:15:33 +0200
commitc537f4a809a564ecdab0380bcb356018f79a6900 (patch)
tree6d4ff86965191fb3e586d37d17874c27dcf63a77
parentd678967fcf9489fd943bb17c7baf3c59f930d821 (diff)
downloadworkflow-c537f4a809a564ecdab0380bcb356018f79a6900.tar.gz
workflow-c537f4a809a564ecdab0380bcb356018f79a6900.zip
orch: lane cloud parks a stuck task (Cloud: no), keeps picking
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEAjUkQRrCYX5MZhWxdtj2
-rw-r--r--CHANGES.md1
-rw-r--r--docs/orchestrator.md6
-rw-r--r--tests/test_cloud.py35
-rwxr-xr-xwf.py30
4 files changed, 65 insertions, 7 deletions
diff --git a/CHANGES.md b/CHANGES.md
index 8392cf4..f6d9cc0 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,4 +1,5 @@
# Changes (newest first)
+- 2026-10-07 Lane cloud: one stuck task no longer stops it: `wf orch post <id> cloud` with awaiting / handback / lost parks only that task (awaiting: park as local lanes; handback / lost: `Cloud: no` + status cleared + note, local lanes may retry with pull's Recovery note), prints `alert: …; lane cloud keeps picking`, then the next cloud pick (never the same id). Projects: nothing.
- 2026-10-07 Owner-absent default: workers never wait; owner-only step → `wf add -s human` + `wf set <id> --after <h-id>` + status clear, result `needs-owner <h-id>`. `wf orch post` parks it (After: the h-task, claim cleared, alert), lane keeps picking; owner's `wf done <h-id>` makes it pickable again. Worker def, shared CLAUDE.md, wf-orchestrate, wf-pilot, batch prompt, docs. Projects: nothing.
- 2026-10-07 One stuck task no longer freezes its lane: `wf orch post` with awaiting / handback / post-check-red (local lanes; lane cloud unchanged: pull re-queues) parks only that task (blocked on the a-id named in the result, else `Sessions: owner` + an `orch …` note; handback also clears its claim), commits, prints `alert: <id> <outcome> → tell the owner (…); lane <lane> keeps picking`, then the lane's next pick. `stop lane` only for none / stop file / push-failed / wip / no report. Batch prompt, wf-orchestrate, wf-pilot, docs: keep picking, never wait on the owner while tasks are pickable. Projects: nothing.
- 2026-10-07 `wf orch post` done: leftover TASKS/archive changes in the main tree are committed (`<id> done (code <sha>)` in a split project, else `<id> done (orchestrator)`); split: a `--commit` sha not in the code repo (stale private HEAD) is replaced by the code repo's master sha (noted). wf-worker: the report's commit = first sha of `report: commit …`, never a looked-up HEAD. Projects: nothing.
diff --git a/docs/orchestrator.md b/docs/orchestrator.md
index 6887f15..1e216af 100644
--- a/docs/orchestrator.md
+++ b/docs/orchestrator.md
@@ -88,8 +88,10 @@ Virtual lane `cloud` (projects with `cloud = true`; design: cloud-lane spec §4.
`<id>: <state> (<sid>, $… )` (state done | awaiting | handback | lost; done also `report: commit <sha>`) →
`wf orch post <id> cloud --result <state> [--commit <sha>]` = the worker-report path (done: archive line, branch
`<task lane>/<id>` gone, `wf check`; else leftover commit of the note/claim pull left), log line, next cloud
- pick. `running` (exit 4) → nothing; pull again next wake. awaiting / handback / lost → `stop lane cloud`, tell the
- owner; the task is back in its local lane (handback note `Recovery: cloud attempt …`).
+ pick. `running` (exit 4) → nothing; pull again next wake. awaiting / handback / lost → post parks only that
+ task (awaiting: blocked on its a-id; handback / lost: `Cloud: no`, so the cloud lane never re-sends it; it is back
+ in its local lane with the note `Recovery: cloud attempt …`), prints `alert: …; lane cloud keeps picking`, tell the
+ owner, next cloud pick.
- Fill: one `wf orch pick cloud` per free slot (up to `max_parallel`), each in its own call.
- Batch (`wf batch` on a `cloud = true` project) pulls itself: the job runs `wf_res.py batch-sidecar` around
`claude -p`; every 10 min while it lives (until `out/wf-batch.stop`) it runs `wf cloud pull --all` and per ended
diff --git a/tests/test_cloud.py b/tests/test_cloud.py
index e922f9c..3ac184a 100644
--- a/tests/test_cloud.py
+++ b/tests/test_cloud.py
@@ -1,6 +1,7 @@
import datetime as dt
import io
import json
+import re
import os
import shutil
import sys
@@ -1163,12 +1164,42 @@ class OrchCloud(CloudProject):
out = self.orch("pick", "cloud")
self.assertIn("stop lane cloud: none fit (project not opted in", out)
- def test_post_handback_commits_and_stops(self):
+ def test_post_handback_parks_and_picks_next(self):
self.orch("pick", "cloud", "--id", "t-slow")
self.ok("note", "t-slow", "Recovery: cloud attempt x — red") # what wf cloud pull leaves behind
self.ok("status", "t-slow", "clear")
out = self.orch("post", "t-slow", "cloud", "--result", "handback")
self.assertIn("post: t-slow handback", out)
self.assertIn("committed leftover", out)
- self.assertIn("stop lane cloud: handback", out)
+ self.assertIn("alert: t-slow handback → tell the owner (Cloud: no", out)
+ self.assertIn("lane cloud keeps picking", out)
+ self.assertNotIn("stop lane cloud: handback", out)
+ self.assertIn("pick: t-yes (lane cloud", out) # next pick, not a re-send
+ self.assertNotIn("pick: t-slow", out)
self.assertIn(" cloud opus t-slow handback ", (self.root / "out" / "wf-orch.log").read_text())
+ body = (self.root / "TASKS.md").read_text().split("**t-slow**")[1].split("\n- **")[0]
+ self.assertIn("Cloud: no", body)
+ self.assertNotIn("Sessions: owner", body) # local lanes may still retry it
+ out = self.orch("pick", "cloud")
+ self.assertIn("stop lane cloud: none fit", out) # t-slow never re-sent
+ led = json.loads((self.d / "xdg" / "wf" / "cloud.json").read_text())
+ self.assertEqual([e["id"] for e in led["entries"]], ["t-slow", "t-yes"])
+ self.assertIn("pick: t-slow", self.orch("pick", "slow", "--id", "t-slow"))
+
+ def test_post_awaiting_parks_and_picks_next(self):
+ self.orch("pick", "cloud", "--id", "t-slow")
+ hdr = self.ok("add", "-s", "awaiting", "which way? [[t-slow]]") # what wf cloud pull does
+ a = re.search(r"\*\*(a-[^*]+)\*\*", hdr)[1]
+ self.ok("status", "t-slow", "blocked", a)
+ out = self.orch("post", "t-slow", "cloud", "--result", f"awaiting {a}")
+ self.assertIn(f"alert: t-slow awaiting → tell the owner (already blocked: [[{a}]])", out)
+ self.assertIn("pick: t-yes (lane cloud", out)
+ self.assertIn(f"blocked: [[{a}]]", self.status("t-slow"))
+ self.assertIn("stop lane cloud: none fit", self.orch("pick", "cloud"))
+
+ def test_post_lost_parks_too(self):
+ self.orch("pick", "cloud", "--id", "t-yes")
+ self.ok("status", "t-yes", "clear")
+ out = self.orch("post", "t-yes", "cloud", "--result", "lost")
+ self.assertIn("alert: t-yes lost → tell the owner (Cloud: no", out)
+ self.assertIn("pick: t-slow (lane cloud", out)
diff --git a/wf.py b/wf.py
index ac4aadc..ac279cd 100755
--- a/wf.py
+++ b/wf.py
@@ -1821,6 +1821,28 @@ def park_task(root: Path, id: str, final: str, words: list[str]) -> str:
return done
+def cloud_park(root: Path, id: str, final: str, words: list[str]) -> str:
+ """Lane cloud after a handback/lost/awaiting/needs-owner (wf cloud pull already noted/blocked it):
+ awaiting/needs-owner → park_task; else Cloud: no (the cloud lane never re-sends it; local lanes may retry
+ with pull's Recovery note) + status cleared + note. Returns what it did (alert text)."""
+ if final in ("awaiting", "needs-owner"):
+ return park_task(root, id, final, words)
+ p = load_project(argparse.Namespace(project=str(root)), write=True)
+ if id not in p.doc.ids():
+ return ARCHIVED
+ item, done = p.doc.item(id), "Cloud: no"
+ if item.cloud != "no":
+ tasks.set_fields(p.doc, id, cloud="no")
+ if (item.status or "").startswith("in progress"):
+ tasks.set_status(p.doc, id, None)
+ done += ", status cleared"
+ tasks.add_note(p.doc, id, f"orch {datetime.date.today().isoformat()}: cloud {final} "
+ f"({' '.join(words[1:]) or '-'}) → {done}; lane cloud kept picking, local lanes may retry")
+ p.save(False)
+ unclaim(p.cfg, [id])
+ return done + " (local lanes may retry)"
+
+
def orch_post(main: Path, p: Project, args) -> int:
cfg, id, lane = p.cfg, args.id, args.lane
rec = orch_record(cfg, id)
@@ -1878,8 +1900,10 @@ def orch_post(main: Path, p: Project, args) -> int:
out.append("committed leftover " + " ".join(files) if not r.returncode
else f"leftover commit failed: {(r.stderr.strip() or 'git error').splitlines()[-1]}")
if problems or not outcome.startswith("done"):
- if lane != CLOUD and (problems or outcome in PARK and not raised): # cloud: pull already re-queued
+ if lane != CLOUD and (problems or outcome in PARK and not raised):
parked = park_task(cfg.root, id, "post-check-red" if problems else outcome, words)
+ elif lane == CLOUD and not problems and outcome in PARK + ("lost",) and not raised: # no re-send loop
+ parked = cloud_park(cfg.root, id, 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}"
@@ -1915,7 +1939,7 @@ 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 in PARK and lane != CLOUD:
+ if parked:
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"
@@ -2356,7 +2380,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); awaiting/needs-owner/handback/post-check-red park only the task (blocked / After: h-task / Sessions: owner + note, 'alert:' line); then the next pick or 'stop lane'")
+ "out/wf-orch.log line, out/wf-cost.log line (--agent); awaiting/needs-owner/handback/post-check-red park only the task (blocked / After: h-task / Sessions: owner (lane cloud: Cloud: no) + 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)")