diff --git a/.husky/pre-commit b/.husky/pre-commit index b5aad1a9..33cd5f32 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -185,7 +185,7 @@ if git diff --cached --name-only | grep -q "^apps/backend/.*\.py$"; then # Tests to skip: graphiti (external deps), merge_file_tracker/service_orchestrator/worktree/workspace (Windows path/git issues) # Also skip tests that require optional dependencies (pydantic structured outputs) IGNORE_TESTS="--ignore=../../tests/test_graphiti.py --ignore=../../tests/test_merge_file_tracker.py --ignore=../../tests/test_service_orchestrator.py --ignore=../../tests/test_worktree.py --ignore=../../tests/test_workspace.py --ignore=../../tests/test_finding_validation.py --ignore=../../tests/test_sdk_structured_output.py --ignore=../../tests/test_structured_outputs.py" - + # Determine Python executable from venv VENV_PYTHON="" if [ -f ".venv/bin/python" ]; then @@ -193,7 +193,7 @@ if git diff --cached --name-only | grep -q "^apps/backend/.*\.py$"; then elif [ -f ".venv/Scripts/python.exe" ]; then VENV_PYTHON=".venv/Scripts/python.exe" fi - + if [ -n "$VENV_PYTHON" ]; then # Check if pytest is installed in venv if $VENV_PYTHON -c "import pytest" 2>/dev/null; then diff --git a/apps/backend/__tests__/test_gitlab_file_lock.py b/apps/backend/__tests__/test_gitlab_file_lock.py index 47a67b2f..b521aaf0 100644 --- a/apps/backend/__tests__/test_gitlab_file_lock.py +++ b/apps/backend/__tests__/test_gitlab_file_lock.py @@ -71,7 +71,9 @@ class TestFileLock: def try_write(value): try: with FileLock(lock_file, timeout=1.0, exclusive=True): - with open(lock_file.with_suffix(".txt"), "w") as f: + with open( + lock_file.with_suffix(".txt"), "w", encoding="utf-8" + ) as f: f.write(str(value)) results.append(value) except Exception: @@ -122,14 +124,14 @@ class TestAtomicWrite: f.write("test content") assert target_file.exists() - assert target_file.read_text() == "test content" + assert target_file.read_text(encoding="utf-8") == "test content" def test_atomic_write_preserves_on_error(self, target_file): """Test atomic write doesn't corrupt on error.""" from runners.gitlab.utils.file_lock import atomic_write # Create initial content - target_file.write_text("original content") + target_file.write_text("original content", encoding="utf-8") try: with atomic_write(target_file) as f: @@ -139,7 +141,7 @@ class TestAtomicWrite: pass # Original content should be preserved - assert target_file.read_text() == "original content" + assert target_file.read_text(encoding="utf-8") == "original content" def test_atomic_write_context_manager(self, target_file): """Test atomic write context manager.""" @@ -149,7 +151,7 @@ class TestAtomicWrite: f.write("line 1\n") f.write("line 2\n") - content = target_file.read_text() + content = target_file.read_text(encoding="utf-8") assert "line 1" in content assert "line 2" in content @@ -171,7 +173,7 @@ class TestLockedJsonOperations: locked_json_write(data_file, data) assert data_file.exists() - with open(data_file) as f: + with open(data_file, encoding="utf-8") as f: loaded = json.load(f) assert loaded == data @@ -271,7 +273,7 @@ class TestLockedReadWrite: with locked_write(data_file) as f: f.write("test content") - assert data_file.read_text() == "test content" + assert data_file.read_text(encoding="utf-8") == "test content" def test_locked_read(self, data_file): """Test reading with lock.""" @@ -293,7 +295,7 @@ class TestLockedReadWrite: with locked_write(data_file, lock=None) as f: f.write("custom lock") - assert data_file.read_text() == "custom lock" + assert data_file.read_text(encoding="utf-8") == "custom lock" class TestFileLockError: diff --git a/apps/backend/runners/gitlab/bot_detection.py b/apps/backend/runners/gitlab/bot_detection.py index 31948924..680c69df 100644 --- a/apps/backend/runners/gitlab/bot_detection.py +++ b/apps/backend/runners/gitlab/bot_detection.py @@ -87,7 +87,7 @@ class BotDetectionState: if not state_file.exists(): return cls() - with open(state_file) as f: + with open(state_file, encoding="utf-8") as f: return cls.from_dict(json.load(f)) diff --git a/apps/backend/runners/gitlab/utils/file_lock.py b/apps/backend/runners/gitlab/utils/file_lock.py index 065d2028..24d4046c 100644 --- a/apps/backend/runners/gitlab/utils/file_lock.py +++ b/apps/backend/runners/gitlab/utils/file_lock.py @@ -348,7 +348,7 @@ async def locked_read(filepath: str | Path, timeout: float = 5.0) -> Any: try: # Open file for reading - with open(filepath) as f: + with open(filepath, encoding="utf-8") as f: yield f finally: # Release lock @@ -441,7 +441,7 @@ async def locked_json_update( # Read current data def _read_json(): if filepath.exists(): - with open(filepath) as f: + with open(filepath, encoding="utf-8") as f: return json.load(f) return None diff --git a/apps/frontend/src/__tests__/integration/task-lifecycle.test.ts b/apps/frontend/src/__tests__/integration/task-lifecycle.test.ts index fffbed82..b548ed46 100644 --- a/apps/frontend/src/__tests__/integration/task-lifecycle.test.ts +++ b/apps/frontend/src/__tests__/integration/task-lifecycle.test.ts @@ -379,4 +379,4 @@ describe('Task Lifecycle Integration', () => { }); }); -}); \ No newline at end of file +}); diff --git a/apps/frontend/src/main/__tests__/project-store.test.ts b/apps/frontend/src/main/__tests__/project-store.test.ts index d39f79d9..ba71a112 100644 --- a/apps/frontend/src/main/__tests__/project-store.test.ts +++ b/apps/frontend/src/main/__tests__/project-store.test.ts @@ -28,7 +28,7 @@ function setupTestDirs(): void { TEST_DIR = mkdtempSync(path.join(tmpdir(), 'project-store-test-')); USER_DATA_PATH = path.join(TEST_DIR, 'userData'); TEST_PROJECT_PATH = path.join(TEST_DIR, 'test-project'); - + mkdirSync(USER_DATA_PATH, { recursive: true }); mkdirSync(path.join(USER_DATA_PATH, 'store'), { recursive: true }); mkdirSync(TEST_PROJECT_PATH, { recursive: true }); diff --git a/apps/frontend/src/main/claude-profile/usage-monitor.ts b/apps/frontend/src/main/claude-profile/usage-monitor.ts index 91c1e12d..9f61c1a5 100644 --- a/apps/frontend/src/main/claude-profile/usage-monitor.ts +++ b/apps/frontend/src/main/claude-profile/usage-monitor.ts @@ -19,11 +19,11 @@ export class UsageMonitor extends EventEmitter { private currentUsage: ClaudeUsageSnapshot | null = null; private isChecking = false; private useApiMethod = true; // Try API first, fall back to CLI if it fails - + // Swap loop protection: track profiles that recently failed auth private authFailedProfiles: Map = new Map(); // profileId -> timestamp private static AUTH_FAILURE_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes cooldown - + // Debug flag for verbose logging private readonly isDebug = process.env.DEBUG === 'true'; @@ -159,12 +159,12 @@ export class UsageMonitor extends EventEmitter { if ((error as any).statusCode === 401 || (error as any).statusCode === 403) { const profileManager = getClaudeProfileManager(); const activeProfile = profileManager.getActiveProfile(); - + if (activeProfile) { // Mark this profile as auth-failed to prevent swap loops this.authFailedProfiles.set(activeProfile.id, Date.now()); console.warn('[UsageMonitor] Auth failure detected, marked profile as failed:', activeProfile.id); - + // Clean up expired entries from the failed profiles map const now = Date.now(); this.authFailedProfiles.forEach((timestamp, profileId) => { @@ -172,7 +172,7 @@ export class UsageMonitor extends EventEmitter { this.authFailedProfiles.delete(profileId); } }); - + try { const excludeProfiles = Array.from(this.authFailedProfiles.keys()); console.warn('[UsageMonitor] Attempting proactive swap (excluding failed profiles):', excludeProfiles); @@ -287,7 +287,7 @@ export class UsageMonitor extends EventEmitter { if (error?.statusCode === 401 || error?.statusCode === 403) { throw error; } - + console.error('[UsageMonitor] API fetch failed:', error); return null; } @@ -347,12 +347,12 @@ export class UsageMonitor extends EventEmitter { additionalExclusions: string[] = [] ): Promise { const profileManager = getClaudeProfileManager(); - + // Get all profiles to swap to, excluding current and any additional exclusions const allProfiles = profileManager.getProfilesSortedByAvailability(); const excludeIds = new Set([currentProfileId, ...additionalExclusions]); const eligibleProfiles = allProfiles.filter(p => !excludeIds.has(p.id)); - + if (eligibleProfiles.length === 0) { console.warn('[UsageMonitor] No alternative profile for proactive swap (excluded:', Array.from(excludeIds), ')'); this.emit('proactive-swap-failed', { @@ -362,7 +362,7 @@ export class UsageMonitor extends EventEmitter { }); return; } - + // Use the best available from eligible profiles const bestProfile = eligibleProfiles[0]; diff --git a/apps/frontend/src/renderer/components/task-detail/task-review/WorkspaceMessages.tsx b/apps/frontend/src/renderer/components/task-detail/task-review/WorkspaceMessages.tsx index d9ea0e2f..614d3834 100644 --- a/apps/frontend/src/renderer/components/task-detail/task-review/WorkspaceMessages.tsx +++ b/apps/frontend/src/renderer/components/task-detail/task-review/WorkspaceMessages.tsx @@ -151,14 +151,14 @@ export function StagedInProjectMessage({ task, projectPath, hasWorktree = false, const handleReviewAgain = async () => { if (!onReviewAgain) return; - + setIsResetting(true); setError(null); try { // Clear the staged flag via IPC const result = await window.electronAPI.clearStagedState(task.id); - + if (!result.success) { setError(result.error || 'Failed to reset staged state'); return; @@ -238,7 +238,7 @@ export function StagedInProjectMessage({ task, projectPath, hasWorktree = false, )} - + {/* Secondary actions row */}
{/* Mark Done Only (when worktree exists) - allows keeping worktree */} @@ -263,7 +263,7 @@ export function StagedInProjectMessage({ task, projectPath, hasWorktree = false, )} )} - + {/* Review Again button - only show if worktree exists and callback provided */} {hasWorktree && onReviewAgain && (
- + {error && (

{error}

)} - + {hasWorktree && (

"Delete Worktree & Mark Done" cleans up the isolated workspace. "Mark Done Only" keeps it for reference. diff --git a/scripts/check_encoding.py b/scripts/check_encoding.py index f5b8195d..439bce30 100644 --- a/scripts/check_encoding.py +++ b/scripts/check_encoding.py @@ -50,8 +50,21 @@ class EncodingChecker: # Check 1: open() without encoding # Pattern: open(...) without encoding= parameter # Use negative lookbehind to exclude os.open(), urlopen(), etc. - for match in re.finditer(r'(? 0: + if content[end_pos] == '(': + paren_depth += 1 + elif content[end_pos] == ')': + paren_depth -= 1 + end_pos += 1 + + call = content[match.start():end_pos] # Skip if it's binary mode (must contain 'b' in mode string) # Matches: "rb", "wb", "ab", "r+b", "w+b", etc.