@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.
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/scripts/caddis_gate.py +107 -0
- package/bundles/antigravity-plugin/skills/ship/SKILL.md +17 -0
- package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +27 -0
- package/bundles/manifest.json +2 -2
- package/package.json +1 -1
|
@@ -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
|
package/bundles/manifest.json
CHANGED
package/package.json
CHANGED