diff --git a/Dockerfile b/Dockerfile index c71014f..c40addc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ # protoAgent core forward on the same image roll — core and member bumps are # decoupled. Bump this deliberately (and re-verify), keeping it in step with the # manifest's `verified_against`. Tag format is bare semver (no `v` prefix). -FROM ghcr.io/protolabsai/protoagent:0.145.0 +FROM ghcr.io/protolabsai/protoagent:0.163.0 USER root @@ -27,11 +27,11 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ # Bake the bundle members at their RELEASE TAGS (both public — no build secrets). # The tags mirror protoagent.bundle.yaml's pins; bump both together (the manifest # is the source of truth, this bake is its image form). -ARG GITHUB_PLUGIN_REF=v0.5.0 +ARG GITHUB_PLUGIN_REF=v0.7.0 RUN git clone --depth 1 --branch "${GITHUB_PLUGIN_REF}" \ https://github.com/protoLabsAI/github-plugin.git /opt/protoagent/plugins/github \ && rm -rf /opt/protoagent/plugins/github/.git -ARG PR_REVIEWER_PLUGIN_REF=v0.36.0 +ARG PR_REVIEWER_PLUGIN_REF=v0.38.0 RUN git clone --depth 1 --branch "${PR_REVIEWER_PLUGIN_REF}" \ https://github.com/protoLabsAI/pr-reviewer-plugin.git /opt/protoagent/plugins/pr-reviewer \ && rm -rf /opt/protoagent/plugins/pr-reviewer/.git diff --git a/README.md b/README.md index 36ed7d9..b1197a4 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ python -m server plugin install https://github.com/protoLabsAI/qaEngineer | Member | Pin | Role | |---|---|---| | `workflows` (builtin) | core | the recipe engine the review panels run on | -| [github-plugin](https://github.com/protoLabsAI/github-plugin) | v0.5.0 | the verdict surface — formal Review API tools with CI-terminal + self-review guards inside the tools | -| [pr-reviewer-plugin](https://github.com/protoLabsAI/pr-reviewer-plugin) | v0.36.0 | the machinery — webhook chokepoint, structural trigger, panel dispatch, evidence grounding, convergence, approve-on-green sweep, the `QA panel` check run, on-demand summon, telemetry + eval | +| [github-plugin](https://github.com/protoLabsAI/github-plugin) | v0.7.0 | the verdict surface — formal Review API tools with CI-terminal + self-review guards inside the tools | +| [pr-reviewer-plugin](https://github.com/protoLabsAI/pr-reviewer-plugin) | v0.38.0 | the machinery — webhook chokepoint, structural trigger, panel dispatch, evidence grounding, convergence, approve-on-green sweep, the `QA panel` check run, on-demand summon, telemetry + eval | Persona: [`SOUL.md`](./SOUL.md) (Vera — verdict system, three-layer verification, 80% bar, self-restriction), also inlined in the manifest's `archetype.soul` so the new-agent picker @@ -119,10 +119,19 @@ event (ADR 0039). The inference script is therefore scheduled for deletion — b yet, and the distinction matters: **pinning a version is not running it.** Vera rolls on watchtower after a merge, so between the pin landing and the roll completing she is on the old core with no event at all. Retire the script once the running instance reports -0.145.0 *and* the event has been seen firing; deleting the inference before its +the pinned core *and* the event has been seen firing; deleting the inference before its replacement is observed working would leave the silent-degrade window covered by neither. +**#2995 is FIXED in core 0.148.0** (carried since): the observable failover above only +ever wired onto the *lead* agent — every panel finder runs as a subagent via +`_run_subagent()`, which built its own middleware stack and omitted +`routing.fallback_models` entirely. A rate-limited primary therefore killed the whole +panel instead of failing over (this is what actually happened on 2026-08-23: the +gateway fallback counter sat unchanged across two rate-limit incidents while panels +exhausted). The subagent stack now mirrors the lead's, so `fallback_models` finally +protects reviews, not just conversational turns. + **Reviews are not cheap.** One structural review is nine LLM steps and 5–9 minutes of wall clock. On a hosted frontier model that's roughly $0.12–0.15 each; on local inference it's free but occupies the box. @@ -201,7 +210,7 @@ from the payload): ## Deploying Vera (the reference host) This repo doubles as Vera's image source: `Dockerfile` = stock protoAgent (**pinned -base** — `protoagent:0.145.0`, in step with the manifest's `verified_against`; bump +base** — `protoagent:0.163.0`, in step with the manifest's `verified_against`; bump deliberately so a member-pin bump can't drag the core forward on the same roll) + node/`clawpatch` + the bundle members baked at their manifest pins + `deploy/vera.langgraph-config.yaml` (seed, not force) + `SOUL.md`. @@ -241,7 +250,6 @@ exit 2 = unreachable kept as distinct alarms). Every one of them exists because | `drift` | `check_card_drift.py` | does the live card still match the seed? | | `fallback` | `check_model_fallback.py` | did she silently answer from her fallback model? (gateway-metrics inference — protoAgent#2956) | | `oauth` | `check_oauth_health.py` | is the subscription credential still signed in, refreshable, and coherent with `model.name`? (a no-op on a gateway lane) | -| `prune` | `prune_checkout_cache.py` | **stopgap** — bound the checkout cache, since the plugin's own `prune()` is never called (pr-reviewer-plugin#87) | The wrapper runs **installed copies** in `~/.local/bin`, not `scripts/*.py` — this repo is also the deploy source, so a branch switch would silently disarm a guard that lived inside diff --git a/protoagent.bundle.yaml b/protoagent.bundle.yaml index 5b62aa6..742ed21 100644 --- a/protoagent.bundle.yaml +++ b/protoagent.bundle.yaml @@ -25,12 +25,12 @@ description: >- own work; never posts a blocking verdict against pending CI. # The core version this pin set was last verified against (ADR 0049 rule 2). -verified_against: 0.145.0 +verified_against: 0.163.0 plugins: - { id: workflows, builtin: true } # the recipe engine (code-review panels run through it) - - { id: github, url: https://github.com/protoLabsAI/github-plugin, ref: v0.5.0 } - - { id: pr-reviewer, url: https://github.com/protoLabsAI/pr-reviewer-plugin, ref: v0.36.0 } + - { id: github, url: https://github.com/protoLabsAI/github-plugin, ref: v0.7.0 } + - { id: pr-reviewer, url: https://github.com/protoLabsAI/pr-reviewer-plugin, ref: v0.38.0 } enabled: [workflows, github, pr-reviewer] # suggested turn-on list (applied to plugins.enabled) diff --git a/scripts/prune_checkout_cache.py b/scripts/prune_checkout_cache.py deleted file mode 100755 index 3e92bfa..0000000 --- a/scripts/prune_checkout_cache.py +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env python3 -"""Stopgap: bound Vera's checkout cache, because the plugin's own pruner never runs. - -`CheckoutCache.prune()` exists in pr-reviewer, is documented in its module docstring, -and is unit-tested — and nothing in the plugin ever calls it (filed as -pr-reviewer-plugin#87). Measured consequence on this deployment: 43 GiB across 1248 -entries against the module's own 5 GiB / 50-entry / 1-hour-TTL caps, with 1247 of those -entries already past the TTL. It was the largest consumer on the host and took it to -92% disk. After a manual sweep the cache regrew ~0.5 GiB in 90 minutes (~8 GiB/day), so -this is not a one-time cleanup — it needs a schedule until the upstream fix lands. - -DELETE THIS SCRIPT once #87 ships and the plugin prunes itself. It exists only because -a cache with a documented, tested, uncalled pruner is indistinguishable from a cache -with no pruner at all — and the disk is where you find out. - -POLICY, and why it is not simply the plugin's TTL: - - * Keep the N newest entries per repo (default 3). A pure TTL sweep would evict a - checkout the very next review re-clones — the cache exists so an unchanged head - reaffirms in under a second, and buying disk with latency on every repo is a bad - trade. Three covers the head plus a re-review or two. - * Keep ANYTHING touched inside the protect window (default 1h), regardless of count. - Reviews run 3-10 minutes and several can be in flight; deleting a checkout out from - under a running panel is the one way this script could cause the failure it exists - to prevent. The window is deliberately much longer than the longest observed review. - -Both rules are additive — an entry survives if EITHER holds. - -Usage (dry run by default; --apply to delete): - python3 scripts/prune_checkout_cache.py --container vera - python3 scripts/prune_checkout_cache.py --container vera --apply - -Exit 0 = swept (or nothing to do), 2 = could not reach the container. -""" - -from __future__ import annotations - -import argparse -import json -import subprocess -import sys - -CACHE_ROOT = "/sandbox/pr-reviewer/checkouts" -DEFAULT_KEEP_PER_REPO = 3 -DEFAULT_PROTECT_MIN = 60 - -# Runs INSIDE the container: the cache lives on a named volume owned by uid 1001, and -# reaching it from the host would mean guessing the volume mountpoint and the uid. The -# body is kept dependency-free (stdlib only) because the image's python is not ours to -# add packages to. -_SWEEP = r""" -import json, os, shutil, sys, time -ROOT, KEEP, PROTECT, APPLY = sys.argv[1], int(sys.argv[2]), float(sys.argv[3]), sys.argv[4] == "apply" -now = time.time() -def dsize(p): - n = 0 - for dp, _, fs in os.walk(p): - for f in fs: - try: n += os.lstat(os.path.join(dp, f)).st_size - except OSError: pass - return n -deleted = kept = 0 -freed = 0 -errors = [] -if os.path.isdir(ROOT): - for repo in sorted(os.listdir(ROOT)): - rp = os.path.join(ROOT, repo) - if not os.path.isdir(rp): continue - try: - ents = [(os.path.getmtime(os.path.join(rp, d)), os.path.join(rp, d)) - for d in os.listdir(rp) if os.path.isdir(os.path.join(rp, d))] - except OSError as e: - errors.append(f"{repo}: {e}"); continue - ents.sort(reverse=True) - keep = {p for _, p in ents[:KEEP]} | {p for m, p in ents if now - m < PROTECT} - for _, p in ents: - if p in keep: - kept += 1 - continue - freed += dsize(p) - deleted += 1 - if APPLY: - shutil.rmtree(p, ignore_errors=True) -print(json.dumps({"deleted": deleted, "kept": kept, "freed_bytes": freed, "errors": errors[:5]})) -""" - - -def main() -> int: - ap = argparse.ArgumentParser() - ap.add_argument("--container", default="vera") - ap.add_argument("--keep-per-repo", type=int, default=DEFAULT_KEEP_PER_REPO) - ap.add_argument("--protect-min", type=float, default=DEFAULT_PROTECT_MIN) - ap.add_argument("--apply", action="store_true", help="actually delete (default is a dry run)") - args = ap.parse_args() - - try: - out = subprocess.run( - [ - "docker", "exec", args.container, "python3", "-c", _SWEEP, - CACHE_ROOT, str(args.keep_per_repo), str(args.protect_min * 60), - "apply" if args.apply else "dry", - ], - capture_output=True, text=True, timeout=1800, - ) - if out.returncode != 0: - print(f"UNREACHABLE: docker exec {args.container} failed: {out.stderr.strip()[:300]}") - return 2 - result = json.loads(out.stdout.strip().splitlines()[-1]) - except Exception as exc: # noqa: BLE001 — every failure here is operational - print(f"UNREACHABLE: {exc}") - return 2 - - gib = result["freed_bytes"] / 1024**3 - verb = "deleted" if args.apply else "would delete" - print(f"{verb} {result['deleted']} checkout entries ({gib:.1f} GiB), kept {result['kept']}") - for e in result.get("errors") or []: - print(f" warning: {e}") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/vera-watchdog.sh b/scripts/vera-watchdog.sh index 9cff2e4..de611f8 100755 --- a/scripts/vera-watchdog.sh +++ b/scripts/vera-watchdog.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Runner + alerter for Vera's watchdogs — health, drift, fallback, oauth, prune +# Runner + alerter for Vera's watchdogs — health, drift, fallback, oauth # (qaEngineer#37 shipped the first two; the model-lane pair arrived with the move to a # native Claude subscription). # @@ -18,7 +18,6 @@ # live inside the thing it watches. Refresh these copies when the repo version changes: # # install -m 644 ~/dev/qaEngineer/scripts/vera_api.py ~/.local/bin/vera_api.py -# install -m 755 ~/dev/qaEngineer/scripts/prune_checkout_cache.py ~/.local/bin/vera-prune-cache.py # install -m 755 ~/dev/qaEngineer/scripts/check_review_health.py ~/.local/bin/vera-review-health.py # install -m 755 ~/dev/qaEngineer/scripts/check_card_drift.py ~/.local/bin/vera-card-drift.py # install -m 755 ~/dev/qaEngineer/scripts/check_model_fallback.py ~/.local/bin/vera-model-fallback.py @@ -33,7 +32,7 @@ # agent) are deliberately different alerts — the scripts draw that line on purpose, and # collapsing it would let an outage read as a clean gate. # -# Usage: vera-watchdog.sh health|drift|fallback|oauth|prune [extra args passed to the check] +# Usage: vera-watchdog.sh health|drift|fallback|oauth [extra args passed to the check] # Exit: passes the underlying check's exit code through (0 ok, 1 verdict, 2 unreachable) set -uo pipefail @@ -113,16 +112,8 @@ case "$MODE" in # outlive its refresh token with no traffic to reveal it. out="$("$BIN/vera-oauth-health.py" --container vera "$@" 2>&1)"; rc=$? ;; - prune) - # STOPGAP, not a watchdog: bound the checkout cache, because pr-reviewer's own - # CheckoutCache.prune() is defined, documented, unit-tested and never called - # (pr-reviewer-plugin#87). It reached 43 GiB / 1248 entries against its own - # 5 GiB / 50-entry caps and took ava to 92% disk. Delete this mode when #87 ships. - # Runs with --apply here; the underlying script dry-runs by default. - out="$("$BIN/vera-prune-cache.py" --container vera --apply "$@" 2>&1)"; rc=$? - ;; *) - echo "usage: $(basename "$0") health|drift|fallback|oauth|prune" >&2; exit 64 ;; + echo "usage: $(basename "$0") health|drift|fallback|oauth" >&2; exit 64 ;; esac echo "$out"