aboutsummaryrefslogtreecommitdiffziptar.gz
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-07 09:40:59 +0200
committergodosa <godosa@godosa.eu>2026-10-07 09:40:59 +0200
commit0e1e107192882809da7e660c09ba501c09331c38 (patch)
tree32b2fd332464ca028c4dc5a8b03a216edeeb3229
parent811de09feb866c5b344034399154bde77acdd5ba (diff)
downloadworkflow-0e1e107192882809da7e660c09ba501c09331c38.tar.gz
workflow-0e1e107192882809da7e660c09ba501c09331c38.zip
split projects 6/6: wf check split rules; docs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEAjUkQRrCYX5MZhWxdtj2
-rw-r--r--CHANGES.md1
-rw-r--r--docs/manual.md7
-rw-r--r--docs/multi-session.md4
-rw-r--r--docs/orchestrator.md7
-rw-r--r--shared/agents/wf-worker.md2
-rw-r--r--tests/test_split.py18
-rw-r--r--wflib/check.py25
7 files changed, 59 insertions, 5 deletions
diff --git a/CHANGES.md b/CHANGES.md
index 5f92a19..9dc765d 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,4 +1,5 @@
# Changes (newest first)
+- 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.
- 2026-10-07 split projects 4/6: workflow.toml `push` (cwd root, env WF_MAIN), `wf push`, `.wf/push-failed` marker (`wf next` shows it); finish report gains ` push-failed <rc>`. Projects: split projects set push (follow-up migration task).
- 2026-10-07 split projects 3/6: `wf finish` / `wf merge` / `wf wip` across code + private repo: paths under the private root are committed there (with TASKS/archive, message `<msg> (code <sha>)`), code paths on the branch; no code paths → no code commit; report `commit <code|-> books <private>`; `wf done` merge advice names the private books. Projects: nothing.
diff --git a/docs/manual.md b/docs/manual.md
index 26661b6..c9f7bd1 100644
--- a/docs/manual.md
+++ b/docs/manual.md
@@ -238,3 +238,10 @@ wf add "Cave seams. Close the slit beside the lintel." -p 1 -e '<1h' --model son
| A worker or batch does nothing | `wf list --runner` is empty: tasks lack a Done line (`wf batch --prep`), are blocked on an awaiting item, or wait on `After:`. `wf lanes` shows counts per lane. |
| `wf check` errors after a hand edit | Read the message (duplicate id, broken `[[link]]`, bad section) and fix the item. Ids are never reused. |
| `project format N is newer than this wf` | Update the tool: `git -C /projects/public/workflow pull`. |
+
+## Split projects (private home repo + public code repo)
+`code_root` in workflow.toml points at another git repo: TASKS/archive/specs stay in the private project, code goes to the
+code repo. `wf start` makes code worktrees (branch in the code repo, `.wf-home` file pointing back); `wf finish`/`wf merge`/
+`wf wip` commit private paths in the private repo and code paths in the code repo. `push = [cmd…]` runs in the private root
+after a merge (env `WF_MAIN`); `wf push` runs it by hand; failure writes `.wf/push-failed`. Add `.wf-home` to the code repo's
+`.gitignore`. The cloud lane is refused. `wf check` warns: no `push`, `.wf-home` not ignored, merged leftover code worktrees.
diff --git a/docs/multi-session.md b/docs/multi-session.md
index 628aad9..225809e 100644
--- a/docs/multi-session.md
+++ b/docs/multi-session.md
@@ -44,3 +44,7 @@ sessions/workers never lose a TASKS.md edit or race a merge-back.
## 4. Commits
Shared rule: stage explicit paths, never `git add -A` / `git commit -a`.
+
+## Split projects
+Code worktrees come from `wf start <id> --worktree <path> --branch <b>` run in the private project: it creates them in the
+code repo (`code_root`) and writes `.wf-home` there. Inside such a worktree wf resolves the private project via `.wf-home`.
diff --git a/docs/orchestrator.md b/docs/orchestrator.md
index 845c527..f878855 100644
--- a/docs/orchestrator.md
+++ b/docs/orchestrator.md
@@ -35,7 +35,7 @@ The steps they automate:
1. Pick: `wf list --runner --lane <lane>` → first id (lanes: `wf lanes`). Claim: `wf status <id> progress "worker"`. No commit of
its own: `wf merge` or any later bookkeeping commit takes TASKS.md (and the claim) along; harmless.
-2. Worktree: `<main>/.worktrees/<lane>`; if a live session or another worker uses it, `<lane>-2`, `-3`, …
+2. Worktree: `<main>/.worktrees/<lane>` (split project: under the code repo, `<code_root top>/.worktrees/<lane>`); if a live session or another worker uses it, `<lane>-2`, `-3`, …
(live interactive sessions: `~/.claude/sessions/*.json` field `cwd`). Branch `<lane>/<id>`.
3. Spawn: Agent tool, `subagent_type: wf-worker`, `model: <task Model, none = opus>`, background, no worktree isolation
(it blocks `wf merge` on the main checkout). Prompt:
@@ -150,3 +150,8 @@ TASKS.md, the log and the batch summaries.
Safe to clear only with no background worker running. Tested (2026-10-05): `/clear` leaves a background
subagent alive and its hand-back + completion notice reach the new context, but SIGKILLs its running Bash
command (exit 137): a worker mid-test or mid-commit loses that step and reports a false failure or half state.
+
+## Split projects
+`code_root` = another git repo: worktrees and branches live in the code repo, `wf start` makes them (with `.wf-home`
+pointing at the private project). `wf orch post` checks the code repo. A done worker whose push (`push` key) failed
+leaves `.wf/push-failed`: the lane stops (`push-failed`); fix the push, delete the marker, continue.
diff --git a/shared/agents/wf-worker.md b/shared/agents/wf-worker.md
index 555b20c..f60e35b 100644
--- a/shared/agents/wf-worker.md
+++ b/shared/agents/wf-worker.md
@@ -35,7 +35,7 @@ Run the `Verify` commands `wf ctx` printed (long ones via
`wf res run`, waited for in the foreground; red → fix or hand back), then ONE call:
`wf finish <id> -m "<entry>" --commit "<msg + footer>" <explicit paths>` (in the worktree; no code → no `--commit`)
= quick gate → done → commit → `wf merge`; a refusal (gate red, stray uncommitted files) leaves the task open: fix, rerun.
-Paths = this worktree's files only (another repo's: commit there first, finish without them); never hand-edit or commit
+Paths = this worktree's files only (another repo's: commit there first, finish without them). Split project (code_root = another repo; worktree is in the code repo): private-repo paths (docs, specs) are allowed in `wf finish`/`wf wip` and committed there; the report's commit line is `report: commit <sha> [books <sha>] [tool <sha>] [push-failed N]`; never hand-edit or commit
TASKS.md/archive (wf writes the main tree's, `wf merge` commits them). Failed after `done:` → fix, rerun the same `wf finish` (it resumes).
Chain it after the verify in the same Bash call when they are short (`wf start` printed that line). A verify step that says background / gate on a commit → run it after `wf finish`, in the
foreground, from the main tree (`cd <main>`, so logs land in its `out/`), on the merged sha
diff --git a/tests/test_split.py b/tests/test_split.py
index 965f667..934bf6f 100644
--- a/tests/test_split.py
+++ b/tests/test_split.py
@@ -258,3 +258,21 @@ class OrchSplitTest(SplitCli):
self.assertIn("stop lane fast: push-failed", out)
self.assertNotIn("branch fast/t-three still there", out)
self.assertIn(f" {self.sha(self.code)} ", (self.root / "out" / "wf-orch.log").read_text())
+
+
+class CheckSplitTest(SplitCli):
+ def test_cloud_error_push_and_ignore_warnings(self):
+ (self.root / "workflow.toml").write_text(SplitCli.toml + "cloud = true\n")
+ (self.code / ".gitignore").write_text(".worktrees/\n")
+ git(self.code, "commit", "-qam", "no wf-home ignore")
+ code, out, err = self.wf("check")
+ self.assertEqual(code, 1)
+ self.assertIn("cloud lane needs a single repo", out)
+ self.assertIn("split project without push", out)
+ self.assertIn(".wf-home not git-ignored", out)
+
+ def test_clean_split_project(self):
+ (self.root / "workflow.toml").write_text(SplitCli.toml + 'push = ["true"]\n')
+ code, out, err = self.wf("check")
+ self.assertEqual(code, 0, out)
+ self.assertNotIn("split", out)
diff --git a/wflib/check.py b/wflib/check.py
index f86b2fe..c528c43 100644
--- a/wflib/check.py
+++ b/wflib/check.py
@@ -8,7 +8,7 @@ from dataclasses import dataclass
from pathlib import Path
from . import areas, refs, tasks
-from .config import NAME, Config
+from .config import NAME, WF_HOME, Config, git_top
NUMBERED_RE = re.compile(r"^\d+\. ")
NUMBER_REF_RE = re.compile(r"(?<![\w/&#])#\d+\b")
@@ -301,7 +301,7 @@ def _git(root: Path, *args: str) -> str | None:
return out.stdout.strip() if out.returncode == 0 else None
-def merged_tool_worktrees(tool_root: Path) -> list[Problem]:
+def merged_tool_worktrees(tool_root: Path, label: str = "tool") -> list[Problem]:
"""Worktrees of the wf tool repo whose branch is behind master (merged, leftover after release).
A branch equal to master, or a worktree created < 2h ago, is not flagged."""
listing = _git(tool_root, "worktree", "list", "--porcelain")
@@ -326,11 +326,27 @@ def merged_tool_worktrees(tool_root: Path) -> list[Problem]:
except OSError:
pass
if _git(tool_root, "merge-base", "--is-ancestor", branch, "master") is not None:
- out.append(Problem("wf-tool", None, "", f"tool worktree '{path}' (branch {branch}) is merged into "
+ out.append(Problem("wf-tool", None, "", f"{label} worktree '{path}' (branch {branch}) is merged into "
f"master: git worktree remove it, git branch -d {branch}"))
return out
+def split_problems(cfg: Config) -> tuple[list[Problem], list[Problem]]:
+ """Split project rules (code_root = another repo): cloud refused, push expected, .wf-home ignored, merged code worktrees."""
+ if not cfg.split:
+ return [], []
+ code = git_top(cfg.code_root)
+ errors, warnings = [], []
+ if cfg.cloud:
+ errors.append(Problem("workflow.toml", None, "", "cloud lane needs a single repo: split project (code_root is another repo)"))
+ if not cfg.push:
+ warnings.append(Problem("workflow.toml", None, "", "split project without push: public repo pushed unscanned or not at all"))
+ if _git(code, "check-ignore", "-q", WF_HOME) is None:
+ warnings.append(Problem(f"{code}/.gitignore", None, "", f"{WF_HOME} not git-ignored (wf start writes it into code worktrees)"))
+ warnings += merged_tool_worktrees(code, label="code")
+ return errors, warnings
+
+
def _area_problems(cfg: Config) -> list[Problem]:
file = cfg.areas_file
if not file.is_file():
@@ -371,6 +387,9 @@ def check(cfg: Config, tasks_text: str | None = None, archive_text: str | None =
if slow:
warnings += _stale_awaiting(cfg, text)
warnings += _area_problems(cfg)
+ e, w = split_problems(cfg)
+ errors += e
+ warnings += w
order: dict[str, int] = {}
for p in errors + warnings:
order.setdefault(p.path, len(order))