DEV-1638: unify local benchmark CLI — fold postgres bootstrap into bird-interact - #75
Conversation
…rd-interact
`bird-interact --dataset <name>` is now the ONE local entrypoint for both
sqlite and postgres benchmarks. It dispatches on `db_backend`:
- Derives `--data`/`--db-path` from the registry when both omitted
(both-or-neither; both backends). Explicit paths still honored.
- For a postgres benchmark, auto-provisions + loads a private sudo-free
local cluster and exports `BIRD_PG_*` UNLESS `BIRD_PG_HOST` is already set.
Gating is on the CONNECTION signal, not on whether paths were derived
(postgres connects via `BIRD_PG_*`, not `--db-path`).
- Best-effort syncs the authoritative task annotations from GCS (all
backends; `--skip-annotations` opt-out; warns on still-missing so the
silent implicit-N1 fallback is visible).
- Loads an auth dotenv via `--env-file` (default `$BIRD_ENV_FILE`; no
machine path baked into the code).
The composable logic moved out of the (un-installed) `scripts/` into the
package so the console script can import it: `env_file.load_env_file`,
`local_postgres.provision_and_export`, `local_annotations.sync_annotations`.
`scripts/{setup_local_postgres,fetch_local_annotations}.py` became thin CLIs;
`scripts/run_local_postgres.py` is retired.
Local ⇄ cloud parity: the SAME `sync_annotations` is wired into the cloud
`submit` pre-build (inside the `require_annotation` gate, self-healing) so
the annotation set the local grader reads == the set the cloud image bakes.
Infra/auth errors propagate (fail-fast); per-blob-absent is counted, not
raised, so the gate reports precisely-missing ids.
Cloud runner (`ray_app`/`run_evaluation`) is untouched — `main()` is the
only local entrypoint and the cloud worker never calls it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DEV-1638 Unify local benchmark CLI: fold postgres bootstrap into `bird-interact` (dispatch by `db_backend`)
ProblemLocal benchmark runs currently have two entrypoints, split by DB backend:
So the postgres-vs-sqlite difference leaks into which command the caller must run. The underlying leak is ProposalUnify the local side behind one CLI.
Net: one local entrypoint; the postgres bootstrap becomes an internal implementation detail dispatched by Scope / non-goals
OriginSurfaced while setting up a local v2 run of the 15 |
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 8 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughLocal Postgres provisioning and annotation sync logic move into package modules, with ChangesLocal Postgres and annotation sync consolidation
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant CLI
participant run_main as run.main
participant local_postgres
participant local_annotations
participant run_evaluation
CLI->>run_main: parse_args(argv)
run_main->>run_main: load_env_file(env_file)
run_main->>run_main: _resolve_data_paths(args)
run_main->>run_main: _effective_instance_ids(args)
run_main->>local_postgres: _maybe_bootstrap_local_postgres()
local_postgres-->>run_main: BIRD_PG_* exports
run_main->>local_annotations: _maybe_sync_annotations()
local_annotations-->>run_main: fetched/missing counts
run_main->>run_evaluation: run_evaluation(args)
sequenceDiagram
participant Client
participant submit_cli as cloud submit
participant local_annotations
participant annotation_gate as missing_annotation_ids
Client->>submit_cli: parse_args(argv)
submit_cli->>local_annotations: sync_annotations(dataset, instance_ids)
local_annotations-->>submit_cli: fetched/missing_in_gcs
submit_cli->>annotation_gate: missing_annotation_ids(dataset, instance_ids)
annotation_gate-->>submit_cli: missing ids or none
submit_cli-->>Client: continue or SystemExit
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (5)
src/bird_interact_agents/local_postgres.py (2)
169-171: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePrefer
shutil.rmtreeover shelling out torm -rf.Avoids depending on an external
rmbinary being on PATH and is more portable.♻️ Proposed fix
+import shutil ... if recreate and p["data"].exists(): stop_cluster(bindir) - subprocess.run(["rm", "-rf", str(p["data"])], check=True) + shutil.rmtree(p["data"])🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/bird_interact_agents/local_postgres.py` around lines 169 - 171, Replace the shell call to remove the data directory in local_postgres.py with a Python-native deletion using shutil.rmtree. The recreate branch in the cluster cleanup logic currently shells out via subprocess.run(["rm", "-rf", ...]), so update that block to use the existing p["data"] path directly and keep the stop_cluster(bindir) flow intact. Make sure the cleanup still runs only when recreate is true and the data path exists.
45-49: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low valueHardcoded
PG_PASSWORDflagged by static analysis (S105).Given roles are created with
-A trust(line 175), the password is never actually used for authentication, so this is effectively a dev-cluster placeholder rather than a real secret. Worth a brief# noqa: S105(or nosec-style) comment to document intent and silence the lint noise for future readers.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/bird_interact_agents/local_postgres.py` around lines 45 - 49, The hardcoded PG_PASSWORD in local_postgres.py is a deliberate dev-cluster placeholder and should be annotated to silence static analysis. Add a brief intent comment or lint suppression рядом with the PG_PASSWORD constant in the module-level defaults so S105 is ignored, and keep the existing PG_USER/_REQUIRED_ROLES context unchanged.Source: Linters/SAST tools
src/bird_interact_agents/local_annotations.py (1)
57-57: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueEmpty
instance_idslist silently syncs the whole benchmark.
targets = instance_ids or list(inst_to_db)treats[]the same asNone. Currently unreachable from the provided callers (the cloud CLI already rejects an empty resolved id list before this point), but as a public library function this is a latent footgun for any future caller that legitimately wants "sync nothing."🛡️ Proposed fix
- targets = instance_ids or list(inst_to_db) + targets = list(inst_to_db) if instance_ids is None else instance_ids🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/bird_interact_agents/local_annotations.py` at line 57, The `sync`/target selection logic in `local_annotations.py` treats an empty `instance_ids` list the same as no input by using `instance_ids or list(inst_to_db)`, which can accidentally sync everything. Update the target selection in the relevant function to distinguish `None` from `[]` so an explicit empty list stays empty, and only fall back to `list(inst_to_db)` when `instance_ids` is not provided at all.tests/test_local_annotations_sync.py (1)
1-147: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMissing coverage for the not-found-instance-id path.
None of the tests exercise
sync_annotationsbeing called with aninstance_idabsent frominst_to_db(theSystemExitbranch atlocal_annotations.pylines 53‑56). Worth a quick added case, especially given the exit-type inconsistency flagged inlocal_annotations.py.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_local_annotations_sync.py` around lines 1 - 147, Add a test for the not-found instance-id path in sync_annotations: cover the branch where an input instance_id is missing from inst_to_db and the function exits via SystemExit. Use the existing local_annotations.sync_annotations fixture setup and extend tests/test_local_annotations_sync.py with a case that passes an unknown instance_id, asserting the exit behavior rather than download/count results. Keep the coverage focused on the sync_annotations lookup logic and the _load_dataset_instance_db_map-based mapping used to resolve instance IDs.src/bird_interact_agents/cloud/cli.py (1)
379-389: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winSilent network I/O and error contract during
parse_args.
sync_annotations(ns.dataset, ns.instance_ids)performs GCS network calls inside argument parsing with no observability (no print/log of what/whether it fetched anything), unlikelocal_annotations.main()'s script wrapper which reportsfetched=/already_local=/missing_in_gcs=. Since this now runs on everysubmitwith--require-annotation(the default), a slow or partially-failing GCS call is invisible to the operator until the subsequent gate fails or an unrelated-looking exception surfaces. Consider emitting a short stderr summary (or reusing the counts already returned bysync_annotations) so the self-healing step is visible.Separately — flagged in
local_annotations.py— an instance_id typo causessync_annotationsto raise a bareSystemExithere, which exits with a different code/format than thep.error()calls used everywhere else in this same gate.The submit gate now runs sync_annotations first before the require_annotation missing-annotation gate.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/bird_interact_agents/cloud/cli.py` around lines 379 - 389, The submit parsing flow now calls sync_annotations(ns.dataset, ns.instance_ids) during parse_args, but it does so silently, so add a short stderr/log summary using the counts returned by sync_annotations (like fetched/already_local/missing_in_gcs) before the require_annotation gate runs. Also fix the instance_id typo path in sync_annotations so it does not raise a bare SystemExit; instead surface the error through the same parser-style error handling used by the require_annotation gate (for example via p.error). Keep the changes centered on parse_args in cli.py and sync_annotations in local_annotations.py so the self-healing step is visible and exits consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/bird_interact_agents/local_annotations.py`:
- Around line 53-56: The sync_annotations validation currently raises SystemExit
for missing instance_ids, which bypasses the normal CLI error handling path.
Update sync_annotations in local_annotations.py to raise ValueError instead of
exiting, and then handle/format that exception at each CLI boundary that calls
it so the cloud CLI can continue using p.error() consistently across callers.
In `@src/bird_interact_agents/local_postgres.py`:
- Around line 104-120: Make the bindir resolver a public API instead of a
private helper: rename `_resolve_bindir` in
`bird_interact_agents.local_postgres` to a non-underscored name (or add a public
alias) because `scripts/setup_local_postgres.py` imports it across the module
boundary. Update the internal call sites in `provision_and_export` and the
import in `scripts/setup_local_postgres.py` to use the public helper name so the
cross-file contract is explicit and stable.
---
Nitpick comments:
In `@src/bird_interact_agents/cloud/cli.py`:
- Around line 379-389: The submit parsing flow now calls
sync_annotations(ns.dataset, ns.instance_ids) during parse_args, but it does so
silently, so add a short stderr/log summary using the counts returned by
sync_annotations (like fetched/already_local/missing_in_gcs) before the
require_annotation gate runs. Also fix the instance_id typo path in
sync_annotations so it does not raise a bare SystemExit; instead surface the
error through the same parser-style error handling used by the
require_annotation gate (for example via p.error). Keep the changes centered on
parse_args in cli.py and sync_annotations in local_annotations.py so the
self-healing step is visible and exits consistently.
In `@src/bird_interact_agents/local_annotations.py`:
- Line 57: The `sync`/target selection logic in `local_annotations.py` treats an
empty `instance_ids` list the same as no input by using `instance_ids or
list(inst_to_db)`, which can accidentally sync everything. Update the target
selection in the relevant function to distinguish `None` from `[]` so an
explicit empty list stays empty, and only fall back to `list(inst_to_db)` when
`instance_ids` is not provided at all.
In `@src/bird_interact_agents/local_postgres.py`:
- Around line 169-171: Replace the shell call to remove the data directory in
local_postgres.py with a Python-native deletion using shutil.rmtree. The
recreate branch in the cluster cleanup logic currently shells out via
subprocess.run(["rm", "-rf", ...]), so update that block to use the existing
p["data"] path directly and keep the stop_cluster(bindir) flow intact. Make sure
the cleanup still runs only when recreate is true and the data path exists.
- Around line 45-49: The hardcoded PG_PASSWORD in local_postgres.py is a
deliberate dev-cluster placeholder and should be annotated to silence static
analysis. Add a brief intent comment or lint suppression рядом with the
PG_PASSWORD constant in the module-level defaults so S105 is ignored, and keep
the existing PG_USER/_REQUIRED_ROLES context unchanged.
In `@tests/test_local_annotations_sync.py`:
- Around line 1-147: Add a test for the not-found instance-id path in
sync_annotations: cover the branch where an input instance_id is missing from
inst_to_db and the function exits via SystemExit. Use the existing
local_annotations.sync_annotations fixture setup and extend
tests/test_local_annotations_sync.py with a case that passes an unknown
instance_id, asserting the exit behavior rather than download/count results.
Keep the coverage focused on the sync_annotations lookup logic and the
_load_dataset_instance_db_map-based mapping used to resolve instance IDs.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 661bf342-14b9-4285-a755-8fc59f822130
📒 Files selected for processing (16)
CLAUDE.mdscripts/fetch_local_annotations.pyscripts/run_local_postgres.pyscripts/setup_local_postgres.pysrc/bird_interact_agents/cloud/cli.pysrc/bird_interact_agents/env_file.pysrc/bird_interact_agents/local_annotations.pysrc/bird_interact_agents/local_postgres.pysrc/bird_interact_agents/run.pytests/cloud/test_cli.pytests/cloud/test_submit_annotation_presync.pytests/scripts/test_local_postgres_helpers.pytests/test_env_file.pytests/test_local_annotations_sync.pytests/test_local_postgres_provision.pytests/test_run_local_dispatch.py
💤 Files with no reviewable changes (1)
- scripts/run_local_postgres.py
- run.py: an empty `--filter-ids` file now errors BEFORE provisioning + sync (symmetric with the `--instance-id` empty guard) instead of falling through to a whole-benchmark provision + GCS sync that `run_evaluation` later rejects. [Codex major] - local_postgres.py: rename `_resolve_bindir` -> `resolve_bindir` (public) — it is imported across the module boundary by scripts/setup_local_postgres.py. [CodeRabbit major] - local_annotations.py: `sync_annotations` is now tolerant of unknown ids (warn + skip, never `SystemExit`) so the shared helper has one uniform error contract across the local run and the cloud submit pre-build. [CodeRabbit] - local_postgres.py: `rm -rf` -> `shutil.rmtree` (portable, no PATH dep). [CodeRabbit nitpick] - CLAUDE.md: genericize the machine-specific `--env-file` path to a `<your-auth-dotenv>` placeholder (the concrete value lives only in the private agent memory). [Codex minor] New tests pin the empty-filter-ids early error and the tolerant-unknown-id sync. Full non-integration suite green (3964 passed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`--limit 0` is an established zero-task idiom (test_slayer_setup_flag). Rather than reject it, `_effective_instance_ids` now returns [] (not None) for a limit, and `_maybe_bootstrap_local_postgres` / `_maybe_sync_annotations` skip on an EMPTY id list — distinct from None (whole benchmark). This prevents the whole-benchmark provision + GCS sync Codex flagged for a zero-task run while keeping `--limit 0` working. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (PR #75 Codex r3) The main CLI guarded empty `effective_ids`, but the shared helpers still used truthiness, so a malformed standalone `--instance-ids ",,,"` (parses to `[]`) would expand to the whole benchmark: - `resolve_dbs_for(benchmark, [])` now returns `[]` (load nothing) instead of every available pg_dump. - `sync_annotations(benchmark, [])` now syncs nothing (no GCS client) instead of every task annotation. `None` (omitted) still means the whole benchmark; only an explicit empty list means "no scope". Tests pin both. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Codex r4) Final None-vs-empty-list spot: `_effective_instance_ids(args, [])` used truthiness and collapsed an explicit empty filter to None (whole benchmark). Now `is not None`, so [] propagates as "no scope" consistent with the downstream helpers. (Unreachable from the guarded main CLI, but keeps the helper contract consistent.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#75 Codex r5) Both standalone CLIs (setup_local_postgres.py, fetch_local_annotations.py via local_annotations.main) used `if args.instance_ids else None`, collapsing an explicit `--instance-ids ""` to None (whole benchmark). Now: OMITTED → None (whole benchmark), explicitly-provided empty (`""` / `",,,"`) → [] (no scope), consistent with the fixed helpers. Parametrized test pins the mapping. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n CLI (PR #75 Codex r6) The last None-vs-empty boundary: `bird-interact --instance-id ""` / `--filter-ids ""` were falsy, skipped the parse branch, and fell through to whole-benchmark scope. Now `is not None` selects the branch and an explicit empty value is rejected with a clear error (before provisioning + sync); an OMITTED flag still means whole benchmark. Parametrized CLI test pins "", ",,,", and empty --filter-ids. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
origin/main advanced with PR #75 (DEV-1638): the local-benchmark CLI unification — postgres bootstrap folded into `bird-interact` behind `--benchmark`, with the provisioning/annotation/env logic moved out of scripts/ into src/{local_postgres,local_annotations,env_file}.py and run.py gaining the db_backend dispatch. Clean auto-merge — DEV-1638 touches the runner/CLI + scripts, DEV-1591 touches the agents/prompts, so the two are disjoint (only CLAUDE.md auto-merged). Full non-integration suite: 3993 passed, 94 skipped, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Problem
Local benchmark runs had two entrypoints split by DB backend: sqlite ran via
bird-interact(but required pre-resolved--data/--db-path), while postgres needed the separatescripts/run_local_postgres.pyorchestrator. The backend leaked into which command you ran.What this does
bird-interact --dataset <name>is now the one local entrypoint for both backends. It dispatches onbenchmark.db_backend:--data/--db-pathfrom the registry when both are omitted (both-or-neither; both backends). Explicit paths still honored.BIRD_PG_*— gated onBIRD_PG_HOSTbeing unset (the connection signal), not on whether paths were derived. Postgres connects viaBIRD_PG_*, and--db-pathis only the data/KB root, so explicit paths do not suppress provisioning.*.task.jsonfrom GCS (all backends;--skip-annotationsopt-out; warns loudly on any id still missing so the silent implicit-N1 grading fallback is visible).--env-fileloads a dotenv before auth resolution (default$BIRD_ENV_FILE; no machine path baked into the code).Architecture
The composable logic moved out of the (un-installed)
scripts/into the package so the installed console script can import it:bird_interact_agents.env_file.load_env_filebird_interact_agents.local_postgres(cluster helpers +provision_and_export)bird_interact_agents.local_annotations.sync_annotationsscripts/{setup_local_postgres,fetch_local_annotations}.pybecame thin CLIs over the package (setup keeps--stop/--recreateas the admin tool).scripts/run_local_postgres.pyis retired.Local ⇄ cloud parity
The same
sync_annotationsis wired into the cloudsubmitpre-build (inside therequire_annotationgate → self-healing: pull missing annotations from GCS first, then the gate only fires for ids absent in GCS too). So the annotation set the local grader reads == the set the cloud image bakes. Infra/auth errors propagate (fail-fast); per-blob-absent is counted, not raised. The cloud runner (ray_app/run_evaluation) is untouched —main()is the only local entrypoint and the cloud worker never calls it.One pre-existing divergence is noted but left out of scope: local creates a
rootrole (dumps doOWNER TO root), cloud does not (relies on the non-fatal error) — table data/schema identical, ownership differs, SELECT grading unaffected.Tests
Full non-integration suite green (3962 passed, 94 skipped). New:
test_env_file,test_local_annotations_sync,test_local_postgres_provision,test_run_local_dispatch(helper + CLI-boundary),tests/cloud/test_submit_annotation_presync. Plan + tests were each Codex-reviewed before implementation.🤖 Generated with Claude Code
Summary by CodeRabbit
--data/--db-pathwhen omitted.