lint: adopt the Sonar/Checkstyle/FindBugs ruff bar, fix the src/ baseline - #104
Merged
Conversation
Owner standard (plan/coding-standards.md §3): what Sonar, Checkstyle and FindBugs would flag on a Java codebase is now flagged on src/agentseam via ruff's C90/N/PLR/PLW/PLC/ERA/T20/ARG/RET/SIM/PIE/FBT/A/B/S/BLE/TRY/RUF families, with the mccabe/pylint thresholds the standard specifies (max-complexity 10; max-args 5, max-branches 12, max-returns 6, max-statements 50). Every per-file-ignore carries a one-line reason. Two carve-outs are scope, not exemptions: tests/, tools/, examples/ and docs/'s two asset scripts were never measured against this rule set (the baseline below is src/-only, per the brief) and are marked TODO(lint-adoption) for a follow-up wave; contract.py/install.py/install_identity.py's PLR0913 is a deliberate DTO/config-object constructor shape, not deferred work. Baseline measured 2026-09-02 on src/, this ruff version (101 findings, close to the plan's independently-measured 100 -- the small drift is noted in the report): fixed category by category in the commits that follow, ending with `ruff check .` clean and CI (already running `ruff check .`) enforcing the new bar with no workflow change needed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
… safe import
RUF022/RUF023 (ruff --fix, reviewed): __all__ and __slots__ tuples sorted;
order has no behavioral meaning for either.
B904: two bare `raise ... from within except` sites now chain explicitly
(`from exc`) so the original cause survives in the traceback.
SIM105: cli.py's try/except/pass around the BrokenPipeError dup2 cleanup
is now contextlib.suppress(OSError).
PLC0415: cli.py's lazy `from datetime import date` had no reason to be
lazy (no circular import, cli.py is never bundled) and is hoisted to the
top. vscode_copilot.py's function-local `import json as _json` is left
alone and noqa'd instead: it is deliberately preserved, per
tests/test_bundler.py::test_function_local_imports_are_left_alone --
bundler.py only hoists a source's *top-level* imports, and this file is
spliced into a bundle whole, so hoisting it would only relocate the
import while touching every call site in respond() for no behavioral
gain. Confirmed by diffing bundler.bundle("vscode_copilot") before/after
attempting the hoist.
RUF005: instructions.py's `[SHARED_FILE] + paths(agent)` is now
`[SHARED_FILE, *paths(agent)]`.
RUF100 and the mechanical S110/B905 targets from the standard were
already clean or resolved by the previous commit's `select` expansion
(the E402 rule going live made two `# noqa: E402` directives genuinely
used, not unused).
Full suite green (1446 passed, 4 skipped); golden fixtures and bundle
equivalence re-run explicitly for the two touched adapter modules.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
… guard PLR2004: cli.py's three magic-value comparisons (rule field counts 2/3, the 90-day staleness window) become named module constants beside the code that reads them. New: tools/literal_duplication.py, seeded from the AST-survey approach plan/coding-standards.md §3 calls for (no ruff rule covers this). It walks .py files under a root, skipping data/templates/tests/__pycache__, and reports any non-docstring string constant >= 20 chars that repeats >= 3 times. tests/test_literal_duplication.py runs it against src/agentseam and fails on anything not in tools/literal_duplication_allowlist.json (each entry there needs a non-empty reason; currently empty). Running it against src/ found three genuine repeats, now named constants instead: "permissionDecisionReason" (the G2/vscode_copilot dialects' one reason key, 4x each in _hook_json.py and vscode_copilot.py -- two separate local constants, since the two modules don't share code) and "escalate_from_transform"/"transform_missing_input" (degrade_notes keys shared by _hook_json.py and _cursor.py -- one constant each in _hook_json.py, imported into _cursor.py the same way it already imports _refusal_text from there; bundler.py's _extract_with_deps already walks cross-module name references this way for the engine composition, so the bundle picks up the constant's definition same as any other symbol). Pure renames -- same string values, same JSON keys emitted. Full suite green (1448 passed, 4 skipped, two more than before for the new check). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
…tified catches, Error-suffixed exceptions, named dead parameters TRY003: install_identity.py's and permissions.py's two vanilla-args raises now read their message from a module constant beside the function, matching contract.py's existing raise style elsewhere. BLE001: contract.py's tool_input_of() narrows to json.JSONDecodeError -- the only exception json.loads can raise here, since the input is already known to be a str. dispatch.py's run() keeps its blind except: it is the outermost boundary before any handler runs, deliberately fails open on any stdin/parse failure (not just malformed JSON), and mirrors the frozen runtime.py.tmpl's identical catch one section down -- noqa'd with that reasoning rather than narrowed. N818: UnsupportedDecision -> UnsupportedDecisionError (dispatch.py, never raised elsewhere, so no other reference to update) and ConfigUnreadable -> ConfigUnreadableError (install_config.py, install.py, install_identity.py, tests/test_install.py -- an exported, raised, and caught name, all four updated together). ARG001: cli.py's _cmd_agents/_cmd_packaging and _antigravity.py's antigravity_claims take their unused argument positionally only (argparse's `args.fn(args)` dispatch; _family.py's ConfigAdapter calling antigravity_claims(self.CONFIG, raw)), so the parameter is safely renamed to `_`-prefixed. vscode_copilot.py's hook_config(..., matcher=) and instructions.py's plan(..., repo_root=...) are both called by keyword at existing call sites (install.py/install_identity.py's **extra unpack; tests/test_instructions.py's repo_root="."), so renaming would break them; both get a noqa naming the interface-parity reason instead. Full suite green (1448 passed, 4 skipped). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
FBT002 (boolean default positional argument): every flagged bool parameter's only callers already pass it by keyword or via a **dict unpack (verified per site before touching the signature), so each becomes keyword-only rather than renamed: - _family.py's _CursorAdapter.hook_config and _hook_entry.py's hook_entry_config: `fail_closed` after `*`; the one positional caller (_family.py's own hook_config -> hook_entry_config call) updated to pass it by keyword too. - matrix_terms.py's _cap: both bools after `*` -- its only two callers (matrix.py) pass zero arguments. - packaging.py's Part.__init__: `executable` after `*` -- every constructor call site (src and tests) already names it. - instructions.py's write: `dry_run` after `*` -- cli.py and the one test call already name it. - dispatch.py's run: `exit` after `*`; every caller already passes it by keyword. Left named `exit` (see A002 below) rather than renamed. runtime.py.tmpl's `exit=True` is the one FBT002 site left alone: it is frozen, bundle-emitted text (per-file-ignore from the config commit), and converting it to keyword-only would edit that emitted text for a lint rule with no behavioral upside. FBT003 (boolean positional value in call): the five call sites passing True/False positionally into `_default_for`/`_refusal_text` (_hook_json.py x4) and `_refusal_text` (_windsurf.py) now name `at_gate=`/`wire=` at the call site. Pure call-site rewrite, same arguments, same order. A002 (builtin-argument-shadowing): dispatch.py's `exit` parameter is a deliberate stdlib-flavored name mirroring sys.exit, shared by every caller and by the bundled runtime.py.tmpl's main(); noqa'd with that reasoning rather than renamed, which would be the actual breaking change. Full suite green (1448 passed, 4 skipped). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
…ving Pure extraction throughout -- same branches, same order, same return values, moved into named helpers so no single function carries the whole shape: - _hook_entry.py hook_entry_config (17/16 -> dispatch over four per-wrapper helpers: _flat_list_wrapper, _cursor_wrapper, _flat_entries_wrapper, _default_wrapper). - _cursor.py cursor_respond (13/15 -> _flag_payload, _prompt_submit_payload, _gate_payload split out; the trailing user_message/agent_message rule stays in cursor_respond since it applies across every gate outcome). - _hook_json.py _g1 (13/9 returns -> _g1_allow, _g1_transform split out; _g1_transform returns None for "not representable here" so _g1 falls through to the shared block path exactly as the original nested ifs did). - _payload.py hj_claims (14/10 returns/13 branches -> _rejected_by_markers and _accepted_by_markers, both pure predicates over the entry's claims config). - vscode_copilot.py respond (13/7 returns -> _top_level_block_body, _nested_block_body, _pre_tool_out split out; the local `import json as _json` stays put per the previous commit's reasoning, so the three helpers build plain dicts and respond() still does every _json.dumps() itself). - bundler.py _extract_with_deps (12 -> _owner_map + _transitive_deps). - install_identity.py installed (11/9 returns -> _installed_toml/ _installed_json split by CONFIG_FORMAT; the shared hook_config() fragment is now built once in installed() and passed down rather than rebuilt in each branch, so PLR0913 doesn't just reappear on the split). Two splits (_hook_entry.py's _default_wrapper, _hook_json.py's _g1_allow) needed a second pass: the first cut left them at 6 params, one over the standard's max-args=5. Both dropped a param that was cheaply re-derivable inside the helper (hook_entry from cfg, at_context_event from wire/v) rather than growing the file's PLR0913 per-file-ignore to cover them. install_config.py picks up one `ruff format` wrap the previous commit's ConfigUnreadable -> ConfigUnreadableError rename left needed (a message string pushed one call over the line-length wrap point) -- caught by `ruff format --check .` while verifying this commit, not new content of its own. `ruff check .` and `ruff format --check .` both clean repo-wide. Full suite green (1448 passed, 4 skipped): golden fixture replay, bundle equivalence, and hook_config-both-matcher-paths tests all exercise every touched function's new call graph. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
The Sonar/Checkstyle/FindBugs bar (plan/coding-standards.md §3) is now in the conventions block beside code_comments/externalized_text: the rule select, mccabe/pylint thresholds, the magic-value remedy order, and the literal-duplication tool/allowlist. No CI workflow change needed -- .github/workflows/ci.yml already runs `ruff check .` and `ruff format --check .` unconditionally, so it picks up pyproject.toml's expanded select and now enforces it on every push. `ruff check .` and `ruff format --check .` both clean repo-wide. Full suite green (1448 passed, 4 skipped). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
…s it CI's lint job pins ruff==0.16.5 (.github/requirements/lint.txt); this session's ruff (0.15.8) never fires PLR0917 (too-many-positional- arguments) at all, so it went unseen while fixing the baseline locally -- flagged as a risk in the worker report before this PR's own CI proved it. 0.16.5 flags install.py's install() and install_identity.py's installed() (7 positional args each, no keyword-only split) alongside the PLR0913 both already carry a documented ignore for; same DTO/ config-knobs reasoning, so PLR0917 joins that ignore rather than getting its own. Reproduced the CI failure first: installed ruff==0.16.5 locally (alongside the session's 0.15.8, which stayed first on PATH -- ran the pinned binary explicitly), confirmed the same two findings CI reported, fixed, then confirmed `ruff check .` and `ruff format --check .` clean under BOTH versions and the full suite still green (1448 passed, 4 skipped). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Adopts the owner's Sonar/Checkstyle/FindBugs static-analysis bar (
plan/coding-standards.md§3) via ruff'sC90/N/PLR/PLW/PLC/ERA/T20/ARG/RET/SIM/PIE/FBT/A/B/S/BLE/TRY/RUFrule families, with the mccabe/pylint thresholds the standard specifies (max-complexity 10; max-args 5, max-branches 12, max-returns 6, max-statements 50), and fixes the measuredsrc/agentseambaseline (101 findings at this ruff version — close to the plan's independently-measured 100; a per-rule count drift is noted in the worker report) category by category in seven bisectable commits:[tool.ruff.lint] select+ thresholds + documented per-file-ignores)__all__/__slots__sort, exception chaining,contextlib.suppress, a safe import hoist)tools/literal_duplication.py, seeded per the standard, no ruff rule covers this)Error-suffixed exception names, named dead parameters)C901/PLR091xfunction split into named helpers, pure extraction)AGENTS.mdrecords the newstatic_analysisstandardNo behavior changes anywhere — every commit is a pure refactor (renames, extractions, keyword-ification, constant extraction).
tests/, tools/, examples/, anddocs/'s two asset scripts are out of scope for this wave (never measured against the new rule set) and are per-file-ignored with aTODO(lint-adoption)reason for a follow-up wave.contract.py/install.py/install_identity.py'sPLR0913(DTO/config-object constructor shape) is a deliberate, documented exception, not deferred work. No version bump; runtime stays stdlib-only;target-versionstayspy39.CI already runs
ruff check .andruff format --check .unconditionally, so no workflow change was needed — it now enforces the expanded rule set automatically.Claim check
Not applicable — this PR touches adapter internals only for lint/refactor purposes (pure extraction, renames, keyword-ification). No capability claim,
MATRIXrow, or payload-shape handling changes.Checks
pytest -qpasses (1448 passed, 4 skipped)ruff check .andruff format --check .passgit commit -s)Notes for the reviewer
noqaor per-file-ignore and a reason:vscode_copilot.py's function-localimport json as _json(hoisting it would edit bundle-emitted text for no behavioral gain —tests/test_bundler.py::test_function_local_imports_are_left_aloneexists specifically to pin this), anddispatch.py'sexitparameter (shadows the builtin on purpose, mirrorssys.exitand the bundledruntime.py.tmpl's ownmain(exit=...)).tools/literal_duplication.py+tests/test_literal_duplication.py) found three genuine repeats insrc/before this PR ("permissionDecisionReason","escalate_from_transform","transform_missing_input"); all three are now named constants, so the allowlist ships empty.plan/spine-a/reports/w51.mdonopen-coder-ai/org-plan.🤖 Generated with Claude Code