@caddis/cli 0.4.6 → 0.4.7

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.93",
3
+ "version": "1.3.94",
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
  }
@@ -480,6 +480,107 @@ def gate_handover_check(doc: Path, repo_root: Path) -> int:
480
480
  return EXIT_BLOCKED
481
481
 
482
482
 
483
+ # ── cross-review trigger ────────────────────────────────────────────────────────────────────
484
+ # serve-sight shipped FOUR production releases in one day, sixteen fixes across SQL, caches,
485
+ # refresh jobs and React. `/caddis:cross-review` ran ZERO times. Not because it was broken:
486
+ # `oss_review.py --check-config` reported it ready with two providers keyed. Nothing called it.
487
+ #
488
+ # Measured there: 4 of 39 plan files mention cross-review, and all four are /feature-plan outputs,
489
+ # because that command writes the line into the plan template it emits. So planned feature work
490
+ # got reviewed and a bug batch did not — REVIEW COVERAGE INVERSELY CORRELATED WITH URGENCY.
491
+ #
492
+ # Two of that day's sixteen fixes were diff-visible defects: a cache whose docstring said "cached
493
+ # for the instance's life" while a FastAPI dependency rebuilt the object per request, so the cache
494
+ # was never once read (5.03s per call, and the docstring described the bug while reading as a
495
+ # design note); and two endpoints with no cache at all on a page with a 1s standard.
496
+ #
497
+ # Every rule below is checkable from `git diff --name-only` plus a line count. NO JUDGEMENT — that
498
+ # is the point, because judgement is exactly what failed. Prose in a command would be one more
499
+ # thing to remember, and 008 section 2 is this repo's record of a rule that lived only in a
500
+ # document and lost to the artefact.
501
+ REVIEW_BIG_DIFF_LINES = 400
502
+
503
+ # (label, why, path-segment needles). Matched against lowercased POSIX path segments and the file
504
+ # stem, never a bare substring of the whole path — `services` must be a directory or a filename
505
+ # part, so `deprecated_services_notes.md` matches and `microservices-README` in a docs tree does
506
+ # not sneak in through a parent directory nobody meant.
507
+ REVIEW_TRIGGERS = [
508
+ ("sql", "where a wrong number is born — presence is not correctness",
509
+ ("sql", "queries", "query", "repositories", "repository", "migrations", "migration")),
510
+ ("services", "the layer the 2026-08-31 cache defects lived in, with no SQL in the diff",
511
+ ("services", "service")),
512
+ ("caching", "hit twice in one day at serve-sight",
513
+ ("cache", "caches", "caching", "refresh", "memo", "memoize")),
514
+ ("auth", "a mistake here is a breach, not a bug",
515
+ ("auth", "rbac", "permission", "permissions", "role", "roles", "authz", "authn")),
516
+ ]
517
+
518
+
519
+ def _changed(repo_root: Path, rev_range: str | None) -> tuple[list[str], int]:
520
+ """(paths, changed-line-count) for the working tree, or for an explicit range."""
521
+ args = ["git", "-C", str(repo_root), "diff", "--numstat"]
522
+ if rev_range:
523
+ args.append(rev_range)
524
+ out = subprocess.run(args, capture_output=True, text=True, timeout=30).stdout
525
+ paths, lines = [], 0
526
+ for row in out.splitlines():
527
+ parts = row.split(" ")
528
+ if len(parts) != 3:
529
+ continue
530
+ add, dele, path = parts
531
+ paths.append(path.strip())
532
+ for n in (add, dele):
533
+ if n.isdigit():
534
+ lines += int(n)
535
+ return paths, lines
536
+
537
+
538
+ def review_triggers(paths: list[str], changed_lines: int) -> list[tuple[str, str, list[str]]]:
539
+ """[(label, why, matching paths)] — empty means the diff does not warrant a second opinion."""
540
+ fired: list[tuple[str, str, list[str]]] = []
541
+ for label, why, needles in REVIEW_TRIGGERS:
542
+ hits = []
543
+ for p in paths:
544
+ segs = [seg.lower() for seg in p.replace("\\", "/").split("/")]
545
+ stem = segs[-1].rsplit(".", 1)[0] if segs else ""
546
+ parts = set(segs[:-1]) | set(stem.replace("-", "_").split("_")) | {stem}
547
+ # `.sql` belongs to the sql rule ONLY. Left in the shared condition it made every
548
+ # trigger fire on any .sql file, so one migration reported sql+services+caching+auth
549
+ # and the reason column became noise.
550
+ if parts & set(needles) or (label == "sql" and segs[-1].endswith(".sql")):
551
+ hits.append(p)
552
+ if hits:
553
+ fired.append((label, why, sorted(hits)))
554
+ if changed_lines > REVIEW_BIG_DIFF_LINES:
555
+ fired.append(("size", f"{changed_lines} changed lines — size alone stops being reviewable",
556
+ []))
557
+ return fired
558
+
559
+
560
+ def gate_review_trigger(repo_root: Path, rev_range: str | None = None) -> int:
561
+ """EXIT_NOTE when the diff warrants cross-review, EXIT_OK when it does not.
562
+
563
+ NOTE, never BLOCKED. This decides when to ASK for a second opinion; it is not itself a verdict
564
+ and must not stop a ship. The honest limit, from the item that raised it: a trigger does not
565
+ make the review good, it makes it happen — and on the measured evidence that was the binding
566
+ constraint, with a configured, capable tool idle through sixteen chances to use it.
567
+ """
568
+ paths, changed = _changed(repo_root, rev_range)
569
+ if not paths:
570
+ print("[gate] review-trigger: empty diff — nothing to review")
571
+ return EXIT_OK
572
+ fired = review_triggers(paths, changed)
573
+ if not fired:
574
+ print(f"[gate] review-trigger: {len(paths)} file(s), {changed} line(s) — no trigger")
575
+ return EXIT_OK
576
+ print(f"[gate] review-trigger: RUN CROSS-REVIEW — {len(paths)} file(s), {changed} line(s)")
577
+ for label, why, hits in fired:
578
+ shown = ", ".join(hits[:4]) + (", ..." if len(hits) > 4 else "")
579
+ print(f" {label}: {why}" + (f"{chr(10)} {shown}" if hits else ""))
580
+ print(" /caddis:cross-review (or: python .github/tools/oss_review.py)")
581
+ return EXIT_NOTE
582
+
583
+
483
584
  def main(argv: list[str] | None = None) -> int:
484
585
  ap = argparse.ArgumentParser(description="caddis machine gates")
485
586
  sub = ap.add_subparsers(dest="cmd", required=True)
@@ -494,6 +595,10 @@ def main(argv: list[str] | None = None) -> int:
494
595
  h.add_argument("--refs", nargs="*", default=[])
495
596
  pl = sub.add_parser("parking-lot")
496
597
  pl.add_argument("--repo-root", default=".")
598
+ rt = sub.add_parser("review-trigger")
599
+ rt.add_argument("--repo-root", default=".")
600
+ rt.add_argument("--range", dest="rev_range", default=None,
601
+ help="revision range, e.g. origin/main...HEAD (default: the working tree)")
497
602
  vd = sub.add_parser("vendor-drift")
498
603
  vd.add_argument("--repo-root", default=".")
499
604
  hc = sub.add_parser("handover-check")
@@ -511,6 +616,8 @@ def main(argv: list[str] | None = None) -> int:
511
616
  return gate_hub_artifacts(Path(a.reports), [Path(r) for r in a.refs])
512
617
  if a.cmd == "parking-lot":
513
618
  return gate_parking_lot(Path(a.repo_root).resolve())
619
+ if a.cmd == "review-trigger":
620
+ return gate_review_trigger(Path(a.repo_root).resolve(), a.rev_range)
514
621
  if a.cmd == "vendor-drift":
515
622
  return gate_vendor_drift(Path(a.repo_root).resolve())
516
623
  if a.cmd == "handover-check":
@@ -44,6 +44,23 @@ git rev-parse HEAD # record SHA for monitoring
44
44
  ```
45
45
  Use conventional commits: `fix:`, `feat:`, `chore:`, `refactor:`, `docs:`. Scope = affected module.
46
46
 
47
+ **2b. CROSS-REVIEW TRIGGER** — `ship` has no review pause at all, which is how sixteen fixes reached production unreviewed in one day.
48
+
49
+ **Run the trigger, do not eyeball the diff:**
50
+
51
+ ```bash
52
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_gate.py" review-trigger --range <base>...HEAD
53
+ ```
54
+
55
+ Exit **0** — say nothing, carry on. Exit **2** — it prints which rule fired and on which files.
56
+ Run `/caddis:cross-review`, show the findings, and let the user decide. It **never blocks**: this
57
+ picks when to ask for a second opinion, it is not a verdict, and a review gate that blocks a ship
58
+ gets switched off inside a week.
59
+
60
+ It fires on SQL and repositories, `services/`, caches and refresh jobs, auth and RBAC, or a diff
61
+ over 400 changed lines. No judgement in any of those — deliberately, because judgement is exactly
62
+ what failed.
63
+
47
64
  **3. PUSH** — to the repo's default branch (confirm the branch first; don't assume `main`):
48
65
  ```
49
66
  git push origin <branch>
@@ -50,6 +50,33 @@ are uncommitted.)
50
50
  - Without a named guard, apply the generic infra-vs-product mixed-diff heuristic as a **warning
51
51
  only** — surface it and proceed (repos without a guard allow mixed PRs by construction).
52
52
 
53
+ ## Step 3b — Cross-review trigger (before anything is pushed)
54
+
55
+ **Run the trigger, do not eyeball the diff:**
56
+
57
+ ```bash
58
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_gate.py" review-trigger --range <base>...HEAD
59
+ ```
60
+
61
+ Exit **0** — say nothing, carry on. Exit **2** — it prints which rule fired and on which files.
62
+ Run `/caddis:cross-review`, show the findings, and let the user decide. It **never blocks**: this
63
+ picks when to ask for a second opinion, it is not a verdict, and a review gate that blocks a ship
64
+ gets switched off inside a week.
65
+
66
+ It fires on SQL and repositories, `services/`, caches and refresh jobs, auth and RBAC, or a diff
67
+ over 400 changed lines. No judgement in any of those — deliberately, because judgement is exactly
68
+ what failed.
69
+
70
+ > **Why this is mechanical and not "consider a review here".** serve-sight shipped four
71
+ > production releases in one day — sixteen fixes across SQL, caches, refresh jobs and React — and
72
+ > `/caddis:cross-review` ran **zero times**. Nothing was broken: the tool reported itself ready
73
+ > with two providers keyed. Nothing decided *when* to call it. Measured there: 4 of 39 plan files
74
+ > mention cross-review, and all four came from `/caddis:feature-plan`, which writes the line into
75
+ > the plan template it emits. So planned feature work got reviewed and a bug batch did not —
76
+ > **review coverage inversely correlated with urgency.** Two of that day's sixteen fixes were
77
+ > diff-visible defects, including a cache whose docstring said "cached for the instance's life"
78
+ > while a dependency rebuilt the object per request, so the cache was never once read.
79
+
53
80
  ## Step 4 — Rebase-safe currency
54
81
 
55
82
  - If the branch is **behind** the default branch, offer to rebase onto it (stale branches are how
@@ -1,7 +1,7 @@
1
1
  {
2
- "poolVersion": "1.3.93",
2
+ "poolVersion": "1.3.94",
3
3
  "bundles": {
4
- "antigravity-plugin": "1.3.93",
4
+ "antigravity-plugin": "1.3.94",
5
5
  "antigravity-plugin-extras": "1.3.28",
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.6",
3
+ "version": "0.4.7",
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",