aboutsummaryrefslogtreecommitdiffziptar.gz
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-07 09:17:34 +0200
committergodosa <godosa@godosa.eu>2026-10-07 09:17:34 +0200
commitdf72f6f16a7a5141ff4df1bab7e1de34207a4163 (patch)
treed248d09369f3713c8f47bccb314c5a2eb82a7d40
parent0f6857c26e5b2777c6be927dbc75fdcdbfe0e750 (diff)
downloadworkflow-df72f6f16a7a5141ff4df1bab7e1de34207a4163.tar.gz
workflow-df72f6f16a7a5141ff4df1bab7e1de34207a4163.zip
split projects 2/6: wf start makes code worktrees (.wf-home), wf next advice
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--tests/test_split.py37
-rwxr-xr-xwf.py16
3 files changed, 50 insertions, 4 deletions
diff --git a/CHANGES.md b/CHANGES.md
index f909bc7..9b0521a 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,4 +1,5 @@
# Changes (newest first)
+- 2026-10-07 split projects 2/6: `wf start` makes code worktrees (branch in the code repo, `.wf-home` pointing at the private project); `wf next` multi-session advice names `wf start` in the code repo. Projects: nothing.
- 2026-10-07 split projects 1/6: Config.split, push key, .wf-home discovery. Projects: nothing.
- 2026-10-07 Shared push rule: in a split project the public `code_root` repo is pushed only via the project's publish command, never `git push home --all` (that pushes local review branches unscanned). Projects: split projects name their publish command in their CLAUDE.md.
- 2026-10-07 Denylist guard at commit time: `.githooks/pre-commit` (staged lines + paths) and `.githooks/commit-msg` run `scripts/denylist_check.py` against the git-ignored `publish-denylist.local` (worktree top + main tree, plus `git config denylist.file` lists; `word` / `!token` exemption / `#`, case-insensitive; output `word #<line>` only; no list → warning). Enable per clone: `git config core.hooksPath .githooks`; audit: `denylist_check.py tree`. `scripts/publish_snapshot.py` removed (history is public now). Projects: nothing.
diff --git a/tests/test_split.py b/tests/test_split.py
index 2026e33..346c138 100644
--- a/tests/test_split.py
+++ b/tests/test_split.py
@@ -1,3 +1,4 @@
+import os
import subprocess
import unittest
from pathlib import Path
@@ -72,3 +73,39 @@ class DiscoveryTest(SplitCli):
self.assertFalse(cfg.split)
self.assertIsNone(config.code_main(cfg))
self.assertEqual(cfg.push, ["true"])
+
+
+class StartSplitTest(SplitCli):
+ toml = SplitCli.toml + 'worktree_setup = ["pwd > where.txt"]\n'
+
+ def test_start_makes_code_worktree_with_wf_home(self):
+ wt = self.code / ".worktrees" / "slow"
+ code, out, err = self.wf("start", "t-three", "--worktree", str(wt), "--branch", "slow/t-three",
+ project=False, env=IDENT)
+ self.assertEqual(code, 0, out + err)
+ self.assertEqual((wt / ".wf-home").read_text(), f"{self.root}\n")
+ self.assertEqual(subprocess.run(["git", "branch", "--show-current"], cwd=wt, capture_output=True,
+ text=True).stdout.strip(), "slow/t-three")
+ self.assertEqual((wt / "where.txt").read_text().strip(), str(wt)) # setup cwd = code worktree
+ self.assertIn("(in progress: slow/t-three)", (self.root / "TASKS.md").read_text())
+ self.assertIn(f"cd {wt} && (make test) && ", out)
+ self.assertFalse((self.root / ".worktrees").exists()) # nothing in the private repo
+
+ def test_setup_in_code_worktree(self):
+ wt = self.add_wt()
+ code, out, err = self.wf("setup", project=False, cwd=wt)
+ self.assertEqual(code, 0, err)
+ self.assertEqual((wt / "where.txt").read_text().strip(), str(wt))
+
+ def env(self, name, pid): # copy of tests/test_claims.py ClaimTest.env
+ sock = self.root / f"{name}.sock"
+ sock.write_text("")
+ return {"CLAUDE_CODE_MESSAGING_SOCKET": str(sock), "CLAUDE_PID": str(pid), "CLAUDE_CODE_SESSION_ID": name}
+
+ def test_next_advice_names_wf_start_in_code_repo(self):
+ # pattern: tests/test_claims.py::test_next_says_worktree_mode_when_two_sessions_live
+ son, me = self.env("s", os.getppid()), self.env("o", os.getpid())
+ self.wf("next", "--lane", "slow", "--as", "sonnet", "--brief", env=son)
+ code, out, err = self.wf("next", "--lane", "fast", "--as", "opus", env=me)
+ self.assertIn(f" wf start <task> --worktree {self.code / '.worktrees' / 'fast'} --branch fast/<task> "
+ f"(run in {self.root}; wf there writes this TASKS.md)\n", out)
diff --git a/wf.py b/wf.py
index 3a6babf..444cd37 100755
--- a/wf.py
+++ b/wf.py
@@ -392,6 +392,11 @@ def multi_lines(p: Project, args, lane: str, extra: int) -> list[str]:
if wt:
return [head + f"you are in worktree {p.cfg.rel(wt[0])} (branch {config.git_branch(wt[1])}); "
"wf writes the main tree's TASKS.md"]
+ if code := config.code_main(p.cfg):
+ master = config.git_branch(code / ".git") or "master"
+ cmd = f"wf start <task> --worktree {code / '.worktrees' / lane} --branch {lane}/<task>"
+ return [head + f"work in your lane's code worktree, never on {master}:",
+ f" {cmd} (run in {p.cfg.root}; wf there writes this TASKS.md)"]
folder, master = p.cfg.root / ".worktrees" / lane, config.git_branch(main_top / ".git") or "master"
cmd = (f"cd {p.cfg.rel(folder)} && git switch -c {lane}/<task> {master}" if folder.is_dir()
else f"git worktree add {p.cfg.rel(folder)} -b {lane}/<task> {master}")
@@ -1424,7 +1429,8 @@ def cmd_start(args) -> int:
p = load_project(args)
id = p.resolve(args.id)
main = Path(git_run(start, "rev-parse", "--show-toplevel").stdout.strip() or p.cfg.root)
- master = config.git_branch(main / ".git") or "master"
+ repo = config.code_main(p.cfg) or main # split project: worktree + branch in the code repo
+ master = config.git_branch(repo / ".git") or "master"
wt = (Path.cwd() / args.worktree).resolve()
shown = p.cfg.rel(wt)
b = args.branch
@@ -1435,14 +1441,14 @@ def cmd_start(args) -> int:
raise Failure(f"git {a[0]} failed: {(r.stderr.strip() or r.stdout.strip() or 'git error').splitlines()[-1]}")
return r.stdout
- has_branch = not git_run(main, "rev-parse", "--verify", "-q", f"refs/heads/{b}").returncode
+ has_branch = not git_run(repo, "rev-parse", "--verify", "-q", f"refs/heads/{b}").returncode
extra = []
if not wt.exists():
if has_branch:
- git_ok(main, "worktree", "add", "-q", str(wt), b)
+ git_ok(repo, "worktree", "add", "-q", str(wt), b)
how = f"new, existing branch {b}: earlier WIP, read the notes"
else:
- git_ok(main, "worktree", "add", "-q", str(wt), "-b", b, master)
+ git_ok(repo, "worktree", "add", "-q", str(wt), "-b", b, master)
how = f"new, branch {b} from {master}"
else:
dirty = git_run(wt, "status", "--porcelain").stdout.rstrip("\n")
@@ -1467,6 +1473,8 @@ def cmd_start(args) -> int:
extra = [f"recovery: uncommitted:\n{dirty}" if dirty else "recovery: uncommitted: none",
f"recovery: commits {master}..HEAD:" + (f"\n{log}" if log else " none")]
print(f"worktree: {shown} ({how})", *extra, sep="\n", flush=True)
+ if repo != main:
+ (wt / config.WF_HOME).write_text(f"{p.cfg.root}\n")
try:
here = wt / p.cfg.root.relative_to(main)
except ValueError: