From 324693b256a254ccf67a93deb0698095aadaec7a Mon Sep 17 00:00:00 2001 From: ProtocolWarden <32967198+ProtocolWarden@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:16:53 -0400 Subject: [PATCH 1/3] fix(custodian): close the vulture fail-open in the pre-push gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gate reported "0 findings, clean" while a Windows box running a newer Custodian reported hundreds. Windows was the correct side: the green gate was a FALSE CLEAN and had been for as long as the pin held. Three things lined up to hide it: 1. .custodian/config.yaml sets tools.vulture: true — the detector is meant to run. 2. pyproject.toml never declared vulture, so `uv pip install -e .[dev]` never installed it. The fleet venv has no vulture, and none is on PATH. 3. The pin d6ba8ab PREDATES Custodian 261bbb5 "fix(vulture): put paths before options, and stop reading a failed run as clean". On that pin the adapter built `vulture --min-confidence=N `, which vulture's argparse rejects (exit 2, empty stdout) — and the empty output read as "no dead code". So even with vulture installed the pinned adapter could not emit a finding: the invocation was malformed and the failure was swallowed. The detector has never run. Bump the pin to 7a780b7 (origin/main, contains 261bbb5) and declare vulture==2.16 beside the existing ruff/ty pins. These must land together — after 261bbb5 a missing vulture fails LOUDLY, so bumping alone would red the gate on "vulture not found". Set tools.vulture_min_confidence: 80 explicitly. Custodian's adapter registry falls back to 60 while its own config loader documents 80 as the default; inheriting whichever wins is how this stays surprising. On this repo 60 yields 621 findings (essentially all UNUSED_METHOD heuristics), 80 yields 32, all at 100% confidence. Of those 32, 22 are names an external contract forces us to accept — the __exit__ protocol, pytest's pytest_sessionfinish hookspec, fixtures requested purely for a side effect, lambda stubs mirroring the callee they replace — plus two compat shims the source already documents as deliberate. Those go in a new .vulture_whitelist.py, which Custodian's adapter picks up automatically. It matches on bare NAME, not location, so it is kept minimal with a justification per entry. The remaining 10 are real and deliberately NOT whitelisted: * observer/cli.py x8 — --format, --skip-validation, --output, --filter-status, --signals-only, --input, --validate-after, --keep are declared as typer options and never read. `layers` and `full` in the same command ARE read, which is what makes these stand out rather than look like a vulture blind spot. `--format yaml` silently yields JSON. * pr_review_watcher/main.py:2508,2543 — `pending_checks` threaded through and never used. CONSEQUENCE: this turns the gate red on those 10 until they are triaged. That is the intended effect — it was green by accident. Whether each observer flag should be wired up or deleted is product work and is not guessed at here. Tracked in .console/backlog.md under Up Next. Co-Authored-By: Claude Opus 5 --- .console/backlog.md | 25 +++++++++++++++++ .console/log.md | 64 ++++++++++++++++++++++++++++++++++++++++++ .custodian/config.yaml | 6 ++++ .vulture_whitelist.py | 49 ++++++++++++++++++++++++++++++++ pyproject.toml | 11 +++++++- 5 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 .vulture_whitelist.py diff --git a/.console/backlog.md b/.console/backlog.md index d1357f1c..89b6938e 100644 --- a/.console/backlog.md +++ b/.console/backlog.md @@ -2,6 +2,31 @@ _Durable work inventory. Update after each meaningful chunk of progress._ +## Up Next + +### Triage the 10 vulture findings the gate now reports (BLOCKS the pre-push gate) +- Turning the vulture detector back on (2026-08-03) leaves 10 genuine findings. + Until they are resolved `custodian-multi --fail-on-findings` is RED, so pushes + need `--no-verify`. This is the intended consequence of closing a fail-open, but + it should not sit unresolved. +- **`src/operations_center/observer/cli.py` ×8** — `--format` (`format_snapshot`), + `--skip-validation`, `--output` (`output_report`), `--filter-status`, + `--signals-only`, `--input` (`input_path`), `--validate-after`, `--keep` + (`keep_count`) are declared as `typer.Option(...)` and never read in the body. + `layers` and `full` in the same command ARE read, so this is not a vulture blind + spot. User-visible: `--format yaml` silently produces JSON. Each flag needs a + decision — wire it up or delete it. Do not whitelist. +- **`src/operations_center/entrypoints/pr_review_watcher/main.py:2508,2543`** — + `pending_checks` parameter passed and never used; remove it and update callers. + +### Push Custodian 5ef3f0f, or the Windows find_tool fix stays unpinnable +- `5ef3f0f fix(adapters): make find_tool's venv-first preference work on Windows` + exists only in the local Custodian checkout (branch `claude/reconcile-june-2026-08-03`, + upstream gone). `origin/main` is at `7a780b7`, which OC now pins. +- Until it is pushed, a Windows Custodian run resolves linters off PATH rather than + a venv. That produced 1222 phantom ruff findings on 2026-08-03 (ruff 0.16 default + rule set vs OC's pinned 0.15.13) before the local checkout picked the commit up. + ## Done ### 2026-07-15: Stage 4 — Refactor existing code to use the new shared helper (✅ COMPLETE) diff --git a/.console/log.md b/.console/log.md index 05306f9b..6a39b288 100644 --- a/.console/log.md +++ b/.console/log.md @@ -1,3 +1,67 @@ +## 2026-08-03 — fix(custodian): close the vulture fail-open in the pre-push gate + +The pre-push Custodian gate reported "0 findings, clean" on this repo while a +Windows box running a newer Custodian reported hundreds. Windows was the correct +side; the green gate was a FALSE CLEAN, and had been for as long as the pin has +been in place. + +Three things had to line up to hide it: + +1. `.custodian/config.yaml` sets `tools.vulture: true` — the detector is meant + to run. +2. `pyproject.toml` never declared `vulture` in the dev extra, so + `uv pip install -e .[dev]` never installed it. The fleet venv has no vulture + and none is on PATH. +3. The custodian pin `d6ba8ab` PREDATES Custodian 261bbb5, "fix(vulture): put + paths before options, and stop reading a failed run as clean". On that pin + the adapter built `vulture --min-confidence=N `, which vulture's + argparse rejects — exit 2, empty stdout — and the empty output was read as + "no dead code". + +So even had vulture been installed, the pinned adapter could not have produced a +finding: the invocation itself was malformed and the failure was swallowed. The +detector has never once run. Fixed by bumping the pin to 7a780b7 (origin/main, +contains 261bbb5) and declaring `vulture==2.16` alongside the existing ruff/ty +pins. The two must land together — after 261bbb5 a missing vulture fails LOUDLY, +so bumping the pin alone would red the gate on "vulture not found". + +Threshold set explicitly to `tools.vulture_min_confidence: 80`. Custodian's +adapter registry falls back to 60 while its own config loader documents 80 as +the intended default; relying on whichever wins is how this stays surprising. On +this repo the difference is stark: 60 yields 621 findings (essentially all +UNUSED_METHOD/attribute heuristics), 80 yields 32, every one at 100% confidence. + +Of those 32, 22 are names an external contract forces us to accept — the +`__exit__` protocol, pytest's `pytest_sessionfinish` hookspec, fixtures +requested purely for a side effect, lambda stubs that must mirror the callee +they replace — plus two compat shims the source already documents as deliberate +(`max_rewrite_attempts` carries `# noqa: ARG002 — kept for signature compat`, +`queue_threshold` carries `# kept for config compat, not used in logic`). Those +are listed in a new `.vulture_whitelist.py`, which Custodian's adapter picks up +automatically when present. The whitelist matches on bare NAME, not location, so +it is kept minimal and each entry carries its justification. + +The remaining 10 are real and are deliberately NOT whitelisted: + +* `observer/cli.py` ×8 — `--format`, `--skip-validation`, `--output`, + `--filter-status`, `--signals-only`, `--input`, `--validate-after`, `--keep` + are declared as typer options and never read. `layers` and `full` in the same + command ARE read, which is what makes these stand out rather than look like a + vulture blind spot. Passing `--format yaml` today silently yields JSON. +* `pr_review_watcher/main.py:2508,2543` — `pending_checks` parameter threaded + through two call sites and never used. + +CONSEQUENCE, stated plainly: merging this turns the gate red on those 10 until +they are triaged. That is the intended effect — the gate was previously green by +accident. Deciding whether each observer flag should be wired up or deleted is +product work and is not guessed at here. + +Also found, not fixable from this repo: the Custodian commit that makes +`find_tool` prefer a venv on Windows (5ef3f0f) exists only in the local checkout +and was never pushed, so it cannot be pinned. Without it a Windows run resolves +linters off PATH; that cost 1222 phantom ruff findings earlier today until the +local checkout picked the commit up mid-session. + ## 2026-07-15 — feat(reviewer): ACTIVATE the council — populate guardrail_paths (§G1) The council's go-live. C1/C2/C3 all merged; `reviewer.council.guardrail_paths` diff --git a/.custodian/config.yaml b/.custodian/config.yaml index 48220e6c..ef91151a 100644 --- a/.custodian/config.yaml +++ b/.custodian/config.yaml @@ -1127,6 +1127,12 @@ maintenance: tools: ruff: true vulture: true + # Custodian's adapter registry falls back to 60 when this is unset, but its + # own config loader documents 80 as the intended default — so set it here + # rather than inherit whichever wins. On this repo 60 yields 621 findings, + # essentially all UNUSED_METHOD/attribute heuristics; 80 yields 32, all at + # 100% confidence. Raise deliberately, not by accident. + vulture_min_confidence: 80 # AI3 is now expressed as a Semgrep rule (no Python AST walker). # See .custodian/rules/semgrep/ai3_no_directory_scanning.yaml. semgrep: diff --git a/.vulture_whitelist.py b/.vulture_whitelist.py new file mode 100644 index 00000000..af11d7fa --- /dev/null +++ b/.vulture_whitelist.py @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 ProtocolWarden +"""Vulture whitelist — names vulture reports as dead that a signature requires. + +Custodian's vulture adapter passes this file as an extra scan path when it +exists (see `custodian/adapters/vulture.py`), so every name referenced here +counts as used. Vulture flags unused *parameters* at 100% confidence, which is +correct for genuinely dead code but wrong whenever an external contract dictates +the signature — a protocol method, a pytest hook, a fixture requested purely for +its side effect, or a lambda that must mirror the callee it replaces. + +Scope discipline: this file is for names a contract forces us to accept, plus +compat shims the source itself already documents as deliberate. Genuinely unused +parameters are NOT listed here — they are left visible so the gate reports them. + +Vulture matches on the bare NAME, not the location, so an entry suppresses that +identifier repo-wide. Keep the list minimal and justified for that reason. +""" + +# --- Language / stdlib protocols ------------------------------------------ +# `__exit__(self, exc_type, exc_val, tb)` — the context-manager protocol fixes +# the signature; this implementation only needs to release the lock. +exc_val # noqa: B018 # src/operations_center/audit_dispatch/locks.py:103 + +# --- pytest hook specifications ------------------------------------------- +# `pytest_sessionfinish(session, exitstatus)` — pytest calls hooks by keyword +# against its own hookspec, so the parameter must exist whether or not it is read. +exitstatus # noqa: B018 # tests/conftest.py:301, observer/pytest_flaky_plugin.py:91 + +# --- pytest fixtures requested for their side effects --------------------- +# Naming the fixture in the signature is what activates it; the body has no +# reason to reference the value. +valid_console_dir # noqa: B018 # tests/unit/detectors/test_r{1,2}_console_*.py +no_cl_env # noqa: B018 # tests/unit/execution/test_coordinator_cl_wrap.py:78 +monkeypatch_modules # noqa: B018 # tests/unit/execution/test_workspace_cov.py:695 + +# --- Test doubles that must mirror the signature they replace ------------- +# monkeypatch/lambda stubs are called with the real callee's arguments, so they +# have to accept them even when the stub ignores them. +lg # noqa: B018 # lambda pm, rr, lg — test_repo_graph_factory_cov.py +indent # noqa: B018 # lambda indent=2 mirroring model_dump_json — *_cov.py +expected_kind # noqa: B018 # def _raise(_path, expected_kind=None) — graph_doctor + +# --- Compat shims the source already documents as deliberate -------------- +# Both carry an in-source comment stating the parameter is retained on purpose +# to avoid churning callers; one already suppresses ruff ARG002. Listed here so +# the two linters agree rather than one of them staying permanently red. +max_rewrite_attempts # noqa: B018 # spec_author/phase_orchestrator.py:141 +queue_threshold # noqa: B018 # spec_author/trigger.py:26 diff --git a/pyproject.toml b/pyproject.toml index 9d3d96d1..77a363eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -137,7 +137,16 @@ dev = [ "pytest-cov>=6.0", "ruff==0.15.13", "ty==0.0.40", - "custodian @ git+https://github.com/ProtocolWarden/Custodian.git@d6ba8ab245c6f4e79e9f8fffd4e4221bfaf266e8", + # vulture is enabled in .custodian/config.yaml (tools.vulture) but was never + # declared here, so it was never installed and the adapter could not run. + "vulture==2.16", + # Was pinned to d6ba8ab, which predates 261bbb5 "fix(vulture): put paths + # before options, and stop reading a failed run as clean". On that pin the + # adapter built `vulture --min-confidence=N `, which vulture's + # argparse rejects (exit 2, empty stdout) — and the failure read as CLEAN. + # Between that and the missing dependency above, the vulture detector could + # never emit a finding: the pre-push gate was fail-open on it. + "custodian @ git+https://github.com/ProtocolWarden/Custodian.git@7a780b7845337810a235111e378e78fb06361dd5", ] [tool.setuptools.packages.find] From a54de65857b603ba7aa25f61e60fed6985304c1a Mon Sep 17 00:00:00 2001 From: ProtocolWarden <32967198+ProtocolWarden@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:31:21 -0400 Subject: [PATCH 2/3] fix(observer): retire the CLI flags the gate's vulture pass exposed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clears the 10 genuine findings that were holding the pre-push gate red after the vulture fail-open was closed. `custodian-multi --fail-on-findings` now exits 0 under a custodian that actually runs vulture — it reported 621 before. Corrects the earlier claim that "`layers` and `full` in the same command ARE read, so parameter-usage detection is working". That was wrong. cmd_observe_and_validate's body reads ONLY `quiet`; `layers` and `full` are equally unread there and escaped the report because vulture matches on bare NAME and those names are used by other commands in the tree. The finding was bigger than 8 stray flags: FOUR commands (observe-and-validate, compare, import, cleanup) are stubs whose entire option lists are ignored, while --help and two user guides advertised them as working. Per the "implement or delete" bar: * The four stubs are documented as PLANNED (STAGE0_CLI_SPECIFICATION.md "Secondary Commands (Planned Future)"; both guides carry "not yet implemented" notes), so deleting the commands was wrong — but keeping parameters they discard was too. Each stub now takes --quiet only. The planned interface stays in the spec, which is where a design belongs; a half-declared signature that typer advertises in --help is not a spec, it is a promise the command breaks. Dropping `import`'s required input path is deliberate: accepting a file and discarding it is indistinguishable from importing it and failing. * `list --filter valid|invalid` deleted. It could never have worked — the listing walks snapshot directories and never loads or caches a validation status to filter on (its observed_at column is a literal "—"). Implementing it needs the caching layer the help text presumed. Also fixed in cmd_cleanup, and NOT one of the vulture findings: it exited EXIT_SUCCESS while deleting nothing, so a scheduled `cleanup --days 30` reported success and silently retained every snapshot, with no way for a caller to tell a working cleanup from a stub. Now exits non-zero. Same fail-open shape as the vulture bug — a green signal that means nothing — which is why it was worth fixing in place. The guide's two runnable cleanup examples are gone and both option tables are relabelled "Planned Options (not accepted today)". Removed `pending_checks` from _update_check_history and _should_escalate_ci_wait plus 16 call sites; neither body read it. The tests passed pending_checks=["audit"] in two places, implying behaviour that could not exist — those assertions were passing for the wrong reason. test_unimplemented_stubs_reject_planned_flags pins the intent: each stub must REJECT the planned flags rather than swallow them, so an ignored option cannot be re-added without a failing test. Nothing was added to .vulture_whitelist.py — every finding was resolved by removing dead code, not by suppressing the report. Co-Authored-By: Claude Opus 5 --- .console/backlog.md | 41 +++-- .console/log.md | 55 ++++++ docs/design/STAGE0_CLI_SPECIFICATION.md | 4 +- docs/user-guides/CLI_QUICK_REFERENCE.md | 15 +- .../SNAPSHOT_VALIDATION_CLI_GUIDE.md | 29 ++-- .../entrypoints/pr_review_watcher/main.py | 17 +- src/operations_center/observer/cli.py | 161 ++++-------------- .../reviewer/test_escalation_ci_thrash.py | 19 +-- tests/unit/observer/test_snapshot_cli.py | 32 +++- 9 files changed, 173 insertions(+), 200 deletions(-) diff --git a/.console/backlog.md b/.console/backlog.md index 89b6938e..de7e58d8 100644 --- a/.console/backlog.md +++ b/.console/backlog.md @@ -4,21 +4,6 @@ _Durable work inventory. Update after each meaningful chunk of progress._ ## Up Next -### Triage the 10 vulture findings the gate now reports (BLOCKS the pre-push gate) -- Turning the vulture detector back on (2026-08-03) leaves 10 genuine findings. - Until they are resolved `custodian-multi --fail-on-findings` is RED, so pushes - need `--no-verify`. This is the intended consequence of closing a fail-open, but - it should not sit unresolved. -- **`src/operations_center/observer/cli.py` ×8** — `--format` (`format_snapshot`), - `--skip-validation`, `--output` (`output_report`), `--filter-status`, - `--signals-only`, `--input` (`input_path`), `--validate-after`, `--keep` - (`keep_count`) are declared as `typer.Option(...)` and never read in the body. - `layers` and `full` in the same command ARE read, so this is not a vulture blind - spot. User-visible: `--format yaml` silently produces JSON. Each flag needs a - decision — wire it up or delete it. Do not whitelist. -- **`src/operations_center/entrypoints/pr_review_watcher/main.py:2508,2543`** — - `pending_checks` parameter passed and never used; remove it and update callers. - ### Push Custodian 5ef3f0f, or the Windows find_tool fix stays unpinnable - `5ef3f0f fix(adapters): make find_tool's venv-first preference work on Windows` exists only in the local Custodian checkout (branch `claude/reconcile-june-2026-08-03`, @@ -29,6 +14,32 @@ _Durable work inventory. Update after each meaningful chunk of progress._ ## Done +### 2026-08-03: Clear the 10 vulture findings blocking the pre-push gate (✅ COMPLETE) +- **Objective**: Re-enabling vulture left 10 genuine findings holding + `custodian-multi --fail-on-findings` RED. Resolve them by removing dead code, not + by whitelisting. +- **Status**: ✅ COMPLETE — gate clean at 0 findings, exit 0. +- **Correction**: the earlier claim that "`layers` and `full` in the same command ARE + read" was wrong — `cmd_observe_and_validate` reads only `quiet`. Those names escaped + the report because vulture matches on bare NAME and they are used by other commands. + The real scope was four stub commands whose entire option lists were ignored while + `--help` and two user guides advertised them. +- **Changes**: + - `observer/cli.py` — stripped `observe-and-validate`, `compare`, `import`, `cleanup` + to `--quiet` only; the planned interface stays in `docs/design/STAGE0_CLI_SPECIFICATION.md`. + Deleted `list --filter` (could never work — nothing caches a validation status). + Fixed `cleanup` exiting EXIT_SUCCESS while deleting nothing (not a vulture finding; + same fail-open shape, found while editing). + - `pr_review_watcher/main.py` — removed `pending_checks` from two functions + 16 call + sites; neither body read it. + - Docs — both user guides relabelled to "Planned Options (not accepted today)"; + removed the runnable `cleanup` examples; corrected the spec's `list` line. + - New test `test_unimplemented_stubs_reject_planned_flags` pins that stubs REJECT + planned flags rather than swallowing them. +- **Verification**: vulture reports nothing; `custodian-multi --fail-on-findings` exits 0; + ruff clean; full suite 10345 passed with the same 6 pre-existing sandbox/timing + failures, each reproduced on an unmodified checkout. `.vulture_whitelist.py` unchanged. + ### 2026-07-15: Stage 4 — Refactor existing code to use the new shared helper (✅ COMPLETE) - **Objective**: Independently re-verify Stage 2's migration against the "refactor existing code" acceptance bar (identified/updated all relevant callsites, replaced redundant diff --git a/.console/log.md b/.console/log.md index 6a39b288..535cb390 100644 --- a/.console/log.md +++ b/.console/log.md @@ -1,3 +1,58 @@ +## 2026-08-03 — fix(observer): retire the CLI flags the gate's vulture pass exposed + +Follow-up to closing the vulture fail-open earlier today. That left 10 genuine +findings holding the pre-push gate red; this clears them. Gate is now clean at +0 findings under a custodian that actually runs vulture (it reported 621 before). + +Correction to the earlier write-up, which claimed "`layers` and `full` in the +same command ARE read, so parameter-usage detection is working". That was wrong. +`cmd_observe_and_validate`'s body reads ONLY `quiet` — `layers` and `full` are +equally unread there. They escaped the report because vulture matches on bare +NAME and those names are used by other commands in the tree. The real finding +was bigger than 8 stray flags: FOUR commands (`observe-and-validate`, `compare`, +`import`, `cleanup`) are stubs whose entire option lists are ignored, and +`--help` plus two user guides advertised them as though they worked. + +Decision per flag, per the "implement or delete" bar: + +* The four stubs are documented as PLANNED (`docs/design/STAGE0_CLI_SPECIFICATION.md` + §"Secondary Commands (Planned Future)"; both user guides carry "not yet + implemented" notes). So deleting the commands was wrong — but so was keeping + parameters they discard. Stripped each stub to `--quiet` only. The planned + interface stays in the spec, which is where a design belongs; a half-declared + signature that typer advertises in `--help` is not a spec, it is a promise the + command breaks. Deleting `import`'s required input path is deliberate: taking + a file and dropping it is indistinguishable from importing it and failing. +* `list --filter valid|invalid` — deleted. It could never have worked: the + listing walks snapshot directories and never loads or caches a validation + status to filter on (its observed_at column is a literal "—"). Implementing it + needs the caching layer the help text presumed, not a flag. + +Also fixed while in `cmd_cleanup`, and NOT one of the vulture findings: it +exited EXIT_SUCCESS while deleting nothing. A scheduled `cleanup --days 30` +therefore reported success and silently retained every snapshot forever, with no +way for the caller to tell a working cleanup from a stub. Now exits non-zero. +Same fail-open shape as the vulture bug itself — a green signal that means +nothing — which is why it was worth fixing rather than leaving for later. The +guide's two runnable `cleanup` examples were removed; the option tables in both +guides are relabelled "Planned Options (not accepted today)". + +`pending_checks` removed from `_update_check_history` and `_should_escalate_ci_wait` +in pr_review_watcher, plus 16 call sites. Neither body ever read it. Note the +tests passed `pending_checks=["audit"]` in two places, implying behaviour that +could not exist — those assertions were passing for the wrong reason. + +New test pins the intent: `test_unimplemented_stubs_reject_planned_flags` asserts +each stub REJECTS the planned flags rather than swallowing them, so nobody +re-adds an ignored option without a failing test. + +Verification: `vulture src tests .vulture_whitelist.py --min-confidence=80` +reports nothing; `custodian-multi --fail-on-findings` exits 0 (clean); ruff +check/format clean; full suite 10345 passed with the same 6 pre-existing +sandbox/timing failures, each reproduced on an unmodified checkout. Nothing was +added to .vulture_whitelist.py — every finding was resolved by removing the dead +code, not by suppressing the report. + ## 2026-08-03 — fix(custodian): close the vulture fail-open in the pre-push gate The pre-push Custodian gate reported "0 findings, clean" on this repo while a diff --git a/docs/design/STAGE0_CLI_SPECIFICATION.md b/docs/design/STAGE0_CLI_SPECIFICATION.md index 1a4eba4f..6f08f80e 100644 --- a/docs/design/STAGE0_CLI_SPECIFICATION.md +++ b/docs/design/STAGE0_CLI_SPECIFICATION.md @@ -479,7 +479,9 @@ operations-center-observer-snapshot observe-and-validate \ #### `list` List available snapshots in storage ```bash -operations-center-observer-snapshot list [--filter recent|all] [--format table|json] +operations-center-observer-snapshot list [--limit N] [--order recent|oldest|name] [--format table|json] +# Note: `list` is implemented. A --filter flag was declared but +# never worked (no validation status is cached to filter on) and was removed. ``` #### `compare` diff --git a/docs/user-guides/CLI_QUICK_REFERENCE.md b/docs/user-guides/CLI_QUICK_REFERENCE.md index a96d404c..6afdae54 100644 --- a/docs/user-guides/CLI_QUICK_REFERENCE.md +++ b/docs/user-guides/CLI_QUICK_REFERENCE.md @@ -164,7 +164,6 @@ operations-center-observer-snapshot list [OPTIONS] ```bash --limit N Max snapshots to list [default: 10] --order ORDER Sort order (recent|oldest|name) [default: recent] ---filter STATUS Filter (valid|invalid) --format FORMAT Output format (table|json|csv) [default: table] --storage-root PATH Storage directory --verbose, -v Include file size, checksum @@ -223,22 +222,14 @@ operations-center-observer-snapshot export snapshot-id export.jsonl operations-center-observer-snapshot cleanup [OPTIONS] ``` -### Options +**Not yet implemented.** This command accepts only `--quiet`; it exits non-zero without doing any work. The planned options below are a design target recorded in `docs/design/STAGE0_CLI_SPECIFICATION.md`, not flags the CLI currently accepts. + +### Planned Options (not accepted today) ```bash --days N Delete snapshots older than N days [default: 30] --keep-count N Keep at least N most recent [default: 50] --dry-run/--no-dry-run Preview changes (default: true) --storage-root PATH Storage directory ---quiet, -q Minimal output -``` - -### Examples -```bash -# Preview cleanup -operations-center-observer-snapshot cleanup --days 30 --keep-count 50 - -# Actually delete (not dry-run) -operations-center-observer-snapshot cleanup --days 30 --keep-count 50 --no-dry-run ``` --- diff --git a/docs/user-guides/SNAPSHOT_VALIDATION_CLI_GUIDE.md b/docs/user-guides/SNAPSHOT_VALIDATION_CLI_GUIDE.md index 6a2d84a4..d33a5cfb 100644 --- a/docs/user-guides/SNAPSHOT_VALIDATION_CLI_GUIDE.md +++ b/docs/user-guides/SNAPSHOT_VALIDATION_CLI_GUIDE.md @@ -178,7 +178,6 @@ operations-center-observer-snapshot list [OPTIONS] |--------|------|---------|-------------| | `--limit` | int | `10` | Maximum snapshots to list | | `--order` | string | `recent` | Sort order: `recent`, `oldest`, `name` | -| `--filter` | string | — | Filter by: `valid`, `invalid` | | `--format` | string | `table` | Output format: `table`, `json`, `csv` | | `--backend` | string | `local` | Storage backend: `local`, `s3`, `http` | | `--storage-root` | path | `tools/report/operations_center/observer` | Storage root directory | @@ -271,7 +270,7 @@ operations-center-observer-snapshot compare SNAPSHOT1 SNAPSHOT2 [OPTIONS] #### Status -**Note**: `compare` command is not yet implemented. Use `show` command to view snapshots for manual comparison. +**Note**: `compare` is not yet implemented — it accepts only `--quiet` and exits non-zero. The options below are a planned design, not flags the CLI accepts today. Use `show` command to view snapshots for manual comparison. --- @@ -343,7 +342,7 @@ operations-center-observer-snapshot import INPUT_PATH [OPTIONS] #### Status -**Note**: `import` command is not yet implemented. +**Note**: `import` is not yet implemented — it accepts only `--quiet` (not even the input path) and exits non-zero. The options below are a planned design. --- @@ -359,6 +358,10 @@ operations-center-observer-snapshot cleanup [OPTIONS] #### Options +`cleanup` accepts only `--quiet` today. + +#### Planned Options (not accepted today) + | Option | Type | Default | Description | |--------|------|---------|-------------| | `--days` | int | `30` | Delete snapshots older than N days | @@ -366,21 +369,15 @@ operations-center-observer-snapshot cleanup [OPTIONS] | `--dry-run` | bool | true | Preview changes without deleting (default) | | `--backend` | string | `local` | Storage backend | | `--storage-root` | path | `tools/report/operations_center/observer` | Storage root directory | -| `--quiet` | `-q` | bool | false | Minimal output | - -#### Example - -```bash -# Preview: snapshots that would be deleted -operations-center-observer-snapshot cleanup --days 30 --keep-count 50 - -# Actually delete (not dry-run) -operations-center-observer-snapshot cleanup --days 30 --keep-count 50 --no-dry-run -``` #### Status -**Note**: `cleanup` command is not yet fully implemented. +**Note**: `cleanup` is not implemented. It deletes nothing and exits non-zero. + +It previously exited **0** while doing no work, so a scheduled +`cleanup --days 30` reported success and silently retained every snapshot. +Do not wire it into automation until it is implemented — there is deliberately +no runnable example here. --- @@ -410,7 +407,7 @@ operations-center-observer-snapshot observe-and-validate [OPTIONS] #### Status -**Note**: `observe-and-validate` command requires RepoObserver integration (not yet implemented). +**Note**: `observe-and-validate` requires RepoObserver integration (not yet implemented). It accepts only `--quiet` and exits non-zero. The options below are a planned design. --- diff --git a/src/operations_center/entrypoints/pr_review_watcher/main.py b/src/operations_center/entrypoints/pr_review_watcher/main.py index f4ba2252..fb95a67f 100644 --- a/src/operations_center/entrypoints/pr_review_watcher/main.py +++ b/src/operations_center/entrypoints/pr_review_watcher/main.py @@ -2505,7 +2505,6 @@ def _update_check_history( state: dict, failed_checks: list[str], completed_checks: list[str], - pending_checks: list[str], current_head_sha: str, ) -> None: """Track check outcomes to distinguish transient from stuck checks. @@ -2540,7 +2539,6 @@ def _should_escalate_ci_wait( state: dict, missing_required: list[str], failed_checks: list[str], - pending_checks: list[str], ci_wait_cycles_first_registration: int = 60, ci_wait_cycles_already_seen: int = 40, ci_flakiness_threshold_pct: int = 30, @@ -3027,7 +3025,7 @@ def _phase1( pr_data=pr_data, ignored_checks=ignored, ) - _update_check_history(state, failed, completed, [], current_head_sha or "") + _update_check_history(state, failed, completed, current_head_sha or "") # Get configured required checks for this repo repo_required = ( @@ -3039,7 +3037,6 @@ def _phase1( state, missing_required=failed, # Failed checks as missing required failed_checks=failed, - pending_checks=[], ci_wait_cycles_first_registration=60, ci_wait_cycles_already_seen=40, ci_flakiness_threshold_pct=30, @@ -3129,7 +3126,7 @@ def _phase1( state["ci_wait_cycles"] = state.get("ci_wait_cycles", 0) + 1 # Track check history for classification - _update_check_history(state, [], completed, pending, current_head_sha or "") + _update_check_history(state, [], completed, current_head_sha or "") if pending: _why = f"{len(pending)} still running: {', '.join(pending[:5])}" @@ -4054,12 +4051,16 @@ def _run_council( now = datetime.now(UTC) available = [ - member for member in _COUNCIL_PANEL if not _member_on_cooldown(usage_store, *member[:2], now=now) + member + for member in _COUNCIL_PANEL + if not _member_on_cooldown(usage_store, *member[:2], now=now) ] min_members = getattr(council, "min_council_members", 3) if len(available) < min_members: - cooled = sorted(f"{b}/{m}" for (b, m, _lens) in _COUNCIL_PANEL if (b, m, _lens) not in available) + cooled = sorted( + f"{b}/{m}" for (b, m, _lens) in _COUNCIL_PANEL if (b, m, _lens) not in available + ) detail = ( f"Guardrail-surface PR requires a {min_members}-member cross-family council " f"(COUNCIL_VERDICT.md C1); only {len(available)}/{len(_COUNCIL_PANEL)} panel seats " @@ -4470,7 +4471,7 @@ def main() -> int: # Containment self-check (audit Track A3): surface a broken posture at boot. for problem in verify_containment(): logger.error( - 'pr_review_watcher: containment self-check FAILED — %s ' + "pr_review_watcher: containment self-check FAILED — %s " '{"event": "containment_selfcheck_failed", "problem": "%s"}', problem, problem, diff --git a/src/operations_center/observer/cli.py b/src/operations_center/observer/cli.py index c2fea338..12ad6bd0 100644 --- a/src/operations_center/observer/cli.py +++ b/src/operations_center/observer/cli.py @@ -365,47 +365,6 @@ def cmd_validate( @app.command("observe-and-validate") def cmd_observe_and_validate( - repo_path: Path | None = typer.Option( - None, - "--repo-path", - help="Repository path (default: current directory)", - ), - output_dir: Path = typer.Option( - Path("tools/report/operations_center/observer"), - "--output-dir", - help="Where to save snapshot", - ), - format_snapshot: str = typer.Option( - "json", - "--format", - help="Snapshot format: json|yaml", - ), - layers: str | None = typer.Option( - None, - "--layers", - help="Validation layers to run (default: 1,2,3)", - ), - full: bool = typer.Option( - False, - "--full", - help="Include slow layers (4,5) — takes 60-120s", - ), - skip_validation: bool = typer.Option( - False, - "--skip-validation", - help="Collect snapshot but skip validation", - ), - output_report: Path | None = typer.Option( - None, - "--output", - help="Save validation report to file", - ), - verbose: bool = typer.Option( - False, - "--verbose", - "-v", - help="Detailed output", - ), quiet: bool = typer.Option( False, "--quiet", @@ -413,7 +372,13 @@ def cmd_observe_and_validate( help="Minimal output", ), ) -> None: - """Generate snapshot and validate it.""" + """Generate snapshot and validate it (not yet implemented). + + The planned option set lives in docs/design/STAGE0_CLI_SPECIFICATION.md + under "Secondary Commands (Planned Future)". It is deliberately NOT declared + here: typer would advertise the flags in --help and accept values this stub + then discards, which reads to the caller as though the option took effect. + """ if not quiet: console.print("[cyan]observe-and-validate[/cyan] command not yet implemented") console.print("This command requires RepoObserver integration.") @@ -432,11 +397,12 @@ def cmd_list( "--order", help="Sort order: recent|oldest|name", ), - filter_status: str | None = typer.Option( - None, - "--filter", - help="Filter: valid|invalid (if validation cached)", - ), + # `--filter valid|invalid` was declared here and never read. It could not + # have worked: the listing is built by walking snapshot directories and + # never loads or caches a validation status to filter on (the table's + # observed_at column is likewise a literal "—"). Removed rather than + # stubbed — filtering on data the command does not have needs the caching + # layer the help text presumed, not a flag. format_str: str = typer.Option( "table", "--format", @@ -610,33 +576,6 @@ def cmd_show( @app.command("compare") def cmd_compare( - snapshot1: str = typer.Argument(..., help="First snapshot path/ID"), - snapshot2: str = typer.Argument(..., help="Second snapshot path/ID"), - format_str: str = typer.Option( - "diff", - "--format", - help="Output format: diff|json|table", - ), - signals_only: str | None = typer.Option( - None, - "--signals", - help="Compare specific signals (comma-separated)", - ), - stats: bool = typer.Option( - False, - "--stats", - help="Show change statistics", - ), - output: Path | None = typer.Option( - None, - "--output", - help="Save comparison to file", - ), - backend: str = typer.Option( - "local", - "--backend", - help="Storage backend", - ), quiet: bool = typer.Option( False, "--quiet", @@ -644,7 +583,11 @@ def cmd_compare( help="Minimal output", ), ) -> None: - """Compare two snapshots.""" + """Compare two snapshots (not yet implemented). + + Planned arguments and options: see docs/design/STAGE0_CLI_SPECIFICATION.md. + Not declared here — see the note on observe-and-validate. + """ if not quiet: console.print("[cyan]compare[/cyan] command not yet implemented") raise typer.Exit(EXIT_CONFIG_ERROR) @@ -738,30 +681,6 @@ def cmd_export( @app.command("import") def cmd_import( - input_path: Path = typer.Argument( - ..., - help="Input file path (JSON/YAML/JSONL)", - ), - format_str: str | None = typer.Option( - None, - "--format", - help="Format: json|yaml (auto-detect if not set)", - ), - backend: str = typer.Option( - "local", - "--backend", - help="Storage backend", - ), - output_dir: Path | None = typer.Option( - None, - "--output-dir", - help="Where to store (local backend)", - ), - validate_after: bool = typer.Option( - True, - "--validate-after/--no-validate-after", - help="Run validation after import", - ), quiet: bool = typer.Option( False, "--quiet", @@ -769,7 +688,12 @@ def cmd_import( help="Minimal output", ), ) -> None: - """Import snapshot from file.""" + """Import snapshot from file (not yet implemented). + + Planned arguments and options: see docs/design/STAGE0_CLI_SPECIFICATION.md. + The input path is deliberately not accepted while this is a stub — taking a + file and discarding it is indistinguishable from importing it and failing. + """ if not quiet: console.print("[cyan]import[/cyan] command not yet implemented") raise typer.Exit(EXIT_CONFIG_ERROR) @@ -777,31 +701,6 @@ def cmd_import( @app.command("cleanup") def cmd_cleanup( - days: int = typer.Option( - 30, - "--days", - help="Delete snapshots older than N days", - ), - keep_count: int = typer.Option( - 50, - "--keep-count", - help="Keep at least N most recent snapshots", - ), - dry_run: bool = typer.Option( - True, - "--dry-run/--no-dry-run", - help="Actually delete (default: dry-run preview)", - ), - backend: str = typer.Option( - "local", - "--backend", - help="Storage backend", - ), - storage_root: Path | None = typer.Option( - None, - "--storage-root", - help="Storage root directory (local backend)", - ), quiet: bool = typer.Option( False, "--quiet", @@ -809,10 +708,18 @@ def cmd_cleanup( help="Minimal output", ), ) -> None: - """Remove old snapshots.""" + """Remove old snapshots (not yet implemented). + + Planned options: see docs/design/STAGE0_CLI_SPECIFICATION.md. + + Exits non-zero. This previously returned EXIT_SUCCESS while deleting + nothing, so a scheduled `cleanup --days 30` reported success and silently + retained every snapshot forever — the caller had no way to tell a working + cleanup from a stub. An unimplemented command must not claim success. + """ if not quiet: console.print("[cyan]cleanup[/cyan] command not yet implemented") - raise typer.Exit(EXIT_SUCCESS) + raise typer.Exit(EXIT_CONFIG_ERROR) @app.command("query-flaky-tests") diff --git a/tests/integration/reviewer/test_escalation_ci_thrash.py b/tests/integration/reviewer/test_escalation_ci_thrash.py index a27844c7..bda99403 100644 --- a/tests/integration/reviewer/test_escalation_ci_thrash.py +++ b/tests/integration/reviewer/test_escalation_ci_thrash.py @@ -89,7 +89,6 @@ def test_flaky_check_passes_eventually_does_not_escalate_at_cycle_20( state, missing_required=["tests"], failed_checks=["tests"], - pending_checks=[], ci_wait_cycles_first_registration=60, ci_wait_cycles_already_seen=40, ci_flakiness_threshold_pct=30, @@ -119,7 +118,6 @@ def test_flaky_check_escalates_at_cycle_40_with_high_failure_rate( state, missing_required=["tests"], failed_checks=["tests"], - pending_checks=[], ci_wait_cycles_first_registration=60, ci_wait_cycles_already_seen=40, ci_flakiness_threshold_pct=30, @@ -134,17 +132,17 @@ def test_flaky_check_history_tracks_passes_and_failures(self, state_path: Path) state = _new_state(REPO_KEY, PR_NUMBER) # Cycle 1: check passes - _update_check_history(state, [], ["tests"], [], "sha1") + _update_check_history(state, [], ["tests"], "sha1") assert state["ci_check_history"]["tests"]["times_passed"] == 1 assert state["ci_check_history"]["tests"]["times_failed"] == 0 # Cycle 2: check fails - _update_check_history(state, ["tests"], ["tests"], [], "sha2") + _update_check_history(state, ["tests"], ["tests"], "sha2") assert state["ci_check_history"]["tests"]["times_passed"] == 1 assert state["ci_check_history"]["tests"]["times_failed"] == 1 # Cycle 3: check passes - _update_check_history(state, [], ["tests"], [], "sha3") + _update_check_history(state, [], ["tests"], "sha3") assert state["ci_check_history"]["tests"]["times_passed"] == 2 assert state["ci_check_history"]["tests"]["times_failed"] == 1 @@ -167,7 +165,6 @@ def test_late_registering_check_waits_until_cycle_60(self, state_path: Path) -> state, missing_required=["audit"], failed_checks=[], - pending_checks=["audit"], ci_wait_cycles_first_registration=60, ci_wait_cycles_already_seen=40, ci_flakiness_threshold_pct=30, @@ -189,7 +186,6 @@ def test_late_registering_check_escalates_at_cycle_60(self, state_path: Path) -> state, missing_required=["audit"], failed_checks=[], - pending_checks=["audit"], ci_wait_cycles_first_registration=60, ci_wait_cycles_already_seen=40, ci_flakiness_threshold_pct=30, @@ -350,7 +346,6 @@ def test_lgtm_on_green_ci_no_escalation_needed(self, state_path: Path) -> None: state, missing_required=[], failed_checks=[], - pending_checks=[], ci_wait_cycles_first_registration=60, ci_wait_cycles_already_seen=40, ci_flakiness_threshold_pct=30, @@ -382,7 +377,6 @@ def test_persistent_failure_escalates_at_threshold(self, state_path: Path) -> No state, missing_required=["build"], failed_checks=["build"], - pending_checks=[], ci_wait_cycles_first_registration=60, ci_wait_cycles_already_seen=40, ci_flakiness_threshold_pct=30, @@ -434,7 +428,6 @@ def test_check_history_memory_efficient(self, state_path: Path) -> None: state, failed_checks=[] if i % 3 else [f"check_{i}"], completed_checks=[f"check_{i}"], - pending_checks=[], current_head_sha=f"sha{i}", ) @@ -461,14 +454,13 @@ def test_full_flaky_check_flow_30_cycles(self, state_path: Path) -> None: state["ci_wait_cycles"] = cycle # Flaky pattern: fails every 10th cycle failed = ["tests"] if cycle % 10 == 0 else [] - _update_check_history(state, failed, ["tests"], [], f"sha{cycle}") + _update_check_history(state, failed, ["tests"], f"sha{cycle}") # After 30 cycles: should NOT escalate (below 40 threshold) should_escalate, _ = _should_escalate_ci_wait( state, missing_required=["tests"], failed_checks=["tests"], - pending_checks=[], ci_wait_cycles_first_registration=60, ci_wait_cycles_already_seen=40, ci_flakiness_threshold_pct=30, @@ -483,14 +475,13 @@ def test_full_flaky_check_flow_40_cycles(self, state_path: Path) -> None: for cycle in range(1, 41): state["ci_wait_cycles"] = cycle failed = ["tests"] if cycle % 10 == 0 else [] - _update_check_history(state, failed, ["tests"], [], f"sha{cycle}") + _update_check_history(state, failed, ["tests"], f"sha{cycle}") # At 40 cycles: should escalate if still failing should_escalate, reason = _should_escalate_ci_wait( state, missing_required=["tests"], failed_checks=["tests"], - pending_checks=[], ci_wait_cycles_first_registration=60, ci_wait_cycles_already_seen=40, ci_flakiness_threshold_pct=30, diff --git a/tests/unit/observer/test_snapshot_cli.py b/tests/unit/observer/test_snapshot_cli.py index 9727e4b4..7be048dd 100644 --- a/tests/unit/observer/test_snapshot_cli.py +++ b/tests/unit/observer/test_snapshot_cli.py @@ -264,7 +264,12 @@ def test_export_auto_format_detection(self) -> None: class TestUnimplementedCommands: - """Tests for unimplemented commands.""" + """Tests for unimplemented commands. + + These stubs accept only --quiet. The planned argument/option sets live in + docs/design/STAGE0_CLI_SPECIFICATION.md rather than in the signatures, so + that --help never advertises a flag the stub would silently discard. + """ def test_observe_and_validate_not_implemented(self) -> None: """Test observe-and-validate command.""" @@ -274,23 +279,36 @@ def test_observe_and_validate_not_implemented(self) -> None: def test_compare_not_implemented(self) -> None: """Test compare command.""" - result = runner.invoke(app, ["compare", "snap1", "snap2"]) + result = runner.invoke(app, ["compare"]) assert result.exit_code == EXIT_CONFIG_ERROR assert "not yet implemented" in result.stdout def test_import_not_implemented(self) -> None: """Test import command.""" - with tempfile.NamedTemporaryFile(suffix=".json") as f: - result = runner.invoke(app, ["import", f.name]) - assert result.exit_code == EXIT_CONFIG_ERROR - assert "not yet implemented" in result.stdout + result = runner.invoke(app, ["import"]) + assert result.exit_code == EXIT_CONFIG_ERROR + assert "not yet implemented" in result.stdout def test_cleanup_not_implemented(self) -> None: """Test cleanup command.""" result = runner.invoke(app, ["cleanup"]) - assert result.exit_code == EXIT_SUCCESS + # Non-zero on purpose: this used to exit 0 while deleting nothing, so a + # scheduled cleanup reported success and retained every snapshot. + assert result.exit_code == EXIT_CONFIG_ERROR assert "not yet implemented" in result.stdout + def test_unimplemented_stubs_reject_planned_flags(self) -> None: + """A stub must not silently swallow a flag it cannot honour.""" + for argv in ( + ["compare", "snap1", "snap2"], + ["import", "snapshot.json"], + ["cleanup", "--keep-count", "50"], + ["observe-and-validate", "--skip-validation"], + ): + result = runner.invoke(app, argv) + assert result.exit_code != EXIT_SUCCESS, argv + assert "not yet implemented" not in result.stdout, argv + class TestGlobalOptions: """Tests for global options.""" From 748de6353ea21f95f9c58ffc21097e355f89c188 Mon Sep 17 00:00:00 2001 From: ProtocolWarden <32967198+ProtocolWarden@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:39:01 -0400 Subject: [PATCH 3/3] fix(ci): bump the audit workflow's Custodian pin in lockstep with pyproject MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The custodian-audit workflow hardcodes its own Custodian SHA, separate from pyproject's, and its comment requires the two be bumped together. This PR moved pyproject d6ba8ab -> 7a780b7 without it, so CI would have kept installing the old adapter and the vulture fail-open would have survived in the one place it matters most — the required gate. d6ba8ab predates Custodian 261bbb5, which fixed the adapter building `vulture --min-confidence=N `, an argument order vulture's argparse rejects (exit 2, empty stdout) that was then read as "no dead code". That is why #492 observed "vulture was clean in CI" while vulture was installed and this repo in fact had 621 findings at the default confidence: every run failed and every failure was swallowed. It is the same vacuous-green mode the adjacent step already warns about for a missing ruff. Also drops the unpinned `pip install vulture`. vulture is a dev dependency now, so `.[dev]` pins it (2.16) beside ruff and ty — removing the moving part rather than relocating it. Co-Authored-By: Claude Opus 5 --- .console/log.md | 22 ++++++++++++++++++++++ .github/workflows/custodian-audit.yml | 16 +++++++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/.console/log.md b/.console/log.md index 43a3715b..72ed885e 100644 --- a/.console/log.md +++ b/.console/log.md @@ -1,3 +1,25 @@ +## 2026-08-04 — fix(ci): bump the audit workflow's Custodian pin in lockstep with pyproject + +`.github/workflows/custodian-audit.yml` hardcodes its OWN Custodian SHA, separate +from pyproject's, and its comment explicitly requires the two move together. The +vulture fail-open fix bumped pyproject d6ba8ab -> 7a780b7 but missed the +workflow, so CI would have kept installing the old adapter — leaving the +fail-open alive in the one place it matters most, the required `audit` gate. + +This also explains an observation in #492, which landed on main today: it noted +"the Custodian audit reported 1222 findings (the ruff group alone — vulture was +clean in CI)". Vulture WAS installed in CI. It was not clean: on d6ba8ab the +adapter builds `vulture --min-confidence=N `, an argument order +vulture's argparse rejects (exit 2, empty stdout), and the empty output was read +as "no dead code". This repo had 621 findings at vulture's default confidence +the whole time. Independent corroboration of the fail-open from a different +author on a different day. + +Also dropped the workflow's unpinned `pip install vulture`. vulture is a dev +dependency now, so `.[dev]` pins it (2.16) beside ruff and ty — removing the +moving part rather than relocating it, which is exactly the argument #492's own +comment makes one level down about ruff. + ## 2026-08-03 — fix(observer): retire the CLI flags the gate's vulture pass exposed Follow-up to closing the vulture fail-open earlier today. That left 10 genuine diff --git a/.github/workflows/custodian-audit.yml b/.github/workflows/custodian-audit.yml index 04ec4842..fa8bd4ba 100644 --- a/.github/workflows/custodian-audit.yml +++ b/.github/workflows/custodian-audit.yml @@ -23,13 +23,23 @@ jobs: # emitting a phantom finding fleet-wide despite OC's `r1_enabled: false`, # red-failing the audit on every repo between two PRs. Bump this SHA in # lockstep with pyproject when intentionally adopting newer detectors. + # + # Bumped d6ba8ab -> 7a780b7 in lockstep with pyproject, as this comment + # requires. d6ba8ab predates Custodian 261bbb5, which fixed the vulture + # adapter building `vulture --min-confidence=N ` — an order + # vulture's argparse rejects (exit 2, empty stdout) that was then read as + # "no dead code". That is why this gate has reported vulture clean while + # vulture was installed: the run failed every time and the failure was + # swallowed. Same vacuous-green failure mode the step below warns about. run: | python -m pip install --upgrade pip - pip install "custodian[tools] @ git+https://github.com/ProtocolWarden/Custodian.git@d6ba8ab245c6f4e79e9f8fffd4e4221bfaf266e8" - pip install vulture + pip install "custodian[tools] @ git+https://github.com/ProtocolWarden/Custodian.git@7a780b7845337810a235111e378e78fb06361dd5" - name: Install repo and its pinned lint toolchain - # `.[dev]` (not plain `.`) so the adapters run OC's OWN pinned ruff/ty. + # `.[dev]` (not plain `.`) so the adapters run OC's OWN pinned ruff/ty/vulture. + # vulture used to be installed unpinned in the step above — the same moving + # part this comment objects to. It is a dev dependency now, so `.[dev]` + # pins it (2.16) alongside ruff and ty. # The reproducibility argument in the step above applies one level down: # pinning Custodian while installing `ruff` unpinned just moves the moving # part. It floated to 0.16.1 and this gate reported 1222 findings against a