diff --git a/pyproject.toml b/pyproject.toml index 3e45e233..be210777 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ opencode = ["httpx>=0.28"] # dev gets tui via `uv sync --all-extras` (groups can't self-reference an extra). dev = [ "pytest>=8.0", - "ruff>=0.4", + "ruff>=0.15.17", "pytest-asyncio>=1.0", "pytest-rerunfailures>=16.0", "pytest-xdist>=3.6", @@ -60,6 +60,18 @@ target-version = "py311" # skill assets contain template placeholders that are not valid Python extend-exclude = [".agents", ".claude", "src/bmad_loop/data/skills"] +[tool.ruff.lint] +# CI lints with trunk's ruff (pinned 0.15.17), whose plugin injects no `--select` +# and so runs ruff's own default rule set. With no lint table that default is +# resolved per ruff version and can drift, so pin the families explicitly. The +# repo's ruff is pinned to 0.15.17 via trunk (`trunk check` = what CI runs and +# the documented local lint, see CONTRIBUTING) and floored at >=0.15.17 in the +# dev deps (uv.lock resolves 0.15.17), so those paths match CI; a bare, newer +# ruff gets rule-family parity only — new rules can land under these prefixes. +# This spells out ruff 0.15.17's default set (`E4`, `E7`, `E9`, `F`) (#246). Do +# NOT ratchet stricter than CI without moving CI first. +select = ["E4", "E7", "E9", "F"] + [tool.black] line-length = 100 target-version = ["py311"] diff --git a/src/bmad_loop/adapters/base.py b/src/bmad_loop/adapters/base.py index 0b13fb69..92c39fd9 100644 --- a/src/bmad_loop/adapters/base.py +++ b/src/bmad_loop/adapters/base.py @@ -106,7 +106,7 @@ def interactive_env(self, spec: SessionSpec) -> dict[str, str]: """Env vars to layer onto the caller's environment for interactive_argv.""" return dict(spec.env) - def kill(self, handle: SessionHandle) -> None: # noqa: B027 - optional cleanup + def kill(self, handle: SessionHandle) -> None: # optional cleanup pass def read_usage(self, result: SessionResult) -> TokenUsage | None: diff --git a/src/bmad_loop/adapters/generic.py b/src/bmad_loop/adapters/generic.py index 7831c772..e8fe8428 100644 --- a/src/bmad_loop/adapters/generic.py +++ b/src/bmad_loop/adapters/generic.py @@ -766,13 +766,13 @@ def kill(self, handle: SessionHandle) -> None: for pid in repane: try: host.force_kill(pid) - except Exception: # noqa: BLE001 # nosec B110 - already-gone races are fine + except Exception: # nosec B110 - already-gone races are fine pass for pid, identity in tree.items(): if pid not in repane and identity is not None and host.alive_and_ours(pid, identity): try: host.force_kill(pid) - except Exception: # noqa: BLE001 # nosec B110 - already-gone races are fine + except Exception: # nosec B110 - already-gone races are fine pass self.mux.kill_window(handle.native_id) try: @@ -830,7 +830,7 @@ def _confirmed_survivors() -> list[int]: try: host.force_kill(pid) forced.append(pid) - except Exception: # noqa: BLE001 # nosec B110 - already-gone races are fine + except Exception: # nosec B110 - already-gone races are fine pass unreaped = [pid for pid, identity in tree.items() if host.alive_and_ours(pid, identity)] # Distinct field name (`unreaped`, a pid list) from the wedged branch's diff --git a/src/bmad_loop/adapters/multiplexer.py b/src/bmad_loop/adapters/multiplexer.py index d7e0caf0..58773d02 100644 --- a/src/bmad_loop/adapters/multiplexer.py +++ b/src/bmad_loop/adapters/multiplexer.py @@ -354,13 +354,13 @@ def _load_external_backends() -> None: _EXTERNALS_LOADED = True try: eps = importlib.metadata.entry_points(group=MUX_BACKENDS_GROUP) - except Exception as exc: # noqa: BLE001 — diagnostics path, never crash selection + except Exception as exc: # diagnostics path, never crash selection _EXTERNAL_ERRORS[""] = f"{type(exc).__name__}: {exc}" return for ep in eps: try: ep.load() # module import runs register_multiplexer(...) - except Exception as exc: # noqa: BLE001 — one bad package must not hide the rest + except Exception as exc: # one bad package must not hide the rest _EXTERNAL_ERRORS[ep.name] = f"{type(exc).__name__}: {exc}" @@ -442,7 +442,7 @@ def mux_usable(backend: TerminalMultiplexer | None = None) -> bool: _FORCED_UNUSABLE_WARNED = True try: version = backend.version() - except Exception: # noqa: BLE001 — a broken probe must not break the warning + except Exception: # a broken probe must not break the warning version = None print( f"warning: forced multiplexer backend {type(backend).__name__} reports " diff --git a/src/bmad_loop/adapters/opencode_http.py b/src/bmad_loop/adapters/opencode_http.py index 598efb15..e38eb36a 100644 --- a/src/bmad_loop/adapters/opencode_http.py +++ b/src/bmad_loop/adapters/opencode_http.py @@ -134,7 +134,7 @@ def _require_httpx(): """Import httpx lazily — it ships as the ``opencode`` extra, so the dep-free core never pays for it (the ``_psutil()`` pattern).""" try: - import httpx # noqa: PLC0415 (intentional lazy import — optional extra) + import httpx # intentional lazy import — optional extra except ImportError as exc: raise OpencodeServerError( "the opencode-http adapter needs httpx; " @@ -340,7 +340,7 @@ def _spawn_server(self, spec: SessionSpec) -> _ServerSession: try: for _ in range(SPAWN_ATTEMPTS): port = _free_port() - process = subprocess.Popen( # noqa: S603 - argv built from profile + process = subprocess.Popen( # argv built from profile self._serve_argv(resolved, port), cwd=str(spec.cwd), env=env, @@ -387,7 +387,7 @@ def _await_healthy(self, sess: _ServerSession) -> bool: try: resp = client.get("/global/health") healthy = resp.status_code == 200 and resp.json().get("healthy") is True - except Exception: # noqa: BLE001 - not up yet (conn refused, junk) + except Exception: # not up yet (conn refused, junk) healthy = False if sess.process.poll() is not None: return False @@ -461,7 +461,7 @@ def send_text(self, handle: SessionHandle, text: str) -> None: return try: self._prompt(sess, text) - except Exception: # noqa: BLE001 # nosec B110 - next tick's poll() settles liveness + except Exception: # nosec B110 - next tick's poll() settles liveness pass def _start_sse_reader(self, sess: _ServerSession) -> None: @@ -500,7 +500,7 @@ def _sse_loop(self, sess: _ServerSession) -> None: if sess.sse_stop.is_set(): return self._dispatch_sse(sess, event) - except Exception: # noqa: BLE001 # nosec B110 - reader must never die silently + except Exception: # nosec B110 - reader must never die silently pass if sess.sse_stop.is_set(): return @@ -684,7 +684,7 @@ def wait_for_completion(self, handle: SessionHandle, spec: SessionSpec) -> Sessi ) try: self.send_text(handle, BUDGET_NUDGE_TEXT) - except Exception: # noqa: BLE001 # nosec B110 - best-effort nudge + except Exception: # nosec B110 - best-effort nudge # a dead/hung server can't take the nudge; the # grace still arms — the next tick's process # poll scores a dead server crashed. @@ -881,7 +881,7 @@ def _session_status(self, sess: _ServerSession) -> bool | None: return None status = resp.json().get(sess.session_id) or {} return status.get("type") in ("busy", "retry") - except Exception: # noqa: BLE001 - probe is advisory + except Exception: # probe is advisory return None def _probe_completion(self, sess: _ServerSession) -> bool: @@ -904,7 +904,7 @@ def _probe_completion(self, sess: _ServerSession) -> bool: continue done_ms = (info.get("time") or {}).get("completed") or 0 completed = max(completed, int(done_ms)) - except Exception: # noqa: BLE001 - probe is advisory + except Exception: # probe is advisory return False if completed > sess.floor_ms: sess.floor_ms = completed @@ -916,7 +916,7 @@ def _abort(self, sess: _ServerSession) -> None: return try: sess.client.post(f"/session/{sess.session_id}/abort") - except Exception: # noqa: BLE001 # nosec B110 - abort is best-effort + except Exception: # nosec B110 - abort is best-effort pass # ----------------------------------------------------------------- usage @@ -933,7 +933,7 @@ def _sample_weighted_usage(self, sess: _ServerSession, spec: SessionSpec) -> int if resp.status_code != 200: return None usage = _sum_usage(resp.json()) - except Exception: # noqa: BLE001 - sampling is advisory + except Exception: # sampling is advisory return None return usage.weighted_total(spec.cache_read_weight) @@ -953,7 +953,7 @@ def _capture_usage(self, handle: SessionHandle, sess: _ServerSession) -> str | N path.write_text(json.dumps(messages, ensure_ascii=False, indent=2), encoding="utf-8") self._usage[sess.session_id] = _sum_usage(messages) return str(path) - except Exception: # noqa: BLE001 - usage is metadata, never a gate + except Exception: # usage is metadata, never a gate return None def read_usage(self, result: SessionResult) -> TokenUsage | None: @@ -980,7 +980,7 @@ def _teardown(self, sess: _ServerSession) -> None: if sess.client is not None: try: sess.client.close() - except Exception: # noqa: BLE001 # nosec B110 - closing is best-effort + except Exception: # nosec B110 - closing is best-effort pass try: sess.log_fh.close() @@ -1029,7 +1029,7 @@ def _kill_process(self, sess: _ServerSession) -> None: # wrapper is gone `/T` can never enumerate the child again. try: host.force_kill(process.pid) - except Exception: # noqa: BLE001 # nosec B110 - already-gone races are fine + except Exception: # nosec B110 - already-gone races are fine pass else: try: @@ -1041,7 +1041,7 @@ def _kill_process(self, sess: _ServerSession) -> None: except subprocess.TimeoutExpired: try: host.force_kill(process.pid) - except Exception: # noqa: BLE001 # nosec B110 - already-gone races are fine + except Exception: # nosec B110 - already-gone races are fine pass try: process.wait(timeout=self.kill_wait_s) @@ -1082,7 +1082,7 @@ def _survivors() -> list[int]: for pid in survivors: try: host.force_kill(pid) - except Exception: # noqa: BLE001 # nosec B110 - already-gone races are fine + except Exception: # nosec B110 - already-gone races are fine pass def _atexit_sweep(self) -> None: diff --git a/src/bmad_loop/data/plugins/tea/tea_plugin.py b/src/bmad_loop/data/plugins/tea/tea_plugin.py index 40729417..4a2d6d87 100644 --- a/src/bmad_loop/data/plugins/tea/tea_plugin.py +++ b/src/bmad_loop/data/plugins/tea/tea_plugin.py @@ -94,7 +94,7 @@ def _review_token(line: str) -> str | None: return None -def _scan_markdown(text: str, labels: tuple[str, ...], normalize) -> str | None: # noqa: ANN001 +def _scan_markdown(text: str, labels: tuple[str, ...], normalize) -> str | None: """First *concrete* labeled line that yields a verdict. Lines carrying ``{}`` placeholders or ``|`` table syntax are skipped — they are template scaffolding, not a generated decision — so a real artifact's value line is what's read.""" @@ -237,7 +237,7 @@ def _gate_verdict(self, gate: str, artifacts_dir: Path) -> str | None: continue try: verdict = self._parse_artifact(kind, path) - except Exception: # noqa: BLE001 # nosec B112 - fail-open: a parse error never blocks + except Exception: # nosec B112 - fail-open: a parse error never blocks continue if verdict is not None: return verdict diff --git a/src/bmad_loop/data/plugins/unity/unity_plugin.py b/src/bmad_loop/data/plugins/unity/unity_plugin.py index 7e490b03..3bccfbdd 100644 --- a/src/bmad_loop/data/plugins/unity/unity_plugin.py +++ b/src/bmad_loop/data/plugins/unity/unity_plugin.py @@ -332,7 +332,7 @@ def _reap_dialog_probe(self, ctx) -> None: path = self._probe_pid_path(ctx) if path is None: return - from bmad_loop import runs # noqa: PLC0415 - lazy: keep import cost off the hot path + from bmad_loop import runs # lazy: keep import cost off the hot path pid, identity = runs.read_named_pid_identity(path) if pid is not None: diff --git a/src/bmad_loop/data/plugins/unity/unity_teardown.py b/src/bmad_loop/data/plugins/unity/unity_teardown.py index bb1d847b..38ef1920 100644 --- a/src/bmad_loop/data/plugins/unity/unity_teardown.py +++ b/src/bmad_loop/data/plugins/unity/unity_teardown.py @@ -59,7 +59,7 @@ def _psutil(): dep-free core never imports it; raise a clear, actionable error if it's missing on a platform that needs it.""" try: - import psutil # noqa: PLC0415 (intentional lazy import — keeps the core dep-free) + import psutil # intentional lazy import — keeps the core dep-free except ImportError as exc: # pragma: no cover - exercised only off Linux raise RuntimeError( f"unity_teardown: process discovery on {sys.platform!r} needs psutil; " @@ -251,7 +251,7 @@ def _probe_pidfile_pids(run_dir: Path | None) -> list[int]: still alive AND still our probe (identity-checked). The primary reap handle.""" if run_dir is None: return [] - from bmad_loop.runs import read_named_pid_identity # noqa: PLC0415 - lazy import + from bmad_loop.runs import read_named_pid_identity # lazy import pid, identity = read_named_pid_identity(run_dir / _DIALOG_PROBE_PID_FILE) if pid is None: diff --git a/src/bmad_loop/diagnostics.py b/src/bmad_loop/diagnostics.py index 5795be27..6912ba5c 100644 --- a/src/bmad_loop/diagnostics.py +++ b/src/bmad_loop/diagnostics.py @@ -217,7 +217,7 @@ def collect_env() -> EnvInfo: mux = type(backend).__name__ raw = backend.version() tmux_v = sanitize.scrub_text(raw, max_lines=1) if raw else None - except Exception: # noqa: BLE001 # nosec B110 - env probe is best-effort; absent mux is fine + except Exception: # nosec B110 - env probe is best-effort; absent mux is fine pass return EnvInfo( os=platform.system(), @@ -475,7 +475,7 @@ def collect( for run_dir in run_dirs: try: runs.append(collect_run(run_dir, pseudo=pseudo, cap=cap)) - except Exception as e: # noqa: BLE001 — one bad run never sinks the dump + except Exception as e: # one bad run never sinks the dump runs.append(_unreadable_run(run_dir, e)) return Diagnostics( schema_version=SCHEMA_VERSION, diff --git a/src/bmad_loop/engine.py b/src/bmad_loop/engine.py index 5f7b2f78..b74b0c49 100644 --- a/src/bmad_loop/engine.py +++ b/src/bmad_loop/engine.py @@ -348,7 +348,7 @@ def run(self) -> RunSummary: # rather than let it mask the stop. self._gc_run_worktrees() self._emit("post_run") - except Exception as finalize_exc: # noqa: BLE001 - see comment above + except Exception as finalize_exc: # see comment above self.journal.append("run-stop-finalize-error", error=str(finalize_exc)) remaining = self._remaining_estimate() self._graceful_remaining = remaining @@ -379,7 +379,7 @@ def run(self) -> RunSummary: kill_session(self.state.run_id) except ( BaseException - ): # noqa: BLE001 # nosec B110 - best-effort teardown; the stop must still record + ): # nosec B110 - best-effort teardown; the stop must still record pass if self._is_nested: raise @@ -405,7 +405,7 @@ def run(self) -> RunSummary: kill_session(self.state.run_id) except ( Exception - ): # noqa: BLE001 # nosec B110 - best-effort teardown; a crashing run must still record + ): # nosec B110 - best-effort teardown; a crashing run must still record pass if self._is_nested: raise # nested auto-sweep: let the owner record the failure @@ -424,7 +424,7 @@ def run(self) -> RunSummary: ) except ( Exception - ): # noqa: BLE001 # nosec B110 - journal write is best-effort; crash.txt + state flag already persisted + ): # nosec B110 - journal write is best-effort; crash.txt + state flag already persisted pass finally: # Any pending stop-request control file that outlived this run @@ -473,7 +473,7 @@ def _install_stop_signals(self) -> None: if sigbreak is not None: windows_ctrl_signals.add(sigbreak) - def handler(signum, frame): # noqa: ANN001 - stdlib signal signature + def handler(signum, frame): # stdlib signal signature if sys.platform == "win32" and signum in windows_ctrl_signals: # best-effort: a journal error must never escape a signal handler. with contextlib.suppress(Exception): @@ -601,7 +601,7 @@ def _remaining_estimate(self) -> int | None: for s in ss.stories if s.status in ACTIONABLE_STATUSES and s.key not in self.state.tasks ) - except Exception: # noqa: BLE001 - a hint must never break the stop + except Exception: # a hint must never break the stop return None def _check_graceful_stop(self) -> None: @@ -2083,7 +2083,7 @@ def _run_session( status="aborted", error=type(exc).__name__ if exc is not None else None, ) - except Exception: # noqa: BLE001 # nosec B110 + except Exception: # nosec B110 pass self._save() self._emit( @@ -2460,7 +2460,7 @@ def _maybe_auto_sweep(self, kind: str, trigger: str) -> None: try: self.sweep_factory(trigger) self.journal.append("sweep-auto-finished", trigger=trigger) - except Exception as e: # noqa: BLE001 — child must never break the parent + except Exception as e: # child must never break the parent self.journal.append("sweep-auto-failed", trigger=trigger, error=str(e)) gates.notify(self.policy, self.run_dir, "auto sweep failed", f"{trigger}: {e}") diff --git a/src/bmad_loop/install.py b/src/bmad_loop/install.py index 0594e5ff..073c8f92 100644 --- a/src/bmad_loop/install.py +++ b/src/bmad_loop/install.py @@ -488,7 +488,7 @@ def _warn_if_policy_tracked(project: Path) -> None: means nothing to warn about.""" try: tracked = ( - subprocess.run( # noqa: S603, S607 — fixed argv, no shell + subprocess.run( # fixed argv, no shell ["git", "ls-files", "--error-unmatch", ".bmad-loop/policy.toml"], cwd=project, capture_output=True, diff --git a/src/bmad_loop/plugins/bus.py b/src/bmad_loop/plugins/bus.py index 23cf22f3..27eacdf5 100644 --- a/src/bmad_loop/plugins/bus.py +++ b/src/bmad_loop/plugins/bus.py @@ -236,7 +236,7 @@ def _dispatch_python(self, lp: LoadedPlugin, stage: str, ctx: HookContext) -> No ctx._current_plugin = lp.name try: instance.hook(stage, ctx) # type: ignore[union-attr] - except Exception as e: # noqa: BLE001 - isolate plugin failures; never BaseException + except Exception as e: # isolate plugin failures; never BaseException self._log("plugin-error", plugin=lp.name, stage=stage, error=f"{type(e).__name__}: {e}") # disable the misbehaving instance for the rest of the run; its # declarative hooks (if any) keep working — they are out-of-process. diff --git a/src/bmad_loop/plugins/registry.py b/src/bmad_loop/plugins/registry.py index 117ee419..eec399c6 100644 --- a/src/bmad_loop/plugins/registry.py +++ b/src/bmad_loop/plugins/registry.py @@ -95,7 +95,7 @@ def _resolve(manifest: PluginManifest, policy, journal) -> LoadedPlugin: try: instance = _instantiate(manifest, settings) - except Exception as e: # noqa: BLE001 - isolate plugin failures; never BaseException + except Exception as e: # isolate plugin failures; never BaseException if journal is not None: journal.append("plugin-error", plugin=manifest.name, error=f"{type(e).__name__}: {e}") return LoadedPlugin(manifest=manifest, disabled=True, error=str(e), settings=settings) diff --git a/src/bmad_loop/probe.py b/src/bmad_loop/probe.py index c246df1e..2e523101 100644 --- a/src/bmad_loop/probe.py +++ b/src/bmad_loop/probe.py @@ -611,7 +611,7 @@ def probe( mux = get_multiplexer() try: mux_ready = bool(mux.available()) - except Exception: # noqa: BLE001 — a raising host probe means "cannot probe", not a crash + except Exception: # a raising host probe means "cannot probe", not a crash mux_ready = False if not mux_ready or not shutil.which(binary): # finding.binary, not the raw local — see the identical note in scan() diff --git a/src/bmad_loop/process_host.py b/src/bmad_loop/process_host.py index 67ec9fb5..2baa386f 100644 --- a/src/bmad_loop/process_host.py +++ b/src/bmad_loop/process_host.py @@ -322,9 +322,9 @@ def _psutil_descendants(pid: int) -> dict[int, float | None]: if not child.is_running(): # generation changed under us — don't stamp it continue out[child.pid] = identity - except Exception: # noqa: BLE001 # nosec B112 - gone/reused mid-walk: omit it + except Exception: # nosec B112 - gone/reused mid-walk: omit it continue - except Exception: # noqa: BLE001 - the kill-path seam must never raise + except Exception: # the kill-path seam must never raise return {} return out @@ -335,7 +335,7 @@ def _psutil(): probes. The dep-free core never imports it on Linux; raise a clear, actionable error if it's missing where it's needed.""" try: - import psutil # noqa: PLC0415 (intentional lazy import — keeps the core dep-free) + import psutil # intentional lazy import — keeps the core dep-free except ImportError as exc: # pragma: no cover - exercised only off Linux raise ProcessHostError( f"process_host: pid operations on {sys.platform!r} need psutil; " diff --git a/src/bmad_loop/recovery_flow.py b/src/bmad_loop/recovery_flow.py index 53e5ee56..892bbdac 100644 --- a/src/bmad_loop/recovery_flow.py +++ b/src/bmad_loop/recovery_flow.py @@ -271,7 +271,7 @@ def prune_preserve_refs(self) -> None: ): try: deleted = prune(workspace.root, keep) - except Exception as exc: # noqa: BLE001 - housekeeping must never crash the + except Exception as exc: # housekeeping must never crash the # run: a git timeout/OSError here would otherwise escape to the crash # handler, so anything beyond the expected GitError is journalled too # A partial prune (PrunePreserveError) already deleted refs before one diff --git a/src/bmad_loop/resolve.py b/src/bmad_loop/resolve.py index f0f1c8a7..ac62008a 100644 --- a/src/bmad_loop/resolve.py +++ b/src/bmad_loop/resolve.py @@ -201,5 +201,5 @@ def run_session(adapter, project: Path, run_dir: Path, story_key: str, *, model: marker.unlink(missing_ok=True) argv = adapter.interactive_argv(spec) env = {**os.environ, **adapter.interactive_env(spec)} - subprocess.run(argv, cwd=str(project), env=env) # noqa: S603 - attached, inherited stdio + subprocess.run(argv, cwd=str(project), env=env) # attached, inherited stdio return marker.is_file() diff --git a/src/bmad_loop/runs.py b/src/bmad_loop/runs.py index 1def4836..ab02bc1c 100644 --- a/src/bmad_loop/runs.py +++ b/src/bmad_loop/runs.py @@ -542,7 +542,7 @@ def _state_or_none(run_dir: Path): never reclaim) what you cannot positively read.""" try: return load_state(run_dir) - except Exception: # noqa: BLE001 - unreadable/corrupt state ⇒ leave it alone + except Exception: # unreadable/corrupt state ⇒ leave it alone return None @@ -890,7 +890,7 @@ def rearm_escalation( untracked = sorted(verify.untracked_files(repo) - stale_residue) task.baseline_commit = head task.baseline_untracked = untracked - except Exception: # noqa: BLE001 # nosec B110 - best-effort git read, must not fail re-arm + except Exception: # nosec B110 - best-effort git read, must not fail re-arm pass # Patch-restore only: re-stamp the spec's own baseline to the advanced one. @@ -979,7 +979,7 @@ def _stale_restore_residue( if old_baseline: try: shas = verify.commits_above(repo, old_baseline) - except Exception: # noqa: BLE001 # nosec B110 - warn-only, must not fail re-arm + except Exception: # nosec B110 - warn-only, must not fail re-arm shas = [] if shas: journal.append( diff --git a/src/bmad_loop/runsetup.py b/src/bmad_loop/runsetup.py index f2499f43..b79ffaf1 100644 --- a/src/bmad_loop/runsetup.py +++ b/src/bmad_loop/runsetup.py @@ -121,7 +121,7 @@ def make_adapters(project: Path, run_dir: Path, policy) -> dict[str, CodingCLIAd if not mux_usable(mux): try: version = mux.version() - except Exception: # noqa: BLE001 — diagnosing must not mask the refusal + except Exception: # diagnosing must not mask the refusal version = None raise SystemExit( f"error: multiplexer backend {type(mux).__name__} is not usable on " @@ -203,12 +203,12 @@ def platform_preflight() -> list[Finding]: {"backend": label, "available": False, "version": version}, ) ) - except Exception as e: # noqa: BLE001 — selection or readiness must not abort validate + except Exception as e: # selection or readiness must not abort validate found.append(Finding("mux.preflight", "problem", f"multiplexer preflight failed: {e}")) try: infos = detect_multiplexers() - except Exception: # noqa: BLE001 — detection is advisory; never break validate + except Exception: # detection is advisory; never break validate infos = [] if len(infos) > 1: # a lone tmux needs no listing; keep single-backend output stable listed = ", ".join( @@ -280,7 +280,7 @@ def platform_preflight() -> list[Finding]: try: host = type(get_process_host()).__name__ found.append(Finding("host.process", "ok", f"process host: {host}", {"host": host})) - except Exception as e: # noqa: BLE001 — a bad BMAD_LOOP_PROCESS_HOST must report, not crash + except Exception as e: # a bad BMAD_LOOP_PROCESS_HOST must report, not crash found.append(Finding("host.process", "problem", f"process host preflight failed: {e}")) return found diff --git a/src/bmad_loop/stories_engine.py b/src/bmad_loop/stories_engine.py index 688fd0e9..f60378ae 100644 --- a/src/bmad_loop/stories_engine.py +++ b/src/bmad_loop/stories_engine.py @@ -682,5 +682,5 @@ def _remaining_estimate(self) -> int | None: break # a blocked story stops the scan; the rest is unreachable remaining += 1 # actionable return remaining - except Exception: # noqa: BLE001 - a hint must never break the stop + except Exception: # a hint must never break the stop return None diff --git a/src/bmad_loop/sweep.py b/src/bmad_loop/sweep.py index b5e681a2..fc4c0380 100644 --- a/src/bmad_loop/sweep.py +++ b/src/bmad_loop/sweep.py @@ -416,7 +416,7 @@ def _remaining_estimate(self) -> int | None: ledger = self.workspace.paths.deferred_work text = ledger.read_text(encoding="utf-8") if ledger.is_file() else "" return len(deferredwork.open_ids(text)) - except Exception: # noqa: BLE001 - a hint must never break the stop + except Exception: # a hint must never break the stop return None # ------------------------------------------------------------ main loop diff --git a/src/bmad_loop/tui/app.py b/src/bmad_loop/tui/app.py index 5a63f4e1..7b52298f 100644 --- a/src/bmad_loop/tui/app.py +++ b/src/bmad_loop/tui/app.py @@ -1070,7 +1070,7 @@ def _show_validate(self) -> None: try: _rc, out, _err = launch.run_captured_streams([*tail, "--json"]) doc = widgets.validate_document(out) - except Exception: # noqa: BLE001 — a JSON-leg failure degrades, never kills the app + except Exception: # a JSON-leg failure degrades, never kills the app doc = None if doc is None: rc, merged = self._run_captured_guarded(tail) @@ -1094,7 +1094,7 @@ def _run_captured_guarded(self, tail: list[str]) -> tuple[int, str]: """ try: return launch.run_captured(tail) - except Exception as exc: # noqa: BLE001 — a failed spawn is a modal, not a crash + except Exception as exc: # a failed spawn is a modal, not a crash return 1, f"could not run: {exc}" @work(thread=True, exclusive=True, group="captured") diff --git a/src/bmad_loop/tui/widgets.py b/src/bmad_loop/tui/widgets.py index 881fb49d..626ebb16 100644 --- a/src/bmad_loop/tui/widgets.py +++ b/src/bmad_loop/tui/widgets.py @@ -527,7 +527,7 @@ def validate_findings(doc: dict, *, details: bool) -> Table: for finding in findings: try: rows = _finding_rows(finding, details=details) - except Exception: # noqa: BLE001 — a bad finding costs its row, never the modal + except Exception: # a bad finding costs its row, never the modal rows = [ ( Text("?", style="dim"), diff --git a/src/bmad_loop/verify.py b/src/bmad_loop/verify.py index 4df6078a..e36adf68 100644 --- a/src/bmad_loop/verify.py +++ b/src/bmad_loop/verify.py @@ -346,7 +346,7 @@ def _prune_refs( for name in refs[keep:]: try: delete(name) - except Exception as exc: # noqa: BLE001 - a git timeout/OSError on one ref + except Exception as exc: # a git timeout/OSError on one ref # must not wedge the tail behind it any more than a GitError does; the # per-ref best-effort contract holds for the whole subprocess surface failed.append(f"{name} ({exc})") diff --git a/tests/test_hook_bus.py b/tests/test_hook_bus.py index 6bba528b..838b9a22 100644 --- a/tests/test_hook_bus.py +++ b/tests/test_hook_bus.py @@ -72,7 +72,7 @@ def test_zero_plugin_fast_path(): def test_active_only_for_bound_stages(): class P(Plugin): - def on_pre_commit(self, c): # noqa: ANN001 + def on_pre_commit(self, c): pass bus = HookBus(registry_of(py_plugin(P))) @@ -84,7 +84,7 @@ def test_observe_sees_readonly_context(): seen = {} class P(Plugin): - def on_pre_story(self, c): # noqa: ANN001 + def on_pre_story(self, c): seen["story"] = c.story_key seen["stage"] = c.stage @@ -95,11 +95,11 @@ def on_pre_story(self, c): # noqa: ANN001 def test_mutations_pipeline_last_writer_wins(): # lower priority runs first; the later plugin sees the earlier edit and wins class First(Plugin): - def on_pre_commit(self, c): # noqa: ANN001 + def on_pre_commit(self, c): c.proposed_commit_message = "first" class Second(Plugin): - def on_pre_commit(self, c): # noqa: ANN001 + def on_pre_commit(self, c): assert c.proposed_commit_message == "first" # sees the earlier edit c.proposed_commit_message = "second" @@ -113,11 +113,11 @@ def on_pre_commit(self, c): # noqa: ANN001 def test_veto_resolves_most_conservative(): class Skip(Plugin): - def on_pre_story(self, c): # noqa: ANN001 + def on_pre_story(self, c): c.veto("skip", "skip me") class Pause(Plugin): - def on_pre_story(self, c): # noqa: ANN001 + def on_pre_story(self, c): c.veto("pause", "stop everything") # registered skip-first; resolution must still pick pause (no short-circuit) @@ -133,7 +133,7 @@ def test_python_exception_is_isolated_and_disables_instance(): calls = {"n": 0} class Boom(Plugin): - def on_pre_story(self, c): # noqa: ANN001 + def on_pre_story(self, c): calls["n"] += 1 raise RuntimeError("kaboom") @@ -147,7 +147,7 @@ def on_pre_story(self, c): # noqa: ANN001 def test_baseexception_propagates(): class Sig(Plugin): - def on_pre_story(self, c): # noqa: ANN001 + def on_pre_story(self, c): raise KeyboardInterrupt("sigint-like") with pytest.raises(KeyboardInterrupt): @@ -158,7 +158,7 @@ def test_fail_closed_python_vetoes_on_raise(): class Strict(Plugin): fail_closed = True - def on_pre_story(self, c): # noqa: ANN001 + def on_pre_story(self, c): raise RuntimeError("nope") c = ctx() @@ -179,7 +179,7 @@ def declarative(stage: str, *, blocking=False, fail_closed=False, name="d") -> L def test_declarative_nonzero_exit_vetoes_blocking(): runs = {} - def runner(cmd, *, cwd, env, timeout): # noqa: ANN001 + def runner(cmd, *, cwd, env, timeout): runs["env_stage"] = env["BMAD_LOOP_STAGE"] return 3, "build failed" @@ -248,7 +248,7 @@ def test_real_subprocess_runner_reports_exit_code(tmp_path): def test_shared_persists_across_stages(): class P(Plugin): - def on_pre_dev_phase(self, c): # noqa: ANN001 + def on_pre_dev_phase(self, c): c.shared["count"] = c.shared.get("count", 0) + 1 bus = HookBus(registry_of(py_plugin(P))) @@ -265,7 +265,7 @@ class _FakeJournal: def __init__(self): self.entries: list[dict] = [] - def append(self, kind, **fields): # noqa: ANN001 + def append(self, kind, **fields): self.entries.append({"kind": kind, **fields}) def kinds(self): @@ -308,7 +308,7 @@ def test_zero_plugin_run_is_byte_identical(project): def test_prompt_mutation_reaches_the_session(project): class P(Plugin): - def on_pre_session(self, c): # noqa: ANN001 + def on_pre_session(self, c): if c.role == "dev": c.proposed_prompt = "/custom-dev-prompt" @@ -324,7 +324,7 @@ def test_commit_message_mutation_reaches_git(project): from conftest import git class P(Plugin): - def on_pre_commit(self, c): # noqa: ANN001 + def on_pre_commit(self, c): c.proposed_commit_message = f"plugin-authored: {c.story_key}" engine, _ = make_engine(project, one_story(project), registry_of(py_plugin(P, "msgmut"))) @@ -356,7 +356,7 @@ def test_pre_commit_hook_fires_on_commit_resume(project): a plugin's rewrite has to reach the squashed commit.""" class P(Plugin): - def on_pre_commit(self, c): # noqa: ANN001 + def on_pre_commit(self, c): c.proposed_commit_message = f"plugin-authored: {c.story_key}" reg = registry_of(py_plugin(P, "msgmut")) @@ -378,7 +378,7 @@ def test_pre_commit_pause_veto_on_commit_resume_escalates(project): is the legal move) with the attempt's commits left intact above baseline.""" class P(Plugin): - def on_pre_commit(self, c): # noqa: ANN001 + def on_pre_commit(self, c): c.veto("pause", "halt") reg = registry_of(py_plugin(P, "vpcommit")) @@ -398,7 +398,7 @@ def on_pre_commit(self, c): # noqa: ANN001 def test_veto_defer_routes_to_defer(project): class P(Plugin): - def on_pre_story(self, c): # noqa: ANN001 + def on_pre_story(self, c): c.veto("defer", "not now") engine, _ = make_engine(project, one_story(project), registry_of(py_plugin(P, "vd"))) @@ -410,7 +410,7 @@ def on_pre_story(self, c): # noqa: ANN001 def test_veto_pause_routes_to_escalation(project): class P(Plugin): - def on_pre_story(self, c): # noqa: ANN001 + def on_pre_story(self, c): c.veto("pause", "halt the line") engine, _ = make_engine(project, one_story(project), registry_of(py_plugin(P, "vp"))) @@ -422,7 +422,7 @@ def test_session_veto_retries_then_defers(project): # a vetoed dev session synthesizes status="vetoed"; decide_dev retries within # budget, then defers — never silently proceeds. class P(Plugin): - def on_pre_dev_session(self, c): # noqa: ANN001 + def on_pre_dev_session(self, c): c.veto("defer", "dev not allowed") policy = Policy( @@ -441,7 +441,7 @@ def on_pre_dev_session(self, c): # noqa: ANN001 def test_plugin_exception_does_not_crash_the_run(project): class P(Plugin): - def on_pre_story(self, c): # noqa: ANN001 + def on_pre_story(self, c): raise RuntimeError("plugin bug") engine, _ = make_engine(project, one_story(project), registry_of(py_plugin(P, "buggy"))) @@ -452,10 +452,10 @@ def on_pre_story(self, c): # noqa: ANN001 def test_shared_state_persists_into_run_state(project): class P(Plugin): - def on_pre_story(self, c): # noqa: ANN001 + def on_pre_story(self, c): c.shared["seen_story"] = c.story_key - def on_post_commit(self, c): # noqa: ANN001, E301 + def on_post_commit(self, c): c.shared["committed"] = True engine, _ = make_engine(project, one_story(project), registry_of(py_plugin(P, "sh"))) diff --git a/tests/test_plugin_tea.py b/tests/test_plugin_tea.py index 5789b00f..38442b5c 100644 --- a/tests/test_plugin_tea.py +++ b/tests/test_plugin_tea.py @@ -132,7 +132,7 @@ def workflow_effect(captured: list, status: str = "completed"): status (mirrors test_plugin_workflows).""" from bmad_loop.adapters.base import SessionResult - def effect(spec): # noqa: ANN001 + def effect(spec): captured.append(spec) return SessionResult(status=status, result_json={}) diff --git a/tests/test_plugin_workflows.py b/tests/test_plugin_workflows.py index e3d78fec..7bfdf736 100644 --- a/tests/test_plugin_workflows.py +++ b/tests/test_plugin_workflows.py @@ -93,7 +93,7 @@ def workflow_effect(captured: list, status: str = "completed"): """A scripted session standing in for an injected workflow session: record the spec (to assert prompt substitution + task_id) and return ``status``.""" - def effect(spec): # noqa: ANN001 + def effect(spec): captured.append(spec) return SessionResult(status=status, result_json={}) diff --git a/uv.lock b/uv.lock index 1b03f5c3..6c4f5d05 100644 --- a/uv.lock +++ b/uv.lock @@ -66,7 +66,7 @@ dev = [ { name = "pytest-asyncio", specifier = ">=1.0" }, { name = "pytest-rerunfailures", specifier = ">=16.0" }, { name = "pytest-xdist", specifier = ">=3.6" }, - { name = "ruff", specifier = ">=0.4" }, + { name = "ruff", specifier = ">=0.15.17" }, ] [[package]]