@caddis/cli 0.4.61 → 0.4.63

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.
Files changed (24) hide show
  1. package/bundles/antigravity-plugin/caddis_statusline.py +78 -6
  2. package/bundles/antigravity-plugin/guard_agy.py +2 -1
  3. package/bundles/antigravity-plugin/plugin.json +1 -1
  4. package/bundles/antigravity-plugin/repo_freshness.py +13 -1
  5. package/bundles/antigravity-plugin/scripts/caddis_hidden_desktop.py +232 -0
  6. package/bundles/antigravity-plugin/scripts/caddis_lanes.py +3 -2
  7. package/bundles/antigravity-plugin/scripts/caddis_roadmap.py +62 -5
  8. package/bundles/antigravity-plugin/scripts/caddis_roadmap_page.py +306 -54
  9. package/bundles/antigravity-plugin/scripts/caddis_roadmap_run.py +10 -4
  10. package/bundles/antigravity-plugin/scripts/caddis_spend.py +25 -3
  11. package/bundles/antigravity-plugin/scripts/caddis_statusline.py +78 -6
  12. package/bundles/antigravity-plugin/scripts/caddis_todo.py +4 -1
  13. package/bundles/antigravity-plugin/scripts/caddis_workstreams.py +3 -1
  14. package/bundles/antigravity-plugin/scripts/caddis_worktree_root.py +109 -0
  15. package/bundles/antigravity-plugin/skills/cloud-phases/SKILL.md +5 -3
  16. package/bundles/antigravity-plugin/skills/pr-merge/SKILL.md +5 -3
  17. package/bundles/antigravity-plugin/skills/roadmap/SKILL.md +1 -1
  18. package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +5 -3
  19. package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +5 -3
  20. package/bundles/antigravity-plugin/skills/using-git-worktrees/SKILL.md +10 -3
  21. package/bundles/codex/.codex/skills/devops/using-git-worktrees/SKILL.md +10 -3
  22. package/bundles/codex/.codex/skills/workflow/roadmap/SKILL.md +1 -1
  23. package/bundles/manifest.json +2 -2
  24. package/package.json +1 -1
@@ -42,6 +42,8 @@ CLI:
42
42
  CADDIS_SPEND_RUN_TIMEOUT_S, else 3600 s) and its process tree was killed. 75 and 78 are never
43
43
  agy's own codes for a refusal, so a caller can tell "the cap refused" from "agy failed". Each
44
44
  refusal also prints a `caddis_spend:` line.
45
+ On Windows the command runs on a hidden desktop (caddis_hidden_desktop.py), so the console windows agy opens
46
+ for its tools never show; CADDIS_AGY_VISIBLE=1 runs it on the normal desktop.
45
47
  """
46
48
 
47
49
  from __future__ import annotations
@@ -50,6 +52,7 @@ import argparse
50
52
  import json
51
53
  import math
52
54
  import os
55
+ import shutil
53
56
  import subprocess
54
57
  import sys
55
58
  import time
@@ -385,11 +388,30 @@ def _kill_tree(proc: subprocess.Popen) -> None:
385
388
  pass
386
389
 
387
390
 
391
+ HIDDEN_DESKTOP_SCRIPT = Path(__file__).with_name("caddis_hidden_desktop.py")
392
+ AGY_VISIBLE_VAR = "CADDIS_AGY_VISIBLE"
393
+
394
+
395
+ def hidden_argv(argv: list[str], env: dict | None = None) -> list[str]:
396
+ """`argv` wrapped to run on the hidden desktop (caddis_hidden_desktop.py), so the console windows agy opens for
397
+ its tools never show on the owner's screen. Unchanged when: not Windows, env CADDIS_AGY_VISIBLE=1, the launcher
398
+ is not beside this file, or the program is not found. In the last case the caller's own start raises
399
+ FileNotFoundError as before; the program is resolved here, so the launcher never has to report it."""
400
+ env = os.environ if env is None else env
401
+ if os.name != "nt" or not argv or env.get(AGY_VISIBLE_VAR) == "1" or not HIDDEN_DESKTOP_SCRIPT.is_file():
402
+ return list(argv)
403
+ program = shutil.which(argv[0], path=env.get("PATH"))
404
+ if program is None:
405
+ return list(argv)
406
+ return [sys.executable, str(HIDDEN_DESKTOP_SCRIPT), "--", program, *argv[1:]]
407
+
408
+
388
409
  def _run_child(argv: list[str], env: dict, timeout_s: float) -> tuple[int | None, str, str]:
389
- """Run `argv` with a time limit. Returns (returncode, stdout, stderr); returncode is None on a timeout, after
390
- the whole process tree was killed. Raises FileNotFoundError when the program does not exist."""
410
+ """Run `argv` with a time limit, on the hidden desktop on Windows (hidden_argv). Returns (returncode, stdout,
411
+ stderr); returncode is None on a timeout, after the whole process tree was killed. Raises FileNotFoundError when
412
+ the program does not exist."""
391
413
  group = {} if os.name == "nt" else {"start_new_session": True}
392
- proc = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8",
414
+ proc = subprocess.Popen(hidden_argv(argv, env), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8",
393
415
  errors="replace", env=env, **_NO_WINDOW_KW, **group)
394
416
  try:
395
417
  out, err = proc.communicate(timeout=timeout_s)
@@ -71,8 +71,9 @@ DEFAULT_CONFIG = {
71
71
  "bar_width": 8,
72
72
  "token_counts": True, # ◉ 412k/1M — off falls back to a bare ctx:41%
73
73
  "cached_tokens": True, # ⚡38k cached — rendered LOUD, see render_claude()
74
- "ram": True, # 💻 69% (44 GB)
75
- "clock": True, # ◷ 03:41
74
+ "ram": False, # 💻 69% (44 GB) - off by default (owner, 20 Oct 2026)
75
+ "thinking": False, # Claude's "thinking" flag, agy's "thinking" state - off by default (20 Oct 2026)
76
+ "clock": False, # ◷ 03:41 - off by default (owner, 20 Oct 2026)
76
77
  "relay": True, # ⏺ relay (a parked resume pointer exists)
77
78
  "rate_limits": True, # 5h:12% 7d:40%
78
79
  "session_id": False, # #a1b2c3d4
@@ -468,7 +469,7 @@ def cached_segment(c: C, cfg: dict, cached: int) -> str:
468
469
 
469
470
 
470
471
  def ram_segment(c: C, cfg: dict) -> str:
471
- if not cfg.get("ram", True):
472
+ if not cfg.get("ram", False):
472
473
  return ""
473
474
  min_w = as_int(cfg.get("ram_min_width"), 0)
474
475
  if min_w and term_width() < min_w:
@@ -482,7 +483,7 @@ def ram_segment(c: C, cfg: dict) -> str:
482
483
 
483
484
 
484
485
  def clock_segment(c: C, cfg: dict) -> str:
485
- if not cfg.get("clock", True):
486
+ if not cfg.get("clock", False):
486
487
  return ""
487
488
  return c.dim + icon(cfg, "clock") + time.strftime("%H:%M") + c.reset
488
489
 
@@ -549,7 +550,7 @@ def render_claude(data: dict, cfg: dict) -> str:
549
550
  if style and str(style).lower() != "default":
550
551
  parts.append(c.blue + "style:" + str(style) + c.reset)
551
552
 
552
- if dig(data, "thinking", "enabled") is True:
553
+ if cfg.get("thinking", False) and dig(data, "thinking", "enabled") is True:
553
554
  parts.append(c.magenta + "thinking" + c.reset)
554
555
 
555
556
  if cfg.get("rate_limits", True):
@@ -581,7 +582,7 @@ def render_claude(data: dict, cfg: dict) -> str:
581
582
 
582
583
  session_name = dig(data, "session_name")
583
584
  if session_name:
584
- parts.append(c.dim + '"' + str(session_name) + '"' + c.reset)
585
+ parts.append(c.bold + c.magenta + str(session_name) + c.reset) # bright, so the session is easy to spot
585
586
 
586
587
  if cfg.get("session_id", False):
587
588
  sid = str(dig(data, "session_id") or "")
@@ -616,6 +617,8 @@ def render_agy(data: dict, cfg: dict) -> str:
616
617
  parts.append(c.white + c.bold + "agy" + c.reset)
617
618
 
618
619
  state = str(dig(data, "agent_state") or dig(data, "execution_mode") or "").lower()
620
+ if state == "thinking" and not cfg.get("thinking", False):
621
+ state = "" # working and idle still show
619
622
  if state:
620
623
  label, col = {
621
624
  "working": (icon(cfg, "bolt") + "working", c.green),
@@ -704,6 +707,61 @@ HOST_SETTINGS = {
704
707
  }
705
708
 
706
709
 
710
+ VERSION_STAMP = INSTALL_DIR / "statusline.version"
711
+
712
+
713
+ def plugin_version(here: Path | None = None) -> str:
714
+ """The caddis version of the plugin this file ships in: `.claude-plugin/plugin.json` or a root `plugin.json`
715
+ one folder above `scripts/`. "" in a source checkout or when unreadable."""
716
+ base = Path(here or Path(__file__).resolve().parent).parent
717
+ for manifest in (base / ".claude-plugin" / "plugin.json", base / "plugin.json"):
718
+ try:
719
+ data = json.loads(manifest.read_text(encoding="utf-8"))
720
+ except Exception:
721
+ continue
722
+ if isinstance(data, dict) and str(data.get("name", "")).startswith("caddis") and data.get("version"):
723
+ return str(data["version"])
724
+ return ""
725
+
726
+
727
+ def _version_key(text: str):
728
+ parts = str(text or "").strip().split(".")
729
+ return tuple(int(x) for x in parts) if parts and all(x.isdigit() for x in parts) else None
730
+
731
+
732
+ def refresh_installed(src: Path | None = None, version: str | None = None) -> bool:
733
+ """Bring the installed renderer (`~/.caddis/statusline.py`) up to this plugin's copy.
734
+
735
+ Why (20 Oct 2026): only `--install` copied the renderer, so a caddis update never reached it. This machine
736
+ kept a copy whose `git` calls opened a console window each time agy drew its status line.
737
+ Rules: nothing happens unless the status line was installed; the copy happens only when this plugin's version
738
+ is newer than the one stamped at the last copy (an older session never downgrades it); a source checkout
739
+ (no plugin version) never copies. Returns True when it copied."""
740
+ src = Path(src or Path(__file__).resolve())
741
+ version = plugin_version() if version is None else version
742
+ if not INSTALLED_SCRIPT.is_file() or _version_key(version) is None:
743
+ return False
744
+ try:
745
+ stamped = VERSION_STAMP.read_text(encoding="utf-8").strip()
746
+ except OSError:
747
+ stamped = ""
748
+ old = _version_key(stamped)
749
+ if old is not None and old >= _version_key(version):
750
+ return False
751
+ try:
752
+ if src.resolve() != INSTALLED_SCRIPT.resolve() and src.read_bytes() != INSTALLED_SCRIPT.read_bytes():
753
+ tmp = INSTALLED_SCRIPT.with_name("%s.%d.tmp" % (INSTALLED_SCRIPT.name, os.getpid()))
754
+ shutil.copyfile(str(src), str(tmp))
755
+ os.replace(str(tmp), str(INSTALLED_SCRIPT))
756
+ copied = True
757
+ else:
758
+ copied = False
759
+ VERSION_STAMP.write_text(version + "\n", encoding="utf-8")
760
+ return copied
761
+ except OSError:
762
+ return False # a session start must never fail over the status line
763
+
764
+
707
765
  def python_command() -> str:
708
766
  """The interpreter NAME to write into settings — not an absolute path.
709
767
 
@@ -726,6 +784,14 @@ def install(hosts, dry_run: bool = False) -> int:
726
784
  # because the version is in the path.
727
785
  if not dry_run and src != INSTALLED_SCRIPT.resolve():
728
786
  shutil.copyfile(str(src), str(INSTALLED_SCRIPT))
787
+ if not dry_run:
788
+ # Stamp the version, so a later --refresh from an OLDER plugin session cannot replace this copy.
789
+ version = plugin_version()
790
+ if _version_key(version) is not None:
791
+ try:
792
+ VERSION_STAMP.write_text(version + "\n", encoding="utf-8")
793
+ except OSError:
794
+ pass
729
795
  print("renderer -> %s" % INSTALLED_SCRIPT)
730
796
 
731
797
  if not CONFIG_PATH.exists():
@@ -903,8 +969,14 @@ def main(argv=None) -> int:
903
969
  parser.add_argument("--dry-run", action="store_true", help="with --install: print, write nothing")
904
970
  parser.add_argument("--no-color", action="store_true", help="render without ANSI colour")
905
971
  parser.add_argument("--title", action="store_true", help="render terminal title (agy)")
972
+ parser.add_argument("--refresh", action="store_true",
973
+ help="copy this plugin's renderer over an older installed one (session-start hook)")
906
974
  args = parser.parse_args(argv)
907
975
 
976
+ if args.refresh:
977
+ refresh_installed()
978
+ return 0 # silent and never failing: it runs at every session start
979
+
908
980
  # The line is Unicode. On Windows the default cp1252 stdout raises on ◆/⎇/█.
909
981
  try:
910
982
  sys.stdout.reconfigure(encoding="utf-8", errors="replace")
@@ -43,7 +43,10 @@ def _path(root: Path, workstream: str | None) -> tuple[Path, Path]:
43
43
  artifact = _artifact(root)
44
44
  name = _name(workstream) if workstream is not None else "inbox"
45
45
  folder = artifact / "todo-list"
46
- if folder.is_symlink() or not folder.resolve().is_relative_to(artifact.resolve()):
46
+ # Only a folder that EXISTS can point elsewhere. Resolving one that does not exist yet raced a concurrent
47
+ # writer creating it: on Windows the two paths could resolve differently (a short 8.3 name on one side), and a
48
+ # safe add failed with this error (main CI, 20 Oct 2026, test_concurrent_inbox_add_loses_no_item).
49
+ if folder.is_symlink() or (folder.exists() and not folder.resolve().is_relative_to(artifact.resolve())):
47
50
  raise ValueError("todo-list path escapes artifact root")
48
51
  path = folder / f"{name}.md"
49
52
  # Reads are unlocked, so refuse here as well as in locked_path. On Windows a writer replacing the file
@@ -58,7 +58,9 @@ def _plain_name(value: str) -> str:
58
58
  def _artifact(root: Path) -> Path:
59
59
  project = Path(root).resolve()
60
60
  artifact = Path(root) / ".caddis"
61
- if not artifact.resolve().is_relative_to(project) or artifact.is_symlink():
61
+ # Only an existing .caddis can point elsewhere; resolving one that does not exist yet raced a concurrent writer
62
+ # creating it (see caddis_todo._path, 20 Oct 2026).
63
+ if artifact.is_symlink() or (artifact.exists() and not artifact.resolve().is_relative_to(project)):
62
64
  raise ValueError("artifact root escapes project")
63
65
  return artifact
64
66
 
@@ -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.147",
2
+ "poolVersion": "1.3.149",
3
3
  "bundles": {
4
- "antigravity-plugin": "1.3.147",
4
+ "antigravity-plugin": "1.3.149",
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.61",
3
+ "version": "0.4.63",
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",