Fix Windows UTF-8 encoding errors across entire backend (251 instances) (#782)

* 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>
This commit is contained in:
TamerineSky
2026-01-19 14:22:55 -07:00
committed by GitHub
parent 8df66245e1
commit 6a6247bbf2
131 changed files with 1407 additions and 349 deletions
+1
View File
@@ -39,6 +39,7 @@ Follow conventional commits: `<type>: <subject>`
- [ ] I've tested my changes locally
- [ ] I've followed the code principles (SOLID, DRY, KISS)
- [ ] My PR is small and focused (< 400 lines ideally)
- [ ] **(Python only)** All file operations specify `encoding="utf-8"` for text files
## Platform Testing Checklist
+11
View File
@@ -76,6 +76,17 @@ repos:
files: ^package\.json$
pass_filenames: false
# Python encoding check - prevent regression of UTF-8 encoding fixes (PR #782)
- repo: local
hooks:
- id: check-file-encoding
name: Check file encoding parameters
entry: python scripts/check_encoding.py
language: system
types: [python]
files: ^apps/backend/
description: Ensures all file operations specify encoding="utf-8"
# Python linting (apps/backend/)
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.10
+58
View File
@@ -357,6 +357,64 @@ export default function(props) {
- End files with a newline
- Keep line length under 100 characters when practical
### File Encoding (Python)
**Always specify `encoding="utf-8"` for text file operations** to ensure Windows compatibility.
Windows Python defaults to `cp1252` encoding instead of UTF-8, causing errors with:
- Emoji (🚀, ✅, ❌)
- International characters (ñ, é, 中文, العربية)
- Special symbols (™, ©, ®)
**DO:**
```python
# Reading files
with open(path, encoding="utf-8") as f:
content = f.read()
# Writing files
with open(path, "w", encoding="utf-8") as f:
f.write(content)
# Path methods
from pathlib import Path
content = Path(file).read_text(encoding="utf-8")
Path(file).write_text(content, encoding="utf-8")
# JSON files - reading
import json
with open(path, encoding="utf-8") as f:
data = json.load(f)
# JSON files - writing
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
```
**DON'T:**
```python
# Wrong - platform-dependent encoding
with open(path) as f:
content = f.read()
# Wrong - Path methods without encoding
content = Path(file).read_text()
# Wrong - encoding on json.dump (not open!)
json.dump(data, f, encoding="utf-8") # ERROR
```
**Binary files - NO encoding:**
```python
with open(path, "rb") as f: # Correct
data = f.read()
```
Our pre-commit hooks automatically check for missing encoding parameters. See [PR #782](https://github.com/AndyMik90/Auto-Claude/pull/782) for the comprehensive encoding fix and [guides/windows-development.md](guides/windows-development.md) for Windows-specific development guidance.
## Testing
### Python Tests
+1 -1
View File
@@ -226,7 +226,7 @@ async def run_autonomous_agent(
print(" PAUSED BY HUMAN")
print("=" * 70)
pause_content = pause_file.read_text().strip()
pause_content = pause_file.read_text(encoding="utf-8").strip()
if pause_content:
print(f"\nMessage: {pause_content}")
@@ -171,7 +171,7 @@ def create_memory_tools(spec_dir: Path, project_dir: Path) -> list:
# PRIMARY: Save to file-based storage (always works)
# Load existing map or create new
if codebase_map_file.exists():
with open(codebase_map_file) as f:
with open(codebase_map_file, encoding="utf-8") as f:
codebase_map = json.load(f)
else:
codebase_map = {
@@ -187,7 +187,7 @@ def create_memory_tools(spec_dir: Path, project_dir: Path) -> list:
}
codebase_map["last_updated"] = datetime.now(timezone.utc).isoformat()
with open(codebase_map_file, "w") as f:
with open(codebase_map_file, "w", encoding="utf-8") as f:
json.dump(codebase_map, f, indent=2)
# SECONDARY: Also save to Graphiti/LadybugDB (for Memory UI)
@@ -246,7 +246,7 @@ def create_memory_tools(spec_dir: Path, project_dir: Path) -> list:
entry += f"\n\n_Context: {context}_"
entry += "\n"
with open(gotchas_file, "a") as f:
with open(gotchas_file, "a", encoding="utf-8") as f:
if not gotchas_file.exists() or gotchas_file.stat().st_size == 0:
f.write(
"# Gotchas & Pitfalls\n\nThings to watch out for in this codebase.\n"
@@ -303,7 +303,7 @@ def create_memory_tools(spec_dir: Path, project_dir: Path) -> list:
codebase_map_file = memory_dir / "codebase_map.json"
if codebase_map_file.exists():
try:
with open(codebase_map_file) as f:
with open(codebase_map_file, encoding="utf-8") as f:
codebase_map = json.load(f)
discoveries = codebase_map.get("discovered_files", {})
@@ -319,7 +319,7 @@ def create_memory_tools(spec_dir: Path, project_dir: Path) -> list:
gotchas_file = memory_dir / "gotchas.md"
if gotchas_file.exists():
try:
content = gotchas_file.read_text()
content = gotchas_file.read_text(encoding="utf-8")
if content.strip():
result_parts.append("\n## Gotchas")
# Take last 1000 chars to avoid too much context
@@ -333,7 +333,7 @@ def create_memory_tools(spec_dir: Path, project_dir: Path) -> list:
patterns_file = memory_dir / "patterns.md"
if patterns_file.exists():
try:
content = patterns_file.read_text()
content = patterns_file.read_text(encoding="utf-8")
if content.strip():
result_parts.append("\n## Patterns")
result_parts.append(
@@ -57,7 +57,7 @@ def create_progress_tools(spec_dir: Path, project_dir: Path) -> list:
}
try:
with open(plan_file) as f:
with open(plan_file, encoding="utf-8") as f:
plan = json.load(f)
stats = {
+1 -1
View File
@@ -140,7 +140,7 @@ def create_qa_tools(spec_dir: Path, project_dir: Path) -> list:
except json.JSONDecodeError:
tests_passed = {}
with open(plan_file) as f:
with open(plan_file, encoding="utf-8") as f:
plan = json.load(f)
qa_session = _apply_qa_update(plan, status, issues, tests_passed)
@@ -113,7 +113,7 @@ def create_subtask_tools(spec_dir: Path, project_dir: Path) -> list:
}
try:
with open(plan_file) as f:
with open(plan_file, encoding="utf-8") as f:
plan = json.load(f)
subtask_found = _update_subtask_in_plan(plan, subtask_id, status, notes)
+2 -2
View File
@@ -48,9 +48,9 @@ def load_implementation_plan(spec_dir: Path) -> dict | None:
if not plan_file.exists():
return None
try:
with open(plan_file) as f:
with open(plan_file, encoding="utf-8") as f:
return json.load(f)
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
return None
+2 -2
View File
@@ -46,7 +46,7 @@ def analyze_project(project_dir: Path, output_file: Path | None = None) -> dict:
if output_file:
output_file.parent.mkdir(parents=True, exist_ok=True)
with open(output_file, "w") as f:
with open(output_file, "w", encoding="utf-8") as f:
json.dump(results, f, indent=2)
print(f"Project index saved to: {output_file}")
@@ -87,7 +87,7 @@ def analyze_service(
if output_file:
output_file.parent.mkdir(parents=True, exist_ok=True)
with open(output_file, "w") as f:
with open(output_file, "w", encoding="utf-8") as f:
json.dump(results, f, indent=2)
print(f"Service analysis saved to: {output_file}")
+1 -1
View File
@@ -99,7 +99,7 @@ class BaseAnalyzer:
def _read_file(self, path: str) -> str:
"""Read a file relative to the analyzer's path."""
try:
return (self.path / path).read_text()
return (self.path / path).read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
return ""
@@ -126,7 +126,7 @@ class AuthDetector(BaseAnalyzer):
for py_file in all_py_files:
try:
content = py_file.read_text()
content = py_file.read_text(encoding="utf-8")
# Find custom decorators
if (
"@require" in content
@@ -52,7 +52,7 @@ class JobsDetector(BaseAnalyzer):
tasks = []
for task_file in celery_files:
try:
content = task_file.read_text()
content = task_file.read_text(encoding="utf-8")
# Find @celery.task or @shared_task decorators
task_pattern = r"@(?:celery\.task|shared_task|app\.task)\s*(?:\([^)]*\))?\s*def\s+(\w+)"
task_matches = re.findall(task_pattern, content)
@@ -77,7 +77,7 @@ class MonitoringDetector(BaseAnalyzer):
continue
try:
content = file_path.read_text()
content = file_path.read_text(encoding="utf-8")
# Look for actual Prometheus imports or usage patterns
prometheus_patterns = [
"from prometheus_client import",
@@ -52,7 +52,7 @@ class DatabaseDetector(BaseAnalyzer):
for file_path in py_files:
try:
content = file_path.read_text()
content = file_path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
continue
@@ -119,7 +119,7 @@ class DatabaseDetector(BaseAnalyzer):
for file_path in model_files:
try:
content = file_path.read_text()
content = file_path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
continue
@@ -168,7 +168,7 @@ class DatabaseDetector(BaseAnalyzer):
return models
try:
content = schema_file.read_text()
content = schema_file.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
return models
@@ -216,7 +216,7 @@ class DatabaseDetector(BaseAnalyzer):
for file_path in ts_files:
try:
content = file_path.read_text()
content = file_path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
continue
@@ -265,7 +265,7 @@ class DatabaseDetector(BaseAnalyzer):
for file_path in schema_files:
try:
content = file_path.read_text()
content = file_path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
continue
@@ -295,7 +295,7 @@ class DatabaseDetector(BaseAnalyzer):
for file_path in model_files:
try:
content = file_path.read_text()
content = file_path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
continue
@@ -287,6 +287,6 @@ class ProjectAnalyzer:
def _read_file(self, path: str) -> str:
try:
return (self.project_dir / path).read_text()
return (self.project_dir / path).read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
return ""
@@ -66,7 +66,7 @@ class RouteDetector(BaseAnalyzer):
for file_path in files_to_check:
try:
content = file_path.read_text()
content = file_path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
continue
@@ -130,7 +130,7 @@ class RouteDetector(BaseAnalyzer):
for file_path in files_to_check:
try:
content = file_path.read_text()
content = file_path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
continue
@@ -179,7 +179,7 @@ class RouteDetector(BaseAnalyzer):
for file_path in url_files:
try:
content = file_path.read_text()
content = file_path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
continue
@@ -218,7 +218,7 @@ class RouteDetector(BaseAnalyzer):
files_to_check = js_files + ts_files
for file_path in files_to_check:
try:
content = file_path.read_text()
content = file_path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
continue
@@ -283,7 +283,7 @@ class RouteDetector(BaseAnalyzer):
route_path = re.sub(r"\[([^\]]+)\]", r":\1", route_path)
try:
content = route_file.read_text()
content = route_file.read_text(encoding="utf-8")
# Detect exported methods: export async function GET(request)
methods = re.findall(
r"export\s+(?:async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH)",
@@ -348,7 +348,7 @@ class RouteDetector(BaseAnalyzer):
for file_path in go_files:
try:
content = file_path.read_text()
content = file_path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
continue
@@ -384,7 +384,7 @@ class RouteDetector(BaseAnalyzer):
for file_path in rust_files:
try:
content = file_path.read_text()
content = file_path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
continue
+4 -4
View File
@@ -163,7 +163,7 @@ class CIDiscovery:
)
try:
content = wf_file.read_text()
content = wf_file.read_text(encoding="utf-8")
workflow_data = self._parse_yaml(content)
if not workflow_data:
@@ -242,7 +242,7 @@ class CIDiscovery:
)
try:
content = config_file.read_text()
content = config_file.read_text(encoding="utf-8")
data = self._parse_yaml(content)
if not data:
@@ -312,7 +312,7 @@ class CIDiscovery:
)
try:
content = config_file.read_text()
content = config_file.read_text(encoding="utf-8")
data = self._parse_yaml(content)
if not data:
@@ -370,7 +370,7 @@ class CIDiscovery:
)
try:
content = jenkinsfile.read_text()
content = jenkinsfile.read_text(encoding="utf-8")
# Extract sh commands using regex
sh_pattern = re.compile(r'sh\s+[\'"]([^\'"]+)[\'"]')
+2 -2
View File
@@ -237,7 +237,7 @@ def _get_subtask_description(spec_dir: Path, subtask_id: str) -> str:
return f"Subtask: {subtask_id}"
try:
with open(plan_file) as f:
with open(plan_file, encoding="utf-8") as f:
plan = json.load(f)
# Search through phases for the subtask
@@ -280,7 +280,7 @@ def _build_extraction_prompt(inputs: dict) -> str:
prompt_file = Path(__file__).parent / "prompts" / "insight_extractor.md"
if prompt_file.exists():
base_prompt = prompt_file.read_text()
base_prompt = prompt_file.read_text(encoding="utf-8")
else:
# Fallback if prompt file missing
base_prompt = """Extract structured insights from this coding session.
+4 -4
View File
@@ -302,7 +302,7 @@ class TestDiscovery:
try:
with open(package_json, encoding="utf-8") as f:
pkg = json.load(f)
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
return
deps = pkg.get("dependencies", {})
@@ -398,7 +398,7 @@ class TestDiscovery:
# Check pyproject.toml
pyproject = project_dir / "pyproject.toml"
if pyproject.exists():
content = pyproject.read_text()
content = pyproject.read_text(encoding="utf-8")
# Check for pytest
if "pytest" in content:
@@ -418,7 +418,7 @@ class TestDiscovery:
# Check requirements.txt
requirements = project_dir / "requirements.txt"
if requirements.exists():
content = requirements.read_text().lower()
content = requirements.read_text(encoding="utf-8").lower()
if "pytest" in content and not any(
f.name == "pytest" for f in result.frameworks
):
@@ -496,7 +496,7 @@ class TestDiscovery:
if not gemfile.exists():
return
content = gemfile.read_text().lower()
content = gemfile.read_text(encoding="utf-8").lower()
if "rspec" in content or (project_dir / ".rspec").exists():
result.frameworks.append(
+3 -3
View File
@@ -31,7 +31,7 @@ def handle_batch_create_command(batch_file: str, project_dir: str) -> bool:
return False
try:
with open(batch_path) as f:
with open(batch_path, encoding="utf-8") as f:
batch_data = json.load(f)
except json.JSONDecodeError as e:
print_status(f"Invalid JSON in batch file: {e}", "error")
@@ -81,7 +81,7 @@ def handle_batch_create_command(batch_file: str, project_dir: str) -> bool:
}
req_file = spec_dir / "requirements.json"
with open(req_file, "w") as f:
with open(req_file, "w", encoding="utf-8") as f:
json.dump(requirements, f, indent=2, default=str)
created_specs.append(
@@ -145,7 +145,7 @@ def handle_batch_status_command(project_dir: str) -> bool:
if req_file.exists():
try:
with open(req_file) as f:
with open(req_file, encoding="utf-8") as f:
req = json.load(f)
title = req.get("task_description", title)
except json.JSONDecodeError:
+1 -1
View File
@@ -421,7 +421,7 @@ def _handle_build_interrupt(
if human_input:
# Save to HUMAN_INPUT.md
input_file = spec_dir / "HUMAN_INPUT.md"
input_file.write_text(human_input)
input_file.write_text(human_input, encoding="utf-8")
content = [
success(f"{icon(Icons.SUCCESS)} INSTRUCTIONS SAVED"),
+3 -3
View File
@@ -121,7 +121,7 @@ def collect_followup_task(spec_dir: Path, max_retries: int = 3) -> str | None:
# Expand ~ and resolve path
file_path = Path(file_path_str).expanduser().resolve()
if file_path.exists():
followup_task = file_path.read_text().strip()
followup_task = file_path.read_text(encoding="utf-8").strip()
if followup_task:
print_status(
f"Loaded {len(followup_task)} characters from file",
@@ -195,7 +195,7 @@ def collect_followup_task(spec_dir: Path, max_retries: int = 3) -> str | None:
# Save to FOLLOWUP_REQUEST.md
request_file = spec_dir / "FOLLOWUP_REQUEST.md"
request_file.write_text(followup_task)
request_file.write_text(followup_task, encoding="utf-8")
# Show confirmation
content = [
@@ -285,7 +285,7 @@ def handle_followup_command(
# Check for prior follow-ups (for sequential follow-up context)
prior_followup_count = 0
try:
with open(plan_file) as f:
with open(plan_file, encoding="utf-8") as f:
plan_data = json.load(f)
phases = plan_data.get("phases", [])
# Count phases that look like follow-up phases (name contains "Follow" or high phase number)
+1 -1
View File
@@ -149,7 +149,7 @@ def read_from_file() -> str | None:
# Expand ~ and resolve path
file_path = Path(file_path_input).expanduser().resolve()
if file_path.exists():
content = file_path.read_text().strip()
content = file_path.read_text(encoding="utf-8").strip()
if content:
print_status(
f"Loaded {len(content)} characters from file",
+1 -1
View File
@@ -182,7 +182,7 @@ def _detect_worktree_base_branch(
config_path = worktree_path / ".auto-claude" / "worktree-config.json"
if config_path.exists():
try:
config = json.loads(config_path.read_text())
config = json.loads(config_path.read_text(encoding="utf-8"))
if config.get("base_branch"):
debug(
MODULE,
+9 -3
View File
@@ -37,8 +37,12 @@ class ContextBuilder:
"""Load project index from file or create new one (.auto-claude is the installed instance)."""
index_file = self.project_dir / ".auto-claude" / "project_index.json"
if index_file.exists():
with open(index_file) as f:
return json.load(f)
try:
with open(index_file, encoding="utf-8") as f:
return json.load(f)
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
# Corrupted or legacy-encoded file, regenerate
pass
# Try to create one
from analyzer import analyze_project
@@ -230,7 +234,9 @@ class ContextBuilder:
if context_file.exists():
return {
"source": "SERVICE_CONTEXT.md",
"content": context_file.read_text()[:2000], # First 2000 chars
"content": context_file.read_text(encoding="utf-8")[
:2000
], # First 2000 chars
}
# Generate basic context from service info
+1 -1
View File
@@ -72,7 +72,7 @@ def build_task_context(
if output_file:
output_file.parent.mkdir(parents=True, exist_ok=True)
with open(output_file, "w") as f:
with open(output_file, "w", encoding="utf-8") as f:
json.dump(result, f, indent=2)
print(f"Task context saved to: {output_file}")
+1 -1
View File
@@ -38,7 +38,7 @@ class PatternDiscoverer:
for match in reference_files[:max_files]:
try:
file_path = self.project_dir / match.path
content = file_path.read_text(errors="ignore")
content = file_path.read_text(encoding="utf-8", errors="ignore")
# Look for common patterns
for keyword in keywords:
+1 -1
View File
@@ -41,7 +41,7 @@ class CodeSearcher:
for file_path in self._iter_code_files(service_path):
try:
content = file_path.read_text(errors="ignore")
content = file_path.read_text(encoding="utf-8", errors="ignore")
content_lower = content.lower()
# Score this file
+2 -2
View File
@@ -41,7 +41,7 @@ def save_context(context: TaskContext, output_file: Path) -> None:
output_file: Path to output JSON file
"""
output_file.parent.mkdir(parents=True, exist_ok=True)
with open(output_file, "w") as f:
with open(output_file, "w", encoding="utf-8") as f:
json.dump(serialize_context(context), f, indent=2)
@@ -55,5 +55,5 @@ def load_context(input_file: Path) -> dict:
Returns:
Context dictionary
"""
with open(input_file) as f:
with open(input_file, encoding="utf-8") as f:
return json.load(f)
+1 -1
View File
@@ -875,7 +875,7 @@ def create_client(
# Write settings to a file in the project directory
settings_file = project_dir / ".claude_settings.json"
with open(settings_file, "w") as f:
with open(settings_file, "w", encoding="utf-8") as f:
json.dump(security_settings, f, indent=2)
print(f"Security settings: {settings_file}")
+1 -1
View File
@@ -110,7 +110,7 @@ def _write_log(message: str, to_file: bool = True) -> None:
import re
clean_message = re.sub(r"\033\[[0-9;]*m", "", message)
with open(log_file, "a") as f:
with open(log_file, "a", encoding="utf-8") as f:
f.write(clean_message + "\n")
except Exception:
pass # Silently fail file logging
+12 -12
View File
@@ -43,7 +43,7 @@ def count_subtasks(spec_dir: Path) -> tuple[int, int]:
return 0, 0
try:
with open(plan_file) as f:
with open(plan_file, encoding="utf-8") as f:
plan = json.load(f)
total = 0
@@ -56,7 +56,7 @@ def count_subtasks(spec_dir: Path) -> tuple[int, int]:
completed += 1
return completed, total
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
return 0, 0
@@ -81,7 +81,7 @@ def count_subtasks_detailed(spec_dir: Path) -> dict:
return result
try:
with open(plan_file) as f:
with open(plan_file, encoding="utf-8") as f:
plan = json.load(f)
for phase in plan.get("phases", []):
@@ -94,7 +94,7 @@ def count_subtasks_detailed(spec_dir: Path) -> dict:
result["pending"] += 1
return result
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
return result
@@ -182,7 +182,7 @@ def print_progress_summary(spec_dir: Path, show_next: bool = True) -> None:
# Phase summary
try:
with open(spec_dir / "implementation_plan.json") as f:
with open(spec_dir / "implementation_plan.json", encoding="utf-8") as f:
plan = json.load(f)
print("\nPhases:")
@@ -230,8 +230,8 @@ def print_progress_summary(spec_dir: Path, show_next: bool = True) -> None:
f" {icon(Icons.ARROW_RIGHT)} Next: {highlight(next_id)} - {next_desc}"
)
except (OSError, json.JSONDecodeError):
pass
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
pass # Ignore corrupted/unreadable progress files
else:
print()
print_status("No implementation subtasks yet - planner needs to run", "pending")
@@ -302,7 +302,7 @@ def get_plan_summary(spec_dir: Path) -> dict:
}
try:
with open(plan_file) as f:
with open(plan_file, encoding="utf-8") as f:
plan = json.load(f)
summary = {
@@ -355,7 +355,7 @@ def get_plan_summary(spec_dir: Path) -> dict:
return summary
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
return {
"workflow_type": None,
"total_phases": 0,
@@ -376,7 +376,7 @@ def get_current_phase(spec_dir: Path) -> dict | None:
return None
try:
with open(plan_file) as f:
with open(plan_file, encoding="utf-8") as f:
plan = json.load(f)
for phase in plan.get("phases", []):
@@ -396,7 +396,7 @@ def get_current_phase(spec_dir: Path) -> dict | None:
return None
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
return None
@@ -470,7 +470,7 @@ def get_next_subtask(spec_dir: Path) -> dict | None:
return None
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
return None
+1 -1
View File
@@ -53,7 +53,7 @@ def _get_version() -> str:
if package_json.exists():
import json
with open(package_json) as f:
with open(package_json, encoding="utf-8") as f:
data = json.load(f)
return data.get("version", "0.0.0")
except Exception as e:
+4 -4
View File
@@ -93,7 +93,7 @@ class MergeLock:
os.close(fd)
# Write our PID to the lock file
self.lock_file.write_text(str(os.getpid()))
self.lock_file.write_text(str(os.getpid()), encoding="utf-8")
self.acquired = True
return self
@@ -101,7 +101,7 @@ class MergeLock:
# Lock file exists - check if process is still running
if self.lock_file.exists():
try:
pid = int(self.lock_file.read_text().strip())
pid = int(self.lock_file.read_text(encoding="utf-8").strip())
# Import locally to avoid circular dependency
import os as _os
@@ -183,7 +183,7 @@ class SpecNumberLock:
os.close(fd)
# Write our PID to the lock file
self.lock_file.write_text(str(os.getpid()))
self.lock_file.write_text(str(os.getpid()), encoding="utf-8")
self.acquired = True
return self
@@ -191,7 +191,7 @@ class SpecNumberLock:
# Lock file exists - check if process is still running
if self.lock_file.exists():
try:
pid = int(self.lock_file.read_text().strip())
pid = int(self.lock_file.read_text(encoding="utf-8").strip())
import os as _os
try:
+6 -5
View File
@@ -412,10 +412,10 @@ def setup_workspace(
if PROFILE_FILENAME in security_files_copied:
profile_path = worktree_info.path / PROFILE_FILENAME
try:
with open(profile_path) as f:
with open(profile_path, encoding="utf-8") as f:
profile_data = json.load(f)
profile_data["inherited_from"] = str(project_dir.resolve())
with open(profile_path, "w") as f:
with open(profile_path, "w", encoding="utf-8") as f:
json.dump(profile_data, f, indent=2)
debug(
MODULE, f"Marked security profile as inherited from {project_dir}"
@@ -474,7 +474,7 @@ def ensure_timeline_hook_installed(project_dir: Path) -> None:
# Handle worktrees (where .git is a file, not directory)
if git_dir.is_file():
content = git_dir.read_text().strip()
content = git_dir.read_text(encoding="utf-8").strip()
if content.startswith("gitdir:"):
git_dir = Path(content.split(":", 1)[1].strip())
else:
@@ -484,7 +484,7 @@ def ensure_timeline_hook_installed(project_dir: Path) -> None:
# Check if hook already installed
if hook_path.exists():
if "FileTimelineTracker" in hook_path.read_text():
if "FileTimelineTracker" in hook_path.read_text(encoding="utf-8"):
debug(MODULE, "FileTimelineTracker hook already installed")
return
@@ -522,7 +522,7 @@ def initialize_timeline_tracking(
if source_spec_dir:
plan_path = source_spec_dir / "implementation_plan.json"
if plan_path.exists():
with open(plan_path) as f:
with open(plan_path, encoding="utf-8") as f:
plan = json.load(f)
task_title = plan.get("title", spec_name)
task_intent = plan.get("description", "")
@@ -533,6 +533,7 @@ def initialize_timeline_tracking(
files_to_modify.extend(subtask.get("files", []))
# Get the current branch point commit
# Note: run_git() already handles capture_output and encoding internally
result = run_git(
["rev-parse", "HEAD"],
cwd=project_dir,
+4 -4
View File
@@ -51,7 +51,7 @@ class ProjectAnalyzer:
project_index_path = self.project_dir / ".auto-claude" / "project_index.json"
if project_index_path.exists():
try:
with open(project_index_path) as f:
with open(project_index_path, encoding="utf-8") as f:
index = json.load(f)
# Extract tech stack from services
for service_name, service_info in index.get("services", {}).items():
@@ -70,7 +70,7 @@ class ProjectAnalyzer:
)
if roadmap_path.exists():
try:
with open(roadmap_path) as f:
with open(roadmap_path, encoding="utf-8") as f:
roadmap = json.load(f)
# Extract planned features
for feature in roadmap.get("features", []):
@@ -87,7 +87,7 @@ class ProjectAnalyzer:
)
if discovery_path.exists() and not context["target_audience"]:
try:
with open(discovery_path) as f:
with open(discovery_path, encoding="utf-8") as f:
discovery = json.load(f)
audience = discovery.get("target_audience", {})
context["target_audience"] = audience.get("primary_persona")
@@ -109,7 +109,7 @@ class ProjectAnalyzer:
spec_file = spec_dir / "spec.md"
if spec_file.exists():
# Extract title from spec
content = spec_file.read_text()
content = spec_file.read_text(encoding="utf-8")
lines = content.split("\n")
for line in lines:
if line.startswith("# "):
+4 -4
View File
@@ -39,7 +39,7 @@ class IdeationFormatter:
existing_session = None
if append and ideation_file.exists():
try:
with open(ideation_file) as f:
with open(ideation_file, encoding="utf-8") as f:
existing_session = json.load(f)
existing_ideas = existing_session.get("ideas", [])
print_status(
@@ -56,7 +56,7 @@ class IdeationFormatter:
type_file = self.output_dir / f"{ideation_type}_ideas.json"
if type_file.exists():
try:
with open(type_file) as f:
with open(type_file, encoding="utf-8") as f:
data = json.load(f)
ideas = data.get(ideation_type, [])
new_ideas.extend(ideas)
@@ -123,7 +123,7 @@ class IdeationFormatter:
ideation_session["summary"]["by_status"].get(idea_status, 0) + 1
)
with open(ideation_file, "w") as f:
with open(ideation_file, "w", encoding="utf-8") as f:
json.dump(ideation_session, f, indent=2)
action = "Updated" if append else "Created"
@@ -139,7 +139,7 @@ class IdeationFormatter:
context_data = {}
if context_file.exists():
try:
with open(context_file) as f:
with open(context_file, encoding="utf-8") as f:
context_data = json.load(f)
except json.JSONDecodeError:
pass
+1 -1
View File
@@ -80,7 +80,7 @@ class IdeationGenerator:
return False, f"Prompt not found: {prompt_path}"
# Load prompt
prompt = prompt_path.read_text()
prompt = prompt_path.read_text(encoding="utf-8")
# Add context
prompt += f"\n\n---\n\n**Output Directory**: {self.output_dir}\n"
+7 -7
View File
@@ -85,7 +85,7 @@ class PhaseExecutor:
if not is_graphiti_enabled():
print_status("Graphiti not enabled, skipping graph hints", "info")
with open(hints_file, "w") as f:
with open(hints_file, "w", encoding="utf-8") as f:
json.dump(
{
"enabled": False,
@@ -131,7 +131,7 @@ class PhaseExecutor:
total_hints += len(result)
# Save hints
with open(hints_file, "w") as f:
with open(hints_file, "w", encoding="utf-8") as f:
json.dump(
{
"enabled": True,
@@ -178,11 +178,11 @@ class PhaseExecutor:
graph_hints = {}
if hints_file.exists():
try:
with open(hints_file) as f:
with open(hints_file, encoding="utf-8") as f:
hints_data = json.load(f)
graph_hints = hints_data.get("hints_by_type", {})
except (OSError, json.JSONDecodeError):
pass
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
pass # Use empty hints if file is corrupted/unreadable
# Write context file
context_data = {
@@ -200,7 +200,7 @@ class PhaseExecutor:
"created_at": datetime.now().isoformat(),
}
with open(context_file, "w") as f:
with open(context_file, "w", encoding="utf-8") as f:
json.dump(context_data, f, indent=2)
print_status("Created ideation_context.json", "success")
@@ -252,7 +252,7 @@ class PhaseExecutor:
if output_file.exists() and not self.refresh:
# Load and validate existing ideas - only skip if we have valid ideas
try:
with open(output_file) as f:
with open(output_file, encoding="utf-8") as f:
data = json.load(f)
count = len(data.get(ideation_type, []))
+2 -2
View File
@@ -48,7 +48,7 @@ class IdeaPrioritizer:
}
try:
content = output_file.read_text()
content = output_file.read_text(encoding="utf-8")
data = json.loads(content)
debug_verbose(
"ideation_prioritizer",
@@ -102,7 +102,7 @@ class IdeaPrioritizer:
return {
"success": False,
"error": f"Invalid JSON: {e}",
"current_content": output_file.read_text()
"current_content": output_file.read_text(encoding="utf-8")
if output_file.exists()
else "",
"count": 0,
+1 -1
View File
@@ -232,7 +232,7 @@ class IdeationOrchestrator:
"""Print summary of ideation generation results."""
ideation_file = self.output_dir / "ideation.json"
if ideation_file.exists():
with open(ideation_file) as f:
with open(ideation_file, encoding="utf-8") as f:
ideation = json.load(f)
ideas = ideation.get("ideas", [])
+5 -5
View File
@@ -57,7 +57,7 @@ def ensure_gitignore_entry(project_dir: Path, entry: str = ".auto-claude/") -> b
# Check if .gitignore exists and if entry is already present
if gitignore_path.exists():
content = gitignore_path.read_text()
content = gitignore_path.read_text(encoding="utf-8")
lines = content.splitlines()
if _entry_exists_in_gitignore(lines, entry):
@@ -72,14 +72,14 @@ def ensure_gitignore_entry(project_dir: Path, entry: str = ".auto-claude/") -> b
content += "\n# Auto Claude data directory\n"
content += entry + "\n"
gitignore_path.write_text(content)
gitignore_path.write_text(content, encoding="utf-8")
return True
else:
# Create new .gitignore with the entry
content = "# Auto Claude data directory\n"
content += entry + "\n"
gitignore_path.write_text(content)
gitignore_path.write_text(content, encoding="utf-8")
return True
@@ -177,7 +177,7 @@ def ensure_all_gitignore_entries(
# Read existing content or start fresh
if gitignore_path.exists():
content = gitignore_path.read_text()
content = gitignore_path.read_text(encoding="utf-8")
lines = content.splitlines()
else:
content = ""
@@ -203,7 +203,7 @@ def ensure_all_gitignore_entries(
content += entry + "\n"
added_entries.append(entry)
gitignore_path.write_text(content)
gitignore_path.write_text(content, encoding="utf-8")
# Auto-commit if requested and entries were added
if auto_commit and added_entries:
+3 -3
View File
@@ -507,7 +507,7 @@ class GraphitiState:
def save(self, spec_dir: Path) -> None:
"""Save state to the spec directory."""
marker_file = spec_dir / GRAPHITI_STATE_MARKER
with open(marker_file, "w") as f:
with open(marker_file, "w", encoding="utf-8") as f:
json.dump(self.to_dict(), f, indent=2)
@classmethod
@@ -518,9 +518,9 @@ class GraphitiState:
return None
try:
with open(marker_file) as f:
with open(marker_file, encoding="utf-8") as f:
return cls.from_dict(json.load(f))
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
return None
def record_error(self, error_msg: str) -> None:
+3 -3
View File
@@ -126,7 +126,7 @@ class LinearProjectState:
def save(self, spec_dir: Path) -> None:
"""Save state to the spec directory."""
marker_file = spec_dir / LINEAR_PROJECT_MARKER
with open(marker_file, "w") as f:
with open(marker_file, "w", encoding="utf-8") as f:
json.dump(self.to_dict(), f, indent=2)
@classmethod
@@ -137,9 +137,9 @@ class LinearProjectState:
return None
try:
with open(marker_file) as f:
with open(marker_file, encoding="utf-8") as f:
return cls.from_dict(json.load(f))
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
return None
@@ -158,9 +158,9 @@ class LinearManager:
return None
try:
with open(plan_file) as f:
with open(plan_file, encoding="utf-8") as f:
return json.load(f)
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
return None
def get_subtasks_for_sync(self) -> list[dict]:
+3 -3
View File
@@ -81,7 +81,7 @@ class LinearTaskState:
def save(self, spec_dir: Path) -> None:
"""Save state to the spec directory."""
state_file = spec_dir / LINEAR_TASK_FILE
with open(state_file, "w") as f:
with open(state_file, "w", encoding="utf-8") as f:
json.dump(self.to_dict(), f, indent=2)
@classmethod
@@ -92,9 +92,9 @@ class LinearTaskState:
return None
try:
with open(state_file) as f:
with open(state_file, encoding="utf-8") as f:
return cls.from_dict(json.load(f))
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
return None
+5 -5
View File
@@ -38,9 +38,9 @@ def update_codebase_map(spec_dir: Path, discoveries: dict[str, str]) -> None:
# Load existing map or create new
if map_file.exists():
try:
with open(map_file) as f:
with open(map_file, encoding="utf-8") as f:
codebase_map = json.load(f)
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
codebase_map = {}
else:
codebase_map = {}
@@ -58,7 +58,7 @@ def update_codebase_map(spec_dir: Path, discoveries: dict[str, str]) -> None:
)
# Write back
with open(map_file, "w") as f:
with open(map_file, "w", encoding="utf-8") as f:
json.dump(codebase_map, f, indent=2, sort_keys=True)
# Also save to Graphiti if enabled
@@ -90,12 +90,12 @@ def load_codebase_map(spec_dir: Path) -> dict[str, str]:
return {}
try:
with open(map_file) as f:
with open(map_file, encoding="utf-8") as f:
codebase_map = json.load(f)
# Remove metadata before returning
codebase_map.pop("_metadata", None)
return codebase_map
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
return {}
+6 -6
View File
@@ -36,7 +36,7 @@ def append_gotcha(spec_dir: Path, gotcha: str) -> None:
# Load existing gotchas
existing_gotchas = set()
if gotchas_file.exists():
content = gotchas_file.read_text()
content = gotchas_file.read_text(encoding="utf-8")
# Extract bullet points
for line in content.split("\n"):
line = line.strip()
@@ -47,7 +47,7 @@ def append_gotcha(spec_dir: Path, gotcha: str) -> None:
gotcha_stripped = gotcha.strip()
if gotcha_stripped and gotcha_stripped not in existing_gotchas:
# Append to file
with open(gotchas_file, "a") as f:
with open(gotchas_file, "a", encoding="utf-8") as f:
if gotchas_file.stat().st_size == 0:
# First entry - add header
f.write("# Gotchas and Pitfalls\n\n")
@@ -80,7 +80,7 @@ def load_gotchas(spec_dir: Path) -> list[str]:
if not gotchas_file.exists():
return []
content = gotchas_file.read_text()
content = gotchas_file.read_text(encoding="utf-8")
gotchas = []
for line in content.split("\n"):
@@ -112,7 +112,7 @@ def append_pattern(spec_dir: Path, pattern: str) -> None:
# Load existing patterns
existing_patterns = set()
if patterns_file.exists():
content = patterns_file.read_text()
content = patterns_file.read_text(encoding="utf-8")
# Extract bullet points
for line in content.split("\n"):
line = line.strip()
@@ -123,7 +123,7 @@ def append_pattern(spec_dir: Path, pattern: str) -> None:
pattern_stripped = pattern.strip()
if pattern_stripped and pattern_stripped not in existing_patterns:
# Append to file
with open(patterns_file, "a") as f:
with open(patterns_file, "a", encoding="utf-8") as f:
if patterns_file.stat().st_size == 0:
# First entry - add header
f.write("# Code Patterns\n\n")
@@ -156,7 +156,7 @@ def load_patterns(spec_dir: Path) -> list[str]:
if not patterns_file.exists():
return []
content = patterns_file.read_text()
content = patterns_file.read_text(encoding="utf-8")
patterns = []
for line in content.split("\n"):
+3 -3
View File
@@ -76,7 +76,7 @@ def save_session_insights(
}
# Write to file (always use file-based storage)
with open(session_file, "w") as f:
with open(session_file, "w", encoding="utf-8") as f:
json.dump(session_data, f, indent=2)
# Also save to Graphiti if enabled (non-blocking, errors logged but not raised)
@@ -110,9 +110,9 @@ def load_all_insights(spec_dir: Path) -> list[dict[str, Any]]:
insights = []
for session_file in session_files:
try:
with open(session_file) as f:
with open(session_file, encoding="utf-8") as f:
insights.append(json.load(f))
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
# Skip corrupted files
continue
+2 -2
View File
@@ -61,7 +61,7 @@ class EvolutionStorage:
return {}
try:
with open(self.evolution_file) as f:
with open(self.evolution_file, encoding="utf-8") as f:
data = json.load(f)
evolutions = {}
@@ -88,7 +88,7 @@ class EvolutionStorage:
for file_path, evolution in evolutions.items()
}
with open(self.evolution_file, "w") as f:
with open(self.evolution_file, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
logger.debug(f"Saved evolution data for {len(evolutions)} files")
+17 -3
View File
@@ -83,10 +83,16 @@ def apply_single_task_changes(
# Addition - need to determine where to add
if change.change_type == ChangeType.ADD_IMPORT:
# Add import at top
# Content is already normalized to LF, so only check for \n
has_trailing_newline = content.endswith("\n")
lines = content.splitlines()
import_end = find_import_end(lines, file_path)
lines.insert(import_end, change.content_after)
# Strip trailing newline from content_after to prevent double newlines
# (content_after may include newline from diff generation)
lines.insert(import_end, change.content_after.rstrip("\n\r"))
content = line_ending.join(lines)
if has_trailing_newline:
content += line_ending
elif change.change_type == ChangeType.ADD_FUNCTION:
# Add function at end (before exports)
content += f"{line_ending}{line_ending}{change.content_after}"
@@ -149,13 +155,21 @@ def combine_non_conflicting_changes(
# Add imports
if imports:
# Content is already normalized to LF, so only check for \n
has_trailing_newline = content.endswith("\n")
lines = content.splitlines()
import_end = find_import_end(lines, file_path)
for imp in imports:
if imp.content_after and imp.content_after not in content:
lines.insert(import_end, imp.content_after)
# Strip trailing newline from content_after to prevent double newlines
import_content = (
imp.content_after.rstrip("\n\r") if imp.content_after else ""
)
if import_content and import_content not in content:
lines.insert(import_end, import_content)
import_end += 1
content = line_ending.join(lines)
if has_trailing_newline:
content += line_ending
# Apply modifications
for mod in modifications:
+6 -6
View File
@@ -75,7 +75,7 @@ def install_hook(project_path: Path) -> bool:
# Handle worktrees (where .git is a file, not directory)
if git_dir.is_file():
# Read the gitdir from the file
content = git_dir.read_text().strip()
content = git_dir.read_text(encoding="utf-8").strip()
if content.startswith("gitdir:"):
git_dir = Path(content.split(":", 1)[1].strip())
else:
@@ -93,7 +93,7 @@ def install_hook(project_path: Path) -> bool:
# Check if hook already exists
if hook_path.exists():
existing = hook_path.read_text()
existing = hook_path.read_text(encoding="utf-8")
if "FileTimelineTracker" in existing:
print(f"Hook already installed at {hook_path}")
return True
@@ -104,13 +104,13 @@ def install_hook(project_path: Path) -> bool:
print(f"Backed up existing hook to {backup_path}")
# Append our hook to existing
with open(hook_path, "a") as f:
with open(hook_path, "a", encoding="utf-8") as f:
f.write("\n\n# FileTimelineTracker integration\n")
f.write(HOOK_SCRIPT.split("#!/bin/bash", 1)[1]) # Skip shebang
print(f"Appended FileTimelineTracker hook to {hook_path}")
else:
# Write new hook
hook_path.write_text(HOOK_SCRIPT)
hook_path.write_text(HOOK_SCRIPT, encoding="utf-8")
print(f"Created new hook at {hook_path}")
# Make executable
@@ -127,7 +127,7 @@ def uninstall_hook(project_path: Path) -> bool:
git_dir = project_path / ".git"
if git_dir.is_file():
content = git_dir.read_text().strip()
content = git_dir.read_text(encoding="utf-8").strip()
if content.startswith("gitdir:"):
git_dir = Path(content.split(":", 1)[1].strip())
@@ -137,7 +137,7 @@ def uninstall_hook(project_path: Path) -> bool:
print("No hook to uninstall")
return True
content = hook_path.read_text()
content = hook_path.read_text(encoding="utf-8")
if "FileTimelineTracker" not in content:
print("Hook does not contain FileTimelineTracker integration")
return True
+1 -1
View File
@@ -108,5 +108,5 @@ class MergeReport:
def save(self, path: Path) -> None:
"""Save report to JSON file."""
with open(path, "w") as f:
with open(path, "w", encoding="utf-8") as f:
json.dump(self.to_dict(), f, indent=2)
+4 -4
View File
@@ -71,13 +71,13 @@ class TimelinePersistence:
return timelines
try:
with open(index_path) as f:
with open(index_path, encoding="utf-8") as f:
index = json.load(f)
for file_path in index.get("files", []):
timeline_file = self._get_timeline_file_path(file_path)
if timeline_file.exists():
with open(timeline_file) as f:
with open(timeline_file, encoding="utf-8") as f:
data = json.load(f)
timelines[file_path] = FileTimeline.from_dict(data)
@@ -101,7 +101,7 @@ class TimelinePersistence:
timeline_file = self._get_timeline_file_path(file_path)
timeline_file.parent.mkdir(parents=True, exist_ok=True)
with open(timeline_file, "w") as f:
with open(timeline_file, "w", encoding="utf-8") as f:
json.dump(timeline.to_dict(), f, indent=2)
except Exception as e:
@@ -119,7 +119,7 @@ class TimelinePersistence:
"files": file_paths,
"last_updated": datetime.now().isoformat(),
}
with open(index_path, "w") as f:
with open(index_path, "w", encoding="utf-8") as f:
json.dump(index, f, indent=2)
def _get_timeline_file_path(self, file_path: str) -> Path:
+1 -1
View File
@@ -164,7 +164,7 @@ def load_task_metadata(spec_dir: Path) -> TaskMetadataConfig | None:
return None
try:
with open(metadata_path) as f:
with open(metadata_path, encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, OSError):
return None
+15 -7
View File
@@ -41,21 +41,29 @@ class ContextLoader:
"""Load all context files from spec directory."""
# Read spec.md
spec_file = self.spec_dir / "spec.md"
spec_content = spec_file.read_text() if spec_file.exists() else ""
spec_content = (
spec_file.read_text(encoding="utf-8") if spec_file.exists() else ""
)
# Read project_index.json
index_file = self.spec_dir / "project_index.json"
project_index = {}
if index_file.exists():
with open(index_file) as f:
project_index = json.load(f)
try:
with open(index_file, encoding="utf-8") as f:
project_index = json.load(f)
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
pass # Use empty dict on error
# Read context.json
context_file = self.spec_dir / "context.json"
task_context = {}
if context_file.exists():
with open(context_file) as f:
task_context = json.load(f)
try:
with open(context_file, encoding="utf-8") as f:
task_context = json.load(f)
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
pass # Use empty dict on error
# Determine services involved
services = task_context.get("scoped_services", [])
@@ -89,7 +97,7 @@ class ContextLoader:
requirements_file = self.spec_dir / "requirements.json"
if requirements_file.exists():
try:
with open(requirements_file) as f:
with open(requirements_file, encoding="utf-8") as f:
requirements = json.load(f)
declared_type = _normalize_workflow_type(
requirements.get("workflow_type", "")
@@ -103,7 +111,7 @@ class ContextLoader:
assessment_file = self.spec_dir / "complexity_assessment.json"
if assessment_file.exists():
try:
with open(assessment_file) as f:
with open(assessment_file, encoding="utf-8") as f:
assessment = json.load(f)
declared_type = _normalize_workflow_type(
assessment.get("workflow_type", "")
+1 -1
View File
@@ -52,7 +52,7 @@ def main():
print(f"Error: No implementation_plan.json found in {spec_dir}")
sys.exit(1)
with open(plan_file) as f:
with open(plan_file, encoding="utf-8") as f:
plan = json.load(f)
# Find first pending subtask
+4 -4
View File
@@ -33,7 +33,7 @@ class MemoryLoader:
return []
gotchas = []
content = self.gotchas_file.read_text()
content = self.gotchas_file.read_text(encoding="utf-8")
# Parse markdown list items
for line in content.split("\n"):
@@ -56,7 +56,7 @@ class MemoryLoader:
return []
patterns = []
content = self.patterns_file.read_text()
content = self.patterns_file.read_text(encoding="utf-8")
# Parse markdown sections
current_pattern = None
@@ -89,8 +89,8 @@ class MemoryLoader:
return []
try:
with open(self.history_file) as f:
with open(self.history_file, encoding="utf-8") as f:
history = json.load(f)
return history.get("attempts", [])
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
return []
+2 -2
View File
@@ -69,7 +69,7 @@ class ProjectAnalyzer:
return None
try:
with open(profile_path) as f:
with open(profile_path, encoding="utf-8") as f:
data = json.load(f)
return SecurityProfile.from_dict(data)
except (OSError, json.JSONDecodeError, KeyError):
@@ -80,7 +80,7 @@ class ProjectAnalyzer:
profile_path = self.get_profile_path()
profile_path.parent.mkdir(parents=True, exist_ok=True)
with open(profile_path, "w") as f:
with open(profile_path, "w", encoding="utf-8") as f:
json.dump(profile.to_dict(), f, indent=2)
def compute_project_hash(self) -> str:
+2 -2
View File
@@ -38,7 +38,7 @@ class ConfigParser:
def read_json(self, filename: str) -> dict | None:
"""Read a JSON file from project root."""
try:
with open(self.project_dir / filename) as f:
with open(self.project_dir / filename, encoding="utf-8") as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return None
@@ -59,7 +59,7 @@ class ConfigParser:
def read_text(self, filename: str) -> str | None:
"""Read a text file from project root."""
try:
with open(self.project_dir / filename) as f:
with open(self.project_dir / filename, encoding="utf-8") as f:
return f.read()
except (OSError, FileNotFoundError):
return None
+1 -1
View File
@@ -248,7 +248,7 @@ class StackDetector:
"**/*.yaml"
) + self.parser.glob_files("**/*.yml"):
try:
with open(yaml_file) as f:
with open(yaml_file, encoding="utf-8") as f:
content = f.read()
if "apiVersion:" in content and "kind:" in content:
self.stack.infrastructure.append("kubernetes")
+4 -4
View File
@@ -942,13 +942,13 @@ if insights["discoveries"]["patterns_found"]:
# Load existing patterns
existing_patterns = set()
if patterns_file.exists():
content = patterns_file.read_text()
content = patterns_file.read_text(encoding="utf-8")
for line in content.split("\n"):
if line.strip().startswith("- "):
existing_patterns.add(line.strip()[2:])
# Add new patterns
with open(patterns_file, "a") as f:
with open(patterns_file, "a", encoding="utf-8") as f:
if patterns_file.stat().st_size == 0:
f.write("# Code Patterns\n\n")
f.write("Established patterns to follow in this codebase:\n\n")
@@ -965,13 +965,13 @@ if insights["discoveries"]["gotchas_encountered"]:
# Load existing gotchas
existing_gotchas = set()
if gotchas_file.exists():
content = gotchas_file.read_text()
content = gotchas_file.read_text(encoding="utf-8")
for line in content.split("\n"):
if line.strip().startswith("- "):
existing_gotchas.add(line.strip()[2:])
# Add new gotchas
with open(gotchas_file, "a") as f:
with open(gotchas_file, "a", encoding="utf-8") as f:
if gotchas_file.stat().st_size == 0:
f.write("# Gotchas and Pitfalls\n\n")
f.write("Things to watch out for in this codebase:\n\n")
+2 -2
View File
@@ -336,7 +336,7 @@ def load_subtask_context(
full_path = project_dir / pattern_path
if full_path.exists():
try:
lines = full_path.read_text().split("\n")
lines = full_path.read_text(encoding="utf-8").split("\n")
if len(lines) > max_file_lines:
content = "\n".join(lines[:max_file_lines])
content += (
@@ -353,7 +353,7 @@ def load_subtask_context(
full_path = project_dir / file_path
if full_path.exists():
try:
lines = full_path.read_text().split("\n")
lines = full_path.read_text(encoding="utf-8").split("\n")
if len(lines) > max_file_lines:
content = "\n".join(lines[:max_file_lines])
content += (
+8 -8
View File
@@ -173,7 +173,7 @@ def get_planner_prompt(spec_dir: Path) -> str:
"Make sure the auto-claude/prompts/planner.md file exists."
)
prompt = prompt_file.read_text()
prompt = prompt_file.read_text(encoding="utf-8")
# Inject spec directory information at the beginning
spec_context = f"""## SPEC LOCATION
@@ -216,7 +216,7 @@ def get_coding_prompt(spec_dir: Path) -> str:
"Make sure the auto-claude/prompts/coder.md file exists."
)
prompt = prompt_file.read_text()
prompt = prompt_file.read_text(encoding="utf-8")
spec_context = f"""## SPEC LOCATION
@@ -240,7 +240,7 @@ The project root is the parent of auto-claude/. All code goes in the project roo
# Check for human input file
human_input_file = spec_dir / "HUMAN_INPUT.md"
if human_input_file.exists():
human_input = human_input_file.read_text().strip()
human_input = human_input_file.read_text(encoding="utf-8").strip()
if human_input:
spec_context += f"""## HUMAN INPUT (READ THIS FIRST!)
@@ -275,7 +275,7 @@ def _get_recovery_context(spec_dir: Path) -> str:
return ""
try:
with open(attempt_history_file) as f:
with open(attempt_history_file, encoding="utf-8") as f:
history = json.load(f)
# Check for stuck subtasks
@@ -321,7 +321,7 @@ Subtasks with previous attempts:
return ""
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
return ""
@@ -344,7 +344,7 @@ def get_followup_planner_prompt(spec_dir: Path) -> str:
"Make sure the auto-claude/prompts/followup_planner.md file exists."
)
prompt = prompt_file.read_text()
prompt = prompt_file.read_text(encoding="utf-8")
# Inject spec directory information at the beginning
spec_context = f"""## SPEC LOCATION (FOLLOW-UP MODE)
@@ -405,7 +405,7 @@ def is_first_run(spec_dir: Path) -> bool:
# Check if any phase has subtasks
total_subtasks = sum(len(phase.get("subtasks", [])) for phase in phases)
return total_subtasks == 0
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
# If we can't read the file, treat as first run
return True
@@ -426,7 +426,7 @@ def _load_prompt_file(filename: str) -> str:
prompt_file = PROMPTS_DIR / filename
if not prompt_file.exists():
raise FileNotFoundError(f"Prompt file not found: {prompt_file}")
return prompt_file.read_text()
return prompt_file.read_text(encoding="utf-8")
def get_qa_reviewer_prompt(spec_dir: Path, project_dir: Path) -> str:
+3 -3
View File
@@ -21,9 +21,9 @@ def load_implementation_plan(spec_dir: Path) -> dict | None:
if not plan_file.exists():
return None
try:
with open(plan_file) as f:
with open(plan_file, encoding="utf-8") as f:
return json.load(f)
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
return None
@@ -31,7 +31,7 @@ def save_implementation_plan(spec_dir: Path, plan: dict) -> bool:
"""Save the implementation plan JSON."""
plan_file = spec_dir / "implementation_plan.json"
try:
with open(plan_file, "w") as f:
with open(plan_file, "w", encoding="utf-8") as f:
json.dump(plan, f, indent=2)
return True
except OSError:
+1 -1
View File
@@ -38,7 +38,7 @@ def load_qa_fixer_prompt() -> str:
prompt_file = QA_PROMPTS_DIR / "qa_fixer.md"
if not prompt_file.exists():
raise FileNotFoundError(f"QA fixer prompt not found: {prompt_file}")
return prompt_file.read_text()
return prompt_file.read_text(encoding="utf-8")
# =============================================================================
+5 -5
View File
@@ -324,7 +324,7 @@ These issues have appeared {RECURRING_ISSUE_THRESHOLD}+ times without being reso
- `implementation_plan.json` - Full iteration history
"""
escalation_file.write_text(content)
escalation_file.write_text(content, encoding="utf-8")
print(f"\n📝 Escalation file created: {escalation_file}")
@@ -345,7 +345,7 @@ def create_manual_test_plan(spec_dir: Path, spec_name: str) -> Path:
spec_file = spec_dir / "spec.md"
spec_content = ""
if spec_file.exists():
spec_content = spec_file.read_text()
spec_content = spec_file.read_text(encoding="utf-8")
# Extract acceptance criteria from spec if present
acceptance_criteria = []
@@ -439,7 +439,7 @@ _Add any observations or issues found during testing_
"""
manual_plan_file.write_text(content)
manual_plan_file.write_text(content, encoding="utf-8")
return manual_plan_file
@@ -460,9 +460,9 @@ def check_test_discovery(spec_dir: Path) -> dict[str, Any] | None:
return None
try:
with open(discovery_file) as f:
with open(discovery_file, encoding="utf-8") as f:
return json.load(f)
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
return None
+1 -1
View File
@@ -159,7 +159,7 @@ def display_plan_summary(spec_dir: Path) -> None:
return
try:
with open(plan_file) as f:
with open(plan_file, encoding="utf-8") as f:
plan = json.load(f)
except (OSError, json.JSONDecodeError) as e:
print_status(f"Could not read implementation_plan.json: {e}", "error")
+3 -3
View File
@@ -85,7 +85,7 @@ class ReviewState:
def save(self, spec_dir: Path) -> None:
"""Save state to the spec directory."""
state_file = Path(spec_dir) / REVIEW_STATE_FILE
with open(state_file, "w") as f:
with open(state_file, "w", encoding="utf-8") as f:
json.dump(self.to_dict(), f, indent=2)
@classmethod
@@ -100,9 +100,9 @@ class ReviewState:
return cls()
try:
with open(state_file) as f:
with open(state_file, encoding="utf-8") as f:
return cls.from_dict(json.load(f))
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
return cls()
def is_approved(self) -> bool:
@@ -48,7 +48,7 @@ class CacheManager:
return None
print(f"✓ Using cached AI insights ({hours_old:.1f} hours old)")
return json.loads(self.cache_file.read_text())
return json.loads(self.cache_file.read_text(encoding="utf-8"))
def save_result(self, result: dict[str, Any]) -> None:
"""
@@ -57,5 +57,5 @@ class CacheManager:
Args:
result: Analysis result to cache
"""
self.cache_file.write_text(json.dumps(result, indent=2))
self.cache_file.write_text(json.dumps(result, indent=2), encoding="utf-8")
print(f"\n✓ AI insights cached to: {self.cache_file}")
@@ -87,7 +87,7 @@ class ClaudeAnalysisClient:
}
settings_file = self.project_dir / ".claude_ai_analyzer_settings.json"
with open(settings_file, "w") as f:
with open(settings_file, "w", encoding="utf-8") as f:
json.dump(settings, f, indent=2)
return settings_file
+1 -1
View File
@@ -56,7 +56,7 @@ def main() -> int:
print(f"Run: python analyzer.py --project-dir {args.project_dir} --index")
return 1
project_index = json.loads(index_path.read_text())
project_index = json.loads(index_path.read_text(encoding="utf-8"))
# Import here to avoid import errors if dependencies are missing
try:
+2 -2
View File
@@ -369,7 +369,7 @@ class AuditLogger:
try:
log_file = self._get_log_file_path()
with open(log_file, "a") as f:
with open(log_file, "a", encoding="utf-8") as f:
f.write(entry.to_json() + "\n")
except Exception as e:
logger.error(f"Failed to write audit log: {e}")
@@ -585,7 +585,7 @@ class AuditLogger:
for log_file in sorted(self.log_dir.glob("audit_*.jsonl"), reverse=True):
try:
with open(log_file) as f:
with open(log_file, encoding="utf-8") as f:
for line in f:
if not line.strip():
continue
+4 -4
View File
@@ -368,7 +368,7 @@ class IssueBatch:
if not batch_file.exists():
return None
with open(batch_file) as f:
with open(batch_file, encoding="utf-8") as f:
data = json.load(f)
return cls.from_dict(data)
@@ -448,7 +448,7 @@ class IssueBatcher:
"""Load batch index from disk."""
index_file = self.github_dir / "batches" / "index.json"
if index_file.exists():
with open(index_file) as f:
with open(index_file, encoding="utf-8") as f:
data = json.load(f)
self._batch_index = {
int(k): v for k, v in data.get("issue_to_batch", {}).items()
@@ -460,7 +460,7 @@ class IssueBatcher:
batches_dir.mkdir(parents=True, exist_ok=True)
index_file = batches_dir / "index.json"
with open(index_file, "w") as f:
with open(index_file, "w", encoding="utf-8") as f:
json.dump(
{
"issue_to_batch": self._batch_index,
@@ -1107,7 +1107,7 @@ class IssueBatcher:
batches = []
for batch_file in batches_dir.glob("batch_*.json"):
try:
with open(batch_file) as f:
with open(batch_file, encoding="utf-8") as f:
data = json.load(f)
batches.append(IssueBatch.from_dict(data))
except Exception as e:
@@ -230,7 +230,7 @@ class BatchValidator:
}
settings_file = self.project_dir / ".batch_validator_settings.json"
with open(settings_file, "w") as f:
with open(settings_file, "w", encoding="utf-8") as f:
json.dump(settings, f)
try:
+1 -1
View File
@@ -85,7 +85,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))
+6 -6
View File
@@ -248,9 +248,9 @@ class DataCleaner:
) -> bool:
"""Process a single file for cleanup."""
try:
with open(file_path) as f:
with open(file_path, encoding="utf-8") as f:
data = json.load(f)
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
# Corrupted file, mark for deletion
if not dry_run:
file_size = file_path.stat().st_size
@@ -327,7 +327,7 @@ class DataCleaner:
data["_archived_at"] = datetime.now(timezone.utc).isoformat()
data["_original_path"] = str(file_path)
with open(archive_path, "w") as f:
with open(archive_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
# Remove original
@@ -350,7 +350,7 @@ class DataCleaner:
continue
try:
with open(index_path) as f:
with open(index_path, encoding="utf-8") as f:
index_data = json.load(f)
if not isinstance(index_data, dict):
@@ -375,12 +375,12 @@ class DataCleaner:
for key in to_remove:
del items[key]
with open(index_path, "w") as f:
with open(index_path, "w", encoding="utf-8") as f:
json.dump(index_data, f, indent=2)
result.pruned_index_entries += pruned
except (OSError, json.JSONDecodeError, KeyError):
except (OSError, json.JSONDecodeError, UnicodeDecodeError, KeyError):
result.errors.append(f"Error pruning index: {index_path}")
async def _clean_audit_logs(
@@ -782,7 +782,7 @@ class PRContextGatherer:
# Check for package.json (Node.js)
if (self.project_dir / "package.json").exists():
try:
with open(self.project_dir / "package.json") as f:
with open(self.project_dir / "package.json", encoding="utf-8") as f:
pkg_data = json.load(f)
if "workspaces" in pkg_data:
structure_info.append(
+2 -2
View File
@@ -347,7 +347,7 @@ class DuplicateDetector:
if not cache_file.exists():
return {}
with open(cache_file) as f:
with open(cache_file, encoding="utf-8") as f:
data = json.load(f)
cache = {}
@@ -365,7 +365,7 @@ class DuplicateDetector:
"embeddings": [e.to_dict() for e in cache.values()],
"last_updated": datetime.now(timezone.utc).isoformat(),
}
with open(cache_file, "w") as f:
with open(cache_file, "w", encoding="utf-8") as f:
json.dump(data, f)
async def get_embedding(
+15 -8
View File
@@ -212,7 +212,7 @@ class FileLock:
@contextmanager
def atomic_write(filepath: str | Path, mode: str = "w"):
def atomic_write(filepath: str | Path, mode: str = "w", encoding: str = "utf-8"):
"""
Atomic file write using temp file and rename.
@@ -222,6 +222,7 @@ def atomic_write(filepath: str | Path, mode: str = "w"):
Args:
filepath: Target file path
mode: File open mode (default: "w")
encoding: Text encoding (default: "utf-8")
Example:
with atomic_write("/path/to/file.json") as f:
@@ -236,8 +237,9 @@ def atomic_write(filepath: str | Path, mode: str = "w"):
)
try:
# Open temp file with requested mode
with os.fdopen(fd, mode) as f:
# Open temp file with requested mode and encoding
# Only use encoding for text modes (not binary modes)
with os.fdopen(fd, mode, encoding=encoding if "b" not in mode else None) as f:
yield f
# Atomic replace - succeeds or fails completely
@@ -254,7 +256,10 @@ def atomic_write(filepath: str | Path, mode: str = "w"):
@asynccontextmanager
async def locked_write(
filepath: str | Path, timeout: float = 5.0, mode: str = "w"
filepath: str | Path,
timeout: float = 5.0,
mode: str = "w",
encoding: str = "utf-8",
) -> Any:
"""
Async context manager combining file locking and atomic writes.
@@ -266,6 +271,7 @@ async def locked_write(
filepath: Target file path
timeout: Lock timeout in seconds (default: 5.0)
mode: File open mode (default: "w")
encoding: Text encoding (default: "utf-8")
Example:
async with locked_write("/path/to/file.json", timeout=5.0) as f:
@@ -291,7 +297,8 @@ async def locked_write(
try:
# Open temp file and yield to caller
f = os.fdopen(fd, mode)
# Only use encoding for text modes (not binary modes)
f = os.fdopen(fd, mode, encoding=encoding if "b" not in mode else None)
try:
yield f
finally:
@@ -348,7 +355,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 +448,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
@@ -459,7 +466,7 @@ async def locked_json_update(
)
try:
with os.fdopen(fd, "w") as f:
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(updated_data, f, indent=indent)
await asyncio.get_running_loop().run_in_executor(
+1 -1
View File
@@ -299,7 +299,7 @@ class LearningTracker:
"""Load all outcomes from disk."""
for file in self.learning_dir.glob("*_outcomes.json"):
try:
with open(file) as f:
with open(file, encoding="utf-8") as f:
data = json.load(f)
for item in data.get("outcomes", []):
outcome = ReviewOutcome.from_dict(item)
+4 -4
View File
@@ -409,7 +409,7 @@ class LifecycleManager:
if not file.exists():
return None
with open(file) as f:
with open(file, encoding="utf-8") as f:
data = json.load(f)
return IssueLifecycle.from_dict(data)
@@ -426,7 +426,7 @@ class LifecycleManager:
def save(self, lifecycle: IssueLifecycle) -> None:
"""Save lifecycle state."""
file = self._get_file(lifecycle.repo, lifecycle.issue_number)
with open(file, "w") as f:
with open(file, "w", encoding="utf-8") as f:
json.dump(lifecycle.to_dict(), f, indent=2)
def transition(
@@ -509,7 +509,7 @@ class LifecycleManager:
safe_repo = repo.replace("/", "_")
for file in self.lifecycle_dir.glob(f"{safe_repo}_*.json"):
with open(file) as f:
with open(file, encoding="utf-8") as f:
data = json.load(f)
lifecycle = IssueLifecycle.from_dict(data)
if lifecycle.current_state == state:
@@ -523,7 +523,7 @@ class LifecycleManager:
safe_repo = repo.replace("/", "_")
for file in self.lifecycle_dir.glob(f"{safe_repo}_*.json"):
with open(file) as f:
with open(file, encoding="utf-8") as f:
data = json.load(f)
state = data.get("current_state", "new")
counts[state] = counts.get(state, 0) + 1
@@ -193,7 +193,7 @@ class GitHubMemoryIntegration:
insights_file = self.memory_dir / f"{self.repo.replace('/', '_')}_insights.json"
if insights_file.exists():
try:
with open(insights_file) as f:
with open(insights_file, encoding="utf-8") as f:
self._local_insights = json.load(f).get("insights", [])
except (json.JSONDecodeError, KeyError):
self._local_insights = []
@@ -201,7 +201,7 @@ class GitHubMemoryIntegration:
def _save_local_insights(self) -> None:
"""Save insights locally."""
insights_file = self.memory_dir / f"{self.repo.replace('/', '_')}_insights.json"
with open(insights_file, "w") as f:
with open(insights_file, "w", encoding="utf-8") as f:
json.dump(
{
"repo": self.repo,
+5 -5
View File
@@ -550,7 +550,7 @@ class PRReviewResult:
if not review_file.exists():
return None
with open(review_file) as f:
with open(review_file, encoding="utf-8") as f:
return cls.from_dict(json.load(f))
@@ -663,7 +663,7 @@ class TriageResult:
if not triage_file.exists():
return None
with open(triage_file) as f:
with open(triage_file, encoding="utf-8") as f:
return cls.from_dict(json.load(f))
@@ -797,7 +797,7 @@ class AutoFixState:
if not autofix_file.exists():
return None
with open(autofix_file) as f:
with open(autofix_file, encoding="utf-8") as f:
return cls.from_dict(json.load(f))
@@ -879,7 +879,7 @@ class GitHubRunnerConfig:
settings.pop("token", None)
settings.pop("bot_token", None)
with open(config_file, "w") as f:
with open(config_file, "w", encoding="utf-8") as f:
json.dump(settings, f, indent=2)
@classmethod
@@ -890,7 +890,7 @@ class GitHubRunnerConfig:
config_file = github_dir / "config.json"
if config_file.exists():
with open(config_file) as f:
with open(config_file, encoding="utf-8") as f:
settings = json.load(f)
else:
settings = {}
+2 -2
View File
@@ -340,7 +340,7 @@ class MultiRepoConfig:
"repos": [c.to_dict() for c in self.repos.values()],
"last_updated": datetime.now(timezone.utc).isoformat(),
}
with open(file_path, "w") as f:
with open(file_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
@classmethod
@@ -349,7 +349,7 @@ class MultiRepoConfig:
if not config_file.exists():
return cls()
with open(config_file) as f:
with open(config_file, encoding="utf-8") as f:
data = json.load(f)
repos = [RepoConfig.from_dict(r) for r in data.get("repos", [])]
+2 -2
View File
@@ -301,7 +301,7 @@ class OnboardingManager:
if self.state_file.exists():
try:
with open(self.state_file) as f:
with open(self.state_file, encoding="utf-8") as f:
data = json.load(f)
self._state = OnboardingState.from_dict(data)
except (json.JSONDecodeError, KeyError):
@@ -315,7 +315,7 @@ class OnboardingManager:
"""Save onboarding state."""
state = self.get_state()
self.state_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.state_file, "w") as f:
with open(self.state_file, "w", encoding="utf-8") as f:
json.dump(state.to_dict(), f, indent=2)
async def run_setup(
+3 -3
View File
@@ -300,7 +300,7 @@ class OverrideManager:
if not grace_file.exists():
return None
with open(grace_file) as f:
with open(grace_file, encoding="utf-8") as f:
data = json.load(f)
entry_data = data.get("entries", {}).get(str(issue_number))
@@ -778,7 +778,7 @@ class OverrideManager:
if not history_file.exists():
return []
with open(history_file) as f:
with open(history_file, encoding="utf-8") as f:
data = json.load(f)
records = []
@@ -809,7 +809,7 @@ class OverrideManager:
if not history_file.exists():
return {"total": 0, "by_type": {}, "by_actor": {}}
with open(history_file) as f:
with open(history_file, encoding="utf-8") as f:
data = json.load(f)
stats = {
@@ -224,7 +224,7 @@ class PurgeStrategy:
result: PurgeResult to update
"""
try:
with open(file_path) as f:
with open(file_path, encoding="utf-8") as f:
data = json.load(f)
# Verify key matches value
+2 -2
View File
@@ -613,9 +613,9 @@ async def cmd_approve_batches(args) -> int:
# Load approved batches from file
try:
with open(args.batch_file) as f:
with open(args.batch_file, encoding="utf-8") as f:
approved_batches = json.load(f)
except (json.JSONDecodeError, FileNotFoundError) as e:
except (json.JSONDecodeError, FileNotFoundError, UnicodeDecodeError) as e:
safe_print(f"Error loading batch file: {e}")
return 1
@@ -75,11 +75,11 @@ def test_find_test_files(tmp_path):
# Create source file
source_file = src_dir / "utils.ts"
source_file.write_text("export const add = (a, b) => a + b;")
source_file.write_text("export const add = (a, b) => a + b;", encoding="utf-8")
# Create test file
test_file = src_dir / "utils.test.ts"
test_file.write_text("import { add } from './utils';")
test_file.write_text("import { add } from './utils';", encoding="utf-8")
gatherer = PRContextGatherer(project_dir, 1)
@@ -99,11 +99,11 @@ def test_resolve_import_path(tmp_path):
# Create imported file
utils_file = src_dir / "utils.ts"
utils_file.write_text("export const helper = () => {};")
utils_file.write_text("export const helper = () => {};", encoding="utf-8")
# Create importing file
app_file = src_dir / "app.ts"
app_file.write_text("import { helper } from './utils';")
app_file.write_text("import { helper } from './utils';", encoding="utf-8")
gatherer = PRContextGatherer(project_dir, 1)
@@ -128,7 +128,7 @@ def test_detect_repo_structure_monorepo(tmp_path):
# Create package.json with workspaces
package_json = project_dir / "package.json"
package_json.write_text('{"workspaces": ["apps/*"]}')
package_json.write_text('{"workspaces": ["apps/*"]}', encoding="utf-8")
gatherer = PRContextGatherer(project_dir, 1)
@@ -147,7 +147,7 @@ def test_detect_repo_structure_python(tmp_path):
# Create pyproject.toml
pyproject = project_dir / "pyproject.toml"
pyproject.write_text("[tool.poetry]\\nname = 'test'")
pyproject.write_text("[tool.poetry]\nname = 'test'", encoding="utf-8")
gatherer = PRContextGatherer(project_dir, 1)
@@ -163,8 +163,8 @@ def test_find_config_files(tmp_path):
src_dir.mkdir(parents=True)
# Create config files
(src_dir / "tsconfig.json").write_text("{}")
(src_dir / "package.json").write_text("{}")
(src_dir / "tsconfig.json").write_text("{}", encoding="utf-8")
(src_dir / "package.json").write_text("{}", encoding="utf-8")
gatherer = PRContextGatherer(project_dir, 1)
@@ -28,7 +28,7 @@ async def test_basic_file_lock():
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "test.txt"
test_file.write_text("initial content")
test_file.write_text("initial content", encoding="utf-8")
# Acquire lock and hold it
async with FileLock(test_file, timeout=5.0):
@@ -55,7 +55,7 @@ async def test_locked_write():
print(f"✓ Written to {test_file.name}")
# Verify data was written correctly
with open(test_file) as f:
with open(test_file, encoding="utf-8") as f:
loaded = json.load(f)
assert loaded == data
print(f"✓ Data verified: {loaded}")
@@ -113,12 +113,12 @@ async def test_concurrent_updates_without_lock():
test_file = Path(tmpdir) / "unsafe.json"
# Initialize counter
test_file.write_text(json.dumps({"count": 0}))
test_file.write_text(json.dumps({"count": 0}), encoding="utf-8")
async def unsafe_increment():
"""Increment without locking - RACE CONDITION!"""
# Read
with open(test_file) as f:
with open(test_file, encoding="utf-8") as f:
data = json.load(f)
# Simulate some processing
@@ -126,14 +126,14 @@ async def test_concurrent_updates_without_lock():
# Write
data["count"] += 1
with open(test_file, "w") as f:
with open(test_file, "w", encoding="utf-8") as f:
json.dump(data, f)
# Run 10 concurrent increments
await asyncio.gather(*[unsafe_increment() for _ in range(10)])
# Check final count
with open(test_file) as f:
with open(test_file, encoding="utf-8") as f:
final = json.load(f)
print("✗ Expected count: 10")
@@ -182,7 +182,7 @@ async def test_lock_timeout():
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "timeout.json"
test_file.write_text(json.dumps({"data": "test"}))
test_file.write_text(json.dumps({"data": "test"}), encoding="utf-8")
# Acquire lock and hold it
lock1 = FileLock(test_file, timeout=1.0)
+17 -12
View File
@@ -420,9 +420,13 @@ class TrustManager:
state_file = self._get_state_file(repo)
if state_file.exists():
with open(state_file) as f:
data = json.load(f)
state = TrustState.from_dict(data)
try:
with open(state_file, encoding="utf-8") as f:
data = json.load(f)
state = TrustState.from_dict(data)
except (json.JSONDecodeError, UnicodeDecodeError):
# Return default state if file is corrupted
state = TrustState(repo=repo)
else:
state = TrustState(repo=repo)
@@ -438,12 +442,9 @@ class TrustManager:
# Write with restrictive permissions (0o600 = owner read/write only)
fd = os.open(str(state_file), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
try:
with os.fdopen(fd, "w") as f:
json.dump(state.to_dict(), f, indent=2)
except Exception:
os.close(fd)
raise
# os.fdopen takes ownership of fd and will close it when the with block exits
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(state.to_dict(), f, indent=2)
def get_trust_level(self, repo: str) -> TrustLevel:
"""Get current trust level for a repository."""
@@ -514,9 +515,13 @@ class TrustManager:
"""Get trust states for all repos."""
states = []
for file in self.trust_dir.glob("*.json"):
with open(file) as f:
data = json.load(f)
states.append(TrustState.from_dict(data))
try:
with open(file, encoding="utf-8") as f:
data = json.load(f)
states.append(TrustState.from_dict(data))
except (json.JSONDecodeError, UnicodeDecodeError):
# Skip corrupted state files
continue
return states
def get_summary(self) -> dict[str, Any]:
+1 -1
View File
@@ -253,7 +253,7 @@ def load_gitlab_config(project_dir: Path) -> GitLabConfig | None:
return None
try:
with open(config_path) as f:
with open(config_path, encoding="utf-8") as f:
data = json.load(f)
token = data.get("token")
+2 -2
View File
@@ -184,7 +184,7 @@ class MRReviewResult:
mr_dir.mkdir(parents=True, exist_ok=True)
review_file = mr_dir / f"review_{self.mr_iid}.json"
with open(review_file, "w") as f:
with open(review_file, "w", encoding="utf-8") as f:
json.dump(self.to_dict(), f, indent=2)
@classmethod
@@ -194,7 +194,7 @@ class MRReviewResult:
if not review_file.exists():
return None
with open(review_file) as f:
with open(review_file, encoding="utf-8") as f:
return cls.from_dict(json.load(f))
+1 -1
View File
@@ -91,7 +91,7 @@ def get_config(args) -> GitLabRunnerConfig:
config_path = Path(args.project_dir) / ".auto-claude" / "gitlab" / "config.json"
if config_path.exists():
try:
with open(config_path) as f:
with open(config_path, encoding="utf-8") as f:
data = json.load(f)
project = data.get("project", "")
instance_url = data.get("instance_url", instance_url)
+2 -2
View File
@@ -58,7 +58,7 @@ def load_project_context(project_dir: str) -> str:
index_path = Path(project_dir) / ".auto-claude" / "project_index.json"
if index_path.exists():
try:
with open(index_path) as f:
with open(index_path, encoding="utf-8") as f:
index = json.load(f)
# Summarize the index for context
summary = {
@@ -77,7 +77,7 @@ def load_project_context(project_dir: str) -> str:
roadmap_path = Path(project_dir) / ".auto-claude" / "roadmap" / "roadmap.json"
if roadmap_path.exists():
try:
with open(roadmap_path) as f:
with open(roadmap_path, encoding="utf-8") as f:
roadmap = json.load(f)
# Summarize roadmap
features = roadmap.get("features", [])
@@ -123,7 +123,7 @@ Output your findings to competitor_analysis.json.
Returns RoadmapPhaseResult if validation succeeds, None otherwise.
"""
try:
with open(self.analysis_file) as f:
with open(self.analysis_file, encoding="utf-8") as f:
data = json.load(f)
if "competitors" in data:
@@ -146,7 +146,7 @@ Output your findings to competitor_analysis.json.
def _create_disabled_analysis_file(self):
"""Create an analysis file indicating the feature is disabled."""
with open(self.analysis_file, "w") as f:
with open(self.analysis_file, "w", encoding="utf-8") as f:
json.dump(
{
"enabled": False,
@@ -181,5 +181,5 @@ Output your findings to competitor_analysis.json.
if errors:
data["errors"] = errors
with open(self.analysis_file, "w") as f:
with open(self.analysis_file, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
+1 -1
View File
@@ -102,7 +102,7 @@ class AgentExecutor:
return False, f"Prompt not found: {prompt_path}"
# Load prompt
prompt = prompt_path.read_text()
prompt = prompt_path.read_text(encoding="utf-8")
debug_detailed(
"roadmap_executor", "Loaded prompt file", prompt_length=len(prompt)
)
@@ -81,7 +81,7 @@ class GraphHintsProvider:
def _create_disabled_hints_file(self):
"""Create a hints file indicating Graphiti is disabled."""
with open(self.hints_file, "w") as f:
with open(self.hints_file, "w", encoding="utf-8") as f:
json.dump(
{
"enabled": False,
@@ -95,7 +95,7 @@ class GraphHintsProvider:
def _save_hints(self, hints: list):
"""Save retrieved hints to file."""
with open(self.hints_file, "w") as f:
with open(self.hints_file, "w", encoding="utf-8") as f:
json.dump(
{
"enabled": True,
@@ -109,7 +109,7 @@ class GraphHintsProvider:
def _save_error_hints(self, error: str):
"""Save error information to hints file."""
with open(self.hints_file, "w") as f:
with open(self.hints_file, "w", encoding="utf-8") as f:
json.dump(
{
"enabled": True,

Some files were not shown because too many files have changed in this diff Show More