Conversation
Adds the foundation for a local PR-Agent daemon that polls: - GitHub notifications for @mentions on PRs - Railway API for deployment status changes New packages: - pr_agent/polling/ - Polling infrastructure - base.py: Abstract BasePoller with async generator pattern - events.py: Pydantic event models (GitHub/Railway) - github.py: GitHubPoller stub for notification polling - railway.py: RailwayPoller stub for deployment tracking - dispatcher.py: Event routing and handler registration - pr_agent/cli/ - CLI for daemon management - config.py: Config management (~/.pr-agent/config.json) - main.py: Typer CLI (init, start, stop, status, config) Dependencies added: typer, rich, httpx 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace Qodo marketing content with documentation for the local polling-based daemon: - Quick start guide - CLI command reference - Configuration file format - Architecture diagram - Usage examples for PR mentions and Railway integration - Development setup instructions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
pr_agent/polling/github.py
Outdated
| # STUB: Implementation goes in feature/github-poller branch | ||
| raise NotImplementedError( | ||
| "GitHubPoller.poll() not yet implemented. " | ||
| "See feature/github-poller branch." | ||
| ) |
There was a problem hiding this comment.
Avoid shipping daemon with unimplemented poller
The new daemon path crashes immediately because GitHubPoller.poll still raises NotImplementedError; pr-agent start creates this poller and the dispatcher invokes poll, so the process terminates before handling any notifications. The start command in the README/CLI is therefore unusable until this method is implemented or guarded.
Useful? React with 👍 / 👎.
pr_agent/cli/__init__.py
Outdated
| from pr_agent.cli.config import Config, load_config, save_config | ||
| from pr_agent.cli.main import main as run |
There was a problem hiding this comment.
Preserve existing CLI entrypoint behavior
Making pr_agent/cli a package and re-exporting run from the new Typer app means the existing console entry point pr_agent.cli:run now launches the daemon-focused CLI instead of the legacy PR review CLI in pr_agent/cli.py. Users invoking pr-agent --pr_url … review will now hit Typer’s command mismatch rather than the previous behavior. Consider renaming the package or adjusting entry points to avoid shadowing the existing module.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
This PR introduces a local polling-based daemon architecture for PR-Agent, transforming it from a webhook-dependent service to a background process that monitors GitHub notifications and Railway deployments. The implementation provides CLI tooling for daemon management, configuration handling with Pydantic models, and an extensible polling infrastructure with event dispatching.
Key Changes
- Complete README overhaul documenting the new local daemon workflow, CLI commands, and architecture
- New CLI with commands for init, start, stop, status, and config management using Typer and Rich
- Polling infrastructure including base classes, event models, and dispatcher with GitHub and Railway pollers (currently stubs)
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 26 comments.
Show a summary per file
| File | Description |
|---|---|
| requirements.txt | Adds CLI dependencies: typer, rich, and httpx for async HTTP |
| README.md | Complete rewrite documenting local daemon architecture, CLI usage, and configuration |
| pr_agent/cli/init.py | Exposes CLI entry points and configuration utilities |
| pr_agent/cli/config.py | Implements Pydantic-based configuration with credentials, settings, and repo tracking |
| pr_agent/cli/main.py | Typer-based CLI with daemon lifecycle, status reporting, and config management commands |
| pr_agent/polling/init.py | Defines polling subsystem exports and event types |
| pr_agent/polling/events.py | Pydantic models for GitHub and Railway events with type-safe structure |
| pr_agent/polling/base.py | Abstract BasePoller with error handling, backoff, and statistics |
| pr_agent/polling/github.py | GitHub notification poller stub with caching and mention parsing (TODO) |
| pr_agent/polling/railway.py | Railway deployment poller stub with GraphQL query preparation (TODO) |
| pr_agent/polling/dispatcher.py | Event routing and handler registration with concurrent execution |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def _build_deployments_query(self) -> str: | ||
| """ | ||
| Build GraphQL query for fetching deployments. | ||
|
|
||
| Returns: | ||
| GraphQL query string | ||
|
|
||
| Example query: | ||
| query GetDeployments($projectId: String!) { | ||
| deployments(input: { projectId: $projectId }, first: 20) { | ||
| edges { | ||
| node { | ||
| id | ||
| status | ||
| createdAt | ||
| service { id name } | ||
| environment { id name } | ||
| meta { commitHash commitMessage } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| """ | ||
| return """ | ||
| query GetDeployments($projectId: String!) { | ||
| deployments(input: { projectId: $projectId }, first: 20) { | ||
| edges { | ||
| node { | ||
| id | ||
| status | ||
| createdAt | ||
| service { id name } | ||
| environment { id name } | ||
| meta { commitHash commitMessage } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| """ |
There was a problem hiding this comment.
The _build_deployments_query method returns a hardcoded GraphQL query but doesn't actually use the class attributes like service_ids or environments to filter the query itself. This means the API will return all deployments, and filtering happens in post-processing. For efficiency, consider building the GraphQL query dynamically to include filters at the API level, reducing data transfer and processing.
| for repo in config.repos: | ||
| if repo.railway_project_id: | ||
| railway_poller = RailwayPoller( | ||
| token=railway_token, | ||
| project_id=repo.railway_project_id, | ||
| poll_interval=config.settings.poll_interval_railway, | ||
| service_ids=repo.railway_service_ids or None, | ||
| ) | ||
| dispatcher.add_poller(railway_poller) |
There was a problem hiding this comment.
The Railway poller setup doesn't handle potential errors when creating pollers. If RailwayPoller.__init__ raises an exception (e.g., invalid credentials), it will crash the daemon startup. Consider wrapping poller creation in try-except blocks with appropriate error logging and graceful degradation.
| result = subprocess.run( | ||
| ["git", "remote", "get-url", "origin"], | ||
| cwd=path, | ||
| capture_output=True, | ||
| text=True, | ||
| ) |
There was a problem hiding this comment.
The subprocess call in detect_github_repo doesn't specify a timeout, which means it could hang indefinitely if the git command hangs. Consider adding a timeout parameter to subprocess.run() to prevent the CLI from becoming unresponsive.
| while self._running: | ||
| try: | ||
| events = await self.poll() | ||
| self._last_poll = datetime.utcnow() | ||
| self._total_polls += 1 | ||
| self._error_count = 0 # Reset on success | ||
|
|
||
| for event in events: | ||
| self._total_events += 1 | ||
| logger.debug(f"[{self.name}] Yielding event: {event.type.value}") | ||
| yield event | ||
|
|
||
| except Exception as e: | ||
| self._error_count += 1 | ||
| logger.error( | ||
| f"[{self.name}] Poll error ({self._error_count}/{self.max_consecutive_errors}): {e}" | ||
| ) | ||
|
|
||
| if self._error_count >= self.max_consecutive_errors: | ||
| backoff = self.poll_interval * self.error_backoff_multiplier | ||
| logger.warning( | ||
| f"[{self.name}] Max errors reached, backing off for {backoff}s" | ||
| ) | ||
| await asyncio.sleep(backoff) | ||
| self._error_count = 0 | ||
|
|
||
| await asyncio.sleep(self.poll_interval) |
There was a problem hiding this comment.
The async iterator pattern in run() has a potential issue with exception handling. When an exception occurs in poll(), the error is logged but execution continues. However, if the exception happens during event yielding (line 130), it could leave the generator in an inconsistent state. Consider restructuring to ensure proper cleanup even if exceptions occur during yielding.
| if self._error_count >= self.max_consecutive_errors: | ||
| backoff = self.poll_interval * self.error_backoff_multiplier | ||
| logger.warning( | ||
| f"[{self.name}] Max errors reached, backing off for {backoff}s" | ||
| ) | ||
| await asyncio.sleep(backoff) | ||
| self._error_count = 0 |
There was a problem hiding this comment.
The backoff mechanism resets the error count to 0 after sleeping, which means consecutive errors that trigger backoff will cause the poller to immediately retry at normal intervals. This could lead to rapid retries if the underlying issue persists. Consider implementing exponential backoff that persists across multiple error cycles, or add a minimum recovery period before resetting the error count.
|
|
||
| async def setup(self) -> None: | ||
| """Initialize HTTP client.""" | ||
| self._client = httpx.AsyncClient(timeout=30.0) |
There was a problem hiding this comment.
The HTTP client initialization in setup() doesn't configure connection pooling or retry logic. For production use, consider adding connection pool limits, retry strategies, and proper timeout configurations. The default httpx.AsyncClient settings might not be optimal for long-running polling operations.
| self._client = httpx.AsyncClient(timeout=30.0) | |
| limits = httpx.Limits(max_keepalive_connections=10, max_connections=50) | |
| timeout = httpx.Timeout( | |
| connect=5.0, | |
| read=10.0, | |
| write=10.0, | |
| pool=None, | |
| ) | |
| retry = httpx.Retry( | |
| max_attempts=3, | |
| backoff_factor=0.5, | |
| statuses={502, 503, 504}, | |
| methods={"GET", "POST"}, | |
| ) | |
| transport = httpx.AsyncHTTPTransport(limits=limits, retries=retry) | |
| self._client = httpx.AsyncClient(timeout=timeout, transport=transport) |
|
|
||
| import asyncio | ||
| from collections import defaultdict | ||
| from typing import Awaitable, Callable, Optional |
There was a problem hiding this comment.
Import of 'Optional' is not used.
| from typing import Awaitable, Callable, Optional | |
| from typing import Awaitable, Callable |
| from pr_agent.polling.events import ( | ||
| Event, | ||
| EventType, | ||
| GitHubMentionEvent, | ||
| GitHubPROpenedEvent, | ||
| ) |
There was a problem hiding this comment.
Import of 'EventType' is not used.
Import of 'GitHubMentionEvent' is not used.
Import of 'GitHubPROpenedEvent' is not used.
| from pr_agent.polling.events import ( | |
| Event, | |
| EventType, | |
| GitHubMentionEvent, | |
| GitHubPROpenedEvent, | |
| ) | |
| from pr_agent.polling.events import Event |
…ction Implements a modular webhook infrastructure that can: - Detect webhook source from headers and payload structure - Verify signatures for each platform - Parse into typed Pydantic event models - Route to registered handlers Supported platforms: - GitHub (X-GitHub-Event, HMAC SHA256) - Vercel (x-vercel-signature, HMAC SHA1) - Linear (Linear-Signature, HMAC SHA256) - Slack (X-Slack-Signature, HMAC SHA256) - Discord (X-Signature-Ed25519, Ed25519) - Gmail (Cloud Pub/Sub message structure) - Railway (User-Agent, payload structure) New files: - pr_agent/webhooks/models.py - WebhookSource, WebhookPayload, *Event models - pr_agent/webhooks/router.py - Detection, verification, routing logic Closes #8 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Rename pr_agent/cli/ to pr_agent/daemon/ to avoid shadowing the existing pr_agent/cli.py module (P1 fix) - Add pr-agent-daemon entry point in pyproject.toml - Fix GitHubPoller.poll() to return empty list instead of raising NotImplementedError - allows daemon to start without crashing (P1 fix) - Fix RailwayPoller.poll() same as above - Update README with pr-agent-daemon command references 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Railway Poller: - Full implementation of poll() and _fetch_deployments() - GraphQL query for project deployments - Status change detection (SUCCESS, FAILED, CRASHED) - Service and environment filtering - Logs URL construction Cloudflare Poller: - New CloudflarePoller for Pages and Workers - REST API polling for deployment status - Pages: tracks deployment stages (queued → deploy) - Workers: tracks new deployments - Project and script name filtering Event Models: - CloudflarePagesEvent for Pages deployments - CloudflareWorkersEvent for Workers deployments - New EventTypes for Cloudflare states Config Updates: - cloudflare_token and cloudflare_account_id credentials - poll_interval_cloudflare setting - cloudflare_pages_project and cloudflare_workers in RepoConfig Closes #9 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This pull request introduces a local polling-based daemon architecture for PR-Agent, enabling it to run as a background service that monitors GitHub notifications and Railway deployments without relying on webhooks. The changes include a major rewrite of the
README.mdto document the new workflow, the addition of a CLI and configuration management system, and the foundation for a polling infrastructure.Major features and architectural changes:
Documentation and User Experience
README.mdto describe the new local daemon, CLI commands, configuration, architecture, and usage examples, replacing legacy Qodo-focused content.CLI and Configuration Management
pr_agent/cli/config.pyimplementing a robust configuration system using Pydantic models, supporting credentials, settings, and multi-repo tracking with environment variable overrides.pr_agent/cli/__init__.pyto expose CLI entry points and configuration utilities.Polling Infrastructure
pr_agent/polling/__init__.pyto define the polling subsystem and event types for GitHub and Railway sources.pr_agent/polling/base.pyproviding an extensibleBasePollerabstract class for implementing polling event sources with error handling and statistics.