aboutsummaryrefslogtreecommitdiffziptar.gz
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-07 11:06:25 +0200
committergodosa <godosa@godosa.eu>2026-10-07 11:06:25 +0200
commitf23d7d865eb042512ebabad87e89b35b11cc1c64 (patch)
treeae575d025b2c0d0d86c72ec9e79f1d833a38371b
parent959275f862e84ee20207f7eef3f1c0f999f6993d (diff)
downloadworkflow-f23d7d865eb042512ebabad87e89b35b11cc1c64.tar.gz
workflow-f23d7d865eb042512ebabad87e89b35b11cc1c64.zip
wf orch post: commit leftover books with code sha; reject private sha in split
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--shared/agents/wf-worker.md2
-rw-r--r--tests/test_split.py37
-rwxr-xr-xwf.py20
4 files changed, 57 insertions, 3 deletions
diff --git a/CHANGES.md b/CHANGES.md
index a7e85db..6b2ee5f 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,4 +1,5 @@
# Changes (newest first)
+- 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.
- 2026-10-07 split projects 6/6: `wf check` split rules (cloud error; warns: no push, `.wf-home` not git-ignored, merged code worktrees); docs. Projects: split projects: push key, .wf-home in the code repo's .gitignore (migration task in the workflow project).
- 2026-10-07 split projects 5/6: `wf orch pick/post` use the code repo (worktrees, branch/merge checks, commit sha); `.wf/push-failed` after a done worker stops the lane (`push-failed`). Projects: nothing.
diff --git a/shared/agents/wf-worker.md b/shared/agents/wf-worker.md
index f60e35b..82c8f14 100644
--- a/shared/agents/wf-worker.md
+++ b/shared/agents/wf-worker.md
@@ -49,5 +49,5 @@ 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
- commit: <merged sha or -> (copy `wf finish`'s last line `report: commit <sha> [tool <sha>]`)
+ 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/tests/test_split.py b/tests/test_split.py
index 9d19308..f8495eb 100644
--- a/tests/test_split.py
+++ b/tests/test_split.py
@@ -290,6 +290,43 @@ class OrchSplitTest(SplitCli):
self.assertIn(f" {self.sha(self.code)} ", (self.root / "out" / "wf-orch.log").read_text())
+ def _finish_then_dirty_books(self):
+ self.wf("orch", "pick", "fast", "--id", "t-three", project=False, env=IDENT)
+ wt = self.code / ".worktrees" / "fast"
+ self.wf("start", "t-three", "--worktree", str(wt), "--branch", "fast/t-three", project=False, env=IDENT)
+ (wt / "code.txt").write_text("x\n")
+ code, out, err = self.wf("finish", "t-three", "-m", "ok", "--commit", "impl", "code.txt",
+ project=False, cwd=wt, env=IDENT)
+ self.assertEqual(code, 0, out + err)
+ self.wf("note", "t-one", "later edit", project=False, env=IDENT) # books dirty in the private tree
+ self.assertTrue(subprocess.run(["git", "status", "--porcelain", "TASKS.md"], cwd=self.root,
+ capture_output=True, text=True).stdout.strip())
+
+ def test_post_done_commits_leftover_books_with_code_sha(self):
+ self._finish_then_dirty_books()
+ csha = self.sha(self.code)
+ code, out, err = self.wf("orch", "post", "t-three", "fast", "--result", "done", "--commit", csha,
+ "--no-pick", project=False, env=IDENT)
+ self.assertEqual(code, 0, out + err)
+ self.assertEqual(self.log(self.root).splitlines()[:2], [f"t-three done (code {csha})"] * 2)
+ self.assertFalse(subprocess.run(["git", "status", "--porcelain", "TASKS.md"], cwd=self.root,
+ capture_output=True, text=True).stdout.strip())
+ self.assertIn(f" t-three done {csha} ", (self.root / "out" / "wf-orch.log").read_text())
+
+ def test_post_stale_private_sha_replaced_by_code_sha(self):
+ self._finish_then_dirty_books()
+ stale = self.sha(self.root) # worker reported the private repo's HEAD
+ code, out, err = self.wf("orch", "post", "t-three", "fast", "--result", "done", "--commit", stale,
+ "--no-pick", project=False, env=IDENT)
+ self.assertEqual(code, 0, out + err)
+ csha = self.sha(self.code)
+ self.assertIn(f"commit {stale} not in code repo", out)
+ self.assertIn(f" t-three done {csha} ", (self.root / "out" / "wf-orch.log").read_text())
+ self.assertEqual(self.log(self.root).splitlines()[:2], [f"t-three done (code {csha})"] * 2)
+ self.assertFalse(subprocess.run(["git", "status", "--porcelain", "TASKS.md"], cwd=self.root,
+ capture_output=True, text=True).stdout.strip())
+
+
class CheckSplitTest(SplitCli):
def test_cloud_error_push_and_ignore_warnings(self):
(self.root / "workflow.toml").write_text(SplitCli.toml + "cloud = true\n")
diff --git a/wf.py b/wf.py
index dda4914..108f3da 100755
--- a/wf.py
+++ b/wf.py
@@ -1793,6 +1793,17 @@ def orch_post(main: Path, p: Project, args) -> int:
master = config.git_branch(repo / ".git") or "master"
files = [str(f.relative_to(main)) for f in (cfg.tasks, cfg.archive)]
problems, out = [], []
+ split = repo.resolve() != main.resolve()
+
+ def report_commit() -> str: # after any merge below; split: a private sha → the code repo's master
+ c = args.commit or (git_run(repo, "rev-parse", "--short", master).stdout.strip()
+ if outcome.startswith("done") else "-")
+ if split and c != "-" and git_run(repo, "cat-file", "-e", f"{c}^{{commit}}").returncode:
+ fixed = git_run(repo, "rev-parse", "--short", master).stdout.strip()
+ out.append(f"commit {c} not in code repo {repo} (a private sha?): using its {master} {fixed}")
+ c = fixed
+ return c
+ commit = None
with project_lock(cfg.root):
if outcome.startswith("done"):
if id not in p.archived:
@@ -1815,6 +1826,12 @@ def orch_post(main: Path, p: Project, args) -> int:
errors = checks.check(config.load(cfg.root))[0]
if errors:
problems.append(f"wf check: {len(errors)} errors: {errors[0]}")
+ commit = report_commit()
+ if not problems and git_run(main, "status", "--porcelain", "--", *files).stdout.strip():
+ msg = f"{id} done" + (f" (code {commit})" if split and commit != "-" else " (orchestrator)")
+ 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
@@ -1828,8 +1845,7 @@ def orch_post(main: Path, p: Project, args) -> int:
model = rec.get("model") or (item.model if item else am) or "?"
dur = args.duration if args.duration else ( # None/0 (orchestrator lacked duration_ms) → since the pick
int(datetime.datetime.now().timestamp()) - rec["at"] if isinstance(rec.get("at"), int) else None)
- commit = args.commit or (git_run(repo, "rev-parse", "--short", master).stdout.strip()
- if outcome.startswith("done") else "-")
+ commit = commit or report_commit()
now = datetime.datetime.now().isoformat(timespec="seconds")
(cfg.root / "out").mkdir(exist_ok=True)
with open(cfg.root / "out" / "wf-orch.log", "a") as f: