@caddis/cli 0.4.53 → 0.4.55

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 (32) hide show
  1. package/bundles/antigravity-plugin/guard_agy.py +184 -0
  2. package/bundles/antigravity-plugin/plugin.json +1 -1
  3. package/bundles/antigravity-plugin/scripts/caddis_budget.py +283 -0
  4. package/bundles/antigravity-plugin/scripts/caddis_checkout_guard.py +5 -1
  5. package/bundles/antigravity-plugin/scripts/caddis_fleet.py +439 -0
  6. package/bundles/antigravity-plugin/scripts/caddis_gate.py +29 -0
  7. package/bundles/antigravity-plugin/scripts/caddis_lanes.py +127 -10
  8. package/bundles/antigravity-plugin/scripts/caddis_notify.py +5 -1
  9. package/bundles/antigravity-plugin/scripts/caddis_roadmap.py +56 -3
  10. package/bundles/antigravity-plugin/scripts/caddis_roadmap_page.py +676 -0
  11. package/bundles/antigravity-plugin/scripts/caddis_roadmap_run.py +39 -6
  12. package/bundles/antigravity-plugin/scripts/caddis_routing.py +4 -4
  13. package/bundles/antigravity-plugin/skills/_registry.md +2 -0
  14. package/bundles/antigravity-plugin/skills/catchup/SKILL.md +4 -0
  15. package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +6 -0
  16. package/bundles/antigravity-plugin/skills/fleet-close/SKILL.md +167 -0
  17. package/bundles/antigravity-plugin/skills/fleet-open/SKILL.md +110 -0
  18. package/bundles/antigravity-plugin/skills/gh-cli/SKILL.md +2 -1
  19. package/bundles/antigravity-plugin/skills/handoff/SKILL.md +101 -1
  20. package/bundles/antigravity-plugin/skills/migrate-dir/SKILL.md +3 -2
  21. package/bundles/antigravity-plugin/skills/pr-merge/SKILL.md +20 -6
  22. package/bundles/antigravity-plugin/skills/refactoring/SKILL.md +2 -1
  23. package/bundles/antigravity-plugin/skills/review-pr/SKILL.md +1 -1
  24. package/bundles/antigravity-plugin/skills/roadmap/SKILL.md +1 -0
  25. package/bundles/antigravity-plugin/skills/ship/SKILL.md +2 -2
  26. package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +9 -5
  27. package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +3 -2
  28. package/bundles/codex/.codex/skills/coding/refactoring/SKILL.md +2 -1
  29. package/bundles/codex/.codex/skills/devops/gh-cli/SKILL.md +2 -1
  30. package/bundles/codex/.codex/skills/workflow/roadmap/SKILL.md +1 -0
  31. package/bundles/manifest.json +2 -2
  32. package/package.json +1 -1
@@ -85,7 +85,11 @@ def push(message: str, *, run: Runner = subprocess.run, claude: Sequence[str] |
85
85
  """The fallback: a short `claude -p` call whose only task is the push notification. The prompt goes on stdin: on
86
86
  Windows `claude` is a `.cmd` shim that cuts a multi-line argument at its first newline."""
87
87
  claude = list(claude) if claude else [shutil.which("claude") or "claude"]
88
- cmd = [*claude, "-p", "--output-format", "json", "--max-budget-usd", PUSH_BUDGET_USD]
88
+ # An explicit, least mode: the only task is one PushNotification call. Since Claude Code 2.1.302 a headless run with
89
+ # no mode depends on the provider and the machine's settings; this one must not inherit bypassPermissions.
90
+ # `--allowedTools PushNotification`: in `default` mode a tool that asks would stall a print session with no human.
91
+ cmd = [*claude, "-p", "--permission-mode", "default", "--allowedTools", "PushNotification",
92
+ "--output-format", "json", "--max-budget-usd", PUSH_BUDGET_USD]
89
93
  try:
90
94
  proc = run(cmd, input=push_prompt(message), capture_output=True, text=True, encoding="utf-8",
91
95
  errors="replace", timeout=300)
@@ -100,6 +100,7 @@ EXIT_CHECK_TIMEOUT = 300
100
100
  RECHECK_TIMEOUT = 120
101
101
  LOCK_TIMEOUT = 30
102
102
  LOCK_MALFORMED_STALE_S = 60 # an empty or non-JSON lock file older than this is stale
103
+ _WINDOWS = os.name == "nt" # a create can fail with PermissionError while a deleted lock's delete is pending
103
104
  OUTPUT_TAIL_BYTES = 64 * 1024
104
105
  OUTPUT_FILE_CAP = 1024 * 1024 # a check's on-disk output never grows past this
105
106
  GIT_NET_TIMEOUT = 120
@@ -1273,12 +1274,19 @@ class _Lock:
1273
1274
  def __init__(self, path: Path, timeout: float | None = None):
1274
1275
  self.path, self.fd = path, None
1275
1276
  self.timeout = LOCK_TIMEOUT if timeout is None else timeout
1277
+ self.denied: PermissionError | None = None # the last Windows "busy" refusal, reported on timeout
1276
1278
 
1277
1279
  def _try(self) -> bool:
1278
1280
  try:
1279
1281
  self.fd = os.open(str(self.path), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
1280
1282
  except FileExistsError:
1281
1283
  return False
1284
+ except PermissionError as exc:
1285
+ # Windows refuses a create while the last holder's delete is still pending: busy, not an error.
1286
+ if not _WINDOWS:
1287
+ raise
1288
+ self.denied = exc
1289
+ return False
1282
1290
  os.write(self.fd, json.dumps({"pid": os.getpid(), "host": socket.gethostname(),
1283
1291
  "started_utc": _utc()}).encode())
1284
1292
  return True
@@ -1337,7 +1345,8 @@ class _Lock:
1337
1345
  if self._take_over():
1338
1346
  print(f"took over a stale lock {self.path}", file=sys.stderr)
1339
1347
  return self
1340
- raise RoadmapError(f"{self.path} is held by another write after {self.timeout:.0f} s",
1348
+ why = f" (last refusal: {self.denied})" if self.denied else ""
1349
+ raise RoadmapError(f"{self.path} is held by another write after {self.timeout:.0f} s{why}",
1341
1350
  caddis_exit.NOT_RUN)
1342
1351
  time.sleep(0.2)
1343
1352
  return self
@@ -1943,14 +1952,25 @@ def _repo_root(raw: str) -> Path:
1943
1952
 
1944
1953
  def list_roadmaps(root: Path) -> list[str]:
1945
1954
  """Slugs of the roadmaps on origin's default branch: a folder under .caddis/roadmaps/ that holds roadmap.md."""
1955
+ return list_roadmaps_checked(root)[0]
1956
+
1957
+
1958
+ def list_roadmaps_checked(root: Path) -> tuple[list[str], set[str] | None]:
1959
+ """(slugs, every folder name under .caddis/roadmaps/ on origin). The folder set is None when git could not list
1960
+ the tree: a caller that deletes by "not on origin" (the page prune) must then delete nothing."""
1946
1961
  branch = fetch(root)
1947
1962
  tree = _git(root, "ls-tree", "--name-only", f"origin/{branch}", ".caddis/roadmaps/", check=False)
1963
+ folders: set[str] | None = set() if tree.returncode == 0 else None
1948
1964
  slugs: list[str] = []
1949
1965
  for folder in tree.stdout.splitlines():
1950
1966
  folder = folder.strip().rstrip("/")
1951
- if folder and _git(root, "cat-file", "-e", f"origin/{branch}:{folder}/roadmap.md", check=False).returncode == 0:
1967
+ if not folder:
1968
+ continue
1969
+ if folders is not None:
1970
+ folders.add(folder.rsplit("/", 1)[-1])
1971
+ if _git(root, "cat-file", "-e", f"origin/{branch}:{folder}/roadmap.md", check=False).returncode == 0:
1952
1972
  slugs.append(folder.rsplit("/", 1)[-1])
1953
- return sorted(slugs)
1973
+ return sorted(slugs), folders
1954
1974
 
1955
1975
 
1956
1976
  def _build_parser() -> argparse.ArgumentParser:
@@ -1992,6 +2012,12 @@ def _build_parser() -> argparse.ArgumentParser:
1992
2012
  p = add("rollup", write=True)
1993
2013
  p.add_argument("--id", required=True)
1994
2014
  add("recheck", write=True)
2015
+ p = sub.add_parser("render")
2016
+ which = p.add_mutually_exclusive_group(required=True)
2017
+ which.add_argument("--roadmap")
2018
+ which.add_argument("--all", action="store_true", help="every roadmap on origin (used by hooks and commands)")
2019
+ p.add_argument("--root", default=".")
2020
+ p.add_argument("--out", default=None, help="the HTML file (default ~/.caddis/roadmap-pages/<repo>/<slug>.html)")
1995
2021
  return parser
1996
2022
 
1997
2023
 
@@ -2036,6 +2062,33 @@ def _dispatch(args: argparse.Namespace) -> int:
2036
2062
  return rollup(root, args.roadmap, args.id, args.dry_run)
2037
2063
  if args.cmd == "recheck":
2038
2064
  return recheck(root, args.roadmap, args.dry_run)
2065
+ if args.cmd == "render":
2066
+ import caddis_roadmap_page # the page is separate; only `render` needs it
2067
+ if args.all and args.out:
2068
+ raise RoadmapError("--out works with one --roadmap, not with --all", caddis_exit.ERROR)
2069
+ slugs, on_origin = list_roadmaps_checked(root) if args.all else ([args.roadmap], None)
2070
+ failed = written = pruned = 0
2071
+ for slug in slugs: # --all: one bad roadmap must not stop the others
2072
+ try:
2073
+ path = caddis_roadmap_page.write_page(root, slug, Path(args.out) if args.out else None,
2074
+ sys.modules[__name__])
2075
+ written += 1
2076
+ print(f"[ROADMAP] page written: {path.resolve()}")
2077
+ except Exception as exc: # --all: one roadmap must never stop the others
2078
+ if not args.all:
2079
+ raise
2080
+ failed += 1
2081
+ print(f"[ROADMAP] {slug}: page not written: {exc}", file=sys.stderr)
2082
+ try: # the hub is a convenience: nothing below ever fails a render or changes its exit code
2083
+ if args.all and on_origin is not None: # a roadmap removed from origin leaves the hub too; a listing
2084
+ # that failed removes nothing, and a folder still on origin keeps its page even if its render failed
2085
+ pruned = caddis_roadmap_page.prune_folder(
2086
+ caddis_roadmap_page.default_page_path(root, "_").parent, on_origin | set(slugs))
2087
+ if not args.out and (written or pruned):
2088
+ print(f"[ROADMAP] hub written: {caddis_roadmap_page.write_hub().resolve()}")
2089
+ except Exception as exc:
2090
+ print(f"[ROADMAP] hub not written: {exc}", file=sys.stderr)
2091
+ return caddis_exit.BLOCKED if failed else caddis_exit.CLEAN
2039
2092
  return caddis_exit.ERROR
2040
2093
 
2041
2094