6a6247bbf2
* Fix UTF-8 encoding for Priorities 1-2 (Core & Agents - 18 instances) Add encoding="utf-8" to file operations in: - Priority 1: Core Infrastructure (8 instances) - core/progress.py (6 read operations) - core/debug.py (1 append operation) - core/workspace/setup.py (1 read operation) - Priority 2: Agent System (10 instances) - agents/utils.py (1 read) - agents/tools_pkg/tools/subtask.py (1 read, 1 write) - agents/tools_pkg/tools/memory.py (2 read, 1 write, 1 append) - agents/tools_pkg/tools/qa.py (1 read, 1 write) - agents/tools_pkg/tools/progress.py (1 read) All changes use double quotes for ruff format compliance. * Fix UTF-8 encoding for Priorities 3-4 (Spec & Project - 26 instances) Add encoding="utf-8" to file operations in: - Priority 3: Spec Pipeline (21 instances) - spec/context.py (4: 2 read, 2 write) - spec/complexity.py (3: 2 read, 1 write) - spec/requirements.py (3: 2 read, 1 write) - spec/validator.py (3 write operations) - spec/writer.py (2: 1 read, 1 write) - spec/discovery.py (1 read) - spec/pipeline/orchestrator.py (2 read) - spec/phases/requirements_phases.py (1 write) - spec/validate_pkg/auto_fix.py (2: 1 read, 1 write) - Priority 4: Project Analyzer (5 instances) - project/analyzer.py (2: 1 read, 1 write) - project/config_parser.py (2 read operations) - project/stack_detector.py (1 read) All changes use double quotes for ruff format compliance. * Fix UTF-8 encoding for Priorities 5-7 (Services, Analysis, Ideation - 43 instances) Add encoding="utf-8" to file operations in: - Priority 5: Services (12 instances) - services/recovery.py (8: 4 read, 4 write) - services/context.py (4 read operations) - Priority 6: Analysis & QA (6 instances) - analysis/analyzers/__init__.py (2 write) - analysis/insight_extractor.py (1 read) - qa/criteria.py (2: 1 read, 1 write) - qa/report.py (1 read) - Priority 7: Ideation & Roadmap (25 instances) - ideation/analyzer.py (3 read) - ideation/formatter.py (4 read, 1 write) - ideation/phase_executor.py (5: 3 read, 2 write) - ideation/runner.py (1 read) - runners/roadmap/competitor_analyzer.py (3: 1 read, 2 write) - runners/roadmap/graph_integration.py (3 write) - runners/roadmap/orchestrator.py (1 read) - runners/roadmap/phases.py (2 read) - runners/insights_runner.py (3 read) All changes use double quotes for ruff format compliance. * Fix UTF-8 encoding for Priorities 8-14 (All remaining - 85+ instances) Add encoding="utf-8" to file operations across all remaining modules: Priorities 8-10 (Merge, Memory, Integrations - 26 instances): - merge/ (4 files) - memory/ (3 files) - context/ (3 files) - integrations/ (4 files) Priorities 11-14 (GitHub, GitLab, AI, Other - 59 instances): - runners/github/ (19 files) - runners/gitlab/ (3 files) - runners/ai_analyzer/ (1 file) All changes use double quotes for ruff format compliance. Applied using Python regex script for efficiency. * Fix UTF-8 encoding for missed instances (23 instances) Fix remaining instances missed by batch script: - cli/batch_commands.py (3 instances) - cli/followup_commands.py (1 instance) - core/client.py (1 instance) - phase_config.py (1 instance) - planner_lib/context.py (4 instances) - prediction/main.py (1 instance) - prediction/memory_loader.py (1 instance) - prompts_pkg/prompts.py (2 instances) - review/formatters.py (1 instance) - review/state.py (2 instances) - spec/phases/spec_phases.py (1 instance) - spec/pipeline/models.py (1 instance) - spec/validate_pkg/validators/context_validator.py (1 instance) - spec/validate_pkg/validators/implementation_plan_validator.py (1 instance) - ui/status.py (2 instances) All encoding parameters use double quotes for ruff format compliance. Verified: 0 instances without encoding remain in source code. * Fix missed os.fdopen() calls and duplicate encoding bug Thorough verification found 3 additional issues: - runners/github/file_lock.py:462 - os.fdopen missing encoding - runners/github/trust.py:442 - os.fdopen missing encoding - runners/insights_runner.py:372 - duplicate encoding parameter All fixed. Final count: 251 instances with encoding="utf-8" * Fix missed Path.read_text() and Path.write_text() encoding (99 instances) Gemini Code Assist review found instances we missed: - Path.read_text() without encoding: 77 instances → fixed - Path.write_text() without encoding: 22 instances → fixed Total UTF-8 encoding fixes: 350 instances across codebase - open() operations: 251 instances - Path.read_text(): 98 instances - Path.write_text(): 30 instances All text file operations now explicitly use encoding="utf-8". Addresses feedback from PR #782 review. * Fix critical syntax errors from CodeRabbit review - Fix os.getpid() syntax error in core/workspace/models.py (2 instances) Changed: os.getpid(, encoding="utf-8") -> str(os.getpid()) - Fix json.dumps invalid encoding parameter (3 instances) json.dumps() doesn't accept encoding parameter Changed: json.dumps(data, encoding="utf-8") -> json.dumps(data) Files: runners/ai_analyzer/cache_manager.py, runners/github/test_file_lock.py - Fix tempfile.NamedTemporaryFile missing encoding Added encoding="utf-8" to spec/requirements.py:22 - Fix subprocess.run text=True to encoding Changed: text=True -> encoding="utf-8" in core/workspace/setup.py:375 All critical syntax errors from CodeRabbit review resolved. * Fix critical syntax errors in test_context_gatherer.py - Line 78: Move encoding="utf-8" outside of JS string content Changed: write_text("...encoding="utf-8"...") To: write_text("...", encoding="utf-8") - Line 102: Move encoding="utf-8" outside of JS string content Changed: write_text("...encoding="utf-8"...") To: write_text("...", encoding="utf-8") Fixes syntax errors where encoding parameter was incorrectly placed inside the JavaScript code string instead of as write_text() parameter. * Fix CodeRabbit issues: UnicodeDecodeError handling and trailing newlines - Add UnicodeDecodeError to exception handling in agents/utils.py and spec/validate_pkg/auto_fix.py - Fix trailing newline preservation in merge/file_merger.py (2 locations) - Add encoding parameter to atomic_write() in runners/github/file_lock.py These fixes ensure robust error handling for malformed UTF-8 files and preserve file formatting during merge operations. * Fix test fixture to use UTF-8 encoding consistently Update spec_file fixture in tests/conftest.py to write spec file with encoding="utf-8" to match how it's read in validators. This ensures consistency between test fixtures and production code. * Fix linting errors and security vulnerabilities from merge - Remove unused tree-sitter methods in semantic_analyzer.py that caused F821 undefined name errors - Fix regex injection vulnerability in bump-version.js by properly escaping all regex special characters - Add escapeRegex() function to prevent security issues when version string is used in RegExp constructor Resolves ruff linting failures and CodeQL security alerts. * Fix code formatting for ruff compliance Apply formatting fixes to meet line length requirements: - context/builder.py: Split long line with array slicing - planner_lib/context.py: Split long ternary expression - spec/requirements.py: Split long tempfile.NamedTemporaryFile call Resolves ruff format check failures. * Fix missing UTF-8 encoding in init.py gitignore operations Found by pre-commit hook testing in PR #795: - Line 96: Path.read_text() without encoding - Line 122: Path.write_text() without encoding These handle .gitignore file operations and could fail on Windows with special characters in gitignore comments or entries. Total fixes in PR #782: 253 instances (was 251, +2 from init.py) * Add pre-commit hook for UTF-8 encoding enforcement 1. Encoding Check Script (scripts/check_encoding.py): - Validates all file operations have encoding="utf-8" - Checks open(), Path.read_text(), Path.write_text() - Checks json.load/dump with open() - Allows binary mode without encoding - Windows-compatible emoji output with UTF-8 reconfiguration 2. Pre-commit Config (.pre-commit-config.yaml): - Added check-file-encoding hook for apps/backend/ - Runs automatically before commits - Scoped to backend Python files only 3. Tests (tests/test_check_encoding.py): - Comprehensive test coverage (10 tests, all passing) - Tests detection of missing encoding - Tests allowlist for binary files - Tests multiple issues in single file - Tests file type filtering Purpose: - Prevent regression of 251 UTF-8 encoding fixes from PR #782 - Catch missing encoding in new code during development - Fast feedback loop for developers Implementation Notes: - Hook scoped to apps/backend/ to avoid false positives in test code - Uses simple regex matching for speed - Compatible with existing pre-commit infrastructure - Already caught 6 real issues in apps/backend/core/progress.py Related: PR #782 - Fix Windows UTF-8 encoding errors * Address CodeRabbit and Gemini review feedback Fixes based on automated review comments: 1. Binary Mode Detection (Critical Fix): - Replaced brittle regex with robust pattern: r'["'][rwax+]*b[rwax+]*["']' - Now correctly detects all binary modes: rb, wb, ab, r+b, w+b, etc. - Prevents false positives on text mode 'w' without 'b' - Added comprehensive tests for wb, ab, and text w modes 2. Encoding Detection Robustness (Critical Fix): - Changed from 'encoding=' string match to word boundary regex: r'\bencoding\s*=' - Now handles encoding with spaces: encoding = "utf-8" - Prevents false matches of substrings containing 'encoding=' - Applied across all checks (open, read_text, write_text, json.load, json.dump) - Added test for spaces around equals sign 3. Test Coverage Improvements: - Added json.dump() with encoding test (passing case) - Added json.dump() without encoding test (failing case) - Fixed test assertions to match actual behavior (== 1 not == 2) - Added 6 new tests for improved binary/text mode coverage - Total tests increased from 10 to 16, all passing ✅ 4. Code Cleanup: - Removed unused pytest import (CodeQL warning) - Simplified check_files() to remove unused variable tracking All changes validated with comprehensive test suite (16/16 passing). Related: PR #795 review feedback from CodeRabbit and Gemini Code Assist * docs: Add UTF-8 encoding guidelines and Windows development guide 1. CONTRIBUTING.md: - Added concise file encoding section after Code Style - DO/DON'T examples for common file operations - Covers open(), Path methods, json operations - References PR #782 and windows-development.md 2. guides/windows-development.md (NEW): - Comprehensive Windows development guide - File encoding (cp1252 vs UTF-8 issue) - Line endings, path separators, shell commands - Development environment recommendations - Common pitfalls and solutions - Testing guidelines 3. .github/PULL_REQUEST_TEMPLATE.md: - Added encoding checklist item for Python PRs - Helps catch missing encoding during review 4. guides/README.md: - Added windows-development.md to guide index - Organized with CLI-USAGE and linux guides Purpose: Educate developers about UTF-8 encoding requirements to prevent regressions of the 251 encoding issues fixed in PR #782. Automated checking via pre-commit hooks (PR #795) + developer education ensures long-term Windows compatibility. Related: - PR #782: Fix Windows UTF-8 encoding errors (251 instances) - PR #795: Add pre-commit hooks for encoding enforcement * Address review comments from CodeRabbit and Gemini 1. Fix CONTRIBUTING.md markdown linting issues - Add blank lines around code blocks (MD031) - Add JSON write example with ensure_ascii=False (Gemini suggestion) 2. Fix guides/windows-development.md markdown linting (39 violations) - Rename duplicate headings: "The Problem"/"The Solution" → "Problem"/"Solution" (MD024) - Add blank lines around all code blocks (MD031) - Add language specifiers to code blocks (MD040) - Add blank lines before/after headings (MD022) - Wrap long lines to <=80 characters (MD013) - Add blank line before list (MD032) - Use Gemini's idiomatic line ending normalization pattern 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix additional UTF-8 encoding issues and improve encoding check script - Add encoding="utf-8" to 5 files that were missing it: - cli/workspace_commands.py: read_text for worktree config - context/pattern_discovery.py: read_text with errors param - context/search.py: read_text with errors param - core/sentry.py: open for package.json version detection - core/workspace/setup.py: open for security profile JSON - Improve check_encoding.py script to reduce false positives: - Use negative lookbehind to exclude os.open(), urlopen(), etc. - Handle nested parentheses correctly when checking args - Skip self.method.read_text() calls (custom methods, not Path) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix missing UTF-8 encoding in locked_write() function Add encoding parameter to locked_write() async context manager and use it in os.fdopen() call. This fixes HIGH priority issue from PR review where locked_write() was missing UTF-8 encoding support, which could cause encoding errors on Windows when writing files with non-ASCII content. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add UnicodeDecodeError handling for file loading resilience Address CodeRabbit review feedback: - runner.py: Add UnicodeDecodeError to exception handling when loading batch files - trust.py: Add exception handling in get_state() and get_all_states() to gracefully handle corrupted state files instead of failing completely Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix atomic_write to handle binary mode correctly The atomic_write function was unconditionally passing encoding to os.fdopen, which would crash with ValueError if called with binary mode (e.g., 'wb'). Apply the same fix used in locked_write: only pass encoding for text modes. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix run_git() call with invalid parameters in setup.py Remove capture_output and encoding kwargs from run_git() call - these parameters are already handled internally by run_git() and passing them causes TypeError since the function doesn't accept them. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix CodeQL warnings and potential double-newline bug - Remove unused is_path_call variables in check_encoding.py - Remove unused failed_count variable in check_encoding.py - Remove unused escapeRegex function in bump-version.js - Fix potential double-newline when adding imports in file_merger.py (strip trailing newlines from content_after before inserting) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix Ruff formatting: wrap long line in file_merger.py Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add UnicodeDecodeError handling to all JSON file loading Comprehensively add UnicodeDecodeError to exception handlers across the codebase to handle legacy-encoded or corrupted files gracefully: - 32+ locations now catch UnicodeDecodeError alongside OSError and json.JSONDecodeError - context/builder.py: Regenerate index on decode failure - planner_lib/context.py: Use empty dicts on decode failure - check_encoding.py: Handle OSError for unreadable files - cleanup.py: Handle decode errors in index pruning This ensures the codebase is robust against non-UTF-8 files that may exist from previous Windows runs with cp1252 encoding. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add explanatory comments to empty except clauses Address CodeQL notices about empty except clauses with just 'pass' by adding explanatory comments describing the intent. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix review issues from Andy's Auto Claude PR Review 1. [HIGH] Fix double-close bug in trust.py:449 - Remove try/except around os.fdopen since it takes ownership of fd - The with statement handles closing, no need for explicit os.close() 2. [LOW] Fix dead code in file_merger.py:87,159 - Simplify endswith check to just '\n' since content is already normalized to LF at that point 3. [LOW] Fix escaped backslash-n in test_context_gatherer.py:150 - Change "\n" (literal backslash-n) to "\n" (actual newline) 4. [LOW] Fix coder.md examples missing encoding parameter - Add encoding="utf-8" to read_text() and open() calls in examples Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: TamerineSky <TamerineSky@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
924 lines
33 KiB
Python
924 lines
33 KiB
Python
"""
|
|
GitHub Automation Data Models
|
|
=============================
|
|
|
|
Data structures for GitHub automation features.
|
|
Stored in .auto-claude/github/pr/ and .auto-claude/github/issues/
|
|
|
|
All save() operations use file locking to prevent corruption in concurrent scenarios.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from pathlib import Path
|
|
|
|
try:
|
|
from .file_lock import locked_json_update, locked_json_write
|
|
except (ImportError, ValueError, SystemError):
|
|
from file_lock import locked_json_update, locked_json_write
|
|
|
|
|
|
class ReviewSeverity(str, Enum):
|
|
"""Severity levels for PR review findings."""
|
|
|
|
CRITICAL = "critical"
|
|
HIGH = "high"
|
|
MEDIUM = "medium"
|
|
LOW = "low"
|
|
|
|
|
|
class ReviewCategory(str, Enum):
|
|
"""Categories for PR review findings."""
|
|
|
|
SECURITY = "security"
|
|
QUALITY = "quality"
|
|
STYLE = "style"
|
|
TEST = "test"
|
|
DOCS = "docs"
|
|
PATTERN = "pattern"
|
|
PERFORMANCE = "performance"
|
|
VERIFICATION_FAILED = "verification_failed" # NEW: Cannot verify requirements/paths
|
|
REDUNDANCY = "redundancy" # NEW: Duplicate code/logic detected
|
|
|
|
|
|
class ReviewPass(str, Enum):
|
|
"""Multi-pass review stages."""
|
|
|
|
QUICK_SCAN = "quick_scan"
|
|
SECURITY = "security"
|
|
QUALITY = "quality"
|
|
DEEP_ANALYSIS = "deep_analysis"
|
|
STRUCTURAL = "structural" # Feature creep, architecture, PR structure
|
|
AI_COMMENT_TRIAGE = "ai_comment_triage" # Verify other AI tool comments
|
|
|
|
|
|
class MergeVerdict(str, Enum):
|
|
"""Clear verdict for whether PR can be merged."""
|
|
|
|
READY_TO_MERGE = "ready_to_merge" # No blockers, good to go
|
|
MERGE_WITH_CHANGES = "merge_with_changes" # Minor issues, fix before merge
|
|
NEEDS_REVISION = "needs_revision" # Significant issues, needs rework
|
|
BLOCKED = "blocked" # Critical issues, cannot merge
|
|
|
|
|
|
# Constants for branch-behind messaging (DRY - used across multiple reviewers)
|
|
BRANCH_BEHIND_BLOCKER_MSG = (
|
|
"Branch Out of Date: PR branch is behind the base branch and needs to be updated"
|
|
)
|
|
BRANCH_BEHIND_REASONING = (
|
|
"Branch is out of date with base branch. Update branch first - "
|
|
"if no conflicts arise, you can merge. If merge conflicts arise, "
|
|
"resolve them and run follow-up review again."
|
|
)
|
|
|
|
|
|
class AICommentVerdict(str, Enum):
|
|
"""Verdict on AI tool comments (CodeRabbit, Cursor, Greptile, etc.)."""
|
|
|
|
CRITICAL = "critical" # Must be addressed before merge
|
|
IMPORTANT = "important" # Should be addressed
|
|
NICE_TO_HAVE = "nice_to_have" # Optional improvement
|
|
TRIVIAL = "trivial" # Can be ignored
|
|
FALSE_POSITIVE = "false_positive" # AI was wrong
|
|
ADDRESSED = "addressed" # Valid issue that was fixed in a subsequent commit
|
|
|
|
|
|
class TriageCategory(str, Enum):
|
|
"""Issue triage categories."""
|
|
|
|
BUG = "bug"
|
|
FEATURE = "feature"
|
|
DOCUMENTATION = "documentation"
|
|
QUESTION = "question"
|
|
DUPLICATE = "duplicate"
|
|
SPAM = "spam"
|
|
FEATURE_CREEP = "feature_creep"
|
|
|
|
|
|
class AutoFixStatus(str, Enum):
|
|
"""Status for auto-fix operations."""
|
|
|
|
# Initial states
|
|
PENDING = "pending"
|
|
ANALYZING = "analyzing"
|
|
|
|
# Spec creation states
|
|
CREATING_SPEC = "creating_spec"
|
|
WAITING_APPROVAL = "waiting_approval" # P1-3: Human review gate
|
|
|
|
# Build states
|
|
BUILDING = "building"
|
|
QA_REVIEW = "qa_review"
|
|
|
|
# PR states
|
|
PR_CREATED = "pr_created"
|
|
MERGE_CONFLICT = "merge_conflict" # P1-3: Conflict resolution needed
|
|
|
|
# Terminal states
|
|
COMPLETED = "completed"
|
|
FAILED = "failed"
|
|
CANCELLED = "cancelled" # P1-3: User cancelled
|
|
|
|
# Special states
|
|
STALE = "stale" # P1-3: Issue updated after spec creation
|
|
RATE_LIMITED = "rate_limited" # P1-3: Waiting for rate limit reset
|
|
|
|
@classmethod
|
|
def terminal_states(cls) -> set[AutoFixStatus]:
|
|
"""States that represent end of workflow."""
|
|
return {cls.COMPLETED, cls.FAILED, cls.CANCELLED}
|
|
|
|
@classmethod
|
|
def recoverable_states(cls) -> set[AutoFixStatus]:
|
|
"""States that can be recovered from."""
|
|
return {cls.FAILED, cls.STALE, cls.RATE_LIMITED, cls.MERGE_CONFLICT}
|
|
|
|
@classmethod
|
|
def active_states(cls) -> set[AutoFixStatus]:
|
|
"""States that indicate work in progress."""
|
|
return {
|
|
cls.PENDING,
|
|
cls.ANALYZING,
|
|
cls.CREATING_SPEC,
|
|
cls.BUILDING,
|
|
cls.QA_REVIEW,
|
|
cls.PR_CREATED,
|
|
}
|
|
|
|
def can_transition_to(self, new_state: AutoFixStatus) -> bool:
|
|
"""Check if transition to new_state is valid."""
|
|
valid_transitions = {
|
|
AutoFixStatus.PENDING: {
|
|
AutoFixStatus.ANALYZING,
|
|
AutoFixStatus.CANCELLED,
|
|
},
|
|
AutoFixStatus.ANALYZING: {
|
|
AutoFixStatus.CREATING_SPEC,
|
|
AutoFixStatus.FAILED,
|
|
AutoFixStatus.CANCELLED,
|
|
AutoFixStatus.RATE_LIMITED,
|
|
},
|
|
AutoFixStatus.CREATING_SPEC: {
|
|
AutoFixStatus.WAITING_APPROVAL,
|
|
AutoFixStatus.BUILDING,
|
|
AutoFixStatus.FAILED,
|
|
AutoFixStatus.CANCELLED,
|
|
AutoFixStatus.STALE,
|
|
},
|
|
AutoFixStatus.WAITING_APPROVAL: {
|
|
AutoFixStatus.BUILDING,
|
|
AutoFixStatus.CANCELLED,
|
|
AutoFixStatus.STALE,
|
|
},
|
|
AutoFixStatus.BUILDING: {
|
|
AutoFixStatus.QA_REVIEW,
|
|
AutoFixStatus.FAILED,
|
|
AutoFixStatus.CANCELLED,
|
|
AutoFixStatus.RATE_LIMITED,
|
|
},
|
|
AutoFixStatus.QA_REVIEW: {
|
|
AutoFixStatus.PR_CREATED,
|
|
AutoFixStatus.BUILDING, # Fix loop
|
|
AutoFixStatus.FAILED,
|
|
AutoFixStatus.CANCELLED,
|
|
},
|
|
AutoFixStatus.PR_CREATED: {
|
|
AutoFixStatus.COMPLETED,
|
|
AutoFixStatus.MERGE_CONFLICT,
|
|
AutoFixStatus.FAILED,
|
|
},
|
|
AutoFixStatus.MERGE_CONFLICT: {
|
|
AutoFixStatus.BUILDING, # Retry after conflict resolution
|
|
AutoFixStatus.FAILED,
|
|
AutoFixStatus.CANCELLED,
|
|
},
|
|
AutoFixStatus.STALE: {
|
|
AutoFixStatus.ANALYZING, # Re-analyze with new issue content
|
|
AutoFixStatus.CANCELLED,
|
|
},
|
|
AutoFixStatus.RATE_LIMITED: {
|
|
AutoFixStatus.PENDING, # Resume after rate limit
|
|
AutoFixStatus.CANCELLED,
|
|
},
|
|
# Terminal states - no transitions
|
|
AutoFixStatus.COMPLETED: set(),
|
|
AutoFixStatus.FAILED: {AutoFixStatus.PENDING}, # Allow retry
|
|
AutoFixStatus.CANCELLED: set(),
|
|
}
|
|
return new_state in valid_transitions.get(self, set())
|
|
|
|
|
|
@dataclass
|
|
class PRReviewFinding:
|
|
"""A single finding from a PR review."""
|
|
|
|
id: str
|
|
severity: ReviewSeverity
|
|
category: ReviewCategory
|
|
title: str
|
|
description: str
|
|
file: str
|
|
line: int
|
|
end_line: int | None = None
|
|
suggested_fix: str | None = None
|
|
fixable: bool = False
|
|
# Evidence-based validation: actual code proving the issue exists
|
|
evidence: str | None = None # Actual code snippet showing the issue
|
|
verification_note: str | None = (
|
|
None # What evidence is missing or couldn't be verified
|
|
)
|
|
redundant_with: str | None = None # Reference to duplicate code (file:line)
|
|
|
|
# Finding validation fields (from finding-validator re-investigation)
|
|
validation_status: str | None = (
|
|
None # confirmed_valid, dismissed_false_positive, needs_human_review
|
|
)
|
|
validation_evidence: str | None = None # Code snippet examined during validation
|
|
validation_explanation: str | None = None # Why finding was validated/dismissed
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"id": self.id,
|
|
"severity": self.severity.value,
|
|
"category": self.category.value,
|
|
"title": self.title,
|
|
"description": self.description,
|
|
"file": self.file,
|
|
"line": self.line,
|
|
"end_line": self.end_line,
|
|
"suggested_fix": self.suggested_fix,
|
|
"fixable": self.fixable,
|
|
# Evidence-based validation fields
|
|
"evidence": self.evidence,
|
|
"verification_note": self.verification_note,
|
|
"redundant_with": self.redundant_with,
|
|
# Validation fields
|
|
"validation_status": self.validation_status,
|
|
"validation_evidence": self.validation_evidence,
|
|
"validation_explanation": self.validation_explanation,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> PRReviewFinding:
|
|
return cls(
|
|
id=data["id"],
|
|
severity=ReviewSeverity(data["severity"]),
|
|
category=ReviewCategory(data["category"]),
|
|
title=data["title"],
|
|
description=data["description"],
|
|
file=data["file"],
|
|
line=data["line"],
|
|
end_line=data.get("end_line"),
|
|
suggested_fix=data.get("suggested_fix"),
|
|
fixable=data.get("fixable", False),
|
|
# Evidence-based validation fields
|
|
evidence=data.get("evidence"),
|
|
verification_note=data.get("verification_note"),
|
|
redundant_with=data.get("redundant_with"),
|
|
# Validation fields
|
|
validation_status=data.get("validation_status"),
|
|
validation_evidence=data.get("validation_evidence"),
|
|
validation_explanation=data.get("validation_explanation"),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class AICommentTriage:
|
|
"""Triage result for an AI tool comment (CodeRabbit, Cursor, Greptile, etc.)."""
|
|
|
|
comment_id: int
|
|
tool_name: str # "CodeRabbit", "Cursor", "Greptile", etc.
|
|
original_comment: str
|
|
verdict: AICommentVerdict
|
|
reasoning: str
|
|
response_comment: str | None = None # Comment to post in reply
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"comment_id": self.comment_id,
|
|
"tool_name": self.tool_name,
|
|
"original_comment": self.original_comment,
|
|
"verdict": self.verdict.value,
|
|
"reasoning": self.reasoning,
|
|
"response_comment": self.response_comment,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> AICommentTriage:
|
|
return cls(
|
|
comment_id=data["comment_id"],
|
|
tool_name=data["tool_name"],
|
|
original_comment=data["original_comment"],
|
|
verdict=AICommentVerdict(data["verdict"]),
|
|
reasoning=data["reasoning"],
|
|
response_comment=data.get("response_comment"),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class StructuralIssue:
|
|
"""Structural issue with the PR (feature creep, architecture, etc.)."""
|
|
|
|
id: str
|
|
issue_type: str # "feature_creep", "scope_creep", "architecture_violation", "poor_structure"
|
|
severity: ReviewSeverity
|
|
title: str
|
|
description: str
|
|
impact: str # Why this matters
|
|
suggestion: str # How to fix
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"id": self.id,
|
|
"issue_type": self.issue_type,
|
|
"severity": self.severity.value,
|
|
"title": self.title,
|
|
"description": self.description,
|
|
"impact": self.impact,
|
|
"suggestion": self.suggestion,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> StructuralIssue:
|
|
return cls(
|
|
id=data["id"],
|
|
issue_type=data["issue_type"],
|
|
severity=ReviewSeverity(data["severity"]),
|
|
title=data["title"],
|
|
description=data["description"],
|
|
impact=data["impact"],
|
|
suggestion=data["suggestion"],
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class PRReviewResult:
|
|
"""Complete result of a PR review."""
|
|
|
|
pr_number: int
|
|
repo: str
|
|
success: bool
|
|
findings: list[PRReviewFinding] = field(default_factory=list)
|
|
summary: str = ""
|
|
overall_status: str = "comment" # approve, request_changes, comment
|
|
review_id: int | None = None
|
|
reviewed_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
error: str | None = None
|
|
|
|
# NEW: Enhanced verdict system
|
|
verdict: MergeVerdict = MergeVerdict.READY_TO_MERGE
|
|
verdict_reasoning: str = ""
|
|
blockers: list[str] = field(default_factory=list) # Issues that MUST be fixed
|
|
|
|
# NEW: Risk assessment
|
|
risk_assessment: dict = field(
|
|
default_factory=lambda: {
|
|
"complexity": "low", # low, medium, high
|
|
"security_impact": "none", # none, low, medium, critical
|
|
"scope_coherence": "good", # good, mixed, poor
|
|
}
|
|
)
|
|
|
|
# NEW: Structural issues and AI comment triages
|
|
structural_issues: list[StructuralIssue] = field(default_factory=list)
|
|
ai_comment_triages: list[AICommentTriage] = field(default_factory=list)
|
|
|
|
# NEW: Quick scan summary preserved
|
|
quick_scan_summary: dict = field(default_factory=dict)
|
|
|
|
# Follow-up review tracking
|
|
reviewed_commit_sha: str | None = None # HEAD SHA at time of review
|
|
reviewed_file_blobs: dict[str, str] = field(
|
|
default_factory=dict
|
|
) # filename → blob SHA at time of review (survives rebases)
|
|
is_followup_review: bool = False # True if this is a follow-up review
|
|
previous_review_id: int | None = None # Reference to the review this follows up on
|
|
resolved_findings: list[str] = field(default_factory=list) # Finding IDs now fixed
|
|
unresolved_findings: list[str] = field(
|
|
default_factory=list
|
|
) # Finding IDs still open
|
|
new_findings_since_last_review: list[str] = field(
|
|
default_factory=list
|
|
) # New issues in recent commits
|
|
|
|
# Posted findings tracking (for frontend state sync)
|
|
has_posted_findings: bool = False # True if any findings have been posted to GitHub
|
|
posted_finding_ids: list[str] = field(
|
|
default_factory=list
|
|
) # IDs of posted findings
|
|
posted_at: str | None = None # Timestamp when findings were posted
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"pr_number": self.pr_number,
|
|
"repo": self.repo,
|
|
"success": self.success,
|
|
"findings": [f.to_dict() for f in self.findings],
|
|
"summary": self.summary,
|
|
"overall_status": self.overall_status,
|
|
"review_id": self.review_id,
|
|
"reviewed_at": self.reviewed_at,
|
|
"error": self.error,
|
|
# NEW fields
|
|
"verdict": self.verdict.value,
|
|
"verdict_reasoning": self.verdict_reasoning,
|
|
"blockers": self.blockers,
|
|
"risk_assessment": self.risk_assessment,
|
|
"structural_issues": [s.to_dict() for s in self.structural_issues],
|
|
"ai_comment_triages": [t.to_dict() for t in self.ai_comment_triages],
|
|
"quick_scan_summary": self.quick_scan_summary,
|
|
# Follow-up review fields
|
|
"reviewed_commit_sha": self.reviewed_commit_sha,
|
|
"reviewed_file_blobs": self.reviewed_file_blobs,
|
|
"is_followup_review": self.is_followup_review,
|
|
"previous_review_id": self.previous_review_id,
|
|
"resolved_findings": self.resolved_findings,
|
|
"unresolved_findings": self.unresolved_findings,
|
|
"new_findings_since_last_review": self.new_findings_since_last_review,
|
|
# Posted findings tracking
|
|
"has_posted_findings": self.has_posted_findings,
|
|
"posted_finding_ids": self.posted_finding_ids,
|
|
"posted_at": self.posted_at,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> PRReviewResult:
|
|
return cls(
|
|
pr_number=data["pr_number"],
|
|
repo=data["repo"],
|
|
success=data["success"],
|
|
findings=[PRReviewFinding.from_dict(f) for f in data.get("findings", [])],
|
|
summary=data.get("summary", ""),
|
|
overall_status=data.get("overall_status", "comment"),
|
|
review_id=data.get("review_id"),
|
|
reviewed_at=data.get("reviewed_at", datetime.now().isoformat()),
|
|
error=data.get("error"),
|
|
# NEW fields
|
|
verdict=MergeVerdict(data.get("verdict", "ready_to_merge")),
|
|
verdict_reasoning=data.get("verdict_reasoning", ""),
|
|
blockers=data.get("blockers", []),
|
|
risk_assessment=data.get(
|
|
"risk_assessment",
|
|
{
|
|
"complexity": "low",
|
|
"security_impact": "none",
|
|
"scope_coherence": "good",
|
|
},
|
|
),
|
|
structural_issues=[
|
|
StructuralIssue.from_dict(s) for s in data.get("structural_issues", [])
|
|
],
|
|
ai_comment_triages=[
|
|
AICommentTriage.from_dict(t) for t in data.get("ai_comment_triages", [])
|
|
],
|
|
quick_scan_summary=data.get("quick_scan_summary", {}),
|
|
# Follow-up review fields
|
|
reviewed_commit_sha=data.get("reviewed_commit_sha"),
|
|
reviewed_file_blobs=data.get("reviewed_file_blobs", {}),
|
|
is_followup_review=data.get("is_followup_review", False),
|
|
previous_review_id=data.get("previous_review_id"),
|
|
resolved_findings=data.get("resolved_findings", []),
|
|
unresolved_findings=data.get("unresolved_findings", []),
|
|
new_findings_since_last_review=data.get(
|
|
"new_findings_since_last_review", []
|
|
),
|
|
# Posted findings tracking
|
|
has_posted_findings=data.get("has_posted_findings", False),
|
|
posted_finding_ids=data.get("posted_finding_ids", []),
|
|
posted_at=data.get("posted_at"),
|
|
)
|
|
|
|
async def save(self, github_dir: Path) -> None:
|
|
"""Save review result to .auto-claude/github/pr/ with file locking."""
|
|
pr_dir = github_dir / "pr"
|
|
pr_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
review_file = pr_dir / f"review_{self.pr_number}.json"
|
|
|
|
# Atomic locked write
|
|
await locked_json_write(review_file, self.to_dict(), timeout=5.0)
|
|
|
|
# Update index with locking
|
|
await self._update_index(pr_dir)
|
|
|
|
async def _update_index(self, pr_dir: Path) -> None:
|
|
"""Update the PR review index with file locking."""
|
|
index_file = pr_dir / "index.json"
|
|
|
|
def update_index(current_data):
|
|
"""Update function for atomic index update."""
|
|
if current_data is None:
|
|
current_data = {"reviews": [], "last_updated": None}
|
|
|
|
# Update or add entry
|
|
reviews = current_data.get("reviews", [])
|
|
existing = next(
|
|
(r for r in reviews if r["pr_number"] == self.pr_number), None
|
|
)
|
|
|
|
entry = {
|
|
"pr_number": self.pr_number,
|
|
"repo": self.repo,
|
|
"overall_status": self.overall_status,
|
|
"findings_count": len(self.findings),
|
|
"reviewed_at": self.reviewed_at,
|
|
}
|
|
|
|
if existing:
|
|
reviews = [
|
|
entry if r["pr_number"] == self.pr_number else r for r in reviews
|
|
]
|
|
else:
|
|
reviews.append(entry)
|
|
|
|
current_data["reviews"] = reviews
|
|
current_data["last_updated"] = datetime.now().isoformat()
|
|
|
|
return current_data
|
|
|
|
# Atomic locked update
|
|
await locked_json_update(index_file, update_index, timeout=5.0)
|
|
|
|
@classmethod
|
|
def load(cls, github_dir: Path, pr_number: int) -> PRReviewResult | None:
|
|
"""Load a review result from disk."""
|
|
review_file = github_dir / "pr" / f"review_{pr_number}.json"
|
|
if not review_file.exists():
|
|
return None
|
|
|
|
with open(review_file, encoding="utf-8") as f:
|
|
return cls.from_dict(json.load(f))
|
|
|
|
|
|
@dataclass
|
|
class FollowupReviewContext:
|
|
"""Context for a follow-up review."""
|
|
|
|
pr_number: int
|
|
previous_review: PRReviewResult
|
|
previous_commit_sha: str
|
|
current_commit_sha: str
|
|
|
|
# Changes since last review
|
|
commits_since_review: list[dict] = field(default_factory=list)
|
|
files_changed_since_review: list[str] = field(default_factory=list)
|
|
diff_since_review: str = ""
|
|
|
|
# Comments since last review
|
|
contributor_comments_since_review: list[dict] = field(default_factory=list)
|
|
ai_bot_comments_since_review: list[dict] = field(default_factory=list)
|
|
|
|
# PR reviews since last review (formal review submissions from Cursor, CodeRabbit, etc.)
|
|
# These are different from comments - they're full review submissions with body text
|
|
pr_reviews_since_review: list[dict] = field(default_factory=list)
|
|
|
|
# Merge conflict status
|
|
has_merge_conflicts: bool = False # True if PR has conflicts with base branch
|
|
merge_state_status: str = (
|
|
"" # BEHIND, BLOCKED, CLEAN, DIRTY, HAS_HOOKS, UNKNOWN, UNSTABLE
|
|
)
|
|
|
|
# CI status - passed to AI orchestrator so it can factor into verdict
|
|
# Dict with: passing, failing, pending, failed_checks, awaiting_approval
|
|
ci_status: dict = field(default_factory=dict)
|
|
|
|
# Error flag - if set, context gathering failed and data may be incomplete
|
|
error: str | None = None
|
|
|
|
|
|
@dataclass
|
|
class TriageResult:
|
|
"""Result of triaging a single issue."""
|
|
|
|
issue_number: int
|
|
repo: str
|
|
category: TriageCategory
|
|
confidence: float # 0.0 to 1.0
|
|
labels_to_add: list[str] = field(default_factory=list)
|
|
labels_to_remove: list[str] = field(default_factory=list)
|
|
is_duplicate: bool = False
|
|
duplicate_of: int | None = None
|
|
is_spam: bool = False
|
|
is_feature_creep: bool = False
|
|
suggested_breakdown: list[str] = field(default_factory=list)
|
|
priority: str = "medium" # high, medium, low
|
|
comment: str | None = None
|
|
triaged_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"issue_number": self.issue_number,
|
|
"repo": self.repo,
|
|
"category": self.category.value,
|
|
"confidence": self.confidence,
|
|
"labels_to_add": self.labels_to_add,
|
|
"labels_to_remove": self.labels_to_remove,
|
|
"is_duplicate": self.is_duplicate,
|
|
"duplicate_of": self.duplicate_of,
|
|
"is_spam": self.is_spam,
|
|
"is_feature_creep": self.is_feature_creep,
|
|
"suggested_breakdown": self.suggested_breakdown,
|
|
"priority": self.priority,
|
|
"comment": self.comment,
|
|
"triaged_at": self.triaged_at,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> TriageResult:
|
|
return cls(
|
|
issue_number=data["issue_number"],
|
|
repo=data["repo"],
|
|
category=TriageCategory(data["category"]),
|
|
confidence=data["confidence"],
|
|
labels_to_add=data.get("labels_to_add", []),
|
|
labels_to_remove=data.get("labels_to_remove", []),
|
|
is_duplicate=data.get("is_duplicate", False),
|
|
duplicate_of=data.get("duplicate_of"),
|
|
is_spam=data.get("is_spam", False),
|
|
is_feature_creep=data.get("is_feature_creep", False),
|
|
suggested_breakdown=data.get("suggested_breakdown", []),
|
|
priority=data.get("priority", "medium"),
|
|
comment=data.get("comment"),
|
|
triaged_at=data.get("triaged_at", datetime.now().isoformat()),
|
|
)
|
|
|
|
async def save(self, github_dir: Path) -> None:
|
|
"""Save triage result to .auto-claude/github/issues/ with file locking."""
|
|
issues_dir = github_dir / "issues"
|
|
issues_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
triage_file = issues_dir / f"triage_{self.issue_number}.json"
|
|
|
|
# Atomic locked write
|
|
await locked_json_write(triage_file, self.to_dict(), timeout=5.0)
|
|
|
|
@classmethod
|
|
def load(cls, github_dir: Path, issue_number: int) -> TriageResult | None:
|
|
"""Load a triage result from disk."""
|
|
triage_file = github_dir / "issues" / f"triage_{issue_number}.json"
|
|
if not triage_file.exists():
|
|
return None
|
|
|
|
with open(triage_file, encoding="utf-8") as f:
|
|
return cls.from_dict(json.load(f))
|
|
|
|
|
|
@dataclass
|
|
class AutoFixState:
|
|
"""State tracking for auto-fix operations."""
|
|
|
|
issue_number: int
|
|
issue_url: str
|
|
repo: str
|
|
status: AutoFixStatus = AutoFixStatus.PENDING
|
|
spec_id: str | None = None
|
|
spec_dir: str | None = None
|
|
pr_number: int | None = None
|
|
pr_url: str | None = None
|
|
bot_comments: list[str] = field(default_factory=list)
|
|
error: str | None = None
|
|
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"issue_number": self.issue_number,
|
|
"issue_url": self.issue_url,
|
|
"repo": self.repo,
|
|
"status": self.status.value,
|
|
"spec_id": self.spec_id,
|
|
"spec_dir": self.spec_dir,
|
|
"pr_number": self.pr_number,
|
|
"pr_url": self.pr_url,
|
|
"bot_comments": self.bot_comments,
|
|
"error": self.error,
|
|
"created_at": self.created_at,
|
|
"updated_at": self.updated_at,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> AutoFixState:
|
|
issue_number = data["issue_number"]
|
|
repo = data["repo"]
|
|
# Construct issue_url if missing (for backwards compatibility with old state files)
|
|
issue_url = (
|
|
data.get("issue_url") or f"https://github.com/{repo}/issues/{issue_number}"
|
|
)
|
|
|
|
return cls(
|
|
issue_number=issue_number,
|
|
issue_url=issue_url,
|
|
repo=repo,
|
|
status=AutoFixStatus(data.get("status", "pending")),
|
|
spec_id=data.get("spec_id"),
|
|
spec_dir=data.get("spec_dir"),
|
|
pr_number=data.get("pr_number"),
|
|
pr_url=data.get("pr_url"),
|
|
bot_comments=data.get("bot_comments", []),
|
|
error=data.get("error"),
|
|
created_at=data.get("created_at", datetime.now().isoformat()),
|
|
updated_at=data.get("updated_at", datetime.now().isoformat()),
|
|
)
|
|
|
|
def update_status(self, status: AutoFixStatus) -> None:
|
|
"""Update status and timestamp with transition validation."""
|
|
if not self.status.can_transition_to(status):
|
|
raise ValueError(
|
|
f"Invalid state transition: {self.status.value} -> {status.value}"
|
|
)
|
|
self.status = status
|
|
self.updated_at = datetime.now().isoformat()
|
|
|
|
async def save(self, github_dir: Path) -> None:
|
|
"""Save auto-fix state to .auto-claude/github/issues/ with file locking."""
|
|
issues_dir = github_dir / "issues"
|
|
issues_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
autofix_file = issues_dir / f"autofix_{self.issue_number}.json"
|
|
|
|
# Atomic locked write
|
|
await locked_json_write(autofix_file, self.to_dict(), timeout=5.0)
|
|
|
|
# Update index with locking
|
|
await self._update_index(issues_dir)
|
|
|
|
async def _update_index(self, issues_dir: Path) -> None:
|
|
"""Update the issues index with auto-fix queue using file locking."""
|
|
index_file = issues_dir / "index.json"
|
|
|
|
def update_index(current_data):
|
|
"""Update function for atomic index update."""
|
|
if current_data is None:
|
|
current_data = {
|
|
"triaged": [],
|
|
"auto_fix_queue": [],
|
|
"last_updated": None,
|
|
}
|
|
|
|
# Update auto-fix queue
|
|
queue = current_data.get("auto_fix_queue", [])
|
|
existing = next(
|
|
(q for q in queue if q["issue_number"] == self.issue_number), None
|
|
)
|
|
|
|
entry = {
|
|
"issue_number": self.issue_number,
|
|
"repo": self.repo,
|
|
"status": self.status.value,
|
|
"spec_id": self.spec_id,
|
|
"pr_number": self.pr_number,
|
|
"updated_at": self.updated_at,
|
|
}
|
|
|
|
if existing:
|
|
queue = [
|
|
entry if q["issue_number"] == self.issue_number else q
|
|
for q in queue
|
|
]
|
|
else:
|
|
queue.append(entry)
|
|
|
|
current_data["auto_fix_queue"] = queue
|
|
current_data["last_updated"] = datetime.now().isoformat()
|
|
|
|
return current_data
|
|
|
|
# Atomic locked update
|
|
await locked_json_update(index_file, update_index, timeout=5.0)
|
|
|
|
@classmethod
|
|
def load(cls, github_dir: Path, issue_number: int) -> AutoFixState | None:
|
|
"""Load an auto-fix state from disk."""
|
|
autofix_file = github_dir / "issues" / f"autofix_{issue_number}.json"
|
|
if not autofix_file.exists():
|
|
return None
|
|
|
|
with open(autofix_file, encoding="utf-8") as f:
|
|
return cls.from_dict(json.load(f))
|
|
|
|
|
|
@dataclass
|
|
class GitHubRunnerConfig:
|
|
"""Configuration for GitHub automation runners."""
|
|
|
|
# Authentication
|
|
token: str
|
|
repo: str # owner/repo format
|
|
bot_token: str | None = None # Separate bot account token
|
|
|
|
# Auto-fix settings
|
|
auto_fix_enabled: bool = False
|
|
auto_fix_labels: list[str] = field(default_factory=lambda: ["auto-fix"])
|
|
require_human_approval: bool = True
|
|
|
|
# Permission settings
|
|
auto_fix_allowed_roles: list[str] = field(
|
|
default_factory=lambda: ["OWNER", "MEMBER", "COLLABORATOR"]
|
|
)
|
|
allow_external_contributors: bool = False
|
|
|
|
# Triage settings
|
|
triage_enabled: bool = False
|
|
duplicate_threshold: float = 0.80
|
|
spam_threshold: float = 0.75
|
|
feature_creep_threshold: float = 0.70
|
|
enable_triage_comments: bool = False
|
|
|
|
# PR review settings
|
|
pr_review_enabled: bool = False
|
|
auto_post_reviews: bool = False
|
|
allow_fix_commits: bool = True
|
|
review_own_prs: bool = False # Whether bot can review its own PRs
|
|
use_orchestrator_review: bool = (
|
|
True # DEPRECATED: No longer used, kept for config compatibility
|
|
)
|
|
use_parallel_orchestrator: bool = (
|
|
True # Use SDK subagent parallel orchestrator (default)
|
|
)
|
|
|
|
# Model settings
|
|
# Note: Default uses shorthand "sonnet" which gets resolved via resolve_model_id()
|
|
# to respect environment variable overrides (e.g., ANTHROPIC_DEFAULT_SONNET_MODEL)
|
|
model: str = "sonnet"
|
|
thinking_level: str = "medium"
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"token": "***", # Never save token
|
|
"repo": self.repo,
|
|
"bot_token": "***" if self.bot_token else None,
|
|
"auto_fix_enabled": self.auto_fix_enabled,
|
|
"auto_fix_labels": self.auto_fix_labels,
|
|
"require_human_approval": self.require_human_approval,
|
|
"auto_fix_allowed_roles": self.auto_fix_allowed_roles,
|
|
"allow_external_contributors": self.allow_external_contributors,
|
|
"triage_enabled": self.triage_enabled,
|
|
"duplicate_threshold": self.duplicate_threshold,
|
|
"spam_threshold": self.spam_threshold,
|
|
"feature_creep_threshold": self.feature_creep_threshold,
|
|
"enable_triage_comments": self.enable_triage_comments,
|
|
"pr_review_enabled": self.pr_review_enabled,
|
|
"review_own_prs": self.review_own_prs,
|
|
"auto_post_reviews": self.auto_post_reviews,
|
|
"allow_fix_commits": self.allow_fix_commits,
|
|
"model": self.model,
|
|
"thinking_level": self.thinking_level,
|
|
}
|
|
|
|
def save_settings(self, github_dir: Path) -> None:
|
|
"""Save non-sensitive settings to config.json."""
|
|
github_dir.mkdir(parents=True, exist_ok=True)
|
|
config_file = github_dir / "config.json"
|
|
|
|
# Save without tokens
|
|
settings = self.to_dict()
|
|
settings.pop("token", None)
|
|
settings.pop("bot_token", None)
|
|
|
|
with open(config_file, "w", encoding="utf-8") as f:
|
|
json.dump(settings, f, indent=2)
|
|
|
|
@classmethod
|
|
def load_settings(
|
|
cls, github_dir: Path, token: str, repo: str, bot_token: str | None = None
|
|
) -> GitHubRunnerConfig:
|
|
"""Load settings from config.json, with tokens provided separately."""
|
|
config_file = github_dir / "config.json"
|
|
|
|
if config_file.exists():
|
|
with open(config_file, encoding="utf-8") as f:
|
|
settings = json.load(f)
|
|
else:
|
|
settings = {}
|
|
|
|
return cls(
|
|
token=token,
|
|
repo=repo,
|
|
bot_token=bot_token,
|
|
auto_fix_enabled=settings.get("auto_fix_enabled", False),
|
|
auto_fix_labels=settings.get("auto_fix_labels", ["auto-fix"]),
|
|
require_human_approval=settings.get("require_human_approval", True),
|
|
auto_fix_allowed_roles=settings.get(
|
|
"auto_fix_allowed_roles", ["OWNER", "MEMBER", "COLLABORATOR"]
|
|
),
|
|
allow_external_contributors=settings.get(
|
|
"allow_external_contributors", False
|
|
),
|
|
triage_enabled=settings.get("triage_enabled", False),
|
|
duplicate_threshold=settings.get("duplicate_threshold", 0.80),
|
|
spam_threshold=settings.get("spam_threshold", 0.75),
|
|
feature_creep_threshold=settings.get("feature_creep_threshold", 0.70),
|
|
enable_triage_comments=settings.get("enable_triage_comments", False),
|
|
pr_review_enabled=settings.get("pr_review_enabled", False),
|
|
review_own_prs=settings.get("review_own_prs", False),
|
|
auto_post_reviews=settings.get("auto_post_reviews", False),
|
|
allow_fix_commits=settings.get("allow_fix_commits", True),
|
|
# Note: model is stored as shorthand and resolved via resolve_model_id()
|
|
model=settings.get("model", "sonnet"),
|
|
thinking_level=settings.get("thinking_level", "medium"),
|
|
)
|