@caddis/cli 0.4.62 → 0.4.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -953,7 +953,8 @@ def shared_checkout_reason(command: str, root: str | None) -> str:
953
953
  if _moves_tree(sub, args, cwd) and _declares_shared(cwd):
954
954
  return (f"`git {sub.lower()}` moves the tree of a shared checkout (AGENTS.md says Shared checkout: yes; "
955
955
  "a dev service or a peer uses it). Work in your own worktree instead: "
956
- "`git worktree add \"$TEMP/<name>\" -b <branch> origin/<default>`, and remove it when done")
956
+ "`git worktree add <worktree root>/<repo>/<name> -b <branch> origin/<default>` (never under %TEMP%: "
957
+ "`caddis_worktree_root.py <name>` prints the path), and remove it when done")
957
958
  return ""
958
959
 
959
960
 
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "caddis",
3
- "version": "1.3.148",
3
+ "version": "1.3.150",
4
4
  "description": "caddis — agent-agnostic dev harness as an Antigravity (agy) plugin: TDD skills + commands, single-sourced with the Claude Code plugin."
5
5
  }
@@ -37,6 +37,7 @@ import caddis_checkout_guard
37
37
  import caddis_routing
38
38
  import caddis_redact
39
39
  import caddis_spend
40
+ import caddis_worktree_root
40
41
  import caddis_worktree_safety
41
42
 
42
43
  if hasattr(sys.stdout, "reconfigure"):
@@ -61,7 +62,15 @@ DEFAULT_IDLE_TIMEOUT_S = 900
61
62
  IDLE_SAMPLE_S = 15
62
63
  LANE_IDS = ("claude", "glm", "codex-sol", "codex-astra", "agy")
63
64
  TASK_TYPES = ("backend", "hard", "ui", "tests", "docs")
65
+ # Where lane git WORKTREES live.
66
+ # New lane worktrees go to `<worktree root>/<repo>/caddis-lane-<hash>-<plan>-p<NN>-a<K>` (`lane_worktree_path`, which asks
67
+ # `caddis_worktree_root`: $CADDIS_WORKTREE_ROOT, else `<parent of the main checkout>/.worktrees`). Never %TEMP%: Windows
68
+ # cleanup (Storage Sense) can empty it and delete a lane worktree that holds unmerged work (owner decision, 20 Oct 2026).
69
+ # LANES_TMP is the OLD place, kept ONLY so lanes made before that decision keep working where they are: it is read to
70
+ # accept, finish and abandon an old record, and is never written to. Nothing else lives under it. Lane scratch that is not
71
+ # a worktree (packets, locks) is under `<repo>/.caddis/orchestrator/lane-runs/` (`run_dir`); it never used LANES_TMP.
64
72
  LANES_TMP = Path(tempfile.gettempdir()) / "caddis-lanes"
73
+ LANE_WORKTREE_PREFIX = "caddis-lane-"
65
74
  DEFAULT_MODELS = {
66
75
  "claude": "claude-opus-5",
67
76
  "glm": "glm-5.3[1m]",
@@ -227,6 +236,32 @@ def _git_output(repo_root: Path, *args: str) -> str:
227
236
  return result.stdout.strip()
228
237
 
229
238
 
239
+ def lane_worktree_path(repo_root: Path, plan_stem: str, phase: int, attempt: int) -> Path:
240
+ """The path of a NEW lane worktree: `<worktree root>/<repo>/caddis-lane-<repo hash>-<plan>-p<NN>-a<K>`.
241
+ Creates nothing. The hash keeps two repos with the same folder name apart."""
242
+ repo_hash = hashlib.sha256(str(repo_root.resolve()).encode("utf-8")).hexdigest()[:8]
243
+ short_name = f"{LANE_WORKTREE_PREFIX}{repo_hash}-{plan_stem}-p{phase:02d}-a{attempt}"
244
+ try:
245
+ return caddis_worktree_root.worktree_path(repo_root, short_name)
246
+ except (ValueError, caddis_worktree_root.NotARepoError) as exc:
247
+ raise LaneConfigError(str(exc)) from exc
248
+
249
+
250
+ def is_lane_worktree_path(repo_root: Path, path: Path) -> bool:
251
+ """True for a folder that a lane may remove: an OLD one under LANES_TMP (any depth, as before), or a NEW one named
252
+ `caddis-lane-*` directly inside this repo's folder under the current worktree root."""
253
+ resolved = path.resolve()
254
+ if LANES_TMP.resolve() in resolved.parents:
255
+ return True
256
+ if not resolved.name.startswith(LANE_WORKTREE_PREFIX):
257
+ return False
258
+ try:
259
+ lane_dir = caddis_worktree_root.worktree_path(repo_root, resolved.name).parent
260
+ except (ValueError, caddis_worktree_root.NotARepoError):
261
+ return False
262
+ return resolved.parent == lane_dir.resolve()
263
+
264
+
230
265
  def create_phase_worktree(
231
266
  repo_root: Path,
232
267
  plan_stem: str,
@@ -235,14 +270,11 @@ def create_phase_worktree(
235
270
  ) -> Worktree:
236
271
  base_sha = _git_output(repo_root, "rev-parse", "HEAD")
237
272
  branch = f"lane/{plan_stem}/phase-{phase:02d}-a{attempt}"
238
- repo_hash = hashlib.sha256(str(repo_root.resolve()).encode("utf-8")).hexdigest()[:8]
239
- path = LANES_TMP / (
240
- f"{repo_root.name}-{repo_hash}-{plan_stem}-p{phase:02d}-a{attempt}"
241
- )
273
+ path = lane_worktree_path(repo_root, plan_stem, phase, attempt)
242
274
  if path.exists():
243
275
  raise LaneConfigError(f"worktree already exists: {path} — run abandon first")
244
276
 
245
- LANES_TMP.mkdir(parents=True, exist_ok=True)
277
+ path.parent.mkdir(parents=True, exist_ok=True)
246
278
  _git_output(
247
279
  repo_root,
248
280
  "worktree",
@@ -262,8 +294,8 @@ def remove_phase_worktree(
262
294
  delete_branch: bool,
263
295
  ) -> None:
264
296
  resolved = wt.path.resolve()
265
- if LANES_TMP.resolve() not in resolved.parents:
266
- raise LaneConfigError(f"refusing to remove {resolved}: not under {LANES_TMP}")
297
+ if not is_lane_worktree_path(repo_root, resolved):
298
+ raise LaneConfigError(f"refusing to remove {resolved}: not a lane worktree (a caddis-lane-* folder under the worktree root, or an old one under {LANES_TMP})")
267
299
  if delete_branch and not wt.branch.startswith("lane/"):
268
300
  raise LaneConfigError(f"refusing to delete non-lane branch: {wt.branch}")
269
301
 
@@ -2250,7 +2282,7 @@ def _accept_command(
2250
2282
  unsafe = shared_checkout_reason(repo)
2251
2283
  if unsafe and not getattr(args, "allow_shared", False):
2252
2284
  print(f"refusing: the session checkout is not safe to fast-forward ({_printable(unsafe)}). "
2253
- "Run the programme from a worktree of origin/<default> under %TEMP% (git worktree add), "
2285
+ "Run the programme from a worktree of origin/<default> (git worktree add; `caddis_worktree_root.py` prints the path, never %TEMP%), "
2254
2286
  "or pass --allow-shared to fast-forward here anyway")
2255
2287
  return EXIT_USAGE
2256
2288
  if unsafe:
@@ -2342,8 +2374,8 @@ def _validate_packet_worktree(
2342
2374
  ) -> None:
2343
2375
  resolved = wt.path.resolve()
2344
2376
  expected_branch = f"lane/{plan.stem}/phase-{phase:02d}-a{attempt}"
2345
- if LANES_TMP.resolve() not in resolved.parents:
2346
- raise LaneConfigError(f"refusing packet worktree outside {LANES_TMP}: {resolved}")
2377
+ if not is_lane_worktree_path(repo, resolved):
2378
+ raise LaneConfigError(f"refusing packet worktree that is not a lane worktree: {resolved}")
2347
2379
  if wt.branch != expected_branch:
2348
2380
  raise LaneConfigError(
2349
2381
  f"refusing packet branch {wt.branch}: expected {expected_branch}"
@@ -2392,8 +2424,7 @@ def _finish_command(args) -> int:
2392
2424
  resolved = path.resolve()
2393
2425
  if resolved == main_path:
2394
2426
  continue
2395
- in_lanes_tmp = LANES_TMP.resolve() in resolved.parents
2396
- if not branch.startswith(prefix) or not in_lanes_tmp:
2427
+ if not branch.startswith(prefix) or not is_lane_worktree_path(repo, resolved):
2397
2428
  print(f"OTHER {path} {branch or 'HEAD'}")
2398
2429
  continue
2399
2430
  if branch_is_locked(branch):
@@ -30,7 +30,8 @@ with new commits on that branch (still checked out, nothing under `.caddis/roadm
30
30
  persistent claim for the phase, pushes the branch, opens a PR (`gh api`) and stops `BLOCKED waiting for merge
31
31
  <plan>#<phase> <url>` (exit 1); `--last-stop` lets the recheck task restart it once the PR merged; the restart checks
32
32
  the merged origin roadmap against the hash taken at publish time, removes the claim, rolls that milestone up and goes
33
- on. Every roadmap runs its sessions in ONE dedicated worktree, `%TEMP%/caddis-roadmap-<slug>-<repo tag>` (made with
33
+ on. Every roadmap runs its sessions in ONE dedicated worktree, `<worktree root>/<repo>/caddis-roadmap-<slug>-<repo tag>`
34
+ (`$CADDIS_WORKTREE_ROOT`, else `<parent of the main checkout>/.worktrees`, never %TEMP%; `caddis_worktree_root.py`; made with
34
35
  `git worktree add --detach` from origin/<default> on the first session, reused on restart, recorded in the runner
35
36
  folder, never the `--repo` checkout); when the roadmap is finished the runner removes it if it is clean, and keeps it
36
37
  (saying so in the stop line) if not. The caps (hours, sessions, USD per session and in total) are per run, in memory. CONTINUE with the phase already merged on
@@ -111,7 +112,6 @@ import re
111
112
  import socket
112
113
  import subprocess
113
114
  import sys
114
- import tempfile
115
115
  import time
116
116
  import uuid
117
117
  from collections import Counter
@@ -135,6 +135,7 @@ import caddis_redact # noqa: E402
135
135
  import caddis_review # noqa: E402
136
136
  import caddis_roadmap # noqa: E402
137
137
  import caddis_spend # noqa: E402
138
+ import caddis_worktree_root # noqa: E402
138
139
  import caddis_worktree_safety # noqa: E402
139
140
  import caddis_workstreams # noqa: E402
140
141
 
@@ -1545,11 +1546,13 @@ def _registered_worktrees(repo: Path) -> set[Path]:
1545
1546
 
1546
1547
 
1547
1548
  def worktree_path(repo: Path, slug: str, root: Path | None = None) -> Path:
1548
- """The roadmap's session worktree: the recorded path, else `<root or %TEMP%>/caddis-roadmap-<slug>-<repo tag>`."""
1549
+ """The roadmap's session worktree: the recorded path, else (a NEW one) `caddis-roadmap-<slug>-<repo tag>` under
1550
+ `<worktree root>/<repo>/` (`caddis_worktree_root`: `root`, `$CADDIS_WORKTREE_ROOT`, else `<parent of the main
1551
+ checkout>/.worktrees`; never %TEMP%)."""
1549
1552
  recorded = _read_record(_state_file(repo, slug, "worktree")).get("path")
1550
1553
  if isinstance(recorded, str) and recorded:
1551
1554
  return Path(recorded)
1552
- return Path(root or tempfile.gettempdir()) / f"{WORKTREE_PREFIX}{slug}-{_repo_tag(repo)}"
1555
+ return caddis_worktree_root.worktree_path(repo, f"{WORKTREE_PREFIX}{slug}-{_repo_tag(repo)}", root)
1553
1556
 
1554
1557
 
1555
1558
  def _repo_tag(repo: Path) -> str:
@@ -118,7 +118,9 @@ def _hash(text: str) -> str:
118
118
 
119
119
  def read_relay(root: Path, name: str) -> tuple[str, str | None]:
120
120
  artifact, path = _path(root, name)
121
- if not path.resolve().is_relative_to(artifact.resolve()) or path.is_symlink():
121
+ # Only a relay that EXISTS can point elsewhere; resolving one that does not exist yet raced a concurrent writer
122
+ # creating its folder (see caddis_todo._path, #103).
123
+ if path.is_symlink() or (path.exists() and not path.resolve().is_relative_to(artifact.resolve())):
122
124
  raise ValueError("relay path escapes artifact root")
123
125
  try:
124
126
  text = path.read_bytes().decode("utf-8")
@@ -0,0 +1,109 @@
1
+ """Where a NEW git worktree goes. One rule, one place; every caller and every doc uses it.
2
+
3
+ Never %TEMP%: Windows cleanup / Storage Sense can empty it and delete a worktree that holds unmerged work (owner
4
+ decision, 20 Oct 2026). The path is
5
+
6
+ <root>/<main checkout folder name>/<short-name>
7
+
8
+ where <root> is, in order:
9
+ 1. the `root` argument (a test seam and an explicit override);
10
+ 2. the `CADDIS_WORKTREE_ROOT` environment variable, when set and not blank;
11
+ 3. `<parent folder of the repo's MAIN checkout>/.worktrees`. For `E:\\Projects\\<repo>` that is
12
+ `E:\\Projects\\.worktrees\\<repo>\\<short-name>`. No machine path is written in code. There is no %TEMP% fallback.
13
+
14
+ The main checkout is the parent of `git rev-parse --git-common-dir`, so the answer is the same from the main checkout,
15
+ from a subfolder and from any linked worktree.
16
+
17
+ Only the default for a NEW worktree lives here. A worktree that already exists, or whose path a runner recorded,
18
+ stays where it is: moving it breaks a venv, which keeps absolute paths.
19
+
20
+ worktree_root(repo, root=None) -> the folder that holds the per-repo folders
21
+ worktree_path(repo, short_name, root=None) -> the path for a new worktree (nothing is created)
22
+ main_checkout(repo) -> the main checkout's folder
23
+
24
+ CLI: `python caddis_worktree_root.py [--repo PATH] [--root PATH] <short-name>` prints the path. Exit 2 on a bad name
25
+ or a folder that is not in a git repo.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import argparse
31
+ import os
32
+ import subprocess
33
+ import sys
34
+ from pathlib import Path
35
+
36
+ ENV_VAR = "CADDIS_WORKTREE_ROOT"
37
+ FALLBACK_DIR = ".worktrees"
38
+
39
+
40
+ class NotARepoError(RuntimeError):
41
+ """The folder is not inside a git repository."""
42
+
43
+
44
+ def _git_out(cwd: Path, *args: str) -> str | None:
45
+ try:
46
+ res = subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True, encoding="utf-8",
47
+ errors="replace", stdin=subprocess.DEVNULL, check=False)
48
+ except OSError:
49
+ return None
50
+ return res.stdout.strip() if res.returncode == 0 else None
51
+
52
+
53
+ def main_checkout(repo: Path | str) -> Path:
54
+ """The MAIN checkout's folder for `repo` (the main checkout, a subfolder of it or a linked worktree).
55
+
56
+ It is the parent of the git common dir. A common dir not named `.git` (a submodule's, a bare repo's) has no such
57
+ parent: then the work-tree top is used, or the common dir itself when there is no work tree."""
58
+ repo = Path(repo).absolute()
59
+ common = _git_out(repo, "rev-parse", "--git-common-dir") if repo.is_dir() else None
60
+ if not common:
61
+ raise NotARepoError(f"{repo} is not inside a git repository")
62
+ common_dir = Path(os.path.normpath(repo / common)) # relative to `repo`, or absolute already
63
+ if common_dir.name == ".git":
64
+ return common_dir.parent
65
+ top = _git_out(repo, "rev-parse", "--show-toplevel")
66
+ return Path(os.path.normpath(top)) if top else common_dir
67
+
68
+
69
+ def _explicit_root(root: Path | str | None) -> Path | None:
70
+ """`root`, else `$CADDIS_WORKTREE_ROOT` when not blank, else None (the fallback needs the main checkout)."""
71
+ if root:
72
+ return Path(root)
73
+ env = os.environ.get(ENV_VAR, "").strip()
74
+ return Path(env) if env else None
75
+
76
+
77
+ def worktree_root(repo: Path | str, root: Path | str | None = None) -> Path:
78
+ """The folder that holds one folder per repo: `root`, else `$CADDIS_WORKTREE_ROOT`, else
79
+ `<parent of the main checkout>/.worktrees`."""
80
+ return _explicit_root(root) or main_checkout(repo).parent / FALLBACK_DIR
81
+
82
+
83
+ def worktree_path(repo: Path | str, short_name: str, root: Path | str | None = None) -> Path:
84
+ """The path for a NEW worktree of `repo`: `<root>/<main checkout folder name>/<short_name>`. Creates nothing.
85
+ `short_name` is one plain folder name: ValueError for an empty name, `.`, `..`, or one with a path separator or a
86
+ colon (a drive such as `C:foo` would replace the root on Windows)."""
87
+ if (not short_name or short_name in (".", "..") or any(c in short_name for c in "/\:")
88
+ or short_name != short_name.strip()):
89
+ raise ValueError(f"the short name must be one plain folder name, not {short_name!r}")
90
+ main = main_checkout(repo) # one git call
91
+ return (_explicit_root(root) or main.parent / FALLBACK_DIR) / main.name / short_name
92
+
93
+
94
+ def main(argv: list[str] | None = None) -> int:
95
+ ap = argparse.ArgumentParser(description="Print the path for a new git worktree (never under %TEMP%).")
96
+ ap.add_argument("short_name", help="one folder name, e.g. fix-login")
97
+ ap.add_argument("--repo", default=".", help="any folder of the repo or of one of its worktrees (default: .)")
98
+ ap.add_argument("--root", default=None, help=f"override the root (default: ${ENV_VAR}, then <repo parent>/.worktrees)")
99
+ args = ap.parse_args(argv)
100
+ try:
101
+ print(worktree_path(args.repo, args.short_name, args.root))
102
+ except (ValueError, NotARepoError) as exc:
103
+ print(f"error: {exc}", file=sys.stderr)
104
+ return 2
105
+ return 0
106
+
107
+
108
+ if __name__ == "__main__":
109
+ sys.exit(main())
@@ -37,10 +37,10 @@ It prints `SHARED: yes <reason>` or `SHARED: no` and exits 0. It says yes when t
37
37
 
38
38
  - **On `SHARED: no`,** do the steps as written, in this checkout.
39
39
  - **On `SHARED: yes`,** never move this checkout. Leave its changes as they are, and report them. Instead:
40
- - **Rebase:** `git worktree add --detach "$TEMP/<repo>-rebase" <branch>`, then
40
+ - **Rebase:** `git worktree add --detach "$(python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_worktree_root.py" rebase-<name>)" <branch>`, then
41
41
  `git -C <worktree> rebase origin/<default>` and `git -C <worktree> push --force-with-lease origin HEAD:<branch>`.
42
42
  - **Pull a branch to test it:** `git fetch origin`, then
43
- `git worktree add --detach "$TEMP/<repo>-<branch>" origin/<branch>` (for the default branch,
43
+ `git worktree add --detach "$(python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_worktree_root.py" test-<name>)" origin/<branch>` (for the default branch,
44
44
  `origin/<default>`), and run the gates there. Install dependencies fresh; never link `node_modules` or a venv.
45
45
  - **Clean up after a merge:** do not switch or pull. Run `git fetch --prune`, then
46
46
  `git fetch origin <default>:<default>`: it fast-forwards the default branch with no checkout. Git refuses when a
@@ -48,7 +48,9 @@ It prints `SHARED: yes <reason>` or `SHARED: no` and exits 0. It says yes when t
48
48
  checkout has it out.
49
49
  - Remove each worktree you made when its step ends: `git worktree remove <worktree>`.
50
50
 
51
- `$TEMP` is `%TEMP%` (`$env:TEMP` in PowerShell). The caddis guard checks only the `Shared checkout: yes` line in
51
+ `<name>` is the branch name with each `/` written as `-`. `caddis_worktree_root.py` prints the path of a new worktree:
52
+ `$CADDIS_WORKTREE_ROOT/<repo>/<short-name>`, else `<parent of the main checkout>/.worktrees/<repo>/<short-name>`. Never `%TEMP%`: Windows
53
+ cleanup can empty it. The caddis guard checks only the `Shared checkout: yes` line in
52
54
  `AGENTS.md`, not a dirty tree: with that line, it asks before a tree move, and denies it in a headless session.
53
55
  <!-- /checkout-safety -->
54
56
 
@@ -43,10 +43,10 @@ It prints `SHARED: yes <reason>` or `SHARED: no` and exits 0. It says yes when t
43
43
 
44
44
  - **On `SHARED: no`,** do the steps as written, in this checkout.
45
45
  - **On `SHARED: yes`,** never move this checkout. Leave its changes as they are, and report them. Instead:
46
- - **Rebase:** `git worktree add --detach "$TEMP/<repo>-rebase" <branch>`, then
46
+ - **Rebase:** `git worktree add --detach "$(python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_worktree_root.py" rebase-<name>)" <branch>`, then
47
47
  `git -C <worktree> rebase origin/<default>` and `git -C <worktree> push --force-with-lease origin HEAD:<branch>`.
48
48
  - **Pull a branch to test it:** `git fetch origin`, then
49
- `git worktree add --detach "$TEMP/<repo>-<branch>" origin/<branch>` (for the default branch,
49
+ `git worktree add --detach "$(python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_worktree_root.py" test-<name>)" origin/<branch>` (for the default branch,
50
50
  `origin/<default>`), and run the gates there. Install dependencies fresh; never link `node_modules` or a venv.
51
51
  - **Clean up after a merge:** do not switch or pull. Run `git fetch --prune`, then
52
52
  `git fetch origin <default>:<default>`: it fast-forwards the default branch with no checkout. Git refuses when a
@@ -54,7 +54,9 @@ It prints `SHARED: yes <reason>` or `SHARED: no` and exits 0. It says yes when t
54
54
  checkout has it out.
55
55
  - Remove each worktree you made when its step ends: `git worktree remove <worktree>`.
56
56
 
57
- `$TEMP` is `%TEMP%` (`$env:TEMP` in PowerShell). The caddis guard checks only the `Shared checkout: yes` line in
57
+ `<name>` is the branch name with each `/` written as `-`. `caddis_worktree_root.py` prints the path of a new worktree:
58
+ `$CADDIS_WORKTREE_ROOT/<repo>/<short-name>`, else `<parent of the main checkout>/.worktrees/<repo>/<short-name>`. Never `%TEMP%`: Windows
59
+ cleanup can empty it. The caddis guard checks only the `Shared checkout: yes` line in
58
60
  `AGENTS.md`, not a dirty tree: with that line, it asks before a tree move, and denies it in a headless session.
59
61
  <!-- /checkout-safety -->
60
62
 
@@ -154,7 +154,7 @@ unattended; `-Plan <slug>` runs one plain plan instead (the two are exclusive).
154
154
  or the owner, or that has **no Lane line**, is not run (fail closed): the runner adds
155
155
  `block --type owner-gate --reason "phase <n> lane: <lane>"` (`lane: none` when there is no line) and goes on.
156
156
  3. A `todo` milestone is set `in-progress` (a docs PR). Every session of a roadmap runs in ONE dedicated worktree,
157
- `%TEMP%/caddis-roadmap-<slug>-<8 hex of the repo's git common dir>`: made with `git worktree add --detach` from `origin/<default>` on the first
157
+ `<worktree root>/<repo>/caddis-roadmap-<slug>-<8 hex of the repo's git common dir>` (`$CADDIS_WORKTREE_ROOT`, else the `.worktrees` folder beside the main checkout, never `%TEMP%`; an existing recorded worktree stays where it is): made with `git worktree add --detach` from `origin/<default>` on the first
158
158
  session, reused on restart, recorded in the runner folder, never the `-Repo` checkout (plan mode keeps working in
159
159
  the given checkout). In that worktree the runner makes a fresh branch `caddis/roadmap/<slug>/<plan>-p<n>` from
160
160
  `origin/<default>` (it refuses a branch of that name already on origin that it did not make) and ONE session runs
@@ -42,10 +42,10 @@ It prints `SHARED: yes <reason>` or `SHARED: no` and exits 0. It says yes when t
42
42
 
43
43
  - **On `SHARED: no`,** do the steps as written, in this checkout.
44
44
  - **On `SHARED: yes`,** never move this checkout. Leave its changes as they are, and report them. Instead:
45
- - **Rebase:** `git worktree add --detach "$TEMP/<repo>-rebase" <branch>`, then
45
+ - **Rebase:** `git worktree add --detach "$(python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_worktree_root.py" rebase-<name>)" <branch>`, then
46
46
  `git -C <worktree> rebase origin/<default>` and `git -C <worktree> push --force-with-lease origin HEAD:<branch>`.
47
47
  - **Pull a branch to test it:** `git fetch origin`, then
48
- `git worktree add --detach "$TEMP/<repo>-<branch>" origin/<branch>` (for the default branch,
48
+ `git worktree add --detach "$(python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_worktree_root.py" test-<name>)" origin/<branch>` (for the default branch,
49
49
  `origin/<default>`), and run the gates there. Install dependencies fresh; never link `node_modules` or a venv.
50
50
  - **Clean up after a merge:** do not switch or pull. Run `git fetch --prune`, then
51
51
  `git fetch origin <default>:<default>`: it fast-forwards the default branch with no checkout. Git refuses when a
@@ -53,7 +53,9 @@ It prints `SHARED: yes <reason>` or `SHARED: no` and exits 0. It says yes when t
53
53
  checkout has it out.
54
54
  - Remove each worktree you made when its step ends: `git worktree remove <worktree>`.
55
55
 
56
- `$TEMP` is `%TEMP%` (`$env:TEMP` in PowerShell). The caddis guard checks only the `Shared checkout: yes` line in
56
+ `<name>` is the branch name with each `/` written as `-`. `caddis_worktree_root.py` prints the path of a new worktree:
57
+ `$CADDIS_WORKTREE_ROOT/<repo>/<short-name>`, else `<parent of the main checkout>/.worktrees/<repo>/<short-name>`. Never `%TEMP%`: Windows
58
+ cleanup can empty it. The caddis guard checks only the `Shared checkout: yes` line in
57
59
  `AGENTS.md`, not a dirty tree: with that line, it asks before a tree move, and denies it in a headless session.
58
60
  <!-- /checkout-safety -->
59
61
 
@@ -41,10 +41,10 @@ It prints `SHARED: yes <reason>` or `SHARED: no` and exits 0. It says yes when t
41
41
 
42
42
  - **On `SHARED: no`,** do the steps as written, in this checkout.
43
43
  - **On `SHARED: yes`,** never move this checkout. Leave its changes as they are, and report them. Instead:
44
- - **Rebase:** `git worktree add --detach "$TEMP/<repo>-rebase" <branch>`, then
44
+ - **Rebase:** `git worktree add --detach "$(python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_worktree_root.py" rebase-<name>)" <branch>`, then
45
45
  `git -C <worktree> rebase origin/<default>` and `git -C <worktree> push --force-with-lease origin HEAD:<branch>`.
46
46
  - **Pull a branch to test it:** `git fetch origin`, then
47
- `git worktree add --detach "$TEMP/<repo>-<branch>" origin/<branch>` (for the default branch,
47
+ `git worktree add --detach "$(python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_worktree_root.py" test-<name>)" origin/<branch>` (for the default branch,
48
48
  `origin/<default>`), and run the gates there. Install dependencies fresh; never link `node_modules` or a venv.
49
49
  - **Clean up after a merge:** do not switch or pull. Run `git fetch --prune`, then
50
50
  `git fetch origin <default>:<default>`: it fast-forwards the default branch with no checkout. Git refuses when a
@@ -52,7 +52,9 @@ It prints `SHARED: yes <reason>` or `SHARED: no` and exits 0. It says yes when t
52
52
  checkout has it out.
53
53
  - Remove each worktree you made when its step ends: `git worktree remove <worktree>`.
54
54
 
55
- `$TEMP` is `%TEMP%` (`$env:TEMP` in PowerShell). The caddis guard checks only the `Shared checkout: yes` line in
55
+ `<name>` is the branch name with each `/` written as `-`. `caddis_worktree_root.py` prints the path of a new worktree:
56
+ `$CADDIS_WORKTREE_ROOT/<repo>/<short-name>`, else `<parent of the main checkout>/.worktrees/<repo>/<short-name>`. Never `%TEMP%`: Windows
57
+ cleanup can empty it. The caddis guard checks only the `Shared checkout: yes` line in
56
58
  `AGENTS.md`, not a dirty tree: with that line, it asks before a tree move, and denies it in a headless session.
57
59
  <!-- /checkout-safety -->
58
60
 
@@ -31,13 +31,20 @@ If none is true, stop here and use `git switch -c`.
31
31
  1. Fetch: `git fetch origin`.
32
32
  2. Find the default branch (`main` in most repos): `git symbolic-ref --short refs/remotes/origin/HEAD`.
33
33
  It prints `origin/main`. Use the part after `origin/` as `<default>`.
34
- 3. Create the worktree from `origin/<default>`, never from the local HEAD, under `%TEMP%`:
34
+ 3. Create the worktree from `origin/<default>`, never from the local HEAD, at
35
+ `<worktree root>\<repo>\<short-name>`. Never under `%TEMP%`: Windows cleanup (Storage Sense) can empty it
36
+ and delete a worktree that holds unmerged work. The worktree root is `$env:CADDIS_WORKTREE_ROOT` when set,
37
+ else the `.worktrees` folder beside the main checkout (for `E:\Projects\<repo>` that is
38
+ `E:\Projects\.worktrees\<repo>\<short-name>`). `<repo>` is the main checkout's folder name. The caddis
39
+ script `caddis_worktree_root.py` prints the path; use it so the rule lives in one place:
35
40
 
36
41
  ```powershell
37
- git worktree add -b <branch> "$env:TEMP\<repo>-<short-name>" origin/<default>
42
+ $wt = python "<caddis scripts>\caddis_worktree_root.py" <short-name> # run in the repo; prints the path
43
+ git worktree add -b <branch> $wt origin/<default>
38
44
  ```
39
45
 
40
- Do not create worktrees inside the repo (no `.worktrees/`).
46
+ `<caddis scripts>` is the `scripts` folder of the installed caddis plugin. Do not create worktrees inside the
47
+ repo. An existing worktree stays where it is: moving it breaks a venv, which keeps absolute paths.
41
48
  4. State the full path of the worktree. Work only there, and do not switch branches in it.
42
49
 
43
50
  ## Install dependencies fresh
@@ -31,13 +31,20 @@ If none is true, stop here and use `git switch -c`.
31
31
  1. Fetch: `git fetch origin`.
32
32
  2. Find the default branch (`main` in most repos): `git symbolic-ref --short refs/remotes/origin/HEAD`.
33
33
  It prints `origin/main`. Use the part after `origin/` as `<default>`.
34
- 3. Create the worktree from `origin/<default>`, never from the local HEAD, under `%TEMP%`:
34
+ 3. Create the worktree from `origin/<default>`, never from the local HEAD, at
35
+ `<worktree root>\<repo>\<short-name>`. Never under `%TEMP%`: Windows cleanup (Storage Sense) can empty it
36
+ and delete a worktree that holds unmerged work. The worktree root is `$env:CADDIS_WORKTREE_ROOT` when set,
37
+ else the `.worktrees` folder beside the main checkout (for `E:\Projects\<repo>` that is
38
+ `E:\Projects\.worktrees\<repo>\<short-name>`). `<repo>` is the main checkout's folder name. The caddis
39
+ script `caddis_worktree_root.py` prints the path; use it so the rule lives in one place:
35
40
 
36
41
  ```powershell
37
- git worktree add -b <branch> "$env:TEMP\<repo>-<short-name>" origin/<default>
42
+ $wt = python "<caddis scripts>\caddis_worktree_root.py" <short-name> # run in the repo; prints the path
43
+ git worktree add -b <branch> $wt origin/<default>
38
44
  ```
39
45
 
40
- Do not create worktrees inside the repo (no `.worktrees/`).
46
+ `<caddis scripts>` is the `scripts` folder of the installed caddis plugin. Do not create worktrees inside the
47
+ repo. An existing worktree stays where it is: moving it breaks a venv, which keeps absolute paths.
41
48
  4. State the full path of the worktree. Work only there, and do not switch branches in it.
42
49
 
43
50
  ## Install dependencies fresh
@@ -154,7 +154,7 @@ unattended; `-Plan <slug>` runs one plain plan instead (the two are exclusive).
154
154
  or the owner, or that has **no Lane line**, is not run (fail closed): the runner adds
155
155
  `block --type owner-gate --reason "phase <n> lane: <lane>"` (`lane: none` when there is no line) and goes on.
156
156
  3. A `todo` milestone is set `in-progress` (a docs PR). Every session of a roadmap runs in ONE dedicated worktree,
157
- `%TEMP%/caddis-roadmap-<slug>-<8 hex of the repo's git common dir>`: made with `git worktree add --detach` from `origin/<default>` on the first
157
+ `<worktree root>/<repo>/caddis-roadmap-<slug>-<8 hex of the repo's git common dir>` (`$CADDIS_WORKTREE_ROOT`, else the `.worktrees` folder beside the main checkout, never `%TEMP%`; an existing recorded worktree stays where it is): made with `git worktree add --detach` from `origin/<default>` on the first
158
158
  session, reused on restart, recorded in the runner folder, never the `-Repo` checkout (plan mode keeps working in
159
159
  the given checkout). In that worktree the runner makes a fresh branch `caddis/roadmap/<slug>/<plan>-p<n>` from
160
160
  `origin/<default>` (it refuses a branch of that name already on origin that it did not make) and ONE session runs
@@ -1,7 +1,7 @@
1
1
  {
2
- "poolVersion": "1.3.148",
2
+ "poolVersion": "1.3.150",
3
3
  "bundles": {
4
- "antigravity-plugin": "1.3.148",
4
+ "antigravity-plugin": "1.3.150",
5
5
  "antigravity-plugin-extras": "1.3.45",
6
6
  "codex": "unknown"
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caddis/cli",
3
- "version": "0.4.62",
3
+ "version": "0.4.64",
4
4
  "description": "Install, update and diagnose the caddis dev harness across every coding agent you use (Claude Code, agy/Antigravity, and more).",
5
5
  "keywords": [
6
6
  "caddis",