diff options
| author | godosa <godosa@godosa.eu> | 2026-10-07 17:01:53 +0200 |
|---|---|---|
| committer | godosa <godosa@godosa.eu> | 2026-10-07 17:01:53 +0200 |
| commit | d678967fcf9489fd943bb17c7baf3c59f930d821 (patch) | |
| tree | 990e5ea0445f51c3f14c5347f6b7cf38cc3a6b1b | |
| parent | 61a98e767d7c45fb00b17f969c6fb42d2cbaca48 (diff) | |
| download | workflow-d678967fcf9489fd943bb17c7baf3c59f930d821.tar.gz workflow-d678967fcf9489fd943bb17c7baf3c59f930d821.zip | |
orch: needs-owner parks task After: its human task, lane keeps picking
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JEAjUkQRrCYX5MZhWxdtj2
| -rw-r--r-- | CHANGES.md | 1 | ||||
| -rw-r--r-- | docs/manual.md | 2 | ||||
| -rw-r--r-- | docs/orchestrator.md | 2 | ||||
| -rw-r--r-- | shared/CLAUDE.md | 3 | ||||
| -rw-r--r-- | shared/agents/wf-worker.md | 7 | ||||
| -rw-r--r-- | shared/skills/wf-orchestrate/SKILL.md | 2 | ||||
| -rw-r--r-- | shared/skills/wf-pilot/SKILL.md | 2 | ||||
| -rw-r--r-- | templates/batch-prompt.md | 2 | ||||
| -rw-r--r-- | tests/test_orch.py | 14 | ||||
| -rwxr-xr-x | wf.py | 20 |
10 files changed, 42 insertions, 13 deletions
@@ -1,4 +1,5 @@ # Changes (newest first) +- 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. - 2026-10-07 wf start refuses an existing worktree of another repo; .wf-home ignored by finish/merge stray checks. Projects: nothing. diff --git a/docs/manual.md b/docs/manual.md index 3d50ade..101f731 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -122,7 +122,7 @@ switches them to worktree mode (one branch per task, `wf finish` merges it). One After `/clear`, type `/wf-orchestrate` (`go` = start the proposed run without asking, `batch N` = prepare an unattended batch). The orchestrator holds no lane and never reads code. It spawns one fresh `wf-worker` per task (`wf orch pick` / `wf orch post`), one per lane in -parallel, on the task's model, and parks a task that ends awaiting, handback or red post-check +parallel, on the task's model, and parks a task that ends awaiting, needs-owner (waits `After:` a Needs-human task), handback or red post-check (blocked / `Sessions: owner`, alert) while its lane keeps going. Use it when the backlog is runner-ready (section 3) and you want to steer. ### Overnight: `/wf-pilot` diff --git a/docs/orchestrator.md b/docs/orchestrator.md index f3e1432..6887f15 100644 --- a/docs/orchestrator.md +++ b/docs/orchestrator.md @@ -63,7 +63,7 @@ The steps they automate: | model raised by the worker | continue; next pick spawns it on the new model | | sliced | next task in the lane | | done+gate-red <culprit> <fix-id> (async gate red, earlier task's break) | post-check as done; next pick in the lane = the P0 fix (no Done/Model → add them first); never stop the lane | - | awaiting / handback / post-check red | `wf orch post` parks only that task (blocked on its a-id, else `Sessions: owner` + note; handback clears the claim), prints `alert: …`; tell the owner, the lane keeps picking (never wait on the owner while tasks are pickable) | + | awaiting / needs-owner / handback / post-check red | `wf orch post` parks only that task (needs-owner: `After:` its Needs-human h-task, pickable again after the owner's `wf done`; awaiting: blocked on its a-id, else `Sessions: owner` + note; handback clears the claim), prints `alert: …`; tell the owner, the lane keeps picking (never wait on the owner while tasks are pickable) | | wip (after a wrap-up) | stop the lane; reslice or rerun later | | crashed / killed (no report) | one fresh worker, prompt plus `Recovery: <why>` (it inspects git status, git log master..HEAD, `git diff -- TASKS.md` — often just the claim — and `wf show <id>`), then stop | 7. Log one line per task to `out/wf-orch.log` (git-ignored): time, lane, model, id, outcome, commit, duration. diff --git a/shared/CLAUDE.md b/shared/CLAUDE.md index 66b12f4..6a5b18f 100644 --- a/shared/CLAUDE.md +++ b/shared/CLAUDE.md @@ -30,7 +30,8 @@ Orchestrator: `/wf-orchestrate` (never `wf next --as`, never implement). `wf-wor → branch → wait for the merge call. Test runs: one call per red/green; on red print all failures in that call (`… 2>&1 | grep -A15 '^FAIL\|^ERROR' | head -80`), never run then grep. 2. Start: `wf status <id> progress "<branch or note>"`. Decide, record in spec/rulings, report; ask only on real forks. -3. Blocked → `wf add -s awaiting "<q>"` → `wf status <id> blocked <a-id>` → next task. +3. Owner assumed absent: never wait. Blocked → `wf add -s awaiting "<q>"` → `wf status <id> blocked <a-id>` → next task. + Owner-only step (GUI/live/with owner) → `wf add -s human …` → `wf set <id> --after <h-id>` → status clear → next task. 4. Tests must fail when broken; oracle = hand-derived or real data, never the code under test. ## Done → one commit, don't ask diff --git a/shared/agents/wf-worker.md b/shared/agents/wf-worker.md index 82c8f14..898e10b 100644 --- a/shared/agents/wf-worker.md +++ b/shared/agents/wf-worker.md @@ -24,7 +24,10 @@ it prints the task, its refs and relations. `wf` = `python3 /projects/public/wor (`unittest … 2>&1 | grep -A15 '^FAIL\|^ERROR' | head -80`), never run then grep in two calls. - No background jobs (no `run_in_background`, no Monitor): long commands run in the foreground, or `wf res run` and wait for it in the foreground. The end of your turn is the end of your work. -- Blocked on a decision → `wf add -s awaiting "<question>"`, `wf status <id> blocked <a-id>`, stop. +- Owner absent, never wait or ask. Blocked on a decision → `wf add -s awaiting "<question>"`, `wf status <id> blocked <a-id>`, + stop, result `awaiting <a-id>`. Owner-only step (GUI, live game/device, decision made with the owner) → do the rest, + `wf add -s human -p <prio> -e <effort> "<step>. <why>"`, `wf set <id> --after <h-id>`, `wf status <id> clear`, `wf wip` + if WIP, stop, result `needs-owner <h-id>` (owner's `wf done <h-id>` makes it pickable again). - Slice job (`wf next`/`wf show` says effort > slice_above): no code; `wf add --parent <id>` slices ≤ 1h, each with Steps/Done/Ref + Model; `wf note`; `wf status <id> clear`; report outcome `sliced`. - Beyond your model → `wf set <id> --model <higher>`, `wf note <id> "<why>"`, `wf status <id> clear`, stop. - Told to wrap up → one call in the worktree: `wf wip <id> -m "<state + next step>" --commit "<msg + footer>" <paths>` (commits WIP, notes, status clear), stop. @@ -48,6 +51,6 @@ Your final message is exactly these 4 lines, no text before or after (details be entry, `wf note` or new tasks — the orchestrator never reads prose): id: <id> - result: done | done+gate-red <culprit> <fix-id> | awaiting <a-id> | handback <why> | wip + result: done | done+gate-red <culprit> <fix-id> | awaiting <a-id> | needs-owner <h-id> | handback <why> | wip commit: <merged sha or -> (copy `wf finish`'s last line `report: commit <sha> [books <sha>] [tool <sha>]`; never a git HEAD you looked up) followups: <ids from wf add, or none> diff --git a/shared/skills/wf-orchestrate/SKILL.md b/shared/skills/wf-orchestrate/SKILL.md index c5b3d86..63eb470 100644 --- a/shared/skills/wf-orchestrate/SKILL.md +++ b/shared/skills/wf-orchestrate/SKILL.md @@ -25,7 +25,7 @@ command: /projects/public/workflow/docs/orchestrator.md — read it only when a 3. Report (4 lines) → `wf orch post <id> <lane> --result "<result line>" --commit <sha> --agent <agent id> --duration <duration_ms/1000>` = post-check (done: archive line, branch gone, worktree clean + in master, else it runs `wf merge`; `wf check`), leftover bookkeeping commit otherwise, `out/wf-orch.log` + `out/wf-cost.log` lines, then the lane's next pick (spawn it) or `stop lane …`. -4. `alert: <id> <outcome> …` (awaiting/handback/post-check-red: post parked only that task, blocked or `Sessions: owner`) +4. `alert: <id> <outcome> …` (awaiting/needs-owner/handback/post-check-red: post parked only that task, blocked / After: h-task / `Sessions: owner`) → tell the owner, keep spawning the lane's next pick; never wait on the owner while tasks are pickable. `stop lane` (none/stop file/push-failed/wip) → tell the owner. No report (crash) → it prints the one recovery pick (`wf orch pick <lane> --id <id> --recovery "<why>"`), then stop. done+gate-red with a not-runner-ready fix → add its Done/Model, run the printed pick. diff --git a/shared/skills/wf-pilot/SKILL.md b/shared/skills/wf-pilot/SKILL.md index 86a207b..8b228f6 100644 --- a/shared/skills/wf-pilot/SKILL.md +++ b/shared/skills/wf-pilot/SKILL.md @@ -21,7 +21,7 @@ K = `--batch` (4); deadline = now + `--for` (8h); lanes = `--lanes` or all. `wf `wf res wait <rid> --timeout <same>`; end the turn. `fit: 0 of K …; nothing started` → stop (step 6, why: deadline). Exit 3 (busy) → background Bash `sleep 600`, then retry. 2. Wait exit → `wf batch --status` (newest summary) → one line to the owner and `out/wf-orch.log`: `<HH:MM> pilot batch <n> rc=<rc> done=<ids> stopped=<lanes: why> added=<ids>`. No task bodies, no logs. - A handback/awaiting blocks only its task (parked by `wf orch post`, alert); its lane stays in the next batch while it has pickable tasks. Never wait on the owner while tasks are pickable. + A handback/awaiting/needs-owner blocks only its task (parked by `wf orch post`, alert); its lane stays in the next batch while it has pickable tasks. Never wait on the owner while tasks are pickable. 3. Failure (rc≠0, or no new summary file) → retry once; second failure → stop (step 6). Memory: `wf res` throttled warning on a running batch → never kill it (workers mid-task); log it (`<HH:MM> pilot batch <n> throttled`); next batch `--mem` = 1.5 × last (cap per `wf res free`). Batch oom-killed → re-run the same N with 1.5 × `--mem`. diff --git a/templates/batch-prompt.md b/templates/batch-prompt.md index 9dfe564..75fe330 100644 --- a/templates/batch-prompt.md +++ b/templates/batch-prompt.md @@ -1 +1 @@ -You are a batch orchestrator. Follow {here}/docs/orchestrator.md 'One worker' for up to {n} tasks (lanes: {lanes}), never implement. Before EVERY spawn (each round, a lane's P0 fix, a recovery worker) run `test -e {stop}`: it exists → spawn nothing, let running workers finish (never kill one), post-check them, append 'stopped: stop file' to {out}, delete the stop file, exit. Else, before each round run `wf batch --time-left {deadline}`: its line ends in ': stop' (less time left than a task takes, p90) → spawn nothing new, let running workers finish, post-check them, append 'stopped: deadline (<that line>)' to {out}, exit. Else each round: `wf orch pick <lane>` per lane, spawn one wf-worker per picked lane in ONE message (model and prompt: as printed), foreground (not background), wait for all, post-check each with `wf orch post <id> <lane> --result "<result line>" --commit <sha> --agent <agent id> --duration <s> --no-pick`, append one line per task to {out} (it exists with its '# wf-batch' header: only append, never rewrite) (time, lane, id, outcome, commit), repeat. If the project has `cloud = true`, the virtual lane cloud also runs each round (nothing to spawn): `wf orch pick cloud` until a `stop lane cloud` / `none fit` / `max parallel` line; never run `wf cloud pull` or `wf orch post <id> cloud` yourself: the batch job's sidecar pulls every 10 min (`wf cloud pull --all` → `wf orch post <id> cloud … --no-pick`, a 'cloud <id> <state> … (sidecar pull)' line in {out}), also while you wait on workers; a cloud stop never stops the local lanes. An `alert: <id> …` line from post (awaiting / handback / post-check-red) parks only that task: append 'alert <id> <outcome>' to {out} and keep picking in that lane (never wait on the owner while tasks are pickable). Stop a lane only on a `stop lane` / `none` line (none, stop file, push-failed, wip, no report after its one recovery) and keep it stopped until that is resolved or the owner says so; done+gate-red <culprit> <fix-id> is not one (post-check as done, the lane picks the P0 fix next). Never ask the owner: follow-ups → wf add -p 2 --model <model>, decisions → wf add -s awaiting. At batch start note `git branch --list 'fast/*' 'slow/*'` (and worktrees): a branch already there is pre-existing; report it ONCE in {out} as 'pre-existing branch <name>' (+ the awaiting id when a task/awaiting names it), never per round or per task. End with the ids added in that file, then exit. +You are a batch orchestrator. Follow {here}/docs/orchestrator.md 'One worker' for up to {n} tasks (lanes: {lanes}), never implement. Before EVERY spawn (each round, a lane's P0 fix, a recovery worker) run `test -e {stop}`: it exists → spawn nothing, let running workers finish (never kill one), post-check them, append 'stopped: stop file' to {out}, delete the stop file, exit. Else, before each round run `wf batch --time-left {deadline}`: its line ends in ': stop' (less time left than a task takes, p90) → spawn nothing new, let running workers finish, post-check them, append 'stopped: deadline (<that line>)' to {out}, exit. Else each round: `wf orch pick <lane>` per lane, spawn one wf-worker per picked lane in ONE message (model and prompt: as printed), foreground (not background), wait for all, post-check each with `wf orch post <id> <lane> --result "<result line>" --commit <sha> --agent <agent id> --duration <s> --no-pick`, append one line per task to {out} (it exists with its '# wf-batch' header: only append, never rewrite) (time, lane, id, outcome, commit), repeat. If the project has `cloud = true`, the virtual lane cloud also runs each round (nothing to spawn): `wf orch pick cloud` until a `stop lane cloud` / `none fit` / `max parallel` line; never run `wf cloud pull` or `wf orch post <id> cloud` yourself: the batch job's sidecar pulls every 10 min (`wf cloud pull --all` → `wf orch post <id> cloud … --no-pick`, a 'cloud <id> <state> … (sidecar pull)' line in {out}), also while you wait on workers; a cloud stop never stops the local lanes. An `alert: <id> …` line from post (awaiting / needs-owner / handback / post-check-red) parks only that task: append 'alert <id> <outcome>' to {out} and keep picking in that lane (never wait on the owner while tasks are pickable). Stop a lane only on a `stop lane` / `none` line (none, stop file, push-failed, wip, no report after its one recovery) and keep it stopped until that is resolved or the owner says so; done+gate-red <culprit> <fix-id> is not one (post-check as done, the lane picks the P0 fix next). Never ask the owner: follow-ups → wf add -p 2 --model <model>, decisions → wf add -s awaiting. At batch start note `git branch --list 'fast/*' 'slow/*'` (and worktrees): a branch already there is pre-existing; report it ONCE in {out} as 'pre-existing branch <name>' (+ the awaiting id when a task/awaiting names it), never per round or per task. End with the ids added in that file, then exit. diff --git a/tests/test_orch.py b/tests/test_orch.py index 157d7ef..45ade24 100644 --- a/tests/test_orch.py +++ b/tests/test_orch.py @@ -215,6 +215,20 @@ class OrchTest(Cli): self.assertIn(f"blocked: [[{aid}]]", self.wf("show", "t-one")[1]) self.assertIn("pick: t-two", self.orch("pick", "fast")) + def test_post_needs_owner_waits_on_human_task_until_done(self): + self.orch("pick", "fast") + hid = "t-plug" + self.assertEqual(self.wf("add", "-s", "human", "-p", "1", "-e", "<1h", "--id", hid, "Plug in the device. Owner-only step.")[0], 0) + out = self.orch("post", "t-one", "fast", "--result", f"needs-owner {hid}", "--no-pick") + self.assertEqual(out, f"post: t-one needs-owner\n committed leftover TASKS.md tasks/archive.md\n" + f"alert: t-one needs-owner → tell the owner (after {hid}); lane fast keeps picking\n") + shown = self.wf("show", "t-one")[1] + self.assertIn(f"After: [[{hid}]]", shown) + self.assertNotIn("in progress", shown.splitlines()[0]) + self.assertIn("pick: t-two", self.orch("pick", "fast")) # A not picked while H open + self.assertEqual(self.wf("done", hid, "-m", "plugged")[0], 0) + self.assertIn("pick: t-one", self.orch("pick", "fast")) # H done → A picked again + def test_post_done_on_slice_job_counts_as_sliced(self): self.orch("pick", "slow", "--id", "t-big") self.run_wf(self.root, "add", "--parent", "t-big", "-e", "<1h", "--done", "x", "Slice one") @@ -1597,7 +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 +PARK = ("handback", "awaiting", "needs-owner", "post-check-red") # block only that task (park_task), lane keeps picking def orch_main(args) -> tuple[Path, Project]: @@ -1783,7 +1783,8 @@ 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 + """Block only this task after a handback/awaiting/needs-owner/post-check-red: needs-owner → After: its + open Needs-human item (status cleared); awaiting → 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(): @@ -1792,7 +1793,16 @@ def park_task(root: Path, id: str, final: str, words: list[str]) -> str: 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:"): + open_h = {i.id for i in p.doc.section("human").items} + h = next((w.strip("[]") for w in words[1:] if w.strip("[]") in open_h), None) \ + if final == "needs-owner" else None + if h: # owner-only step: After: it, picked again once the owner's wf done archives it + if h not in item.after: + tasks.set_fields(p.doc, id, after=item.after + [h]) + if item.status: + tasks.set_status(p.doc, id, None) + done = f"after {h}" + elif (item.status or "").startswith("blocked:"): done = f"already {item.status}" elif a: tasks.set_status(p.doc, id, f"blocked: [[{a.strip('[]')}]]") @@ -1806,7 +1816,7 @@ def park_task(root: Path, id: str, final: str, words: list[str]) -> str: 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"): + if done.endswith("status cleared") or h: unclaim(p.cfg, [id]) return done @@ -2346,7 +2356,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/handback/post-check-red park only the task (blocked / 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 + 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)") |
