@caddis/cli 0.4.37 → 0.4.39
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.
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/scripts/caddis_gate.py +45 -10
- package/bundles/antigravity-plugin/scripts/caddis_inventory.py +2 -0
- package/bundles/antigravity-plugin/scripts/caddis_lanes.py +7 -1
- package/bundles/antigravity-plugin/scripts/caddis_workstreams.py +15 -1
- package/bundles/manifest.json +2 -2
- package/package.json +1 -1
|
@@ -108,6 +108,18 @@ _LANES_RUN = r'python\s+"?\$\{CLAUDE_PLUGIN_ROOT\}/scripts/caddis_lanes\.py"?\s+
|
|
|
108
108
|
|
|
109
109
|
# ── gate: lane-check ────────────────────────────────────────────────────────
|
|
110
110
|
|
|
111
|
+
_QUOTED = re.compile(r"\"[^\"]*\"|'[^']*'")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _has_print_flag(cmd: str) -> bool:
|
|
115
|
+
"""True when the command carries a real -p/--print flag.
|
|
116
|
+
|
|
117
|
+
The quoted prompt is blanked first: a prompt that merely contains the text " -p " must not
|
|
118
|
+
count, or an interactive child passes the gate (R3 cross-review, 2026-09-30).
|
|
119
|
+
"""
|
|
120
|
+
return bool(re.search(r"(?:^|\s)(?:-p|--print)(?:\s|$)", _QUOTED.sub('""', cmd)))
|
|
121
|
+
|
|
122
|
+
|
|
111
123
|
def gate_lane(plan: Path, phase: int) -> int:
|
|
112
124
|
if not plan.is_file():
|
|
113
125
|
return _degrade(f"no plan at {plan}")
|
|
@@ -157,12 +169,12 @@ def gate_lane(plan: Path, phase: int) -> int:
|
|
|
157
169
|
f"[caddis-gate] phase {phase}'s direct command names Phase {named.group(1)}: {cmd}\n"
|
|
158
170
|
" The lane would run another phase's work. Refusing to run it.\n")
|
|
159
171
|
return EXIT_MALFORMED
|
|
160
|
-
if cmd.startswith("agy") and not
|
|
172
|
+
if cmd.startswith("agy") and not _has_print_flag(cmd):
|
|
161
173
|
sys.stderr.write(
|
|
162
174
|
f"[caddis-gate] phase {phase}'s direct agy command has no -p/--print: {cmd}\n"
|
|
163
175
|
" The child would not run headless. Refusing to run it.\n")
|
|
164
176
|
return EXIT_MALFORMED
|
|
165
|
-
elif not
|
|
177
|
+
elif not _has_print_flag(cmd):
|
|
166
178
|
sys.stderr.write(
|
|
167
179
|
f"[caddis-gate] phase {phase}'s launch command has no -p/--print: {cmd}\n"
|
|
168
180
|
" Without it the child is not marked headless, reads the same Lane line, and spawns\n"
|
|
@@ -496,14 +508,37 @@ def gate_handover_check(doc: Path, repo_root: Path, max_chars: int | None = None
|
|
|
496
508
|
return EXIT_BLOCKED
|
|
497
509
|
|
|
498
510
|
missing: list[str] = []
|
|
499
|
-
|
|
500
|
-
#
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if not
|
|
506
|
-
|
|
511
|
+
try:
|
|
512
|
+
# One rule for both gates: `caddis_spawn.check_document` already treats a bare filename
|
|
513
|
+
# that matches no tracked file as a note (probably another repository's file), and one
|
|
514
|
+
# that matches tracked files as a block that names the full path. Two gates giving
|
|
515
|
+
# different answers to the same question is the defect this replaces.
|
|
516
|
+
here = str(Path(__file__).resolve().parent)
|
|
517
|
+
if here not in sys.path:
|
|
518
|
+
sys.path.insert(0, here)
|
|
519
|
+
import caddis_spawn
|
|
520
|
+
findings = caddis_spawn.check_document(text, repo_root)
|
|
521
|
+
missing = sorted(set(findings.missing_paths))
|
|
522
|
+
for name in findings.unresolved_names:
|
|
523
|
+
sys.stderr.write(
|
|
524
|
+
f"[caddis-gate] note: `{name}` is a bare filename that matches no tracked file here; "
|
|
525
|
+
"not checked\n")
|
|
526
|
+
except Exception as exc:
|
|
527
|
+
# Degrade to the plain existence rule, but say why: a bug inside the shared helper must
|
|
528
|
+
# not hide behind the fallback (an ImportError is the expected, quiet case).
|
|
529
|
+
if not isinstance(exc, ImportError):
|
|
530
|
+
sys.stderr.write(
|
|
531
|
+
f"[caddis-gate] note: shared path check failed ({type(exc).__name__}: {exc}); "
|
|
532
|
+
"using the plain existence rule\n")
|
|
533
|
+
missing = []
|
|
534
|
+
for raw in sorted(set(_DOC_PATH.findall(text))):
|
|
535
|
+
# A backtick often wraps a whole command; the path is the token that looks like one.
|
|
536
|
+
cand = next((tok for tok in raw.split() if "/" in tok or tok.endswith(".md")), raw)
|
|
537
|
+
cand = cand.strip("(),;:'\"")
|
|
538
|
+
if cand.startswith(("http://", "https://", "~", "$")) or ".." in cand:
|
|
539
|
+
continue
|
|
540
|
+
if not (repo_root / cand).exists():
|
|
541
|
+
missing.append(cand)
|
|
507
542
|
|
|
508
543
|
stale: list[dict] = []
|
|
509
544
|
try:
|
|
@@ -263,6 +263,8 @@ def stale_artifacts(root: Path) -> list[dict]:
|
|
|
263
263
|
target = root / m
|
|
264
264
|
if not target.is_file():
|
|
265
265
|
continue
|
|
266
|
+
if not _run(["git", "ls-files", "--", m], root):
|
|
267
|
+
continue # untracked or git-ignored: rebuilt locally, so an old commit says nothing
|
|
266
268
|
t_art = _run(["git", "log", "-1", "--format=%ct", "--", m], root)
|
|
267
269
|
t_gen = _run(["git", "log", "-1", "--format=%ct", "--", f"scripts/{script.name}"], root)
|
|
268
270
|
if not t_art or not t_gen:
|
|
@@ -992,7 +992,13 @@ def next_lane(
|
|
|
992
992
|
|
|
993
993
|
|
|
994
994
|
def _ci_watch_block(output: str) -> dict[str, str] | None:
|
|
995
|
-
"""Read the last complete watch report, ignoring an echoed prompt template.
|
|
995
|
+
"""Read the last complete watch report, ignoring an echoed prompt template.
|
|
996
|
+
|
|
997
|
+
The block must hold exactly RESULT, CHECKS, MERGE_STATE and FAILING, RESULT first. A reordered
|
|
998
|
+
or extended block returns None, and the caller then falls back to `gh` without saying why:
|
|
999
|
+
that is safe (the watch prompt pins the order) but invisible, so keep the prompt and this
|
|
1000
|
+
reader in step.
|
|
1001
|
+
"""
|
|
996
1002
|
fields: dict[str, str] = {}
|
|
997
1003
|
reports: list[dict[str, str]] = []
|
|
998
1004
|
for line in output.splitlines():
|
|
@@ -259,6 +259,7 @@ def render_index(root: Path, session_id: str | None, branch: str | None) -> str:
|
|
|
259
259
|
rows.append((at.timestamp(), f"- {label} ({branch_label}, {_age(at, now)}): {_next_step(content)}"))
|
|
260
260
|
except (ValueError, KeyError, TypeError, AttributeError, OSError, UnicodeError):
|
|
261
261
|
continue
|
|
262
|
+
unnamed: list[tuple[datetime, str, str, str]] = []
|
|
262
263
|
states = artifact / "session-state"
|
|
263
264
|
if states.exists() and not states.resolve().is_relative_to(artifact.resolve()):
|
|
264
265
|
raise ValueError("session-state directory escapes artifact root")
|
|
@@ -277,9 +278,22 @@ def render_index(root: Path, session_id: str | None, branch: str | None) -> str:
|
|
|
277
278
|
asked = request.group(1).strip() if request else ""
|
|
278
279
|
note = _single_line(asked, 80) if asked and not asked.startswith("<") else "recent session"
|
|
279
280
|
branch_label = _single_line(state_branch.group(1), 40) if state_branch else "unknown branch"
|
|
280
|
-
|
|
281
|
+
if session_id and path.stem == session_id:
|
|
282
|
+
rows.append((modified.timestamp(), f"- this session, no relay yet ({branch_label}): {note}"))
|
|
283
|
+
else:
|
|
284
|
+
unnamed.append((modified, label, branch_label, note))
|
|
281
285
|
except (OSError, UnicodeError):
|
|
282
286
|
continue
|
|
287
|
+
if unnamed:
|
|
288
|
+
# One line for every other session without a relay: a forked, peer or never-handed-off
|
|
289
|
+
# session is repeated text at every session start and buries the real workstreams.
|
|
290
|
+
unnamed.sort(key=lambda item: (-item[0].timestamp(), item[1]))
|
|
291
|
+
newest, label, _branch_label, note = unnamed[0]
|
|
292
|
+
count = len(unnamed)
|
|
293
|
+
noun = "session" if count == 1 else "sessions"
|
|
294
|
+
rows.append((newest.timestamp(),
|
|
295
|
+
f"- {count} other {noun} without a relay (newest {label}, {_age(newest, now)}): "
|
|
296
|
+
f"{note}; see .caddis/session-state/"))
|
|
283
297
|
rows.sort(key=lambda item: (-item[0], item[1]))
|
|
284
298
|
heading = _INDEX_MARKER + "# Workstream index\n"
|
|
285
299
|
if session_id or branch:
|
package/bundles/manifest.json
CHANGED
package/package.json
CHANGED