@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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "caddis",
3
- "version": "1.3.124",
3
+ "version": "1.3.126",
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
  }
@@ -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 re.search(r"(?:^|\s)(?:-p|--print)(?:\s|$)", cmd):
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 re.search(r"(?:^|\s)(?:-p|--print)(?:\s|$)", cmd):
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
- for raw in sorted(set(_DOC_PATH.findall(text))):
500
- # A backtick often wraps a whole command; the path is the token that looks like one.
501
- cand = next((tok for tok in raw.split() if "/" in tok or tok.endswith(".md")), raw)
502
- cand = cand.strip("(),;:'\"")
503
- if cand.startswith(("http://", "https://", "~", "$")) or ".." in cand:
504
- continue
505
- if not (repo_root / cand).exists():
506
- missing.append(cand)
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
- rows.append((modified.timestamp(), f"- unnamed session {label} ({branch_label}, {_age(modified, now)}): {note}"))
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:
@@ -1,7 +1,7 @@
1
1
  {
2
- "poolVersion": "1.3.124",
2
+ "poolVersion": "1.3.126",
3
3
  "bundles": {
4
- "antigravity-plugin": "1.3.124",
4
+ "antigravity-plugin": "1.3.126",
5
5
  "antigravity-plugin-extras": "1.3.40",
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.37",
3
+ "version": "0.4.39",
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",