@caddis/cli 0.4.63 → 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.
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
|
@@ -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
|
|
2346
|
-
raise LaneConfigError(f"refusing packet worktree
|
|
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
|
-
|
|
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):
|
|
@@ -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
|
-
|
|
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")
|
package/bundles/manifest.json
CHANGED
package/package.json
CHANGED