Skip to content

Conversation

@MT-superdev
Copy link

@MT-superdev MT-superdev commented Jan 23, 2026

  • Updated AnalysisResponse model to include rules_yaml, pr_plan, and analysis_summary.
  • Implemented hygiene metrics in the analysis process to improve governance insights.
  • Introduced new error handling classes for GitHub API interactions.
  • Refactored GitHub client to support GraphQL queries and improved error logging.
  • Added tests for the new repository analysis features and hygiene metrics.
  • Updated configuration settings to ensure proper handling of mock data during demos.
  • Enhanced logging with structlog for better observability and debugging.

Summary by CodeRabbit

  • New Features

    • Added repository analysis and rule recommendation API for public GitHub repositories
    • Implemented rate limiting: 5 requests/hour for anonymous users, 100/hour for authenticated users
    • Added optional authentication support for private repository analysis
  • Documentation

    • Added documentation on unauthenticated analysis access and rate limiting behavior
    • Documented authentication requirements for private repository analysis
  • Chores

    • Consolidated logging infrastructure
    • Expanded test coverage for API and webhook flows

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 23, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

This PR introduces a graph-based repository analysis system replacing procedural workflows, adds rate limiting and optional authentication, refactors webhook processing to use async task queues, standardizes logging to structlog across the codebase, and expands the GitHub integration with new API methods and GraphQL support.

Changes

Cohort / File(s) Summary
Tooling & Configuration
.github/workflows/docs.yaml, .pre-commit-config.yaml, pyproject.toml, linting_commands.txt, src/core/config/settings.py
Updated GitHub Actions deployment conditions, bumped pre-commit hooks (v5→v6) and consolidated Ruff tooling (v0.9.9→v0.3.0), refactored pyproject.toml to unify Black/Isort/Flake8 under Ruff with tighter mypy rules, added repository analysis feature settings (rate limits, timeouts, mock data flag).
Documentation
README.md, docs/benchmarks.md, docs/features.md
Added documentation for unauthenticated analysis access, rate limiting (5 req/hour anonymous, 100/hour authenticated), and 429 Retry-After handling; minor formatting adjustments.
Core Models & Error Handling
src/core/models.py, src/core/errors.py
Added User model with authentication fields, expanded EventType enum (PULL_REQUEST, CHECK_RUN, DEPLOYMENT, DEPLOYMENT_STATUS), introduced HygieneMetrics model with field validation, and created custom exception hierarchy (GitHubGraphQLError, RepositoryNotFoundError, GitHubRateLimitError, GitHubResourceNotFoundError).
Repository Analysis Agent
src/agents/repository_analysis_agent/agent.py, src/agents/repository_analysis_agent/models.py, src/agents/repository_analysis_agent/nodes.py, src/agents/repository_analysis_agent/prompts.py, src/agents/repository_analysis_agent/__init__.py
Migrated from procedural to LangGraph-based workflow; introduced _build_graph() method with state-driven nodes (fetch_metadata, fetch_pr_signals, generate_rules); added comprehensive data models (AnalysisState, RepositoryAnalysisRequest, RepositoryAnalysisResponse, RuleRecommendation, PRSignal, HygieneMetrics, RepoMetadata); refactored nodes for multi-stage pipeline with fallback handling; replaced template-based prompts with structured system/user prompts for rule generation; expanded public exports.
Base Agent & Feasibility Agent
src/agents/base.py, src/agents/feasibility_agent/agent.py, src/agents/acknowledgment_agent/agent.py, src/agents/engine_agent/agent.py
Added return type annotations, increased default timeout (30→60s), standardized execute signatures to accept **kwargs: Any with runtime validation; refactored agents to extract parameters from kwargs.
GitHub Integration
src/integrations/github/api.py, src/integrations/github/graphql_client.py, src/integrations/github/schemas.py, src/integrations/github/service.py
Extended REST API with anonymous/public access support, added GraphQL client with PR context fetching, introduced GitHubRepository schema for metadata validation, created GitHubService layer for repository metadata and rule analysis with structured error handling.
API Layer
src/api/__init__.py, src/api/dependencies.py, src/api/rate_limit.py, src/api/recommendations.py, src/api/rules.py
Replaced v1 endpoints with new /rules/recommend surface; added rate limiting (in-memory, IP/email-based); introduced optional authentication via Bearer token; implemented AnalyzeRepoRequest/AnalysisResponse models; added helper parse_repo_from_url(); streamlined endpoint logic to delegate to RepositoryAnalysisAgent; added stub proceed-with-pr endpoint.
Webhook System
src/webhooks/models.py, src/webhooks/handlers/base.py, src/webhooks/handlers/pull_request.py, src/webhooks/handlers/push.py, src/webhooks/handlers/deployment.py, src/webhooks/handlers/deployment_protection_rule.py, src/webhooks/handlers/deployment_status.py, src/webhooks/handlers/check_run.py, src/webhooks/handlers/issue_comment.py, src/webhooks/dispatcher.py, src/webhooks/router.py, src/webhooks/auth.py
Introduced WebhookResponse, GitHubEventModel, WebhookSender, WebhookRepository models for standardized payloads; refactored handlers from enqueuing tasks to returning structured responses; updated dispatcher to use TaskQueue-based background processing; migrated logging to structlog; enhanced issue_comment handler with help/acknowledgment/validation logic; normalized event names and improved payload validation in router.
Task Queue & Scheduler
src/tasks/task_queue.py, src/tasks/scheduler/deployment_scheduler.py
Replaced stateful task queue with simplified, deduplicating in-memory queue using asyncio.Queue; added deduplication via SHA-256 task IDs; introduced worker pool management (start_workers/stop_workers); refactored task model with func, args, kwargs fields.
Logging & Utilities
src/core/utils/logging.py, src/core/utils/caching.py, src/main.py
Standardized logging to structlog across modules; streamlined cache initialization and key generation using ternary expressions; updated lifespan/startup/shutdown in main.py with scheduler lifecycle management and event handler registration; adjusted CORS configuration and router prefixes.
Rule System
src/rules/__init__.py, src/rules/models.py, src/rules/utils/contributors.py, src/rules/validators.py
Added RuleCategory enum (SECURITY, QUALITY, COMPLIANCE, VELOCITY, HYGIENE); exposed public types via __init__.py; simplified contributor conditions; refined validator docstrings with TODO/placeholder notes.
Package Initialization
src/__init__.py, src/integrations/providers/bedrock_provider.py, src/integrations/providers/factory.py
Updated module docstrings; refactored Bedrock profile matching with explicit flags; simplified token/temperature calculations in provider factory using ternary expressions.
Testing Infrastructure
tests/conftest.py, tests/unit/agents/, tests/unit/api/, tests/unit/tasks/, tests/unit/webhooks/, tests/integration/
Added test helpers and fixtures (Helpers class, event_loop fixture, mock_settings); created comprehensive unit tests for models, API parsing, task queue, and webhook components; added integration tests for recommendations endpoint, repository analysis, webhook flow, and rules evaluation; removed legacy test_agent.py.

Sequence Diagram(s)

sequenceDiagram
    actor Client as Client (HTTP)
    participant API as /rules/recommend<br/>Endpoint
    participant Agent as Repository<br/>Analysis Agent
    participant Graph as LangGraph<br/>State Machine
    participant GitHub as GitHub<br/>API
    participant LLM as OpenAI<br/>Chat API
    
    Client->>API: POST /rules/recommend<br/>(repo_url, force_refresh)
    API->>API: Validate & Parse URL<br/>Rate Limit Check<br/>Extract Auth
    API->>Agent: execute(**kwargs)
    Agent->>Agent: _build_graph()
    Agent->>Graph: Run graph.invoke()<br/>(AnalysisState)
    Graph->>Graph: fetch_metadata node
    Graph->>GitHub: GET repo metadata<br/>list files, detect languages
    GitHub-->>Graph: repo data
    Graph->>Graph: fetch_pr_signals node
    Graph->>GitHub: GraphQL PR hygiene data
    GitHub-->>Graph: PRs with metrics
    Graph->>Graph: generate_rules node
    Graph->>LLM: POST chat completion<br/>(system prompt + signals)
    LLM-->>Graph: recommendations
    Graph->>Graph: AnalysisState complete
    Agent-->>API: AgentResult<br/>(recommendations)
    API->>API: Map to AnalysisResponse<br/>Generate YAML & PR plan
    API-->>Client: 200 OK<br/>(rules_yaml, pr_plan,<br/>analysis_summary)
Loading
sequenceDiagram
    actor GitHub as GitHub<br/>Webhook
    participant Router as /webhooks<br/>github
    participant Auth as Signature<br/>Verification
    participant Dispatcher as Webhook<br/>Dispatcher
    participant Queue as Task<br/>Queue
    participant Workers as Background<br/>Workers
    participant Handler as Event<br/>Handler
    
    GitHub->>Router: POST webhook event<br/>(PR, Push, Deployment)
    Router->>Auth: verify_github_signature()
    Auth-->>Router: Valid ✓
    Router->>Router: Parse & validate payload<br/>(GitHubEventModel)
    Router->>Dispatcher: dispatch(event)
    Dispatcher->>Queue: enqueue(handler_func,<br/>event_type, payload)
    Queue->>Queue: Generate task_id<br/>Check deduplication
    Queue-->>Dispatcher: bool (enqueued?)
    Dispatcher-->>Router: WebhookResponse<br/>(status)
    Router-->>GitHub: 202 Accepted
    Workers->>Queue: Dequeue task
    Workers->>Handler: handler.handle(event)
    Handler->>Handler: Process event<br/>(log, update state)
    Handler-->>Workers: WebhookResponse
    Workers->>Queue: task_done()
Loading
sequenceDiagram
    actor User as Client<br/>(Anonymous/Auth)
    participant RateLimit as Rate Limiter<br/>Middleware
    participant API as API<br/>Endpoint
    participant Auth as Auth<br/>Dependencies
    participant Service as GitHub<br/>Service
    
    User->>RateLimit: Request
    RateLimit->>Auth: Extract user ID or IP
    Auth-->>RateLimit: User | None
    RateLimit->>RateLimit: Determine limit<br/>(authenticated: 100/hr<br/>anonymous: 5/hr)
    RateLimit->>RateLimit: Check timestamp store<br/>Prune 1-hr window
    alt Within Limit
        RateLimit->>API: Allow request
        API->>Service: Process request
        Service-->>API: Result
        API-->>User: 200 OK
        RateLimit->>RateLimit: Record timestamp
    else Exceeds Limit
        RateLimit-->>User: 429 Too Many<br/>Requests<br/>(Retry-After header)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 A rabbit hops through graphs and flows,
With rates and rules that gently glows,
From webhooks queued to prompts so fine,
The code's now structured, by design!
Async tasks and auth so true,
Makes this PR quite a breakthrough! 🌟

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Matas/fix/fix actions' is vague and does not clearly convey the main changes, which involve major refactoring of repository analysis, hygiene metrics, API responses, and logging infrastructure. Revise the title to clearly summarize the primary change, such as 'Refactor repository analysis with hygiene metrics and structured logging' or 'Add AI Immune System features and improve API response structure'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 963abb2 and a9f74b4.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (40)
  • pyproject.toml
  • src/agents/acknowledgment_agent/agent.py
  • src/agents/acknowledgment_agent/test_agent.py
  • src/agents/engine_agent/agent.py
  • src/agents/feasibility_agent/agent.py
  • src/agents/repository_analysis_agent/agent.py
  • src/agents/repository_analysis_agent/models.py
  • src/agents/repository_analysis_agent/nodes.py
  • src/api/dependencies.py
  • src/api/recommendations.py
  • src/core/models.py
  • src/integrations/github/api.py
  • src/integrations/github/graphql_client.py
  • src/integrations/github/service.py
  • src/integrations/providers/bedrock_provider.py
  • src/main.py
  • src/rules/models.py
  • src/tasks/task_queue.py
  • src/webhooks/auth.py
  • src/webhooks/dispatcher.py
  • src/webhooks/handlers/base.py
  • src/webhooks/handlers/issue_comment.py
  • src/webhooks/handlers/pull_request.py
  • src/webhooks/handlers/push.py
  • src/webhooks/models.py
  • src/webhooks/router.py
  • tests/conftest.py
  • tests/integration/test_recommendations.py
  • tests/integration/test_repo_analysis.py
  • tests/integration/webhooks/__init__.py
  • tests/integration/webhooks/test_webhook_flow.py
  • tests/unit/agents/test_repository_analysis_models.py
  • tests/unit/tasks/__init__.py
  • tests/unit/tasks/test_queue.py
  • tests/unit/test_agents.py
  • tests/unit/test_feasibility_agent.py
  • tests/unit/test_rule_engine_agent.py
  • tests/unit/webhooks/__init__.py
  • tests/unit/webhooks/test_models.py
  • tests/unit/webhooks/test_router.py

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov-commenter
Copy link

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 57.82901% with 439 lines in your changes missing coverage. Please review.

❌ Your patch status has failed because the patch coverage (57.8%) is below the target coverage (80.0%). You can increase the patch coverage or adjust the target coverage.
❌ Your project status has failed because the head coverage (43.4%) is below the target coverage (80.0%). You can increase the head coverage or adjust the target coverage.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

@@           Coverage Diff            @@
##            main     #42      +/-   ##
========================================
+ Coverage   32.8%   43.4%   +10.5%     
========================================
  Files         85     108      +23     
  Lines       5162    6374    +1212     
========================================
+ Hits        1698    2771    +1073     
- Misses      3464    3603     +139     

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f5a3be1...963abb2. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
src/integrations/providers/bedrock_provider.py (4)

202-204: with_structured_output is a no-op stub.

Returning self without modification means structured output isn't actually implemented. Callers expecting schema-validated responses will silently receive unstructured text. This could cause downstream parsing failures or data integrity issues.

Consider either implementing proper structured output support (e.g., using tool calls or JSON mode) or raising NotImplementedError to fail fast.

🔧 Minimal fix to fail explicitly
             def with_structured_output(self, output_model: Any) -> Any:
                 """Add structured output support."""
-                return self
+                raise NotImplementedError(
+                    "Structured output is not supported for AnthropicBedrockWrapper. "
+                    "Use the standard Bedrock client instead."
+                )

239-246: Async method blocks the event loop.

_agenerate calls the synchronous _generate method directly, which will block the event loop during API calls. This defeats the purpose of async and can cause performance issues in concurrent workloads.

Consider using asyncio.to_thread to run the sync call in a thread pool, or implement true async using the Anthropic client's async methods if available.

🔧 Suggested fix using asyncio.to_thread
+            import asyncio
+
             async def _agenerate(
                 self,
                 messages: list[BaseMessage],
                 stop: list[str] | None = None,
                 run_manager: Any | None = None,
             ) -> ChatResult:
                 """Async generate using the Anthropic client."""
-                return self._generate(messages, stop, run_manager)
+                return await asyncio.to_thread(self._generate, messages, stop, run_manager)

219-220: System messages lose their semantics when mapped to "user" role.

Anthropic's API handles system messages via a separate system parameter in the messages.create() call, not as a message with role "user". Mapping system messages to user role may produce unexpected model behavior.

🔧 Suggested approach to handle system messages
             def _generate(
                 self,
                 messages: list[BaseMessage],
                 stop: list[str] | None = None,
                 run_manager: Any | None = None,
             ) -> ChatResult:
                 """Generate a response using the Anthropic client."""
                 anthropic_messages = []
+                system_content = None
                 for msg in messages:
                     if msg.type == "human":
                         role = "user"
                     elif msg.type == "ai":
                         role = "assistant"
                     elif msg.type == "system":
-                        role = "user"
+                        system_content = msg.content
+                        continue
                     else:
                         role = "user"

                     anthropic_messages.append({"role": role, "content": msg.content})

+                create_kwargs = {
+                    "model": self.model_id,
+                    "max_tokens": self.max_tokens,
+                    "temperature": self.temperature,
+                    "messages": anthropic_messages,
+                }
+                if system_content:
+                    create_kwargs["system"] = system_content
+
-                response = self.anthropic_client.messages.create(
-                    model=self.model_id,
-                    max_tokens=self.max_tokens,
-                    temperature=self.temperature,
-                    messages=anthropic_messages,
-                )
+                response = self.anthropic_client.messages.create(**create_kwargs)

50-52: supports_structured_output may return incorrect value for wrapped clients.

This returns True unconditionally, but the AnthropicBedrockWrapper created in the fallback path (lines 44-45) doesn't actually support structured output. This inconsistency could cause callers to expect functionality that won't work.

Consider making this method aware of which client type is in use, or ensure all client types actually support structured output.

src/tasks/task_queue.py (1)

89-110: Avoid logging issue comment bodies at info level.
User-generated comment content can include sensitive data; emitting it in info logs is a compliance/privacy risk. Consider logging only length/hash or gating behind a debug-level redaction policy.

🔒 Suggested redaction
-            logger.info(f"    Comment body: {event_data.get('comment_body', '')[:50]}...")
+            logger.info(f"    Comment body length: {len(event_data.get('comment_body') or '')}")
🤖 Fix all issues with AI agents
In @.pre-commit-config.yaml:
- Around line 6-29: The pinned ruff-pre-commit rev is outdated; update the rev
for the repo "https://github.com/astral-sh/ruff-pre-commit" (the hooks with id:
ruff and id: ruff-format) from v0.3.0 to v0.14.14 in .pre-commit-config.yaml so
the pre-commit config uses the current ruff release.

In `@src/agents/base.py`:
- Around line 114-118: The subclass execute signatures are incompatible with the
BaseAgent.async def execute(self, **kwargs: Any) -> AgentResult; update
RuleFeasibilityAgent.execute, RuleEngineAgent.execute, and
AcknowledgmentAgent.execute to accept **kwargs: Any (matching BaseAgent) and
refactor their bodies to pull needed values from kwargs (e.g., rule_description
= kwargs.get("rule_description"), event_type = kwargs.get("event_type"),
event_data = kwargs.get("event_data"), rules = kwargs.get("rules")) before use
so MyPy substitutability is preserved while behavior remains the same.

In `@src/agents/repository_analysis_agent/models.py`:
- Around line 119-165: Duplicate HygieneMetrics class definitions (one in the
agents module and one in rules) cause maintenance drift; extract a single
canonical HygieneMetrics model into a shared module (e.g., a core/shared models
module) and update both callers to import that single definition, reconciling
field differences by either merging the fields (ci_skip_rate,
new_code_test_coverage, test_coverage_delta_avg, ai_generated_rate) into a
consistent schema with clear optional defaults or, if divergence is intentional,
replace the duplicate with a small variant class that inherits from the shared
HygieneMetrics and add a comment explaining the purposeful differences so the
canonical source remains authoritative (reference class name HygieneMetrics and
update import sites accordingly).

In `@src/agents/repository_analysis_agent/nodes.py`:
- Around line 212-221: The code sets state.hygiene_summary using a field name
test_coverage_delta_avg that doesn't exist on the HygieneMetrics model and will
raise a Pydantic ValidationError; update the initializer for
state.hygiene_summary in nodes.py (the block that constructs HygieneMetrics) to
use the correct field name new_code_test_coverage (or otherwise provide the
actual field names defined on HygieneMetrics) and ensure all keys (e.g.,
unlinked_issue_rate, average_pr_size, first_time_contributor_count,
issue_diff_mismatch_rate, ghost_contributor_rate, new_code_test_coverage,
codeowner_bypass_rate, ai_generated_rate) match the HygieneMetrics definition.
- Around line 366-375: The fallback initialization of state.hygiene_summary uses
the wrong field name test_coverage_delta_avg; update the HygieneMetrics
instantiation to use new_code_test_coverage instead (i.e., replace
test_coverage_delta_avg=0.0 with new_code_test_coverage=0.0) so the keys match
the HygieneMetrics model and avoid runtime attribute/serialization errors when
creating state.hygiene_summary.

In `@src/api/dependencies.py`:
- Around line 33-40: The current get_current_user implementation accepts any
Bearer token and returns a fake User (User(..., github_token=token)),
effectively bypassing auth; change it to gate this mock behavior behind an
explicit demo flag (e.g., DEMO_MODE or a config value) or add minimal token
validation before returning a User. Specifically, update get_current_user to: 1)
check a config/demo flag (referencing get_current_user and auth_header) and only
return the fake User when the flag is enabled; 2) otherwise perform basic
validation (e.g., reject empty token or call a verify_token helper that will be
wired to the IdP later) and return None on failure; and 3) keep the returned
object as User(...) only when validation/demo allows it so protected endpoints
are not bypassed.

In `@src/api/rate_limit.py`:
- Around line 21-28: The rate_limiter function can raise AttributeError when
request.client is None or when user.email is missing; update rate_limiter to
safely derive the key by first checking user and user.email (use AUTH_LIMIT when
user and user.email are present) and otherwise fall back to an IP extracted from
request.client.host only if request.client is not None, else try
request.headers.get("x-forwarded-for") or request.client.peername or use a
constant fallback like "unknown" before forming key (e.g.,
"ip:{ip_or_fallback}"), and ensure limit is set to AUTH_LIMIT or ANON_LIMIT
accordingly so key and limit are always defined without accessing attributes on
None.

In `@src/api/recommendations.py`:
- Around line 68-107: The endpoint recommend_rules currently ignores the
injected user and always calls RepositoryAnalysisAgent.execute with
is_public=True, preventing authenticated/private repo analysis; update
recommend_rules to set is_public based on whether user is present (e.g.,
is_public = False when user is authenticated) and pass the user's
auth/installation context into the agent (either via
RepositoryAnalysisAgent(...) constructor or as parameters to
RepositoryAnalysisAgent.execute) so the agent can perform authenticated GitHub
requests; ensure you use the existing get_current_user_optional injection,
adjust the call sites for RepositoryAnalysisAgent.execute to accept and forward
the token/installation info, and keep parse_repo_from_url and existing error
handling unchanged.

In `@src/integrations/github/api.py`:
- Around line 982-1005: The List PR handler currently sums additions/deletions
from the list response (variables additions, deletions, lines_changed inside the
merged_prs append) but GitHub's List PR endpoint omits those fields; update the
logic so lines_changed is computed from a reliable source: for each PR in the
list call either GET /repos/{owner}/{repo}/pulls/{pull_number} to read
additions/deletions from the full PR object, or call GET
/repos/{owner}/{repo}/pulls/{pull_number}/files and sum file['additions'] and
file['deletions']; replace the current additions/deletions/lines_changed
assignment inside the merged_prs construction with values returned by the per-PR
request and ensure this per-PR fetch is done in the loop that builds merged_prs
(keep _detect_issue_references usage as-is).

In `@src/integrations/github/graphql_client.py`:
- Around line 77-81: The HTTP request in the GraphQL client uses
httpx.Client().post without a timeout which can hang indefinitely; update the
call to include a timeout (either set on the client or per-request) — e.g., pass
timeout=10 (or a configurable constant) to httpx.Client(timeout=...) or
client.post(..., timeout=10) in the method that posts to self.endpoint, so
response.raise_for_status() and response.json() cannot block forever; ensure any
existing tests/configuration read the timeout constant if you make it
configurable.
- Around line 89-96: The commit list comprehension that constructs Commit(...)
assumes node["commit"]["author"]["name"] always exists and can raise KeyError
for commits with null authors; update the extraction in the commits
comprehension (the code that builds Commit instances from
pr_data["commits"]["nodes"]) to safely access the author name using .get() and a
fallback (e.g., author = node["commit"].get("author", {}) and name =
author.get("name") or a sensible default like None or an empty string), matching
how reviews/comments are handled, so Commit(...) receives a safe author value.

In `@src/integrations/github/service.py`:
- Around line 86-109: After each client.get in the block that iterates
files_to_check, explicitly handle non-200 responses so auth/server errors are
surfaced: check resp.status_code — if 200 append filepath to found_files; if 404
treat as legitimately missing and continue; otherwise log the error (use logger
with repo, status_code and response.text) and then call resp.raise_for_status()
(or re-raise) so the existing httpx.HTTPStatusError except block runs; this
change involves the variables/methods shown (files_to_check, resp, found_files,
BASE_URL, logger) and ensures non-404 failures are not silently treated as
missing files.

In `@src/main.py`:
- Around line 111-119: The CORS setup uses allow_origins=["*"] together with
allow_credentials=True which is invalid for credentialed browser requests;
update the app.add_middleware(CORSMiddleware, ...) configuration so
allow_credentials=True only when allow_origins is a specific list of origins
(not "*"). Replace the wildcard with an explicit origins list (e.g., load from
an env var like FRONTEND_ORIGINS and parse into a list) and pass that list to
allow_origins; keep allow_methods and allow_headers as-is and ensure the code
paths that set allow_credentials consult whether the origins list contains
specific entries rather than "*".

In `@src/rules/models.py`:
- Around line 30-74: HygieneMetrics in rules.models is dead code; remove the
entire HygieneMetrics class definition from that module and any leftover imports
referring to it, ensuring the package export list (__all__) in rules.__init__
does not reference HygieneMetrics and that the active HygieneMetrics in
agents.repository_analysis_agent.models remains the canonical model used by
nodes.py and tests.

In `@src/rules/validators.py`:
- Around line 239-243: The has_min_approvals validator currently always returns
True; replace the hardcoded return with real approval logic: read min_approvals
from parameters (e.g., parameters.get("min_approvals", 1)), obtain the PR review
list from the validator context or payload used elsewhere (similar to how
MinApprovalsCondition accesses PR reviews), count reviews with state=="APPROVED"
(and optionally dedupe by reviewer) and return True only if approvals_count >=
min_approvals; if you cannot access reviews in this validator, remove or
feature-flag has_min_approvals to avoid silently passing rules.

In `@src/webhooks/dispatcher.py`:
- Around line 3-9: Dispatcher uses f-string interpolation in structlog calls;
change each logger.* call in dispatcher.py that currently does
logger.info(f"...{var}...") / logger.error(f"...") to structured logging by
passing a plain message string and the variables as keyword context (e.g.,
logger.info("dispatching webhook event", event_type=event.type,
webhook_id=webhook.id, handler=name)), and for exceptions include exc_info or
pass the exception as a context key (e.g., logger.exception("handler failed",
handler=handler_name, error=exc)). Update all such calls related to EventType,
WebhookEvent, and EventHandler usage so message and metadata are separated.

In `@tests/conftest.py`:
- Around line 32-49: The test hardcodes a mock API key (OPENAI_API_KEY) inside
the mock_settings fixture causing a gitleaks false positive; update the test to
avoid embedding a string that looks like a real key by either adding this
specific test value to the gitleaks allowlist configuration or moving the mock
secrets into a gitleaks-ignored test env file and loading them from there, and
ensure the change targets the mock_settings fixture and the OPENAI_API_KEY entry
so CI stops flagging it.

In `@tests/integration/test_recommendations.py`:
- Around line 35-70: The test is mocking httpx calls with respx but
fetch_repository_metadata (used via github_client in
src/integrations/github/api.py) uses aiohttp so those mocks are never applied;
update the test to inject an httpx-backed client for fetch_repository_metadata
(or replace the respx mocks with aioresponses mocks) so the GitHub API stubs
cover both fetch_repository_metadata and fetch_pr_signals (GitHubClient in
src/integrations/github/client.py); additionally update nodes.py to stop
catching only httpx.HTTPStatusError for errors from the aiohttp-based
client—either catch aiohttp.ClientResponseError or use a broader exception (or
normalize exceptions in the injected client) so error handling matches the
actual client implementation.
🟡 Minor comments (11)
src/api/rules.py-9-11 (1)

9-11: event_data field is defined but never used.

The event_data parameter is added to the request model but is not passed to agent.execute(). Looking at the RuleFeasibilityAgent.execute() signature, it only accepts rule_description. Either remove this unused field or implement its usage.

Option 1: Remove unused field
 class RuleEvaluationRequest(BaseModel):
     rule_text: str
-    event_data: dict | None = None  # Advanced: pass extra event data for edge cases.
src/webhooks/handlers/issue_comment.py-35-39 (1)

35-39: Substring matching may cause false positives.

The in check matches any username containing a bot name substring (e.g., "mywatchflowhelper" or "watchflowfan" would be treated as bots). Consider exact matching or word-boundary checks if legitimate users could have these substrings in their names.

Suggested fix using exact matching
-            if commenter and any(bot_name.lower() in commenter.lower() for bot_name in bot_usernames):
+            if commenter and commenter.lower() in [b.lower() for b in bot_usernames]:
docs/benchmarks.md-9-9 (1)

9-9: Inconsistent formatting with "70+" references.

Line 9 uses "70 +" (with spaces) while line 58 uses "70+" (no spaces). This appears to be an unintentional whitespace change that creates inconsistency within the document.

Proposed fix — use consistent formatting
-Our analysis of 70 + enterprise policies from major tech companies revealed a critical insight: **85% of real-world governance policies require context** and cannot be effectively enforced with traditional static rules.
+Our analysis of 70+ enterprise policies from major tech companies revealed a critical insight: **85% of real-world governance policies require context** and cannot be effectively enforced with traditional static rules.
src/agents/feasibility_agent/agent.py-2-2 (1)

2-2: Typo: extra space in docstring.

There's a double space between "handling" and "and" in the module docstring.

Proposed fix
-Rule Feasibility Agent implementation with error handling  and retry logic.
+Rule Feasibility Agent implementation with error handling and retry logic.
docs/features.md-279-287 (1)

279-287: Minor wording adjustment needed — rate limit values are correct.

The rate limit values (5 requests/hour for anonymous, 100 for authenticated) match the implementation in src/api/rate_limit.py. However, change "100 repos/hour" to "100 requests/hour" for authenticated users, as the rate limit is applied per request, not per repository analyzed. The 429 response with Retry-After header is correctly implemented.

src/integrations/github/graphql_client.py-116-120 (1)

116-120: exc_info should be True, not the exception object.

With structlog, passing exc_info=True captures the current exception's traceback. Passing the exception object directly may not produce the expected output.

🐛 Proposed fix
         except httpx.HTTPStatusError as e:
-            logger.error("HTTP error fetching PR context", exc_info=e)
+            logger.error("HTTP error fetching PR context", status_code=e.response.status_code, exc_info=True)
             raise
         except Exception as e:
-            logger.error("An unexpected error occurred", exc_info=e)
+            logger.error("Unexpected error fetching PR context", exc_info=True)
             raise
src/agents/repository_analysis_agent/nodes.py-286-293 (1)

286-293: Potential KeyError if review author is None.

When iterating reviews, review["author"]["login"] will raise KeyError if author is None (e.g., deleted user accounts).

🐛 Proposed defensive access
-            approved = any(review["state"] == "APPROVED" and review["author"]["login"] != author for review in reviews)
+            approved = any(
+                review["state"] == "APPROVED" 
+                and review.get("author", {}).get("login") != author 
+                for review in reviews
+            )
src/integrations/github/schemas.py-16-18 (1)

16-18: Avoid defaulting default_branch to "main" when the field is missing.

If upstream payloads omit default_branch (e.g., partial GraphQL selections), defaulting to "main" can misroute downstream calls for repos using "master" or custom branches. Prefer making this optional so absence is explicit.

💡 Suggested adjustment
-    default_branch: str = Field(default="main")
+    default_branch: str | None = None
src/api/recommendations.py-46-62 (1)

46-62: Tighten GitHub host validation.

"github.com" in p.host will accept evilgithub.com. Prefer an exact match (or a strict allowlist) so non‑GitHub domains don’t pass validation.

🔧 Suggested fix
-    if not p.valid or not p.owner or not p.repo or "github.com" not in p.host:
+    if not p.valid or not p.owner or not p.repo or p.host not in {"github.com", "www.github.com"}:
src/integrations/github/api.py-951-1042 (1)

951-1042: Remove httpx import and catch aiohttp errors instead.

This method uses aiohttp (via self._get_session()), so httpx.HTTPStatusError will never fire. Network failures will be classified as "unknown_error" instead of "network_error". Replace with aiohttp.ClientError:

🔧 Suggested fix
-        import httpx
-
         logger = structlog.get_logger()
@@
-        except httpx.HTTPStatusError as e:
-            logger.error(
-                "pr_fetch_http_error",
-                repo=repo_full_name,
-                status_code=e.response.status_code,
-                error_type="network_error",
-                error=str(e),
-            )
-            return []
+        except aiohttp.ClientError as e:
+            logger.error(
+                "pr_fetch_http_error",
+                repo=repo_full_name,
+                error_type="network_error",
+                error=str(e),
+            )
+            return []
src/integrations/github/client.py-38-45 (1)

38-45: Do not send an empty Authorization header to GitHub API.

When no token is provided, the client currently sends Authorization: "", which GitHub treats as an authentication attempt with invalid credentials. This triggers a 401 Unauthorized response and can cause repeated requests to hit GitHub's failed login limit, resulting in temporary 403 Forbidden responses. The correct approach is to omit the header entirely when no token is available, which allows requests to use GitHub's unauthenticated rate limit (60 req/hour per IP).

Suggested fix
-        self.headers = {
-            "Authorization": f"Bearer {self.token}" if self.token else "",
-            "Accept": "application/vnd.github.v3+json",
-        }
+        self.headers = {"Accept": "application/vnd.github.v3+json"}
+        if self.token:
+            self.headers["Authorization"] = f"Bearer {self.token}"
🧹 Nitpick comments (20)
src/integrations/providers/bedrock_provider.py (1)

156-159: Overly broad keywords may cause incorrect profile matching.

The keywords "general" and "default" in the is_anthropic check could match profiles that aren't actually Anthropic profiles. A profile named "general-purpose-inference" or "default-model-profile" would incorrectly match for any Anthropic model request.

Consider restricting the keywords to provider-specific identifiers only, or verifying the profile's underlying model configuration.

♻️ Suggested fix
-                is_anthropic = any(k in profile_name for k in ["claude", "anthropic", "general", "default"])
+                is_anthropic = any(k in profile_name for k in ["claude", "anthropic"])
src/core/utils/caching.py (1)

161-163: Cache initialization logic looks correct, but ttl=0 edge case silently defaults to 3600.

When ttl=0 is passed, the falsy check causes fallback to TTLCache with ttl=3600. While ttl=0 is unlikely in practice, consider using an explicit None check if you want to distinguish between "no TTL provided" and "TTL explicitly set to 0":

cache = AsyncCache(maxsize=maxsize, ttl=ttl) if ttl is not None else TTLCache(maxsize=maxsize, ttl=3600)

The lint comment (SIM108) can be removed now that the refactor is applied—it documents why the change was made, not what the code does.

src/webhooks/handlers/issue_comment.py (1)

54-58: Extract duplicated PR number resolution into a helper method.

This logic is repeated identically at lines 100-104 and 122-126. A helper method would reduce duplication and ensure consistent behavior.

Suggested helper method
def _get_pr_or_issue_number(self, event: WebhookEvent) -> int | None:
    """Extract PR or issue number from event payload."""
    return (
        event.payload.get("issue", {}).get("number")
        or event.payload.get("pull_request", {}).get("number")
        or event.payload.get("number")
    )

Then replace each occurrence:

-                pr_number = (
-                    event.payload.get("issue", {}).get("number")
-                    or event.payload.get("pull_request", {}).get("number")
-                    or event.payload.get("number")
-                )
+                pr_number = self._get_pr_or_issue_number(event)
src/webhooks/handlers/deployment_status.py (1)

36-42: Consider adding error handling for queue failures.

The updated comment correctly notes that enqueue "may fail if queue overloaded," but there's no try/except around the task_queue.enqueue() call. If the queue fails, the exception propagates and the webhook returns an error to GitHub, which may trigger retries.

Consider wrapping this in error handling to return a graceful degradation response or implement backpressure signaling.

Proposed enhancement
         # Enqueue: async, may fail if queue overloaded.
-        task_id = await task_queue.enqueue(
-            event_type="deployment_status",
-            repo_full_name=repo_full_name,
-            installation_id=installation_id,
-            payload=payload,
-        )
+        try:
+            task_id = await task_queue.enqueue(
+                event_type="deployment_status",
+                repo_full_name=repo_full_name,
+                installation_id=installation_id,
+                payload=payload,
+            )
+        except Exception as e:
+            logger.error(f"Failed to enqueue deployment status event for {repo_full_name}: {e}")
+            return {"status": "error", "message": "Queue temporarily unavailable", "retry": True}
linting_commands.txt (1)

1-9: Consider consolidating into a Makefile or documenting in CONTRIBUTING.md.

These commands are useful, but a standalone .txt file may be easily overlooked. Consider:

  1. Adding a Makefile with targets like make lint, make format, make typecheck
  2. Including these commands in CONTRIBUTING.md for better discoverability
  3. If keeping as a reference file, rename to something like scripts/lint.sh and make it executable
src/core/config/settings.py (2)

114-119: Consider removing demo-specific comments from production code.

The comment "CRITICAL: USE_MOCK_DATA must be False for CEO demo (Phase 5)" is a transient operational note that doesn't belong in the codebase. Consider documenting deployment requirements in a runbook or deployment checklist instead.


148-155: Vertex AI aliases are duplicated with PROVIDER_MAP in factory.py.

This vertex_aliases set mirrors the aliases defined in src/integrations/providers/factory.py (lines 24-28). If aliases are added or removed, both locations must be updated, risking divergence.

Consider extracting a shared constant or reusing PROVIDER_MAP to derive the canonical provider name.

♻️ Example: Extract shared aliases

Create a shared constant in a common location (e.g., provider_config.py):

VERTEX_AI_ALIASES = {"vertex_ai", "garden", "vertex", "vertexai", "model_garden", "gcp"}

Then import and use it in both factory.py and settings.py.

src/integrations/github/graphql_client.py (2)

35-35: Consider making this method async for consistency with the codebase.

This method uses a synchronous httpx.Client, but the rest of the codebase (e.g., contributors.py) uses async patterns. Calling this synchronous method from an async context will block the event loop.

♻️ Async alternative
-    def fetch_pr_context(self, owner: str, repo: str, pr_number: int) -> PRContext:
+    async def fetch_pr_context(self, owner: str, repo: str, pr_number: int) -> PRContext:
         """
         Fetches the context of a pull request from GitHub's GraphQL API.
         """
         # ... query definition ...
 
         try:
-            with httpx.Client() as client:
-                response = client.post(self.endpoint, json={"query": query, "variables": variables}, headers=headers)
+            async with httpx.AsyncClient(timeout=30.0) as client:
+                response = await client.post(
+                    self.endpoint,
+                    json={"query": query, "variables": variables},
+                    headers=headers,
+                )

83-85: Prefer a specific exception type over generic Exception.

Raising a bare Exception makes it harder for callers to distinguish GraphQL errors from other failures. Consider defining a custom exception or including error details.

♻️ Proposed improvement
+class GitHubGraphQLError(Exception):
+    """Raised when a GitHub GraphQL query fails."""
+    def __init__(self, errors: list):
+        self.errors = errors
+        super().__init__(f"GraphQL query failed: {errors}")

 # In fetch_pr_context:
                 if "errors" in data:
                     logger.error("GraphQL query failed", errors=data["errors"])
-                    raise Exception("GraphQL query failed")
+                    raise GitHubGraphQLError(data["errors"])
src/core/models.py (1)

7-18: Consider protecting github_token from accidental serialization/logging.

The github_token field stores a sensitive OAuth token. Pydantic's default serialization behavior will include this field when the model is converted to dict/JSON, potentially exposing it in logs or API responses.

Consider using SecretStr or excluding it from serialization:

♻️ Option 1: Use SecretStr for automatic masking
-from pydantic import BaseModel, Field
+from pydantic import BaseModel, Field, SecretStr

 class User(BaseModel):
     ...
-    github_token: str | None = Field(None, description="OAuth token for GitHub API access")
+    github_token: SecretStr | None = Field(None, description="OAuth token for GitHub API access")

Access the raw value via user.github_token.get_secret_value() when needed.

♻️ Option 2: Exclude from default serialization
-    github_token: str | None = Field(None, description="OAuth token for GitHub API access")
+    github_token: str | None = Field(None, description="OAuth token for GitHub API access", exclude=True)
src/api/rate_limit.py (1)

13-14: In-memory rate limiting won't persist across restarts or scale horizontally.

The in-memory _RATE_LIMIT_STORE is appropriate for single-instance deployments but will reset on process restart and won't synchronize across multiple instances (e.g., in Kubernetes).

This is acceptable for an MVP, but consider Redis-backed rate limiting for production multi-instance deployments.

pyproject.toml (1)

119-119: Duplicate dependency: giturlparse is already a main dependency.

giturlparse>=0.1.0 is listed in both the main dependencies (line 28) and [tool.uv] dev-dependencies (line 119). The dev-dependencies entry is redundant.

♻️ Remove duplicate
 [tool.uv]
 dev-dependencies = [
     "pytest>=7.4.0",
     "pytest-asyncio>=0.21.0",
     "pytest-cov>=4.1.0",
     "mypy>=1.7.0",
     "pre-commit>=3.5.0",
     "ruff>=0.1.0",
-    "giturlparse>=0.1.0",
 ]
src/agents/repository_analysis_agent/models.py (2)

6-17: Consider validating the parsed identifier format.

parse_github_repo_identifier doesn't validate that the result matches owner/repo format. Inputs like "https://github.com/" or "///" would return malformed strings.

♻️ Proposed validation
 def parse_github_repo_identifier(identifier: str) -> str:
     """
     Normalizes various GitHub identifiers into 'owner/repo' format.
     Used by tests to verify repository strings.
     """
     # Remove protocol and domain
     clean_id = identifier.replace("https://github.com/", "").replace("http://github.com/", "")
 
     # Remove .git suffix and trailing slashes
     clean_id = clean_id.replace(".git", "").strip("/")
 
+    # Validate format
+    if "/" not in clean_id or clean_id.count("/") != 1:
+        raise ValueError(f"Invalid repository identifier: {identifier}")
+
     return clean_id

132-165: Missing range validation for percentage fields.

Fields like unlinked_issue_rate, ci_skip_rate, etc. are documented as (0.0-1.0) percentages but lack validation constraints. Invalid values (negative or >1.0) would be accepted silently.

♻️ Add field validators
from pydantic import field_validator

`@field_validator`(
    "unlinked_issue_rate", "ci_skip_rate", "codeowner_bypass_rate",
    "new_code_test_coverage", "issue_diff_mismatch_rate", "ghost_contributor_rate"
)
`@classmethod`
def validate_rate(cls, v: float) -> float:
    if not 0.0 <= v <= 1.0:
        raise ValueError("Rate must be between 0.0 and 1.0")
    return v
tests/unit/agents/test_repository_analysis_models.py (1)

19-28: Tests verify basic instantiation but lack edge case coverage.

Consider adding tests for:

  • Boundary values (unlinked_issue_rate=0.0, unlinked_issue_rate=1.0)
  • Invalid inputs if validation is added (negative rates, rates > 1.0)
  • Default field values verification
src/agents/repository_analysis_agent/nodes.py (1)

36-45: Duplicate AI keywords list.

The same keyword list appears in _map_github_pr_to_signal (lines 36-45) and fetch_pr_signals (lines 244-253). Extract to a module-level constant for DRY compliance.

♻️ Extract constant
+AI_DETECTION_KEYWORDS = [
+    "generated by claude",
+    "cursor",
+    "copilot",
+    "chatgpt",
+    "ai-generated",
+    "llm",
+    "i am an ai",
+    "as an ai",
+]
+
 def _map_github_pr_to_signal(pr_data: dict[str, Any]) -> PRSignal:
     # ...
-    ai_keywords = [
-        "generated by claude",
-        # ...
-    ]
-    is_ai_generated = any(keyword in body or keyword in title for keyword in ai_keywords)
+    is_ai_generated = any(keyword in body or keyword in title for keyword in AI_DETECTION_KEYWORDS)

Also applies to: 244-253

src/webhooks/handlers/check_run.py (1)

18-18: Prefer structured logging over f-strings with structlog.

Using f-strings defeats the purpose of structured logging. Structlog works best with keyword arguments for searchable, parseable log fields.

♻️ Use keyword arguments
-        logger.info(f"🔄 Enqueuing check run event for {event.repo_full_name}")
+        logger.info("enqueuing_check_run_event", repo=event.repo_full_name)
         # ...
-        logger.info(f"✅ Check run event enqueued with task ID: {task_id}")
+        logger.info("check_run_event_enqueued", task_id=task_id)

Also applies to: 27-27

src/api/dependencies.py (1)

8-9: Remove duplicate logger initialization.

The second assignment is redundant.

🧹 Suggested cleanup
-logger = logging.getLogger(__name__)  # Logger: keep at module level for reuse.
-logger = logging.getLogger(__name__)
+logger = logging.getLogger(__name__)  # Logger: keep at module level for reuse.
src/main.py (1)

30-53: Align structlog filtering level with configured log level.

make_filtering_bound_logger(logging.INFO) hardcodes INFO even if config.logging.level is DEBUG, so debug logs are dropped. Use the configured level consistently (with a safe default).

🔧 Suggested adjustment
-# Configure structlog for JSON logging (Phase 5: Observability)
-structlog.configure(
+log_level = getattr(logging, config.logging.level, logging.INFO)
+# Configure structlog for JSON logging (Phase 5: Observability)
+structlog.configure(
     processors=[
         structlog.contextvars.merge_contextvars,
         structlog.processors.add_log_level,
         structlog.processors.TimeStamper(fmt="iso"),
         structlog.processors.JSONRenderer(),
     ],
-    wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
+    wrapper_class=structlog.make_filtering_bound_logger(log_level),
     context_class=dict,
     logger_factory=structlog.PrintLoggerFactory(),
     cache_logger_on_first_use=False,
 )
@@
-logging.basicConfig(
-    level=getattr(logging, config.logging.level),
+logging.basicConfig(
+    level=log_level,
     format="%(message)s",  # structlog handles formatting
 )
src/integrations/github/api.py (1)

232-258: Avoid relying on aiohttp’s private _loop attribute.

Accessing _loop is fragile across aiohttp versions. Consider tracking the loop yourself when you create the session and compare to the current loop.

♻️ Example refactor
-            if self._session is None or self._session.closed:
-                self._session = aiohttp.ClientSession()
+            if self._session is None or self._session.closed:
+                self._session = aiohttp.ClientSession()
+                self._session_loop = asyncio.get_running_loop()
             else:
                 # Check if we're in a different event loop (avoid deprecated .loop property)
                 try:
                     current_loop = asyncio.get_running_loop()
-                    # Try to access session's internal loop to check if it's the same
-                    # If the session's loop is closed, this will fail
-                    if self._session._loop != current_loop or self._session._loop.is_closed():
+                    session_loop = getattr(self, "_session_loop", None)
+                    if session_loop != current_loop or current_loop.is_closed():
                         await self._session.close()
                         self._session = aiohttp.ClientSession()
+                        self._session_loop = current_loop
                 except RuntimeError:
                     # No running loop or loop is closed, recreate session
-                    self._session = aiohttp.ClientSession()
+                    self._session = aiohttp.ClientSession()
+                    self._session_loop = asyncio.get_running_loop()
         except Exception:
             # Fallback: ensure we have a valid session
-            self._session = aiohttp.ClientSession()
+            self._session = aiohttp.ClientSession()
+            self._session_loop = asyncio.get_running_loop()
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5a3be1 and 963abb2.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (55)
  • .github/workflows/docs.yaml
  • .pre-commit-config.yaml
  • README.md
  • docs/benchmarks.md
  • docs/features.md
  • linting_commands.txt
  • pyproject.toml
  • src/__init__.py
  • src/agents/base.py
  • src/agents/feasibility_agent/agent.py
  • src/agents/repository_analysis_agent/__init__.py
  • src/agents/repository_analysis_agent/agent.py
  • src/agents/repository_analysis_agent/models.py
  • src/agents/repository_analysis_agent/nodes.py
  • src/agents/repository_analysis_agent/prompts.py
  • src/agents/repository_analysis_agent/test_agent.py
  • src/api/__init__.py
  • src/api/dependencies.py
  • src/api/rate_limit.py
  • src/api/recommendations.py
  • src/api/rules.py
  • src/core/config/settings.py
  • src/core/errors.py
  • src/core/models.py
  • src/core/utils/caching.py
  • src/core/utils/logging.py
  • src/integrations/github/api.py
  • src/integrations/github/client.py
  • src/integrations/github/graphql_client.py
  • src/integrations/github/schemas.py
  • src/integrations/github/service.py
  • src/integrations/providers/bedrock_provider.py
  • src/integrations/providers/factory.py
  • src/main.py
  • src/rules/__init__.py
  • src/rules/models.py
  • src/rules/utils/contributors.py
  • src/rules/validators.py
  • src/tasks/scheduler/deployment_scheduler.py
  • src/tasks/task_queue.py
  • src/webhooks/auth.py
  • src/webhooks/dispatcher.py
  • src/webhooks/handlers/check_run.py
  • src/webhooks/handlers/deployment.py
  • src/webhooks/handlers/deployment_protection_rule.py
  • src/webhooks/handlers/deployment_status.py
  • src/webhooks/handlers/issue_comment.py
  • src/webhooks/handlers/pull_request.py
  • src/webhooks/router.py
  • tests/conftest.py
  • tests/integration/test_recommendations.py
  • tests/integration/test_repo_analysis.py
  • tests/integration/test_rules_api.py
  • tests/unit/agents/test_repository_analysis_models.py
  • tests/unit/api/test_recommendations.py
💤 Files with no reviewable changes (1)
  • src/agents/repository_analysis_agent/test_agent.py
🧰 Additional context used
🧬 Code graph analysis (24)
src/webhooks/router.py (2)
src/webhooks/handlers/issue_comment.py (1)
  • event_type (19-20)
src/core/models.py (1)
  • EventType (24-38)
src/rules/__init__.py (1)
src/rules/models.py (5)
  • Rule (91-100)
  • RuleAction (84-88)
  • RuleCategory (20-27)
  • RuleCondition (77-81)
  • RuleSeverity (9-17)
src/api/rate_limit.py (2)
src/core/models.py (1)
  • User (7-18)
tests/integration/test_rules_api.py (1)
  • client (21-23)
src/rules/models.py (1)
src/agents/repository_analysis_agent/models.py (1)
  • HygieneMetrics (119-165)
src/webhooks/handlers/deployment_protection_rule.py (2)
src/core/models.py (1)
  • WebhookEvent (41-63)
src/webhooks/handlers/base.py (1)
  • EventHandler (7-26)
src/webhooks/dispatcher.py (4)
src/core/models.py (2)
  • EventType (24-38)
  • WebhookEvent (41-63)
src/webhooks/handlers/base.py (2)
  • EventHandler (7-26)
  • handle (16-26)
src/webhooks/handlers/issue_comment.py (2)
  • handle (25-147)
  • event_type (19-20)
src/webhooks/handlers/pull_request.py (1)
  • handle (18-45)
src/agents/repository_analysis_agent/nodes.py (2)
src/agents/repository_analysis_agent/models.py (3)
  • PRSignal (94-116)
  • RuleRecommendation (81-91)
  • HygieneMetrics (119-165)
src/core/models.py (1)
  • repo_full_name (56-58)
src/webhooks/handlers/deployment.py (3)
src/core/models.py (3)
  • EventType (24-38)
  • WebhookEvent (41-63)
  • repo_full_name (56-58)
src/webhooks/handlers/base.py (1)
  • EventHandler (7-26)
src/core/utils/caching.py (1)
  • get (44-66)
src/api/__init__.py (1)
src/api/recommendations.py (3)
  • AnalysisResponse (33-40)
  • AnalyzeRepoRequest (22-30)
  • parse_repo_from_url (46-62)
tests/integration/test_recommendations.py (1)
src/core/utils/caching.py (1)
  • get (44-66)
src/api/dependencies.py (2)
src/core/models.py (1)
  • User (7-18)
src/integrations/github/service.py (1)
  • GitHubService (15-166)
tests/integration/test_repo_analysis.py (3)
src/agents/repository_analysis_agent/agent.py (1)
  • RepositoryAnalysisAgent (14-76)
src/agents/repository_analysis_agent/models.py (2)
  • AnalysisState (168-195)
  • HygieneMetrics (119-165)
src/rules/models.py (1)
  • HygieneMetrics (30-74)
src/api/rules.py (3)
src/agents/factory.py (1)
  • get_agent (20-52)
src/agents/base.py (1)
  • execute (114-119)
src/agents/feasibility_agent/agent.py (1)
  • execute (57-123)
src/integrations/github/client.py (2)
src/core/errors.py (2)
  • GitHubGraphQLError (6-11)
  • RepositoryNotFoundError (14-17)
src/integrations/github/schemas.py (1)
  • GitHubRepository (6-26)
src/agents/repository_analysis_agent/agent.py (3)
src/agents/base.py (5)
  • AgentResult (16-29)
  • BaseAgent (32-119)
  • _build_graph (55-60)
  • execute (114-119)
  • _execute_with_timeout (93-111)
src/agents/repository_analysis_agent/nodes.py (3)
  • fetch_repository_metadata (58-175)
  • fetch_pr_signals (178-376)
  • generate_rule_recommendations (379-454)
src/core/models.py (1)
  • repo_full_name (56-58)
src/api/recommendations.py (5)
src/agents/repository_analysis_agent/agent.py (2)
  • RepositoryAnalysisAgent (14-76)
  • execute (42-76)
src/api/dependencies.py (1)
  • get_current_user_optional (24-43)
src/api/rate_limit.py (1)
  • rate_limiter (21-41)
src/core/models.py (2)
  • User (7-18)
  • repo_full_name (56-58)
src/agents/base.py (1)
  • execute (114-119)
src/integrations/github/service.py (1)
src/core/errors.py (2)
  • GitHubRateLimitError (20-23)
  • GitHubResourceNotFoundError (26-29)
src/webhooks/handlers/check_run.py (2)
src/core/models.py (1)
  • WebhookEvent (41-63)
src/webhooks/handlers/base.py (1)
  • EventHandler (7-26)
src/agents/base.py (4)
src/agents/repository_analysis_agent/agent.py (2)
  • _build_graph (22-40)
  • execute (42-76)
src/agents/feasibility_agent/agent.py (2)
  • _build_graph (34-55)
  • execute (57-123)
src/agents/acknowledgment_agent/agent.py (2)
  • _build_graph (36-52)
  • execute (212-217)
src/agents/engine_agent/agent.py (2)
  • _build_graph (54-73)
  • execute (75-173)
src/integrations/github/api.py (2)
src/core/models.py (1)
  • repo_full_name (56-58)
src/core/utils/caching.py (1)
  • get (44-66)
tests/unit/api/test_recommendations.py (1)
src/api/recommendations.py (1)
  • parse_repo_from_url (46-62)
src/integrations/providers/factory.py (2)
src/core/config/provider_config.py (2)
  • get_max_tokens_for_agent (57-63)
  • get_temperature_for_agent (65-71)
tests/unit/test_feasibility_agent.py (1)
  • agent (18-31)
src/agents/repository_analysis_agent/models.py (2)
src/rules/models.py (1)
  • HygieneMetrics (30-74)
src/core/models.py (1)
  • repo_full_name (56-58)
tests/conftest.py (1)
src/integrations/github/api.py (1)
  • close (165-168)
🪛 Gitleaks (8.30.0)
src/integrations/github/api.py

[high] 105-105: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 121-121: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

tests/conftest.py

[high] 42-42: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 6 to 29
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: trailing-whitespace
# Removes any trailing whitespace from lines in text files
- id: end-of-file-fixer
# Ensures files end with a newline
- id: check-yaml
exclude: '^helm-chart/templates.*\.yaml$|^mkdocs\.yml$'
# Checks YAML files for syntax errors
- id: check-json
# Checks JSON files for syntax errors
- id: check-ast
name: "Check Python syntax (AST validation)"
# Validates that Python files have valid syntax
- id: check-added-large-files

# Ruff hooks for Python linting and formatting
# Ruff: The One Tool to Rule Them All
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.9
rev: v0.3.0
hooks:
# First, run ruff format - won't change imports but will format them
- id: ruff-format
name: "Format code (without changing imports)"

# Then, use a single ruff pass for both import sorting and linting
# Linter (Fixes imports, modernizes syntax, checks bugs)
- id: ruff
name: "Linting and import sorting"
args: ["--fix"]

# Pyupgrade: Check and fix Python version incompatibilities and outdated syntax
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.2
hooks:
- id: pyupgrade
name: "Upgrade syntax for Python 3.12+"
args: [--py312-plus]
# Auto-fixes outdated syntax to Python 3.12+ compatible code
args: [--fix, --exit-non-zero-on-fix]
# Formatter (Replaces Black)
- id: ruff-format

# Conventional pre-commit hooks for commit messages
# Convention Enforcement
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v4.0.0
rev: v4.3.0
hooks:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

pre-commit-hooks latest release 2025 2026

💡 Result:

The latest release of pre-commit-hooks (pre-commit/pre-commit-hooks) is v6.0.0, released on August 9, 2025 (and it is still the latest as of January 23, 2026). [1], [2]

Where it’s published:

  • GitHub Releases: v6.0.0 (Aug 9, 2025) [1]
  • PyPI package pre-commit-hooks: 6.0.0 (uploaded Aug 9, 2025) [2]

🌐 Web query:

ruff-pre-commit latest release 2025 2026

💡 Result:

  • Latest astral-sh/ruff-pre-commit release (as of Jan 23, 2026): v0.14.14, released Jan 22, 2026. [1]
  • Last release in 2025: v0.14.10, released Dec 18, 2025. [1]

Sources:
[1] GitHub Releases – astral-sh/ruff-pre-commit: https://github.com/astral-sh/ruff-pre-commit/releases


🌐 Web query:

conventional-pre-commit latest release 2025 2026

💡 Result:

As of January 23, 2026, the latest released version of conventional-pre-commit is 4.3.0, released on October 10, 2025. No newer (2026) release is listed in the PyPI release history. [1] [2]

Recent releases:

  • 4.3.0 — Oct 10, 2025 [1]
  • 4.2.0 — May 5, 2025 [1]
  • 4.1.0 — Apr 29, 2025 [1]

Sources: [1] [2]


Update ruff-pre-commit to v0.14.14.

The pinned version v0.3.0 is significantly outdated; the latest release is v0.14.14 (Jan 22, 2026). The other hooks are current: pre-commit-hooks v6.0.0 (Aug 2025) and conventional-pre-commit v4.3.0 (Oct 2025).

🤖 Prompt for AI Agents
In @.pre-commit-config.yaml around lines 6 - 29, The pinned ruff-pre-commit rev
is outdated; update the rev for the repo
"https://github.com/astral-sh/ruff-pre-commit" (the hooks with id: ruff and id:
ruff-format) from v0.3.0 to v0.14.14 in .pre-commit-config.yaml so the
pre-commit config uses the current ruff release.

Comment on lines +114 to +118
async def execute(self, **kwargs: Any) -> AgentResult:
"""
Execute the agent with given parameters.
Explicitly typed **kwargs to satisfy MyPy strict mode.
"""
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all BaseAgent subclasses
echo "=== BaseAgent subclasses ==="
rg -n --type=py 'class .*\(.*BaseAgent.*\):' -C2

echo ""
echo "=== Execute method signatures in agents ==="
rg -n --type=py 'def execute\(' -C2 src/agents

Repository: warestack/watchflow

Length of output: 3468


Fix execute method signatures in RuleFeasibilityAgent, RuleEngineAgent, and AcknowledgmentAgent to match the BaseAgent abstract signature.

Three subclasses have execute method signatures incompatible with the base class:

  • RuleFeasibilityAgent (line 57 of src/agents/feasibility_agent/agent.py): uses rule_description: str
  • RuleEngineAgent (line 75 of src/agents/engine_agent/agent.py): uses event_type: str, event_data: dict[str, Any], rules: list[dict[str, Any]]
  • AcknowledgmentAgent (line 212 of src/agents/acknowledgment_agent/agent.py): uses event_type: str, event_data: dict[str, Any], rules: list[dict[str, Any]]

These violate substitutability and will trigger MyPy "incompatible override" errors under strict mode. Update all subclass execute methods to use **kwargs: Any to match the BaseAgent signature.

🤖 Prompt for AI Agents
In `@src/agents/base.py` around lines 114 - 118, The subclass execute signatures
are incompatible with the BaseAgent.async def execute(self, **kwargs: Any) ->
AgentResult; update RuleFeasibilityAgent.execute, RuleEngineAgent.execute, and
AcknowledgmentAgent.execute to accept **kwargs: Any (matching BaseAgent) and
refactor their bodies to pull needed values from kwargs (e.g., rule_description
= kwargs.get("rule_description"), event_type = kwargs.get("event_type"),
event_data = kwargs.get("event_data"), rules = kwargs.get("rules")) before use
so MyPy substitutability is preserved while behavior remains the same.

Comment on lines +239 to 243
# Unused: min_approvals—left for future logic.
# min_approvals = parameters.get("min_approvals", 1)

# Placeholder logic - in production, this would check the actual PR reviews
# TODO: Check actual PR reviews. Always returns True—demo only.
return True
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

has_min_approvals always returns True—rule is ineffective.
If this validator is registered, it silently disables approval checks. Either implement it or remove/feature-flag it to avoid false “pass” results.

✅ Suggested implementation (aligns with MinApprovalsCondition)
-        # Unused: min_approvals—left for future logic.
-        # min_approvals = parameters.get("min_approvals", 1)
-
-        # TODO: Check actual PR reviews. Always returns True—demo only.
-        return True
+        min_approvals = parameters.get("min_approvals", 1)
+        reviews = event.get("reviews", [])
+        approved_count = sum(1 for review in reviews if review.get("state") == "APPROVED")
+        logger.debug(
+            f"ApprovalCountCondition: PR has {approved_count} approvals, requires {min_approvals}"
+        )
+        return approved_count >= min_approvals
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Unused: min_approvals—left for future logic.
# min_approvals = parameters.get("min_approvals", 1)
# Placeholder logic - in production, this would check the actual PR reviews
# TODO: Check actual PR reviews. Always returns True—demo only.
return True
min_approvals = parameters.get("min_approvals", 1)
reviews = event.get("reviews", [])
approved_count = sum(1 for review in reviews if review.get("state") == "APPROVED")
logger.debug(
f"ApprovalCountCondition: PR has {approved_count} approvals, requires {min_approvals}"
)
return approved_count >= min_approvals
🤖 Prompt for AI Agents
In `@src/rules/validators.py` around lines 239 - 243, The has_min_approvals
validator currently always returns True; replace the hardcoded return with real
approval logic: read min_approvals from parameters (e.g.,
parameters.get("min_approvals", 1)), obtain the PR review list from the
validator context or payload used elsewhere (similar to how
MinApprovalsCondition accesses PR reviews), count reviews with state=="APPROVED"
(and optionally dedupe by reviewer) and return True only if approvals_count >=
min_approvals; if you cannot access reviews in this validator, remove or
feature-flag has_min_approvals to avoid silently passing rules.

…stration in Acknowledgment and Engine Agents
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants