Compare commits

..

5 Commits

Author SHA1 Message Date
Test User 29d6c8cb14 fix: enhance execution phase handling in ProjectStore
- Added logic to correct stale execution phases when a task reaches a terminal status (human_review, done, pr_created) but the persisted executionPhase is outdated.
- Improved the determination of execution progress to ensure accurate representation of task status, particularly for tasks that have transitioned from running to completed states.

These changes improve the reliability of task status updates and enhance user experience by ensuring accurate visual feedback on task execution phases.
2026-02-05 22:18:43 +01:00
Test User 01295eff99 adjust for claude mem 2026-02-05 22:18:25 +01:00
Test User a41e5a0eba fix: improve execution phase handling in ProjectStore and TaskCard components
- Updated ProjectStore to prioritize xstateState for execution phase determination, ensuring accurate task status representation.
- Enhanced TaskCard to only display execution phase badges for actively running tasks, preventing stale badges from appearing for recovered tasks.

These changes enhance the reliability of task status updates and improve user experience by ensuring accurate visual feedback on task execution phases.
2026-02-05 20:34:56 +01:00
Test User 5ed85e86ab feat: enhance Claude Code version checking with force refresh option
- Updated checkClaudeCodeVersion API to accept an optional forceRefresh parameter, allowing users to bypass the cache and fetch fresh data from npm.
- Modified related components to support the new parameter, enabling a manual refresh of the Claude Code version.
- Improved user experience by providing immediate feedback on version checks, especially when the user explicitly requests a refresh.

This change enhances the flexibility of version management for the Claude Code CLI.
2026-02-05 20:10:54 +01:00
Test User 071379a109 feat: enhance task management by implementing maxParallelTasks limit in startTask function
- Updated startTask function to respect maxParallelTasks setting, queuing tasks if the limit is reached.
- Integrated project store to retrieve project-specific settings.
- Added logic to count current in-progress tasks and conditionally queue tasks based on the maxParallelTasks configuration.

This change improves task handling efficiency and prevents overload during concurrent task execution.
2026-02-05 19:47:34 +01:00
833 changed files with 15040 additions and 202265 deletions
+54 -40
View File
@@ -1,36 +1,52 @@
#!/bin/sh
# =============================================================================
# GIT WORKTREE ENVIRONMENT CLEANUP
# GIT WORKTREE CONTEXT HANDLING
# =============================================================================
# Git automatically sets GIT_DIR (and CWD to the working tree root) before
# running hooks -- even in worktrees. We do NOT need to manually parse .git
# files or export GIT_DIR/GIT_WORK_TREE.
# When running in a worktree, we need to preserve git context to prevent HEAD
# corruption. However, we must also CLEAR these variables when NOT in a worktree
# to prevent cross-worktree contamination (files leaking between worktrees).
#
# However, external tools (IDEs, agents, parent shells) may leave stale
# GIT_DIR/GIT_WORK_TREE values in the environment. If these point to a
# different repo or worktree, git commands in this hook would target the
# wrong repository. Unsetting them lets git re-resolve the correct values
# from the working directory.
# The bug: If GIT_DIR/GIT_WORK_TREE are set from a previous worktree session
# and this hook runs in the main repo (where .git is a directory, not a file),
# git commands will target the wrong repository, causing files to appear as
# untracked in the wrong location.
#
# Fix: Explicitly unset these variables when NOT in a worktree context.
# =============================================================================
unset GIT_DIR
unset GIT_WORK_TREE
if [ -f ".git" ]; then
# We're in a worktree (.git is a file pointing to the actual git dir)
# Use -n with /p to only print lines that match the gitdir: prefix, head -1 for safety
WORKTREE_GIT_DIR=$(sed -n 's/^gitdir: //p' .git | head -1)
if [ -n "$WORKTREE_GIT_DIR" ] && [ -d "$WORKTREE_GIT_DIR" ]; then
export GIT_DIR="$WORKTREE_GIT_DIR"
export GIT_WORK_TREE="$(pwd)"
else
# .git file exists but is malformed or points to non-existent directory
# CRITICAL: Clear any inherited GIT_DIR/GIT_WORK_TREE to prevent cross-worktree leakage
unset GIT_DIR
unset GIT_WORK_TREE
fi
else
# We're in the main repo (.git is a directory)
# CRITICAL: Clear any inherited GIT_DIR/GIT_WORK_TREE to prevent cross-worktree leakage
unset GIT_DIR
unset GIT_WORK_TREE
fi
# =============================================================================
# SAFETY CHECK: Detect and fix corrupted core.worktree configuration
# =============================================================================
# core.worktree lives in the SHARED .git/config (not per-worktree). If any
# process accidentally writes it (e.g., running `git init` with a leaked
# GIT_WORK_TREE), ALL repos and worktrees see the wrong working tree root,
# causing files from one worktree to "leak" into others.
#
# This check runs from both main repo and worktree contexts since the config
# is shared and corruption can happen from either.
CORE_WORKTREE=$(git config --get core.worktree 2>/dev/null || true)
if [ -n "$CORE_WORKTREE" ]; then
echo "Warning: Detected corrupted core.worktree setting ('$CORE_WORKTREE'), removing it..."
if ! git config --unset core.worktree 2>/dev/null; then
echo "Warning: Failed to unset core.worktree. Manual intervention may be needed."
# If core.worktree is set in the main repo's config (pointing to a worktree),
# this indicates previous corruption. Fix it automatically.
if [ ! -f ".git" ]; then
CORE_WORKTREE=$(git config --get core.worktree 2>/dev/null || true)
if [ -n "$CORE_WORKTREE" ]; then
echo "Warning: Detected corrupted core.worktree setting, removing it..."
if ! git config --unset core.worktree 2>/dev/null; then
echo "Warning: Failed to unset core.worktree. Manual intervention may be needed."
fi
fi
fi
@@ -162,39 +178,37 @@ if git diff --cached --name-only | grep -q "^apps/backend/.*\.py$"; then
fi
# Run pytest (skip slow/integration tests and Windows-incompatible tests for pre-commit speed)
# Run from repo root (not apps/backend) so tests that use Path.resolve() get correct CWD.
# PYTHONPATH includes apps/backend so imports resolve correctly.
# Use subshell to isolate directory changes and prevent worktree corruption
echo "Running Python tests..."
(
cd apps/backend
# Tests to skip: graphiti (external deps), merge_file_tracker/service_orchestrator/worktree/workspace (Windows path/git issues)
# Also skip tests that require optional dependencies (pydantic structured outputs)
# Also skip gitlab_e2e (e2e test sensitive to test-ordering env contamination, validated by CI)
IGNORE_TESTS="--ignore=tests/test_graphiti.py --ignore=tests/test_merge_file_tracker.py --ignore=tests/test_service_orchestrator.py --ignore=tests/test_worktree.py --ignore=tests/test_workspace.py --ignore=tests/test_finding_validation.py --ignore=tests/test_sdk_structured_output.py --ignore=tests/test_structured_outputs.py --ignore=tests/test_gitlab_e2e.py"
IGNORE_TESTS="--ignore=../../tests/test_graphiti.py --ignore=../../tests/test_merge_file_tracker.py --ignore=../../tests/test_service_orchestrator.py --ignore=../../tests/test_worktree.py --ignore=../../tests/test_workspace.py --ignore=../../tests/test_finding_validation.py --ignore=../../tests/test_sdk_structured_output.py --ignore=../../tests/test_structured_outputs.py"
# Determine Python executable from venv
VENV_PYTHON=""
if [ -f "apps/backend/.venv/bin/python" ]; then
VENV_PYTHON="apps/backend/.venv/bin/python"
elif [ -f "apps/backend/.venv/Scripts/python.exe" ]; then
VENV_PYTHON="apps/backend/.venv/Scripts/python.exe"
if [ -f ".venv/bin/python" ]; then
VENV_PYTHON=".venv/bin/python"
elif [ -f ".venv/Scripts/python.exe" ]; then
VENV_PYTHON=".venv/Scripts/python.exe"
fi
# -k "not windows_path": skip tests using fake Windows paths that break
# Path.resolve() on macOS/Linux. These are validated by CI on all platforms.
if [ -n "$VENV_PYTHON" ]; then
# Check if pytest is installed in venv
if $VENV_PYTHON -c "import pytest" 2>/dev/null; then
PYTHONPATH=apps/backend $VENV_PYTHON -m pytest tests/ -v --tb=short -x -m "not slow and not integration" -k "not windows_path" $IGNORE_TESTS
PYTHONPATH=. $VENV_PYTHON -m pytest ../../tests/ -v --tb=short -x -m "not slow and not integration" $IGNORE_TESTS
else
echo "Warning: pytest not installed in venv. Installing test dependencies..."
$VENV_PYTHON -m pip install -q -r tests/requirements-test.txt
PYTHONPATH=apps/backend $VENV_PYTHON -m pytest tests/ -v --tb=short -x -m "not slow and not integration" -k "not windows_path" $IGNORE_TESTS
$VENV_PYTHON -m pip install -q -r ../../tests/requirements-test.txt
PYTHONPATH=. $VENV_PYTHON -m pytest ../../tests/ -v --tb=short -x -m "not slow and not integration" $IGNORE_TESTS
fi
elif [ -d "apps/backend/.venv" ]; then
elif [ -d ".venv" ]; then
echo "Warning: venv exists but Python not found in it, using system Python"
PYTHONPATH=apps/backend python -m pytest tests/ -v --tb=short -x -m "not slow and not integration" -k "not windows_path" $IGNORE_TESTS
PYTHONPATH=. python -m pytest ../../tests/ -v --tb=short -x -m "not slow and not integration" $IGNORE_TESTS
else
echo "Warning: No .venv found in apps/backend, using system Python"
PYTHONPATH=apps/backend python -m pytest tests/ -v --tb=short -x -m "not slow and not integration" -k "not windows_path" $IGNORE_TESTS
PYTHONPATH=. python -m pytest ../../tests/ -v --tb=short -x -m "not slow and not integration" $IGNORE_TESTS
fi
)
if [ $? -ne 0 ]; then
-2
View File
@@ -74,8 +74,6 @@
### 🐛 Bug Fixes
- Fixed task logs disappearing after app restart in development mode (issue #1657)
- Fixed Kanban board status flip-flopping and multi-location task deletion
- Fixed Windows CLI detection and version selection UX issues
+11
View File
@@ -40,6 +40,17 @@ Auto Claude is a desktop application (+ CLI) where users describe a goal and AI
**PR target** — Always target the `develop` branch for PRs to AndyMik90/Auto-Claude, NOT `main`.
## Memory (claude-mem)
This project uses claude-mem for persistent memory across sessions. MCP search tools are available — use them proactively:
- **Before modifying code** — Search for past work on the same files/features: `search(query="<file or feature>")`. Check if there are known bugs, decisions, or patterns to follow.
- **When debugging** — Search for prior encounters with the same error or symptom: `search(query="<error message>", type="bugfix")`.
- **When making architectural decisions** — Check for past decisions: `search(query="<topic>", type="decision")`.
- **When resuming work** — Use `timeline(anchor=<recent_id>)` to understand where things left off.
Follow the 3-layer workflow: `search` (cheap index) → `timeline` (context) → `get_observations` (full details only for relevant IDs). Never fetch full details without filtering first.
## Project Structure
```
-212
View File
@@ -1,212 +0,0 @@
# Test Optimization Plan
## Problem Summary
- 3 test directories timeout after 600 seconds
- Main culprit: `test_spec_pipeline_orchestrator.py` - integration tests that run real code
- Autouse fixture adding overhead to every test
## Optimization Strategies
### 1. Use Pytest Markers (Immediate Impact)
Already defined in `pytest.ini`:
```ini
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
integration: marks tests as integration tests
```
**Action:** Mark slow integration tests:
```python
@pytest.mark.slow
@pytest.mark.integration
async def test_run_with_auto_approve(self, tmp_path):
...
```
**Run fast tests only:**
```bash
pytest -m "not slow" -v
pytest -m "not integration" -v
```
**Run all tests when needed:**
```bash
pytest -v # or explicitly: pytest -m "slow or integration" -v
```
### 2. Fix Autouse Fixture Overhead
The `ensure_modules_not_mocked()` fixture runs before EVERY test.
**Action:** Remove autouse or make it conditional:
```python
@pytest.fixture(autouse=True, scope="function")
def ensure_modules_not_mocked():
"""Only run for tests that need real claude_agent_sdk"""
# Skip for tests that explicitly mock everything
if os.environ.get("PYTEST_SKIP_MODULE_CLEANUP"):
return
# ... existing code ...
```
### 3. Separate Unit and Integration Tests
**Directory structure:**
```
tests/
├── unit/ # Fast tests (seconds)
│ ├── test_*.py
├── integration/ # Slow tests (minutes)
│ ├── test_*.py
└── e2e/ # Very slow tests (10+ minutes)
├── test_*.py
```
**pytest.ini:**
```ini
[pytest]
testpaths = tests/unit # Default to fast tests
# Override with: pytest --testpaths=integration
```
### 4. Optimize Async Tests
Async tests have overhead. Use sync tests where possible:
```python
# SLOW (async overhead)
@pytest.mark.asyncio
async def test_something():
result = await some_async_function()
assert result
# FASTER (mock the async call)
def test_something():
with patch('module.some_async_function', return_value=expected):
result = some_function_that_calls_async()
assert result
```
### 5. Reduce Test Data Creation
Many tests create large temporary directories. Use shared fixtures:
```python
@pytest.fixture(scope="session")
def shared_test_data():
"""Create test data once per session, not per test"""
data = create_expensive_test_data()
yield data
# cleanup runs at session end
```
### 6. Disable Coverage by Default
Coverage collection adds overhead.
**pytest.ini:**
```ini
[pytest]
addopts = -q --tb=short # No --cov
# Enable coverage explicitly: pytest --cov=apps/backend
```
### 7. Increase Timeout for Integration Tests
For tests that must be slow:
```python
@pytest.mark.timeout(300) # 5 minutes for this specific test
async def test_slow_integration():
...
```
### 8. Use Test Profiles
Create pytest configs for different scenarios:
**pytest.unit.ini:**
```ini
[pytest]
testpaths = tests
addopts = -m "not slow and not integration" -q
```
**pytest.all.ini:**
```ini
[pytest]
testpaths = tests
addopts = -v
timeout = 1200 # 20 minutes
```
**Usage:**
```bash
pytest -c pytest.unit.ini # Fast PR checks
pytest -c pytest.all.ini # Full test suite
```
### 9. Mock Expensive Operations
Identify what's actually slow in the tests:
```python
# SLOW - Real file I/O
async def test_orchestrator():
orchestrator = SpecOrchestrator(project_dir)
await orchestrator.run() # Does real file operations
# FASTER - Mock the expensive parts
async def test_orchestrator():
with patch('spec.pipeline.orchestrator.discover_project'):
with patch('spec.pipeline.orchestrator.write_files'):
orchestrator = SpecOrchestrator(project_dir)
await orchestrator.run() # Just tests the logic
```
### 10. Run Tests in Parallel (Already Done)
Use the scripts we created:
```bash
./scripts/run-parallel-tests.sh # Directory-level
./scripts/run-tests-per-file.sh # File-level
```
## Implementation Priority
1. **HIGH** - Mark slow/integration tests (5 minutes)
- Add `@pytest.mark.slow` to `test_spec_pipeline_orchestrator.py`
- Add `@pytest.mark.integration` to integration tests
2. **HIGH** - Update CI to run fast tests by default (10 minutes)
- CI: `pytest -m "not slow"` for PR checks
- CI: `pytest` (full) for main branch
3. **MEDIUM** - Fix autouse fixture (30 minutes)
- Remove autouse or make it conditional
4. **MEDIUM** - Create test profiles (1 hour)
- Separate unit/integration configs
5. **LOW** - Refactor slow tests (ongoing)
- Better mocking
- Shared fixtures
## Quick Win Script
Add to Makefile or scripts:
```bash
# makefile or script
test-fast:
pytest -m "not slow" -v
test-all:
pytest -v
test-integration:
pytest -m "integration" -v
```
+6 -6
View File
@@ -32,8 +32,8 @@
# API_TIMEOUT_MS=600000
# Model override (OPTIONAL)
# Default: claude-opus-4-6
# AUTO_BUILD_MODEL=claude-opus-4-6
# Default: claude-opus-4-5-20251101
# AUTO_BUILD_MODEL=claude-opus-4-5-20251101
# =============================================================================
@@ -269,9 +269,9 @@ GRAPHITI_ENABLED=true
# OpenRouter Base URL (default: https://openrouter.ai/api/v1)
# OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
# OpenRouter LLM Model (default: anthropic/claude-3.5-sonnet)
# Popular choices: anthropic/claude-3.5-sonnet, openai/gpt-4o, google/gemini-2.0-flash
# OPENROUTER_LLM_MODEL=anthropic/claude-3.5-sonnet
# OpenRouter LLM Model (default: anthropic/claude-sonnet-4)
# Popular choices: anthropic/claude-sonnet-4, openai/gpt-4o, google/gemini-2.0-flash
# OPENROUTER_LLM_MODEL=anthropic/claude-sonnet-4
# OpenRouter Embedding Model (default: openai/text-embedding-3-small)
# OPENROUTER_EMBEDDING_MODEL=openai/text-embedding-3-small
@@ -368,5 +368,5 @@ GRAPHITI_ENABLED=true
# GRAPHITI_LLM_PROVIDER=openrouter
# GRAPHITI_EMBEDDER_PROVIDER=openrouter
# OPENROUTER_API_KEY=sk-or-xxxxxxxx
# OPENROUTER_LLM_MODEL=anthropic/claude-3.5-sonnet
# OPENROUTER_LLM_MODEL=anthropic/claude-sonnet-4
# OPENROUTER_EMBEDDING_MODEL=openai/text-embedding-3-small
+1 -1
View File
@@ -1,3 +1,3 @@
"""Backward compatibility shim - import from core.agent instead."""
from core.agent import * # noqa: F401, F403
from core.agent import * # noqa: F403
+45 -70
View File
@@ -12,24 +12,11 @@ This module provides:
- Utility functions for git and plan management
Uses lazy imports to avoid circular dependencies.
Note: Module-level placeholders are defined to satisfy CodeQL static analysis.
These trigger the actual import on first access through __getattr__.
"""
from __future__ import annotations
from typing import Any
from .base import AUTO_CONTINUE_DELAY_SECONDS, HUMAN_INTERVENTION_FILE
from .utils import (
find_phase_for_subtask,
find_subtask_in_plan,
get_commit_count,
get_latest_commit,
load_implementation_plan,
sync_spec_to_source,
)
# Explicit import required by CodeQL static analysis
# (CodeQL doesn't recognize __getattr__ dynamic exports)
from .utils import sync_spec_to_source
__all__ = [
# Main API
@@ -55,67 +42,55 @@ __all__ = [
"HUMAN_INTERVENTION_FILE",
]
# Module cache for lazy imports
_module_cache = {}
def __getattr__(name):
"""Lazy imports to avoid circular dependencies."""
if name in ("AUTO_CONTINUE_DELAY_SECONDS", "HUMAN_INTERVENTION_FILE"):
from .base import AUTO_CONTINUE_DELAY_SECONDS, HUMAN_INTERVENTION_FILE
def __getattr__(name: str) -> Any:
"""Lazy imports to avoid circular dependencies.
return locals()[name]
elif name == "run_autonomous_agent":
from .coder import run_autonomous_agent
Python 3.7+ calls this for attributes that don't exist at module level.
"""
if name == "run_autonomous_agent":
from . import coder # Ensure agents.coder is registered in sys.modules
return coder.run_autonomous_agent
elif name == "debug_memory_system_status":
from . import (
memory_manager, # Ensure agents.memory_manager is registered in sys.modules
return run_autonomous_agent
elif name in (
"debug_memory_system_status",
"get_graphiti_context",
"save_session_memory",
"save_session_to_graphiti",
):
from .memory_manager import (
debug_memory_system_status,
get_graphiti_context,
save_session_memory,
save_session_to_graphiti,
)
return memory_manager.debug_memory_system_status
elif name == "get_graphiti_context":
from . import (
memory_manager, # Ensure agents.memory_manager is registered in sys.modules
)
return memory_manager.get_graphiti_context
elif name == "save_session_memory":
from . import (
memory_manager, # Ensure agents.memory_manager is registered in sys.modules
)
return memory_manager.save_session_memory
elif name == "save_session_to_graphiti":
from . import (
memory_manager, # Ensure agents.memory_manager is registered in sys.modules
)
return memory_manager.save_session_to_graphiti
return locals()[name]
elif name == "run_followup_planner":
from . import planner # Ensure agents.planner is registered in sys.modules
from .planner import run_followup_planner
return planner.run_followup_planner
elif name == "post_session_processing":
from . import session # Ensure agents.session is registered in sys.modules
return run_followup_planner
elif name in ("post_session_processing", "run_agent_session"):
from .session import post_session_processing, run_agent_session
return session.post_session_processing
elif name == "run_agent_session":
from . import session # Ensure agents.session is registered in sys.modules
return locals()[name]
elif name in (
"find_phase_for_subtask",
"find_subtask_in_plan",
"get_commit_count",
"get_latest_commit",
"load_implementation_plan",
"sync_spec_to_source",
):
from .utils import (
find_phase_for_subtask,
find_subtask_in_plan,
get_commit_count,
get_latest_commit,
load_implementation_plan,
sync_spec_to_source,
)
return session.run_agent_session
return locals()[name]
raise AttributeError(f"module 'agents' has no attribute '{name}'")
def __dir__():
"""Return list of module attributes for autocomplete and dir()."""
return __all__ + [
"__all__",
"__doc__",
"__file__",
"__getattr__",
"__name__",
"__package__",
"__loader__",
"__spec__",
]
-3
View File
@@ -15,9 +15,6 @@ logger = logging.getLogger(__name__)
AUTO_CONTINUE_DELAY_SECONDS = 3
HUMAN_INTERVENTION_FILE = "PAUSE"
# Retry configuration for subtask execution
MAX_SUBTASK_RETRIES = 5 # Maximum attempts before marking subtask as stuck
# Retry configuration for 400 tool concurrency errors
MAX_CONCURRENCY_RETRIES = 5 # Maximum number of retries for tool concurrency errors
INITIAL_RETRY_DELAY_SECONDS = (
+17 -130
View File
@@ -69,7 +69,6 @@ from .base import (
MAX_CONCURRENCY_RETRIES,
MAX_RATE_LIMIT_WAIT_SECONDS,
MAX_RETRY_DELAY_SECONDS,
MAX_SUBTASK_RETRIES,
RATE_LIMIT_CHECK_INTERVAL_SECONDS,
RATE_LIMIT_PAUSE_FILE,
RESUME_FILE,
@@ -88,60 +87,6 @@ from .utils import (
logger = logging.getLogger(__name__)
# =============================================================================
# FILE VALIDATION UTILITIES
# =============================================================================
def validate_subtask_files(subtask: dict, project_dir: Path) -> dict:
"""
Validate all files_to_modify exist before subtask execution.
Args:
subtask: Subtask dictionary containing files_to_modify array
project_dir: Root directory of the project
Returns:
dict with:
- success (bool): True if all files exist
- error (str): Error message if validation fails
- missing_files (list): List of missing file paths
- invalid_paths (list): List of paths that resolve outside the project
- suggestion (str): Actionable suggestion for resolution
"""
missing_files = []
invalid_paths = []
resolved_project = Path(project_dir).resolve()
for file_path in subtask.get("files_to_modify", []):
full_path = (resolved_project / file_path).resolve()
if not full_path.is_relative_to(resolved_project):
invalid_paths.append(file_path)
continue
if not full_path.exists():
missing_files.append(file_path)
if invalid_paths:
return {
"success": False,
"error": f"Paths resolve outside project boundary: {', '.join(invalid_paths)}",
"missing_files": missing_files,
"invalid_paths": invalid_paths,
"suggestion": "Update implementation plan to use paths within the project directory",
}
if missing_files:
return {
"success": False,
"error": f"Planned files do not exist: {', '.join(missing_files)}",
"missing_files": missing_files,
"invalid_paths": [],
"suggestion": "Update implementation plan with correct filenames or create missing files",
}
return {"success": True, "missing_files": [], "invalid_paths": []}
def _check_and_clear_resume_file(
resume_file: Path,
pause_file: Path,
@@ -581,16 +526,18 @@ async def run_autonomous_agent(
phase_model = get_phase_model(spec_dir, current_phase, model)
phase_thinking_budget = get_phase_thinking_budget(spec_dir, current_phase)
# Create client (fresh context) with phase-specific model and thinking
# Use appropriate agent_type for correct tool permissions and thinking budget
client = create_client(
project_dir,
spec_dir,
phase_model,
agent_type="planner" if first_run else "coder",
max_thinking_tokens=phase_thinking_budget,
)
# Generate appropriate prompt
if first_run:
# Create client for planning phase
client = create_client(
project_dir,
spec_dir,
phase_model,
agent_type="planner",
max_thinking_tokens=phase_thinking_budget,
)
prompt = generate_planner_prompt(spec_dir, project_dir)
if planning_retry_context:
prompt += "\n\n" + planning_retry_context
@@ -647,13 +594,15 @@ async def run_autonomous_agent(
"Waiting for implementation plan to be ready...", "progress"
)
for retry_attempt in range(3):
await asyncio.sleep((retry_attempt + 1) * 2) # 2s, 4s, 6s
delay = (retry_attempt + 1) * 2 # 2s, 4s, 6s
await asyncio.sleep(delay)
next_subtask = get_next_subtask(spec_dir)
if next_subtask:
# Successfully found subtask after retry
delay = (retry_attempt + 1) * 2
# Update subtask_id and phase_name after successful retry
subtask_id = next_subtask.get("id")
phase_name = next_subtask.get("phase_name")
print_status(
f"Found subtask {next_subtask.get('id')} after {delay}s delay",
f"Found subtask {subtask_id} after {delay}s delay",
"success",
)
break
@@ -666,68 +615,6 @@ async def run_autonomous_agent(
print("No pending subtasks found - build may be complete!")
break
# Validate that all files_to_modify exist before attempting execution
# This prevents infinite retry loops when implementation plan references non-existent files
validation_result = validate_subtask_files(next_subtask, project_dir)
if not validation_result["success"]:
# File validation failed - record error and skip session
error_msg = validation_result["error"]
suggestion = validation_result.get("suggestion", "")
print()
print_status(f"File validation failed: {error_msg}", "error")
if suggestion:
print(muted(f"Suggestion: {suggestion}"))
print()
# Record the validation failure in recovery manager
recovery_manager.record_attempt(
subtask_id=subtask_id,
session=iteration,
success=False,
approach="File validation failed before execution",
error=error_msg,
)
# Log the validation failure
if task_logger:
task_logger.log_error(
f"File validation failed: {error_msg}", LogPhase.CODING
)
# Check if subtask has exceeded max retries
attempt_count = recovery_manager.get_attempt_count(subtask_id)
if attempt_count >= MAX_SUBTASK_RETRIES:
recovery_manager.mark_subtask_stuck(
subtask_id,
f"File validation failed after {attempt_count} attempts: {error_msg}",
)
print_status(
f"Subtask {subtask_id} marked as STUCK after {attempt_count} failed validation attempts",
"error",
)
print(
muted(
"Consider: update implementation plan with correct filenames"
)
)
# Update status
status_manager.update(state=BuildState.ERROR)
# Small delay before retry
await asyncio.sleep(AUTO_CONTINUE_DELAY_SECONDS)
continue # Skip to next iteration
# Create client for coding phase (after file validation passes)
client = create_client(
project_dir,
spec_dir,
phase_model,
agent_type="coder",
max_thinking_tokens=phase_thinking_budget,
)
# Get attempt count for recovery context
attempt_count = recovery_manager.get_attempt_count(subtask_id)
recovery_hints = (
@@ -847,7 +734,7 @@ async def run_autonomous_agent(
# Check for stuck subtasks
attempt_count = recovery_manager.get_attempt_count(subtask_id)
if not success and attempt_count >= MAX_SUBTASK_RETRIES:
if not success and attempt_count >= 3:
recovery_manager.mark_subtask_stuck(
subtask_id, f"Failed after {attempt_count} attempts"
)
+3 -3
View File
@@ -247,7 +247,7 @@ async def get_graphiti_context(
if memory is not None:
try:
await memory.close()
except Exception:
except Exception as e:
logger.debug(
"Failed to close Graphiti memory connection", exc_info=True
)
@@ -417,9 +417,9 @@ async def save_session_memory(
if memory is not None:
try:
await memory.close()
except Exception:
except Exception as e:
logger.debug(
"Failed to close Graphiti memory connection", exc_info=True
"Failed to close Graphiti memory connection", exc_info=e
)
else:
if is_debug_enabled():
-37
View File
@@ -8,46 +8,9 @@ This script verifies that:
3. Backwards compatibility is maintained
"""
import io
import sys
from pathlib import Path
# Configure safe encoding on Windows to handle Unicode characters in output
if sys.platform == "win32":
for _stream_name in ("stdout", "stderr"):
_stream = getattr(sys, _stream_name)
# Method 1: Try reconfigure (works for TTY)
if hasattr(_stream, "reconfigure"):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
continue
except (
AttributeError,
io.UnsupportedOperation,
OSError,
): # Stream doesn't support reconfigure
pass # no-op
# Method 2: Wrap with TextIOWrapper for piped output
try:
if hasattr(_stream, "buffer"):
_new_stream = io.TextIOWrapper(
_stream.buffer,
encoding="utf-8",
errors="replace",
line_buffering=True,
)
setattr(sys, _stream_name, _new_stream)
except (
AttributeError,
io.UnsupportedOperation,
OSError,
): # Stream doesn't support wrapper
pass # no-op
# Clean up temporary variables
del _stream_name, _stream
if "_new_stream" in dir():
del _new_stream
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent))
+7 -7
View File
@@ -46,7 +46,7 @@ TOOL_UPDATE_QA_STATUS = "mcp__auto-claude__update_qa_status"
# Context7 MCP tools for documentation lookup (always enabled)
CONTEXT7_TOOLS = [
"mcp__context7__resolve-library-id",
"mcp__context7__query-docs",
"mcp__context7__get-library-docs",
]
# Linear MCP tools for project management (when LINEAR_API_KEY is set)
@@ -158,7 +158,7 @@ AGENT_CONFIGS = {
"tools": BASE_READ_TOOLS,
"mcp_servers": [], # Self-critique, no external tools
"auto_claude_tools": [],
"thinking_default": "high",
"thinking_default": "ultrathink",
},
"spec_discovery": {
"tools": BASE_READ_TOOLS + WEB_TOOLS,
@@ -210,7 +210,7 @@ AGENT_CONFIGS = {
TOOL_RECORD_GOTCHA,
TOOL_GET_SESSION_CONTEXT,
],
"thinking_default": "low", # Coding uses minimal thinking (effort: low for Opus, 1024 tokens for Sonnet/Haiku)
"thinking_default": "none", # Coding doesn't use extended thinking
},
# ═══════════════════════════════════════════════════════════════════════
# QA PHASES (Read + test + browser + Graphiti memory)
@@ -247,9 +247,9 @@ AGENT_CONFIGS = {
"tools": BASE_READ_TOOLS + WEB_TOOLS,
"mcp_servers": [],
"auto_claude_tools": [],
# Note: Default to "low" for minimal thinking overhead
# Haiku doesn't support thinking; create_simple_client() handles this
"thinking_default": "low",
# Note: Default to "none" because insight_extractor uses Haiku which doesn't support thinking
# If using Sonnet/Opus models, override max_thinking_tokens in create_simple_client()
"thinking_default": "none",
},
"merge_resolver": {
"tools": [], # Text-only analysis
@@ -524,7 +524,7 @@ def get_default_thinking_level(agent_type: str) -> str:
agent_type: The agent type identifier
Returns:
Thinking level string (low, medium, high)
Thinking level string (none, low, medium, high, ultrathink)
"""
config = get_agent_config(agent_type)
return config.get("thinking_default", "medium")
@@ -79,7 +79,7 @@ async def _save_to_graphiti_async(
# Always close the memory connection (swallow exceptions to avoid overriding)
try:
await memory.close()
except Exception:
except Exception as e:
logger.debug(
"Failed to close Graphiti memory connection", exc_info=True
)
@@ -312,8 +312,8 @@ def create_memory_tools(spec_dir: Path, project_dir: Path) -> list:
for path, info in list(discoveries.items())[:20]: # Limit to 20
desc = info.get("description", "No description")
result_parts.append(f"- `{path}`: {desc}")
except Exception: # Non-critical error; continue
pass # no-op: memory files are optional
except Exception:
pass
# Load gotchas
gotchas_file = memory_dir / "gotchas.md"
@@ -326,8 +326,8 @@ def create_memory_tools(spec_dir: Path, project_dir: Path) -> list:
result_parts.append(
content[-1000:] if len(content) > 1000 else content
)
except Exception: # Non-critical error; continue
pass # no-op: memory files are optional
except Exception:
pass
# Load patterns
patterns_file = memory_dir / "patterns.md"
@@ -339,8 +339,8 @@ def create_memory_tools(spec_dir: Path, project_dir: Path) -> list:
result_parts.append(
content[-1000:] if len(content) > 1000 else content
)
except Exception: # Non-critical error; continue
pass # no-op: memory files are optional
except Exception:
pass
if not result_parts:
return {
-38
View File
@@ -29,47 +29,9 @@ All actual implementation is in focused submodules for better maintainability.
from __future__ import annotations
import io
import json
import sys
from pathlib import Path
# Configure safe encoding on Windows to handle Unicode characters in output
if sys.platform == "win32":
for _stream_name in ("stdout", "stderr"):
_stream = getattr(sys, _stream_name)
# Method 1: Try reconfigure (works for TTY)
if hasattr(_stream, "reconfigure"):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
continue
except (
AttributeError,
io.UnsupportedOperation,
OSError,
): # Stream doesn't support reconfigure
pass # no-op
# Method 2: Wrap with TextIOWrapper for piped output
try:
if hasattr(_stream, "buffer"):
_new_stream = io.TextIOWrapper(
_stream.buffer,
encoding="utf-8",
errors="replace",
line_buffering=True,
)
setattr(sys, _stream_name, _new_stream)
except (
AttributeError,
io.UnsupportedOperation,
OSError,
): # Stream doesn't support wrapper
pass # no-op
# Clean up temporary variables
del _stream_name, _stream
if "_new_stream" in dir():
del _new_stream
# Import from the new modular structure
from .analyzers import (
ProjectAnalyzer,
@@ -14,6 +14,7 @@ Main exports:
from __future__ import annotations
from pathlib import Path
from typing import Any
from .project_analyzer_module import ProjectAnalyzer
from .service_analyzer import ServiceAnalyzer
@@ -362,8 +362,9 @@ class FrameworkAnalyzer(BaseAnalyzer):
dependencies = self._detect_spm_dependencies()
if dependencies:
self.analysis["spm_dependencies"] = dependencies
except Exception: # Silently fail if Swift detection has issues
pass # no-op
except Exception:
# Silently fail if Swift detection has issues
pass
def _detect_spm_dependencies(self) -> list[str]:
"""Detect Swift Package Manager dependencies."""
+12 -14
View File
@@ -115,14 +115,6 @@ class CIDiscovery:
CIConfig if CI found, None otherwise
"""
project_dir = Path(project_dir)
# Return None if project directory doesn't exist or is inaccessible
try:
if not project_dir.exists():
return None
except (OSError, PermissionError):
return None
cache_key = str(project_dir.resolve())
if cache_key in self._cache:
@@ -307,8 +299,8 @@ class CIDiscovery:
if isinstance(variables, dict):
result.environment_variables.extend(variables.keys())
except Exception: # Non-critical error; continue
pass # no-op: CI parsing is best-effort
except Exception:
pass
return result
@@ -365,8 +357,8 @@ class CIDiscovery:
)
)
except Exception: # Non-critical error; continue
pass # no-op: CI parsing is best-effort
except Exception:
pass
return result
@@ -385,11 +377,17 @@ class CIDiscovery:
matches = sh_pattern.findall(content)
steps = []
test_related = False
for cmd in matches:
steps.append(cmd)
self._extract_test_commands(cmd, result)
if any(
kw in cmd.lower() for kw in ["test", "pytest", "jest", "coverage"]
):
test_related = True
# Extract stage names
stage_pattern = re.compile(r'stage\s*\([\'"]([^\'"]+)[\'"]\)')
stages = stage_pattern.findall(content)
@@ -404,8 +402,8 @@ class CIDiscovery:
)
)
except Exception: # Non-critical error; continue
pass # no-op: CI parsing is best-effort
except Exception:
pass
return result
+3 -1
View File
@@ -22,11 +22,13 @@ logger = logging.getLogger(__name__)
# Check for Claude SDK availability
try:
from claude_agent_sdk import ClaudeSDKClient # noqa: F401 (re-export)
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
SDK_AVAILABLE = True
except ImportError:
SDK_AVAILABLE = False
ClaudeAgentOptions = None
ClaudeSDKClient = None
from core.auth import ensure_claude_code_oauth_token, get_auth_token
+15 -50
View File
@@ -31,11 +31,12 @@ from typing import Any
# Import the existing secrets scanner
try:
from security.scan_secrets import get_all_tracked_files, scan_files
from security.scan_secrets import SecretMatch, get_all_tracked_files, scan_files
HAS_SECRETS_SCANNER = True
except ImportError:
HAS_SECRETS_SCANNER = False
SecretMatch = None
# =============================================================================
@@ -107,20 +108,6 @@ class SecurityScanner:
self._bandit_available: bool | None = None
self._npm_available: bool | None = None
def _is_accessible_directory(self, path: Path) -> bool:
"""Check if a directory is accessible for scanning."""
try:
return path.is_dir()
except (OSError, PermissionError):
return False
def _safe_exists(self, path: Path) -> bool:
"""Safely check if a path exists, handling permission errors."""
try:
return path.exists()
except (OSError, PermissionError):
return False
def scan(
self,
project_dir: Path,
@@ -242,8 +229,9 @@ class SecurityScanner:
src_dirs = []
for candidate in ["src", "app", project_dir.name, "."]:
candidate_path = project_dir / candidate
if self._safe_exists(candidate_path) and self._safe_exists(
candidate_path / "__init__.py"
if (
candidate_path.exists()
and (candidate_path / "__init__.py").exists()
):
src_dirs.append(str(candidate_path))
@@ -310,7 +298,7 @@ class SecurityScanner:
) -> None:
"""Run dependency vulnerability audits."""
# npm audit for JavaScript projects
if self._safe_exists(project_dir / "package.json"):
if (project_dir / "package.json").exists():
self._run_npm_audit(project_dir, result)
# pip-audit for Python projects (if available)
@@ -401,15 +389,15 @@ class SecurityScanner:
else None,
)
)
except json.JSONDecodeError: # Invalid JSON; skip
pass # no-op: invalid audit output, skip
except json.JSONDecodeError:
pass
except FileNotFoundError:
pass # pip-audit not available
except subprocess.TimeoutExpired: # Non-critical error; continue
pass # no-op: timeout is non-critical
except Exception: # Non-critical error; continue
pass # no-op: pip-audit is optional
except subprocess.TimeoutExpired:
pass
except Exception:
pass
def _is_python_project(self, project_dir: Path) -> bool:
"""Check if this is a Python project."""
@@ -419,7 +407,7 @@ class SecurityScanner:
project_dir / "setup.py",
project_dir / "setup.cfg",
]
return any(self._safe_exists(p) for p in indicators)
return any(p.exists() for p in indicators)
def _check_bandit_available(self) -> bool:
"""Check if Bandit is available."""
@@ -439,20 +427,8 @@ class SecurityScanner:
"""Redact a secret for safe logging."""
if len(text) <= 8:
return "*" * len(text)
# Show only first 4 and last 4 characters, redact the middle
return text[:4] + "*" * (len(text) - 8) + text[-4:]
def _redact_log_message(self, message: str) -> str:
"""Redact potentially sensitive information from log messages."""
# Remove common secret patterns from log messages
import re
# Redact things that look like API keys, tokens, passwords
redacted = re.sub(r"([a-zA-Z0-9_-]{32,})", "[REDACTED]", message)
# Redact things that look like base64
redacted = re.sub(r"[A-Za-z0-9+/]{64,}={0,2}", "[REDACTED]", redacted)
return redacted[:500] # Also limit length
def _save_results(self, spec_dir: Path, result: SecurityScanResult) -> None:
"""Save scan results to spec directory."""
spec_dir = Path(spec_dir)
@@ -465,12 +441,7 @@ class SecurityScanner:
json.dump(output_data, f, indent=2)
def to_dict(self, result: SecurityScanResult) -> dict[str, Any]:
"""Convert result to dictionary for JSON serialization.
Note: All secret values are redacted via _redact_secret() before being
included in the result dict. Only first 4 and last 4 chars are shown.
"""
# lgtm[py/clear-text-logging-of-sensitive-data] - secrets are redacted
"""Convert result to dictionary for JSON serialization."""
return {
"secrets": result.secrets,
"vulnerabilities": [
@@ -599,7 +570,6 @@ def main() -> None:
)
if args.json:
# lgtm[py/clear-text-logging-of-sensitive-data] - secrets are redacted
print(json.dumps(scanner.to_dict(result), indent=2))
else:
print(f"Secrets Found: {len(result.secrets)}")
@@ -610,12 +580,7 @@ def main() -> None:
if result.secrets:
print("\nSecrets Detected:")
for secret in result.secrets:
# Only log safe fields (pattern name, file path, line number)
# The actual secret value is already redacted in matched_text
pattern = secret.get("pattern", "unknown")
file_path = secret.get("file", "unknown")
line_num = secret.get("line", "?")
print(f" - {pattern} in {file_path}:{line_num}")
print(f" - {secret['pattern']} in {secret['file']}:{secret['line']}")
if result.vulnerabilities:
print(f"\nVulnerabilities ({len(result.vulnerabilities)}):")
+5 -6
View File
@@ -140,17 +140,16 @@ def handle_batch_status_command(project_dir: str) -> bool:
spec_name = spec_dir.name
req_file = spec_dir / "requirements.json"
# Get title from requirements file, default to spec name
status = "unknown"
title = spec_name
if req_file.exists():
try:
with open(req_file, encoding="utf-8") as f:
req = json.load(f)
title = req.get("task_description", spec_name)
title = req.get("task_description", title)
except json.JSONDecodeError:
# Invalid JSON; use default title from directory name
title = spec_name
else:
title = spec_name
pass
# Determine status
if (spec_dir / "spec.md").exists():
+7 -5
View File
@@ -179,10 +179,10 @@ def handle_build_command(
continue_existing = check_existing_build(project_dir, spec_dir.name)
if continue_existing:
# Continue with existing worktree
pass # no-op
pass
else:
# User chose to start fresh or merged existing
pass # no-op
pass
# Choose workspace (skip for parallel mode - it always uses worktrees)
working_dir = project_dir
@@ -297,6 +297,7 @@ def handle_build_command(
except KeyboardInterrupt:
print("\n\nQA validation paused.")
print(f"Resume: python auto-claude/run.py --spec {spec_dir.name} --qa")
qa_approved = False
# Post-build finalization (only for isolated sequential mode)
# This happens AFTER QA validation so the worktree still exists
@@ -448,7 +449,7 @@ def _handle_build_interrupt(
if choice == "skip":
print()
print_status("Resuming build...", "info")
status_manager.update(state=BuildState.BUILDING)
status_manager.update(state=BuildState.RUNNING)
asyncio.run(
run_autonomous_agent(
project_dir=working_dir,
@@ -468,8 +469,9 @@ def _handle_build_interrupt(
status_manager = StatusManager(project_dir)
status_manager.set_inactive()
sys.exit(0)
except EOFError: # stdin closed
pass # no-op
except EOFError:
# stdin closed
pass
# Resume instructions (shown when user provided instructions or chose file/type/paste)
print()
-37
View File
@@ -12,48 +12,11 @@ Usage:
"""
import argparse
import io
import json
import sys
import uuid
from pathlib import Path
# Configure safe encoding on Windows to handle Unicode characters in output
if sys.platform == "win32":
for _stream_name in ("stdout", "stderr"):
_stream = getattr(sys, _stream_name)
# Method 1: Try reconfigure (works for TTY)
if hasattr(_stream, "reconfigure"):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
continue
except (
AttributeError,
io.UnsupportedOperation,
OSError,
): # Stream doesn't support reconfigure
pass # no-op
# Method 2: Wrap with TextIOWrapper for piped output
try:
if hasattr(_stream, "buffer"):
_new_stream = io.TextIOWrapper(
_stream.buffer,
encoding="utf-8",
errors="replace",
line_buffering=True,
)
setattr(sys, _stream_name, _new_stream)
except (
AttributeError,
io.UnsupportedOperation,
OSError,
): # Stream doesn't support wrapper
pass # no-op
# Clean up temporary variables
del _stream_name, _stream
if "_new_stream" in dir():
del _new_stream
from cli.utils import find_specs_dir
+24 -14
View File
@@ -325,32 +325,42 @@ def _detect_parallel_task_conflicts(
try:
from debug import (
debug,
debug_detailed,
debug_error,
debug_section,
debug_success,
debug_verbose,
is_debug_enabled,
)
except ImportError:
def debug(*args, **kwargs):
"""Fallback debug function when debug module is not available."""
pass # no-op
pass
def debug_error(*args, **kwargs):
"""Fallback debug_error function when debug module is not available."""
pass # no-op
def debug_section(*args, **kwargs):
"""Fallback debug_section function when debug module is not available."""
pass # no-op
def debug_success(*args, **kwargs):
"""Fallback debug_success function when debug module is not available."""
pass # no-op
def debug_detailed(*args, **kwargs):
"""Fallback debug_detailed function when debug module is not available."""
pass
def debug_verbose(*args, **kwargs):
"""Fallback debug_verbose function when debug module is not available."""
pass # no-op
pass
def debug_success(*args, **kwargs):
"""Fallback debug_success function when debug module is not available."""
pass
def debug_error(*args, **kwargs):
"""Fallback debug_error function when debug module is not available."""
pass
def debug_section(*args, **kwargs):
"""Fallback debug_section function when debug module is not available."""
pass
def is_debug_enabled():
"""Fallback is_debug_enabled function when debug module is not available."""
return False
MODULE = "cli.workspace_commands"
@@ -807,7 +817,7 @@ def _check_git_merge_conflicts(
import re
match = re.search(
r"Merge conflict in\s+(.+?)\s*$",
r"(?:Merge conflict in|CONFLICT.*?:)\s*(.+?)(?:\s*$|\s+\()",
line,
)
if match:
+3 -6
View File
@@ -5,10 +5,6 @@ Provides Claude API client utilities.
Uses lazy imports to avoid circular dependencies.
"""
# Explicit import required for CodeQL static analysis
# (CodeQL doesn't recognize __getattr__ dynamic exports)
from core.client import create_client as _create_client_impl
def __getattr__(name):
"""Lazy import to avoid circular imports with auto_claude_tools."""
@@ -19,10 +15,11 @@ def __getattr__(name):
def create_client(*args, **kwargs):
"""Create a Claude client instance."""
return _create_client_impl(*args, **kwargs)
from core.client import create_client as _create_client
return _create_client(*args, **kwargs)
# Export list must come after all exports are defined to satisfy CodeQL
__all__ = [
"create_client",
]
+1 -40
View File
@@ -13,7 +13,6 @@ Features:
from __future__ import annotations
import asyncio
import io
import json
import logging
import re
@@ -21,46 +20,8 @@ import sys
from pathlib import Path
from typing import TYPE_CHECKING
# Configure safe encoding on Windows to handle Unicode characters in output
# This is needed because this module may be imported in contexts that log
# Unicode content, and the logging system uses stdout/stderr.
if sys.platform == "win32":
for _stream_name in ("stdout", "stderr"):
_stream = getattr(sys, _stream_name)
# Method 1: Try reconfigure (works for TTY)
if hasattr(_stream, "reconfigure"):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
continue
except (
AttributeError,
io.UnsupportedOperation,
OSError,
): # Stream doesn't support reconfigure
pass # no-op
# Method 2: Wrap with TextIOWrapper for piped output
try:
if hasattr(_stream, "buffer"):
_new_stream = io.TextIOWrapper(
_stream.buffer,
encoding="utf-8",
errors="replace",
line_buffering=True,
)
setattr(sys, _stream_name, _new_stream)
except (
AttributeError,
io.UnsupportedOperation,
OSError,
): # Stream doesn't support wrapper
pass # no-op
# Clean up temporary variables
del _stream_name, _stream
if "_new_stream" in dir():
del _new_stream
if TYPE_CHECKING:
pass # no-op
pass
logger = logging.getLogger(__name__)
+4 -7
View File
@@ -40,12 +40,9 @@ class ContextBuilder:
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 # no-op
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
# Corrupted or legacy-encoded file, regenerate
pass
# Try to create one
from analyzer import analyze_project
@@ -117,7 +114,7 @@ class ContextBuilder:
try:
# Run the async function in a new event loop if necessary
try:
_ = asyncio.get_running_loop()
loop = asyncio.get_running_loop()
# We're already in an async context - this shouldn't happen in CLI
# but handle it gracefully
graph_hints = []
-38
View File
@@ -26,47 +26,9 @@ The context builder will:
4. Output focused context for AI agents
"""
import io
import json
import sys
from pathlib import Path
# Configure safe encoding on Windows to handle Unicode characters in output
if sys.platform == "win32":
for _stream_name in ("stdout", "stderr"):
_stream = getattr(sys, _stream_name)
# Method 1: Try reconfigure (works for TTY)
if hasattr(_stream, "reconfigure"):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
continue
except (
AttributeError,
io.UnsupportedOperation,
OSError,
): # Stream doesn't support reconfigure
pass # no-op
# Method 2: Wrap with TextIOWrapper for piped output
try:
if hasattr(_stream, "buffer"):
_new_stream = io.TextIOWrapper(
_stream.buffer,
encoding="utf-8",
errors="replace",
line_buffering=True,
)
setattr(sys, _stream_name, _new_stream)
except (
AttributeError,
io.UnsupportedOperation,
OSError,
): # Stream doesn't support wrapper
pass # no-op
# Clean up temporary variables
del _stream_name, _stream
if "_new_stream" in dir():
del _new_stream
from context import (
ContextBuilder,
FileMatch,
+15 -38
View File
@@ -3,63 +3,40 @@ Core Framework Module
=====================
Core components for the Auto Claude autonomous coding framework.
Note: We use lazy imports here because the full agent module has many dependencies
that may not be needed for basic operations.
"""
from typing import Any
# Module-level placeholders for CodeQL static analysis.
# The actual exported names trigger __getattr__ for lazy loading.
# Use list placeholder to satisfy CodeQL's "defined but not set to None" check.
run_autonomous_agent: Any = []
run_followup_planner: Any = []
WorktreeManager: Any = []
# Note: We use lazy imports here because the full agent module has many dependencies
# that may not be needed for basic operations like workspace management.
__all__ = [
"run_autonomous_agent",
"run_followup_planner",
"WorkspaceManager",
"WorktreeManager",
"create_claude_client",
"ClaudeClient",
"ProgressTracker",
]
def __getattr__(name: str) -> Any:
def __getattr__(name):
"""Lazy imports to avoid circular dependencies and heavy imports."""
if name == "run_autonomous_agent":
from .agent import run_autonomous_agent
if name in ("run_autonomous_agent", "run_followup_planner"):
from .agent import run_autonomous_agent, run_followup_planner
return run_autonomous_agent
elif name == "run_followup_planner":
from .agent import run_followup_planner
return locals()[name]
elif name == "WorkspaceManager":
from .workspace import WorkspaceManager
return run_followup_planner
return WorkspaceManager
elif name == "WorktreeManager":
from .worktree import WorktreeManager
return WorktreeManager
elif name == "ProgressTracker":
from .progress import ProgressTracker
return ProgressTracker
elif name in ("create_claude_client", "ClaudeClient"):
from . import client as _client
return getattr(_client, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
def _do_lazy_import(name: str) -> Any:
"""Perform the actual lazy import for a given name."""
if name == "run_autonomous_agent":
from .agent import run_autonomous_agent
return run_autonomous_agent
if name == "run_followup_planner":
from .agent import run_followup_planner
return run_followup_planner
if name == "WorktreeManager":
from .worktree import WorktreeManager
return WorktreeManager
raise AssertionError(f"Unknown lazy import name: {name}")
+9 -6
View File
@@ -466,6 +466,7 @@ def _get_token_from_macos_keychain(config_dir: str | None = None) -> str | None:
if not (token.startswith("sk-ant-oat01-") or token.startswith("enc:")):
return None
logger.debug(f"Found token in keychain service '{service_name}'")
return token
except (subprocess.TimeoutExpired, json.JSONDecodeError, KeyError, Exception):
@@ -500,6 +501,7 @@ def _get_token_from_windows_credential_files(
token.startswith("sk-ant-oat01-")
or token.startswith("enc:")
):
logger.debug(f"Found token in {cred_path}")
return token
# If config_dir provided but no token found, don't fall back to default
return None
@@ -595,6 +597,9 @@ def _get_token_from_linux_secret_service(config_dir: str | None = None) -> str |
if token and (
token.startswith("sk-ant-oat01-") or token.startswith("enc:")
):
logger.debug(
f"Found token in secret service with label '{target_label}'"
)
return token
except json.JSONDecodeError:
continue
@@ -655,6 +660,7 @@ def _get_token_from_config_dir(config_dir: str) -> str | None:
if token and (
token.startswith("sk-ant-oat01-") or token.startswith("enc:")
):
logger.debug(f"Found token in {cred_path}")
return token
except (json.JSONDecodeError, KeyError, Exception) as e:
logger.debug(f"Failed to read {cred_path}: {e}")
@@ -861,12 +867,9 @@ def _find_git_bash_path() -> str | None:
git_paths = result.stdout.strip().splitlines()
if git_paths:
git_path = git_paths[0].strip()
except (
subprocess.TimeoutExpired,
FileNotFoundError,
subprocess.SubprocessError,
): # Intentionally suppress errors - best-effort detection with fallback to common paths
pass # no-op
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
# Intentionally suppress errors - best-effort detection with fallback to common paths
pass
# Method 2: Check common installation paths if 'where' didn't work
if not git_path:
+5 -43
View File
@@ -449,9 +449,6 @@ def create_client(
max_thinking_tokens: int | None = None,
output_format: dict | None = None,
agents: dict | None = None,
betas: list[str] | None = None,
effort_level: str | None = None,
fast_mode: bool = False,
) -> ClaudeSDKClient:
"""
Create a Claude Agent SDK client with multi-layered security.
@@ -467,9 +464,10 @@ def create_client(
agent_type: Agent type identifier from AGENT_CONFIGS
(e.g., 'coder', 'planner', 'qa_reviewer', 'spec_gatherer')
max_thinking_tokens: Token budget for extended thinking (None = disabled)
- high: 16384 (spec creation, QA review)
- medium: 4096 (planning, validation)
- low: 1024 (coding)
- ultrathink: 16000 (spec creation)
- high: 10000 (QA review)
- medium: 5000 (planning, validation)
- None: disabled (coding)
output_format: Optional structured output format for validated JSON responses.
Use {"type": "json_schema", "schema": Model.model_json_schema()}
See: https://platform.claude.com/docs/en/agent-sdk/structured-outputs
@@ -477,15 +475,6 @@ def create_client(
Format: {"agent-name": {"description": "...", "prompt": "...",
"tools": [...], "model": "inherit"}}
See: https://platform.claude.com/docs/en/agent-sdk/subagents
betas: Optional list of SDK beta header strings (e.g., ["context-1m-2025-08-07"]
for 1M context window). Use get_phase_model_betas() to compute from config.
effort_level: Optional effort level for adaptive thinking models (e.g., "low",
"medium", "high"). When set, injected as CLAUDE_CODE_EFFORT_LEVEL
env var for the SDK subprocess. Only meaningful for models that
support adaptive thinking (e.g., Opus 4.6).
fast_mode: Enable Fast Mode for faster Opus 4.6 output. When True, injected
as CLAUDE_CODE_FAST_MODE=true env var. Requires extra usage enabled
on Claude subscription; falls back to standard speed automatically.
Returns:
Configured ClaudeSDKClient
@@ -513,24 +502,6 @@ def create_client(
if config_dir:
logger.info(f"Using CLAUDE_CONFIG_DIR for profile: {config_dir}")
# Get the config dir for profile-specific credential lookup
# CLAUDE_CONFIG_DIR enables per-profile Keychain entries with SHA256-hashed service names
config_dir = sdk_env.get("CLAUDE_CONFIG_DIR")
# Configure SDK authentication (OAuth or API profile mode)
configure_sdk_authentication(config_dir)
if config_dir:
logger.info(f"Using CLAUDE_CONFIG_DIR for profile: {config_dir}")
# Inject effort level for adaptive thinking models (e.g., Opus 4.6)
if effort_level:
sdk_env["CLAUDE_CODE_EFFORT_LEVEL"] = effort_level
# Inject fast mode for faster Opus 4.6 output
if fast_mode:
sdk_env["CLAUDE_CODE_FAST_MODE"] = "true"
# Debug: Log git-bash path detection on Windows
if "CLAUDE_CODE_GIT_BASH_PATH" in sdk_env:
logger.info(f"Git Bash path found: {sdk_env['CLAUDE_CODE_GIT_BASH_PATH']}")
@@ -694,12 +665,7 @@ def create_client(
print(" - Worktree permissions: granted for original project directories")
print(" - Bash commands restricted to allowlist")
if max_thinking_tokens:
thinking_info = f"{max_thinking_tokens:,} tokens"
if effort_level:
thinking_info += f" + effort={effort_level}"
if fast_mode:
thinking_info += " + fast mode"
print(f" - Extended thinking: {thinking_info}")
print(f" - Extended thinking: {max_thinking_tokens:,} tokens")
else:
print(" - Extended thinking: disabled")
@@ -868,8 +834,4 @@ def create_client(
if agents:
options_kwargs["agents"] = agents
# Add beta headers if specified (e.g., for 1M context window)
if betas:
options_kwargs["betas"] = betas
return ClaudeSDKClient(options=ClaudeAgentOptions(**options_kwargs))
-118
View File
@@ -1,118 +0,0 @@
"""
Shared Error Utilities
======================
Common error detection and classification functions used across
agent sessions, QA, and other modules.
"""
import re
def is_tool_concurrency_error(error: Exception) -> bool:
"""
Check if an error is a 400 tool concurrency error from Claude API.
Tool concurrency errors occur when too many tools are used simultaneously
in a single API request, hitting Claude's concurrent tool use limit.
Args:
error: The exception to check
Returns:
True if this is a tool concurrency error, False otherwise
"""
error_str = str(error).lower()
# Check for 400 status AND tool concurrency keywords
return "400" in error_str and (
("tool" in error_str and "concurrency" in error_str)
or "too many tools" in error_str
or "concurrent tool" in error_str
)
def is_rate_limit_error(error: Exception) -> bool:
"""
Check if an error is a rate limit error (429 or similar).
Rate limit errors occur when the API usage quota is exceeded,
either for session limits or weekly limits.
Args:
error: The exception to check
Returns:
True if this is a rate limit error, False otherwise
"""
error_str = str(error).lower()
# Check for HTTP 429 with word boundaries to avoid false positives
if re.search(r"\b429\b", error_str):
return True
# Check for other rate limit indicators
return any(
p in error_str
for p in [
"limit reached",
"rate limit",
"too many requests",
"usage limit",
"quota exceeded",
]
)
def is_authentication_error(error: Exception) -> bool:
"""
Check if an error is an authentication error (401, token expired, etc.).
Authentication errors occur when OAuth tokens are invalid, expired,
or have been revoked (e.g., after token refresh on another process).
Validation approach:
- HTTP 401 status code is checked with word boundaries to minimize false positives
- Additional string patterns are validated against lowercase error messages
- Patterns are designed to match known Claude API and OAuth error formats
Known false positive risks:
- Generic error messages containing "unauthorized" or "access denied" may match
even if not related to authentication (e.g., file permission errors)
- Error messages containing these keywords in user-provided content could match
- Mitigation: HTTP 401 check provides strong signal; string patterns are secondary
Real-world validation:
- Pattern matching has been tested against actual Claude API error responses
- False positive rate is acceptable given the recovery mechanism (prompt user to re-auth)
- If false positive occurs, user can simply resume without re-authenticating
Args:
error: The exception to check
Returns:
True if this is an authentication error, False otherwise
"""
error_str = str(error).lower()
# Check for HTTP 401 with word boundaries to avoid false positives
if re.search(r"\b401\b", error_str):
return True
# Check for other authentication indicators
# NOTE: "authentication failed" and "authentication error" are more specific patterns
# to reduce false positives from generic "authentication" mentions
return any(
p in error_str
for p in [
"authentication failed",
"authentication error",
"unauthorized",
"invalid token",
"token expired",
"authentication_error",
"invalid_token",
"token_expired",
"not authenticated",
"http 401",
]
)
+3 -5
View File
@@ -68,11 +68,9 @@ def _run_where_command() -> str | None:
and _verify_gh_executable(found_path)
):
return found_path
except (
subprocess.TimeoutExpired,
OSError,
): # 'where' command failed or timed out - fall through to return None
pass # no-op
except (subprocess.TimeoutExpired, OSError):
# 'where' command failed or timed out - fall through to return None
pass
return None
+3 -9
View File
@@ -15,13 +15,6 @@ import shutil
import subprocess
from pathlib import Path
__all__ = [
"get_git_executable",
"get_isolated_git_env",
"run_git",
"GIT_ENV_VARS_TO_CLEAR",
]
# Git environment variables that can interfere with worktree operations
# when set by pre-commit hooks or other git configurations.
# These must be cleared to prevent cross-worktree contamination.
@@ -86,8 +79,9 @@ def get_git_executable() -> str:
if _cached_git_path is not None:
return _cached_git_path
_cached_git_path = _find_git_executable()
return _cached_git_path
git_path = _find_git_executable()
_cached_git_path = git_path
return git_path
def _find_git_executable() -> str:
+3 -5
View File
@@ -68,11 +68,9 @@ def _run_where_command() -> str | None:
and _verify_glab_executable(found_path)
):
return found_path
except (
subprocess.TimeoutExpired,
OSError,
): # 'where' command failed or timed out - fall through to return None
pass # no-op
except (subprocess.TimeoutExpired, OSError):
# 'where' command failed or timed out - fall through to return None
pass
return None
+8 -16
View File
@@ -13,7 +13,6 @@ cases gracefully.
from __future__ import annotations
import logging
import os
import sys
logger = logging.getLogger(__name__)
@@ -21,9 +20,6 @@ logger = logging.getLogger(__name__)
# Track if pipe is broken to avoid repeated failed writes
_pipe_broken = False
# Check if running in tests to prevent closing stdout
_IN_TESTS = os.environ.get("AUTO_CLAUDE_TESTS", "0") == "1"
def safe_print(message: str, flush: bool = True) -> None:
"""
@@ -49,12 +45,10 @@ def safe_print(message: str, flush: bool = True) -> None:
# Pipe closed by parent process - this is expected during shutdown
_pipe_broken = True
# Quietly close stdout to prevent further errors
# Skip closing stdout during tests to avoid pytest capture issues
if not _IN_TESTS:
try:
sys.stdout.close()
except Exception: # stdout may already be closed
pass # no-op
try:
sys.stdout.close()
except Exception:
pass
logger.debug("Output pipe closed by parent process")
except ValueError as e:
# Handle writes to closed file (can happen after stdout.close())
@@ -68,12 +62,10 @@ def safe_print(message: str, flush: bool = True) -> None:
# Handle other pipe-related errors (EPIPE, etc.)
if e.errno == 32: # EPIPE - Broken pipe
_pipe_broken = True
# Skip closing stdout during tests to avoid pytest capture issues
if not _IN_TESTS:
try:
sys.stdout.close()
except Exception: # stdout may already be closed
pass # no-op
try:
sys.stdout.close()
except Exception:
pass
logger.debug("Output pipe closed (EPIPE)")
else:
# Re-raise unexpected OS errors
+2 -2
View File
@@ -248,8 +248,8 @@ def init_sentry(
parsed = float(env_rate)
if 0 <= parsed <= 1:
traces_sample_rate = parsed
except Exception: # Sentry not configured or unavailable
pass # no-op
except (ValueError, TypeError):
pass
# Configure logging integration to capture errors and warnings
logging_integration = LoggingIntegration(
-12
View File
@@ -44,9 +44,6 @@ def create_simple_client(
cwd: Path | None = None,
max_turns: int = 1,
max_thinking_tokens: int | None = None,
betas: list[str] | None = None,
effort_level: str | None = None,
fast_mode: bool = False,
) -> ClaudeSDKClient:
"""
Create a minimal Claude SDK client for single-turn utility operations.
@@ -68,11 +65,6 @@ def create_simple_client(
max_turns: Maximum conversation turns (default: 1 for single-turn)
max_thinking_tokens: Override thinking budget (None = use agent default from
AGENT_CONFIGS, converted using phase_config.THINKING_BUDGET_MAP)
betas: Optional list of SDK beta header strings (e.g., ["context-1m-2025-08-07"])
effort_level: Optional effort level for adaptive thinking models (e.g., "low",
"medium", "high"). Injected as CLAUDE_CODE_EFFORT_LEVEL env var.
fast_mode: Enable Fast Mode for faster Opus 4.6 output. Injected as
CLAUDE_CODE_FAST_MODE=true env var.
Returns:
Configured ClaudeSDKClient for single-turn operations
@@ -116,10 +108,6 @@ def create_simple_client(
if max_thinking_tokens is not None:
options_kwargs["max_thinking_tokens"] = max_thinking_tokens
# Add beta headers if specified (e.g., for 1M context window)
if betas:
options_kwargs["betas"] = betas
# Optional: Allow CLI path override via environment variable
env_cli_path = os.environ.get("CLAUDE_CLI_PATH")
if env_cli_path and validate_cli_path(env_cli_path):
+16 -5
View File
@@ -44,23 +44,30 @@ try:
debug_success,
debug_verbose,
debug_warning,
is_debug_enabled,
)
except ImportError:
def debug(*args, **kwargs):
pass # no-op
pass
def debug_detailed(*args, **kwargs):
pass
def debug_verbose(*args, **kwargs):
pass # no-op
pass
def debug_success(*args, **kwargs):
pass # no-op
pass
def debug_error(*args, **kwargs):
pass # no-op
pass
def debug_warning(*args, **kwargs):
pass # no-op
pass
def is_debug_enabled():
return False
# Import merge system
@@ -978,6 +985,10 @@ def _check_git_conflicts(project_dir: Path, spec_name: str) -> dict:
debug_warning(MODULE, "Could not find merge base")
return result
_merge_base = (
merge_base_result.stdout.strip()
) # Reserved for future conflict detection
# Get commit hashes
main_commit_result = run_git(
["rev-parse", result["base_branch"]],
+3 -11
View File
@@ -15,19 +15,11 @@ from ui import (
)
from worktree import WorktreeManager
__all__ = [
"print_merge_success",
"print_conflict_info",
"_print_merge_success",
"_print_conflict_info",
"show_build_summary",
"show_changed_files",
]
def show_build_summary(manager: WorktreeManager, spec_name: str) -> None:
"""Show a summary of what was built."""
summary = manager.get_change_summary(spec_name)
files = manager.get_changed_files(spec_name)
total = summary["new_files"] + summary["modified_files"] + summary["deleted_files"]
@@ -233,5 +225,5 @@ def print_conflict_info(result: dict) -> None:
# Export private names for backward compatibility
_print_merge_success = print_merge_success # noqa: F811 (intentional re-export)
_print_conflict_info = print_conflict_info # noqa: F811 (intentional re-export)
_print_merge_success = print_merge_success
_print_conflict_info = print_conflict_info
@@ -88,6 +88,7 @@ def finalize_workspace(
# Get the worktree path for test instructions
worktree_info = manager.get_worktree_info(spec_name)
staging_path = worktree_info.path if worktree_info else None
# Enhanced menu for post-build options
options = [
+2 -2
View File
@@ -503,7 +503,7 @@ def validate_merged_syntax(
return True, "" # Timeout = assume ok
except FileNotFoundError:
return True, "" # No esbuild = skip validation
except Exception:
except Exception as e:
return True, "" # Other errors = skip validation
# Python validation
@@ -589,7 +589,7 @@ def create_conflict_file_with_git(
Path(wt_path).unlink(missing_ok=True)
Path(base_path).unlink(missing_ok=True)
except Exception:
except Exception as e:
return None, False
+10 -14
View File
@@ -53,7 +53,7 @@ class ParallelMergeResult:
class MergeLockError(Exception):
"""Raised when a merge lock cannot be acquired."""
pass # no-op
pass
class MergeLock:
@@ -88,14 +88,12 @@ class MergeLock:
fd = os.open(
str(self.lock_file),
os.O_CREAT | os.O_EXCL | os.O_WRONLY,
0o600,
0o644,
)
# Ensure permissions are correct (override umask)
os.fchmod(fd, 0o600)
# Write PID directly to the file descriptor
os.write(fd, str(os.getpid()).encode("utf-8"))
os.close(fd)
# Write our PID to the lock file
self.lock_file.write_text(str(os.getpid()), encoding="utf-8")
self.acquired = True
return self
@@ -142,7 +140,7 @@ class MergeLock:
class SpecNumberLockError(Exception):
"""Raised when a spec number lock cannot be acquired."""
pass # no-op
pass
class SpecNumberLock:
@@ -180,14 +178,12 @@ class SpecNumberLock:
fd = os.open(
str(self.lock_file),
os.O_CREAT | os.O_EXCL | os.O_WRONLY,
0o600,
0o644,
)
# Ensure permissions are correct (override umask)
os.fchmod(fd, 0o600)
# Write PID directly to the file descriptor
os.write(fd, str(os.getpid()).encode("utf-8"))
os.close(fd)
# Write our PID to the lock file
self.lock_file.write_text(str(os.getpid()), encoding="utf-8")
self.acquired = True
return self
@@ -273,7 +269,7 @@ class SpecNumberLock:
try:
num = int(folder.name[:3])
max_num = max(max_num, num)
except ValueError: # Folder name doesn't start with a number; skip it
pass # no-op: skip invalid folder names
except ValueError:
pass
return max_num
+4 -14
View File
@@ -31,26 +31,16 @@ from worktree import WorktreeManager
from .git_utils import has_uncommitted_changes
from .models import WorkspaceMode
__all__ = [
"setup_workspace",
"copy_spec_to_worktree",
"choose_workspace",
"ensure_timeline_hook_installed",
"initialize_timeline_tracking",
"_ensure_timeline_hook_installed",
"_initialize_timeline_tracking",
]
# Import debug utilities
try:
from debug import debug, debug_warning
except ImportError:
def debug(*args, **kwargs):
pass # no-op
pass
def debug_warning(*args, **kwargs):
pass # no-op
pass
# Track if we've already tried to install the git hook this session
@@ -585,5 +575,5 @@ def initialize_timeline_tracking(
# Export private functions for backward compatibility
_ensure_timeline_hook_installed = ensure_timeline_hook_installed # noqa: F811 (intentional re-export)
_initialize_timeline_tracking = initialize_timeline_tracking # noqa: F811 (intentional re-export)
_ensure_timeline_hook_installed = ensure_timeline_hook_installed
_initialize_timeline_tracking = initialize_timeline_tracking
+11 -12
View File
@@ -153,7 +153,7 @@ class PushAndCreatePRResult(TypedDict, total=False):
class WorktreeError(Exception):
"""Error during worktree operations."""
pass # no-op
pass
@dataclass
@@ -429,8 +429,8 @@ class WorktreeManager:
if current_path.exists() and resolved_path.exists():
if os.path.samefile(resolved_path, current_path):
return line[len("branch refs/heads/") :]
except OSError: # Files may not exist or be inaccessible; fall through to case comparison
pass # no-op: fall through to case comparison
except OSError:
pass
# Fallback to normalized case comparison
if os.path.normcase(str(resolved_path)) == os.path.normcase(
str(current_path)
@@ -509,8 +509,8 @@ class WorktreeManager:
if resolved_path.exists() and registered_path.exists():
if os.path.samefile(resolved_path, registered_path):
return True
except OSError: # Files may not exist or be inaccessible; fall through to case comparison
pass # no-op: fall through to case comparison
except OSError:
pass
# Fallback to normalized case comparison for non-existent paths
if os.path.normcase(str(resolved_path)) == os.path.normcase(
str(registered_path)
@@ -583,11 +583,9 @@ class WorktreeManager:
stats["days_since_last_commit"] = (
datetime.now() - last_commit_date
).days
except (
ValueError,
TypeError,
): # If parsing fails, silently continue without date info
pass # no-op
except (ValueError, TypeError) as e:
# If parsing fails, silently continue without date info
pass
# Diff stats
result = self._run_git(
@@ -1796,8 +1794,9 @@ class WorktreeManager:
try:
data = json.loads(result.stdout)
return data.get("web_url")
except json.JSONDecodeError: # If JSON parsing fails, return None
pass # no-op
except json.JSONDecodeError:
# If JSON parsing fails, return None
pass
except (
subprocess.TimeoutExpired,
FileNotFoundError,
+1 -1
View File
@@ -1,3 +1,3 @@
"""Backward compatibility shim - import from spec.critique instead."""
from spec.critique import * # noqa: F401, F403
from spec.critique import * # noqa: F403
+1 -1
View File
@@ -1,3 +1,3 @@
"""Backward compatibility shim - import from integrations.graphiti.config instead."""
from integrations.graphiti.config import * # noqa: F401, F403
from integrations.graphiti.config import * # noqa: F403
+1 -1
View File
@@ -1,3 +1,3 @@
"""Backward compatibility shim - import from integrations.graphiti.providers_pkg instead."""
from integrations.graphiti.providers_pkg import * # noqa: F401, F403
from integrations.graphiti.providers_pkg import * # noqa: F403
+13 -20
View File
@@ -53,22 +53,15 @@ class ProjectAnalyzer:
try:
with open(project_index_path, encoding="utf-8") as f:
index = json.load(f)
# Extract tech stack from services (only if index is a dict)
if isinstance(index, dict):
for service_name, service_info in index.get(
"services", {}
).items():
if service_info.get("language"):
context["tech_stack"].append(service_info["language"])
if service_info.get("framework"):
context["tech_stack"].append(service_info["framework"])
context["tech_stack"] = list(set(context["tech_stack"]))
except (
json.JSONDecodeError,
KeyError,
AttributeError,
): # Invalid JSON; skip
pass # no-op: skip invalid files
# Extract tech stack from services
for service_name, service_info in index.get("services", {}).items():
if service_info.get("language"):
context["tech_stack"].append(service_info["language"])
if service_info.get("framework"):
context["tech_stack"].append(service_info["framework"])
context["tech_stack"] = list(set(context["tech_stack"]))
except (json.JSONDecodeError, KeyError):
pass
# Get roadmap context if enabled
if self.include_roadmap:
@@ -85,8 +78,8 @@ class ProjectAnalyzer:
# Get target audience
audience = roadmap.get("target_audience", {})
context["target_audience"] = audience.get("primary")
except (json.JSONDecodeError, KeyError): # Invalid JSON; skip
pass # no-op: skip invalid files
except (json.JSONDecodeError, KeyError):
pass
# Also check discovery for audience
discovery_path = (
@@ -104,8 +97,8 @@ class ProjectAnalyzer:
context["existing_features"] = current_state.get(
"existing_features", []
)
except (json.JSONDecodeError, KeyError): # Invalid JSON; skip
pass # no-op: skip invalid files
except (json.JSONDecodeError, KeyError):
pass
# Get kanban context if enabled
if self.include_kanban:
-2
View File
@@ -29,7 +29,6 @@ class IdeationConfigManager:
thinking_level: str = "medium",
refresh: bool = False,
append: bool = False,
fast_mode: bool = False,
):
"""Initialize configuration manager.
@@ -65,7 +64,6 @@ class IdeationConfigManager:
self.model,
self.thinking_level,
self.max_ideas_per_type,
fast_mode=fast_mode,
)
self.analyzer = ProjectAnalyzer(
self.project_dir,
+6 -6
View File
@@ -45,8 +45,8 @@ class IdeationFormatter:
print_status(
f"Preserving {len(existing_ideas)} existing ideas", "info"
)
except json.JSONDecodeError: # Invalid JSON; skip
pass # no-op: treat as empty file
except json.JSONDecodeError:
pass
# Collect new ideas from the enabled types
new_ideas = []
@@ -61,8 +61,8 @@ class IdeationFormatter:
ideas = data.get(ideation_type, [])
new_ideas.extend(ideas)
output_files.append(str(type_file))
except (json.JSONDecodeError, KeyError): # Invalid JSON; skip
pass # no-op: skip invalid files
except (json.JSONDecodeError, KeyError):
pass
# In append mode, filter out ideas from types we're regenerating
# (to avoid duplicates) and keep ideas from other types
@@ -141,6 +141,6 @@ class IdeationFormatter:
try:
with open(context_file, encoding="utf-8") as f:
context_data = json.load(f)
except json.JSONDecodeError: # Invalid JSON; skip
pass # no-op: treat as empty file
except json.JSONDecodeError:
pass
return context_data
+1 -8
View File
@@ -17,12 +17,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from client import create_client
from phase_config import (
get_model_betas,
get_thinking_budget,
get_thinking_kwargs_for_model,
resolve_model_id,
)
from phase_config import get_thinking_budget, resolve_model_id
from ui import print_status
# Ideation types
@@ -64,7 +59,6 @@ class IdeationGenerator:
model: str = "sonnet", # Changed from "opus" (fix #433)
thinking_level: str = "medium",
max_ideas_per_type: int = 5,
fast_mode: bool = False,
):
self.project_dir = Path(project_dir)
self.output_dir = Path(output_dir)
@@ -72,7 +66,6 @@ class IdeationGenerator:
self.thinking_level = thinking_level
self.thinking_budget = get_thinking_budget(thinking_level)
self.max_ideas_per_type = max_ideas_per_type
self.fast_mode = fast_mode
self.prompts_dir = Path(__file__).parent.parent / "prompts"
async def run_agent(
+1 -1
View File
@@ -302,7 +302,7 @@ Generate up to {self.max_ideas_per_type} {self.generator.get_type_label(ideation
Avoid duplicating features that are already planned (see ideation_context.json).
Output your ideas to {output_file.name}.
"""
await self.generator.run_agent(
success, output = await self.generator.run_agent(
prompt_file,
additional_context=context,
)
-3
View File
@@ -46,7 +46,6 @@ class IdeationOrchestrator:
thinking_level: str = "medium",
refresh: bool = False,
append: bool = False,
fast_mode: bool = False,
):
"""Initialize the ideation orchestrator.
@@ -61,7 +60,6 @@ class IdeationOrchestrator:
thinking_level: Thinking level for extended reasoning
refresh: Force regeneration of existing files
append: Preserve existing ideas when merging
fast_mode: Enable Fast Mode for faster Opus 4.6 output
"""
# Initialize configuration manager
self.config_manager = IdeationConfigManager(
@@ -75,7 +73,6 @@ class IdeationOrchestrator:
thinking_level=thinking_level,
refresh=refresh,
append=append,
fast_mode=fast_mode,
)
# Expose configuration for convenience
+1 -1
View File
@@ -207,7 +207,7 @@ class ImplementationPlan:
# (spec is complete, waiting for user to approve before coding starts)
if self.status == "human_review" and self.planStatus == "review":
# Keep the plan approval status - don't reset to backlog
pass # no-op
pass
else:
self.status = "backlog"
self.planStatus = "pending"
+4 -9
View File
@@ -3,14 +3,9 @@ Integrations Module
===================
External service integrations for Auto Claude.
Submodules:
- linear: Linear issue tracking integration
- graphiti: Graphiti knowledge graph integration
"""
# Submodules are accessible via standard Python import mechanism:
# from integrations.linear import ...
# from integrations.graphiti import ...
__all__ = []
__all__ = [
"linear",
"graphiti",
]
+5 -32
View File
@@ -3,23 +3,12 @@ Graphiti Integration
====================
Integration with Graphiti knowledge graph for semantic memory.
Module-level placeholders (with _ prefix) are defined for CodeQL static
analysis. The actual exported names (without _ prefix) trigger __getattr__
for lazy loading.
"""
from typing import Any
# Config imports don't require graphiti package
from .config import GraphitiConfig, validate_graphiti_config
# Module-level placeholders for CodeQL static analysis.
# Use list placeholder to satisfy CodeQL's "defined but not set to None" check.
GraphitiMemory: Any = []
create_llm_client: Any = []
create_embedder: Any = []
# Lazy imports for components that require graphiti package
__all__ = [
"GraphitiConfig",
"validate_graphiti_config",
@@ -29,34 +18,18 @@ __all__ = [
]
def __getattr__(name: str) -> Any:
def __getattr__(name):
"""Lazy import to avoid requiring graphiti package for config-only imports."""
private_map = {
"GraphitiMemory": "_GraphitiMemory",
"create_llm_client": "_create_llm_client",
"create_embedder": "_create_embedder",
}
if name in private_map:
private_name = private_map[name]
globals()[private_name] = _do_lazy_import(name)
return globals()[private_name]
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
def _do_lazy_import(name: str) -> Any:
"""Perform the actual lazy import for a given name."""
if name == "GraphitiMemory":
from .memory import GraphitiMemory
return GraphitiMemory
if name == "create_llm_client":
elif name == "create_llm_client":
from .providers import create_llm_client
return create_llm_client
if name == "create_embedder":
elif name == "create_embedder":
from .providers import create_embedder
return create_embedder
raise AssertionError(f"Unknown lazy import name: {name}")
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+3 -3
View File
@@ -145,7 +145,7 @@ class GraphitiConfig:
# OpenRouter settings (multi-provider aggregator)
openrouter_api_key: str = ""
openrouter_base_url: str = "https://openrouter.ai/api"
openrouter_base_url: str = "https://openrouter.ai/api/v1"
openrouter_llm_model: str = "anthropic/claude-sonnet-4"
openrouter_embedding_model: str = "openai/text-embedding-3-small"
@@ -207,10 +207,10 @@ class GraphitiConfig:
# OpenRouter settings
openrouter_api_key = os.environ.get("OPENROUTER_API_KEY", "")
openrouter_base_url = os.environ.get(
"OPENROUTER_BASE_URL", "https://openrouter.ai/api"
"OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1"
)
openrouter_llm_model = os.environ.get(
"OPENROUTER_LLM_MODEL", "anthropic/claude-3.5-sonnet"
"OPENROUTER_LLM_MODEL", "anthropic/claude-sonnet-4"
)
openrouter_embedding_model = os.environ.get(
"OPENROUTER_EMBEDDING_MODEL", "openai/text-embedding-3-small"
@@ -1,43 +0,0 @@
"""Fixtures for graphiti integration tests."""
from importlib.util import find_spec
from pathlib import Path
import pytest
def _check_graphiti_available() -> bool:
"""Check if graphiti dependencies are available."""
return find_spec("kuzu") is not None or find_spec("real_ladybug") is not None
# Skip all graphiti integration tests if dependencies are not available
pytestmark = pytest.mark.skipif(
not _check_graphiti_available(),
reason="graphiti dependencies (kuzu or real_ladybug) not available",
)
@pytest.fixture
def db_path(tmp_path: Path) -> str:
"""Provide a temporary database path for testing.
Args:
tmp_path: pytest's built-in temporary directory fixture
Returns:
str: Path to temporary database directory
"""
db_dir = tmp_path / "graphiti_db"
db_dir.mkdir(exist_ok=True)
return str(db_dir)
@pytest.fixture
def database() -> str:
"""Provide a test database name.
Returns:
str: Test database name
"""
return "test_db"
+1 -1
View File
@@ -22,7 +22,7 @@ see graphiti/graphiti.py.
from pathlib import Path
# Import config utilities
from integrations.graphiti.config import (
from graphiti_config import (
GraphitiConfig,
is_graphiti_enabled,
)
@@ -9,10 +9,10 @@ Exception classes for provider-related errors.
class ProviderError(Exception):
"""Raised when a provider cannot be initialized."""
pass # no-op
pass
class ProviderNotInstalled(ProviderError):
"""Raised when required packages for a provider are not installed."""
pass # no-op
pass
@@ -35,7 +35,7 @@ def create_openrouter_llm_client(config: "GraphitiConfig") -> Any:
>>> from auto_claude.integrations.graphiti.config import GraphitiConfig
>>> config = GraphitiConfig(
... openrouter_api_key="sk-or-...",
... openrouter_llm_model="anthropic/claude-3.5-sonnet"
... openrouter_llm_model="anthropic/claude-sonnet-4"
... )
>>> client = create_openrouter_llm_client(config)
"""
@@ -80,7 +80,7 @@ async def test_llm_connection(config: "GraphitiConfig") -> tuple[bool, str]:
from .factory import create_llm_client
try:
_llm_client = create_llm_client(config)
llm_client = create_llm_client(config)
# Most clients don't have a ping method, so just verify creation succeeded
return (
True,
@@ -112,7 +112,7 @@ async def test_embedder_connection(config: "GraphitiConfig") -> tuple[bool, str]
return False, msg
try:
_embedder = create_embedder(config)
embedder = create_embedder(config)
return (
True,
f"Embedder created successfully for provider: {config.embedder_provider}",
@@ -139,11 +139,6 @@ async def test_ollama_connection(
"""
import asyncio
# Normalize URL first (used in both paths)
url = base_url.rstrip("/")
if url.endswith("/v1"):
url = url[:-3]
try:
import aiohttp
except ImportError:
@@ -152,12 +147,16 @@ async def test_ollama_connection(
import urllib.request
try:
# Normalize URL (remove /v1 suffix if present)
url = base_url.rstrip("/")
if url.endswith("/v1"):
url = url[:-3]
req = urllib.request.Request(f"{url}/api/tags", method="GET")
with urllib.request.urlopen(req, timeout=5) as response:
status = response.status
if status == 200:
if response.status == 200:
return True, f"Ollama is running at {url}"
return False, f"Ollama returned status {status}"
return False, f"Ollama returned status {response.status}"
except urllib.error.URLError as e:
return False, f"Cannot connect to Ollama at {url}: {e.reason}"
except Exception as e:
@@ -165,20 +164,21 @@ async def test_ollama_connection(
# Use aiohttp if available
try:
# Normalize URL
url = base_url.rstrip("/")
if url.endswith("/v1"):
url = url[:-3]
async with aiohttp.ClientSession() as session:
try:
async with session.get(
f"{url}/api/tags", timeout=aiohttp.ClientTimeout(total=5)
) as response:
status = response.status
if status == 200:
return True, f"Ollama is running at {url}"
return False, f"Ollama returned status {status}"
except asyncio.TimeoutError:
return False, f"Ollama connection timed out at {url}"
except Exception:
# Catch aiohttp.ClientError and any other exceptions from session.get
return False, f"Cannot connect to Ollama at {url}"
except Exception:
# Catch any exceptions from ClientSession creation
return False, f"Cannot connect to Ollama at {url}"
async with session.get(
f"{url}/api/tags", timeout=aiohttp.ClientTimeout(total=5)
) as response:
if response.status == 200:
return True, f"Ollama is running at {url}"
return False, f"Ollama returned status {response.status}"
except asyncio.TimeoutError:
return False, f"Ollama connection timed out at {url}"
except aiohttp.ClientError as e:
return False, f"Cannot connect to Ollama at {url}: {e}"
except Exception as e:
return False, f"Ollama connection error: {e}"
@@ -25,14 +25,6 @@ from .schema import (
GroupIdMode,
)
# Import kuzu_driver_patched for test mocking support
# This module is used internally but needs to be accessible for patching
try:
from . import kuzu_driver_patched # noqa: F401
except ImportError:
# kuzu_driver_patched may not be available if real_ladybug is not installed
kuzu_driver_patched = None # type: ignore
# Re-export for convenience
__all__ = [
"GraphitiMemory",
@@ -362,7 +362,7 @@ class GraphitiMemory:
if result and self.state:
# Episode count updated in queries module
pass # no-op
pass
return result
except Exception as e:
@@ -117,8 +117,9 @@ def create_patched_kuzu_driver(db: str = ":memory:", max_concurrent_queries: int
logger.debug(
f"Dropped existing FTS index: {index_name}"
)
except Exception: # Index might not exist, that's fine
pass # no-op
except Exception:
# Index might not exist, that's fine
pass
# Create the FTS index
conn.execute(query)
@@ -172,7 +173,4 @@ def create_patched_kuzu_driver(db: str = ":memory:", max_concurrent_queries: int
# Run the parent schema setup (creates tables)
super().setup_schema()
# Export the class at module level for tests to import
globals()["PatchedKuzuDriver"] = PatchedKuzuDriver
return PatchedKuzuDriver(db=db, max_concurrent_queries=max_concurrent_queries)
@@ -31,9 +31,6 @@ Usage:
python integrations/graphiti/test_graphiti_memory.py --test ollama
"""
# Exclude this file from pytest collection - this is a standalone test script
__test__ = False
import argparse
import asyncio
import json
@@ -65,8 +62,8 @@ def apply_ladybug_monkeypatch():
sys.modules["kuzu"] = real_ladybug
return True
except Exception: # Test cleanup, ignore errors
pass # no-op
except ImportError:
pass
# Try native kuzu as fallback
try:
@@ -122,15 +119,8 @@ async def test_ladybugdb_connection(db_path: str, database: str) -> bool:
# Test basic query
result = conn.execute("RETURN 1 + 1 as test")
query_result = result.get_as_df()
# Handle both list and DataFrame return types from get_as_df()
if isinstance(query_result, list):
# real-ladybug returns a list of dicts
test_value = query_result[0].get("test") if query_result else None
else:
# pandas DataFrame
test_value = query_result["test"].iloc[0] if len(query_result) > 0 else None
df = result.get_as_df()
test_value = df["test"].iloc[0] if len(df) > 0 else None
if test_value == 2:
print_result("Connection", "SUCCESS - Database responds correctly", True)
@@ -251,29 +241,15 @@ async def test_keyword_search(db_path: str, database: str) -> bool:
try:
result = conn.execute(query)
query_result = result.get_as_df()
df = result.get_as_df()
# Handle both list and DataFrame return types from get_as_df()
if isinstance(query_result, list):
# real-ladybug returns a list of dicts
print(f" Found {len(query_result)} results:")
for row in query_result:
name = row.get("name", "unknown")[:50]
content = str(row.get("content", ""))[:60]
print(f" - {name}: {content}...")
print_result(
"Keyword Search", f"Found {len(query_result)} results", True
)
else:
# pandas DataFrame
print(f" Found {len(query_result)} results:")
for _, row in query_result.iterrows():
name = row.get("name", "unknown")[:50]
content = str(row.get("content", ""))[:60]
print(f" - {name}: {content}...")
print_result(
"Keyword Search", f"Found {len(query_result)} results", True
)
print(f" Found {len(df)} results:")
for _, row in df.iterrows():
name = row.get("name", "unknown")[:50]
content = str(row.get("content", ""))[:60]
print(f" - {name}: {content}...")
print_result("Keyword Search", f"Found {len(df)} results", True)
return True
except Exception as e:
@@ -587,15 +563,8 @@ async def test_database_contents(db_path: str, database: str) -> bool:
for table in tables_to_check:
try:
result = conn.execute(f"MATCH (n:{table}) RETURN count(n) as count")
query_result = result.get_as_df()
# Handle both list and DataFrame return types from get_as_df()
if isinstance(query_result, list):
count = query_result[0].get("count", 0) if query_result else 0
else:
count = (
query_result["count"].iloc[0] if len(query_result) > 0 else 0
)
df = result.get_as_df()
count = df["count"].iloc[0] if len(df) > 0 else 0
print(f" {table}: {count} nodes")
except Exception as e:
if "not exist" in str(e).lower() or "cannot" in str(e).lower():
@@ -613,21 +582,13 @@ async def test_database_contents(db_path: str, database: str) -> bool:
ORDER BY e.created_at DESC
LIMIT 5
""")
query_result = result.get_as_df()
df = result.get_as_df()
# Handle both list and DataFrame return types from get_as_df()
if isinstance(query_result, list):
if len(query_result) == 0:
print(" (none)")
else:
for row in query_result:
print(f" - {row.get('name', 'unknown')}")
if len(df) == 0:
print(" (none)")
else:
if len(query_result) == 0:
print(" (none)")
else:
for _, row in query_result.iterrows():
print(f" - {row.get('name', 'unknown')}")
for _, row in df.iterrows():
print(f" - {row.get('name', 'unknown')}")
except Exception as e:
if "Episodic" in str(e):
print(" (table not created yet)")
@@ -45,9 +45,6 @@ Usage:
python integrations/graphiti/test_ollama_embedding_memory.py --test full-cycle
"""
# Exclude this file from pytest collection - this is a standalone test script
__test__ = False
import argparse
import asyncio
import os
@@ -108,8 +105,8 @@ def apply_ladybug_monkeypatch():
sys.modules["kuzu"] = real_ladybug
return True
except Exception: # Test cleanup, ignore errors
pass # no-op
except ImportError:
pass
# Try native kuzu as fallback
try:
+1 -1
View File
@@ -1,3 +1,3 @@
"""Backward compatibility shim - import from integrations.linear.config instead."""
from integrations.linear.config import * # noqa: F401, F403
from integrations.linear.config import * # noqa: F403
+1 -40
View File
@@ -45,48 +45,9 @@ Usage Examples:
from memory import is_graphiti_memory_enabled
if is_graphiti_memory_enabled():
# Graphiti will automatically store data alongside file-based memory
pass # no-op
pass
"""
import io
import sys
# Configure safe encoding on Windows to handle Unicode characters in output
if sys.platform == "win32":
for _stream_name in ("stdout", "stderr"):
_stream = getattr(sys, _stream_name)
# Method 1: Try reconfigure (works for TTY)
if hasattr(_stream, "reconfigure"):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
continue
except (
AttributeError,
io.UnsupportedOperation,
OSError,
): # Stream doesn't support reconfigure
pass # no-op
# Method 2: Wrap with TextIOWrapper for piped output
try:
if hasattr(_stream, "buffer"):
_new_stream = io.TextIOWrapper(
_stream.buffer,
encoding="utf-8",
errors="replace",
line_buffering=True,
)
setattr(sys, _stream_name, _new_stream)
except (
AttributeError,
io.UnsupportedOperation,
OSError,
): # Stream doesn't support wrapper
pass # no-op
# Clean up temporary variables
del _stream_name, _stream
if "_new_stream" in dir():
del _new_stream
# Re-export all public functions from the memory package
from memory import (
append_gotcha,
+4 -2
View File
@@ -22,13 +22,15 @@ refactored ai_resolver package.
from __future__ import annotations
# Re-export all public APIs from the ai_resolver package
from .ai_resolver.resolver import (
AICallFunction,
from .ai_resolver import (
AIResolver,
ConflictContext,
create_claude_resolver,
)
# For backwards compatibility, also expose the AICallFunction type
from .ai_resolver.resolver import AICallFunction
__all__ = [
"AIResolver",
"ConflictContext",
+2 -3
View File
@@ -28,8 +28,7 @@ auto_merger module. The actual implementation has been split into:
from __future__ import annotations
# Re-export for backward compatibility from the actual implementation modules
from .auto_merger.context import MergeContext
from .auto_merger.merger import AutoMerger
# Re-export for backward compatibility
from .auto_merger import AutoMerger, MergeContext
__all__ = ["AutoMerger", "MergeContext"]
@@ -27,4 +27,4 @@ class MergeStrategyHandler(ABC):
Returns:
MergeResult with merged content or error
"""
pass # no-op
pass
@@ -44,7 +44,7 @@ class HooksStrategy(MergeStrategyHandler):
for change in other_changes:
if change.content_after:
# This is a simplification - in production we'd need smarter merging
pass # no-op
pass
return MergeResult(
decision=MergeDecision.AUTO_MERGED,
@@ -77,7 +77,7 @@ class OrderByTimeStrategy(MergeStrategyHandler):
)
elif change.content_after and not change.content_before:
# Addition - handled by other strategies
pass # no-op
pass
return MergeResult(
decision=MergeDecision.AUTO_MERGED,
+3 -3
View File
@@ -32,13 +32,13 @@ try:
except ImportError:
def debug(*args, **kwargs):
pass # no-op
pass
def debug_detailed(*args, **kwargs):
pass # no-op
pass
def debug_verbose(*args, **kwargs):
pass # no-op
pass
logger = logging.getLogger(__name__)
+2 -2
View File
@@ -50,10 +50,10 @@ try:
except ImportError:
def debug(*args, **kwargs):
pass # no-op
pass
def debug_success(*args, **kwargs):
pass # no-op
pass
logger = logging.getLogger(__name__)
+2 -2
View File
@@ -13,9 +13,9 @@ The actual implementation has been modularized into:
- file_evolution/tracker.py: Main FileEvolutionTracker class
For new code, prefer importing directly from the package:
from .file_evolution.tracker import FileEvolutionTracker
from .file_evolution import FileEvolutionTracker
"""
from .file_evolution.tracker import FileEvolutionTracker
from .file_evolution import FileEvolutionTracker
__all__ = ["FileEvolutionTracker"]
@@ -24,10 +24,10 @@ try:
except ImportError:
def debug(*args, **kwargs):
pass # no-op
pass
def debug_success(*args, **kwargs):
pass # no-op
pass
logger = logging.getLogger(__name__)
@@ -25,10 +25,10 @@ try:
except ImportError:
def debug(*args, **kwargs):
pass # no-op
pass
def debug_warning(*args, **kwargs):
pass # no-op
pass
logger = logging.getLogger(__name__)
+2 -2
View File
@@ -27,10 +27,10 @@ try:
except ImportError:
def debug(*args, **kwargs):
pass # no-op
pass
def debug_success(*args, **kwargs):
pass # no-op
pass
logger = logging.getLogger(__name__)
+5 -3
View File
@@ -151,6 +151,8 @@ def combine_non_conflicting_changes(
other.append(change)
# Apply in order: imports, then modifications, then functions, then other
ext = Path(file_path).suffix.lower()
# Add imports
if imports:
# Content is already normalized to LF, so only check for \n
@@ -206,15 +208,15 @@ def find_import_end(lines: list[str], file_path: str) -> int:
Returns:
Index where imports end (insert position for new imports)
"""
file_ext = Path(file_path).suffix.lower()
ext = Path(file_path).suffix.lower()
last_import = 0
for i, line in enumerate(lines):
stripped = line.strip()
if file_ext == ".py":
if ext == ".py":
if stripped.startswith(("import ", "from ")):
last_import = i + 1
elif file_ext in {".js", ".jsx", ".ts", ".tsx"}:
elif ext in {".js", ".jsx", ".ts", ".tsx"}:
if stripped.startswith("import "):
last_import = i + 1
+1
View File
@@ -73,6 +73,7 @@ class MergePipeline:
Returns:
MergeResult with merged content or conflict info
"""
task_ids = [s.task_id for s in task_snapshots]
logger.info(f"Merging {file_path} with {len(task_snapshots)} task(s)")
if progress_callback:
+11 -7
View File
@@ -51,29 +51,33 @@ try:
debug_success,
debug_verbose,
debug_warning,
is_debug_enabled,
)
except ImportError:
def debug(*args, **kwargs):
pass # no-op
pass
def debug_detailed(*args, **kwargs):
pass # no-op
pass
def debug_verbose(*args, **kwargs):
pass # no-op
pass
def debug_success(*args, **kwargs):
pass # no-op
pass
def debug_error(*args, **kwargs):
pass # no-op
pass
def debug_warning(*args, **kwargs):
pass # no-op
pass
def debug_section(*args, **kwargs):
pass # no-op
pass
def is_debug_enabled():
return False
logger = logging.getLogger(__name__)
+1 -3
View File
@@ -66,9 +66,7 @@ class MergeProgressCallback(Protocol):
percent: int,
message: str,
details: dict[str, Any] | None = None,
) -> None:
"""no-op (abstract method)"""
...
) -> None: ...
def emit_progress(
+10 -6
View File
@@ -11,31 +11,35 @@ rather than line-level diffs.
from __future__ import annotations
import logging
from pathlib import Path
from .types import FileAnalysis
__all__ = ["SemanticAnalyzer", "analyze_with_regex"]
# Import debug utilities
try:
from debug import (
debug,
debug_detailed,
debug_success,
debug_verbose,
)
except ImportError:
# Fallback if debug module not available
def debug(*args, **kwargs):
pass # no-op
pass
def debug_detailed(*args, **kwargs):
pass
def debug_verbose(*args, **kwargs):
pass # no-op
pass
def debug_error(*args, **kwargs):
pass # no-op
def debug_success(*args, **kwargs):
pass
logger = logging.getLogger(__name__)
MODULE = "merge.semantic_analyzer"
# Import regex-based analyzer
+12 -6
View File
@@ -23,11 +23,17 @@ logger = logging.getLogger(__name__)
# Import debug utilities
try:
from debug import debug_warning
from debug import debug, debug_error, debug_warning
except ImportError:
def debug(*args, **kwargs):
pass
def debug_error(*args, **kwargs):
pass
def debug_warning(*args, **kwargs):
pass # no-op
pass
MODULE = "merge.timeline_git"
@@ -168,8 +174,8 @@ class TimelineGitHelper:
else None
)
except Exception: # Non-critical error; continue
pass # no-op: non-critical, skip error
except Exception:
pass
return info
@@ -300,8 +306,8 @@ class TimelineGitHelper:
if "/" in upstream:
return upstream.split("/", 1)[1]
return upstream
except Exception: # Non-critical error; continue
pass # no-op: non-critical, skip error
except Exception:
pass
for branch in ["main", "master", "develop"]:
try:
+1 -1
View File
@@ -29,7 +29,7 @@ try:
except ImportError:
def debug(*args, **kwargs):
pass # no-op
pass
MODULE = "merge.timeline_persistence"
+3 -3
View File
@@ -36,13 +36,13 @@ try:
except ImportError:
def debug(*args, **kwargs):
pass # no-op
pass
def debug_success(*args, **kwargs):
pass # no-op
pass
def debug_warning(*args, **kwargs):
pass # no-op
pass
MODULE = "merge.timeline_tracker"
-37
View File
@@ -12,46 +12,9 @@ Usage:
"""
import argparse
import io
import sys
from pathlib import Path
# Configure safe encoding on Windows to handle Unicode characters in output
if sys.platform == "win32":
for _stream_name in ("stdout", "stderr"):
_stream = getattr(sys, _stream_name)
# Method 1: Try reconfigure (works for TTY)
if hasattr(_stream, "reconfigure"):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
continue
except (
AttributeError,
io.UnsupportedOperation,
OSError,
): # Stream doesn't support reconfigure
pass # no-op
# Method 2: Wrap with TextIOWrapper for piped output
try:
if hasattr(_stream, "buffer"):
_new_stream = io.TextIOWrapper(
_stream.buffer,
encoding="utf-8",
errors="replace",
line_buffering=True,
)
setattr(sys, _stream_name, _new_stream)
except (
AttributeError,
io.UnsupportedOperation,
OSError,
): # Stream doesn't support wrapper
pass # no-op
# Clean up temporary variables
del _stream_name, _stream
if "_new_stream" in dir():
del _new_stream
from .file_timeline import FileTimelineTracker
+2 -39
View File
@@ -15,7 +15,6 @@ Output:
"""
import argparse
import io
import json
import re
import sys
@@ -23,42 +22,6 @@ import urllib.error
import urllib.request
from typing import Any
# Configure safe encoding on Windows to handle Unicode characters in output
if sys.platform == "win32":
for _stream_name in ("stdout", "stderr"):
_stream = getattr(sys, _stream_name)
# Method 1: Try reconfigure (works for TTY)
if hasattr(_stream, "reconfigure"):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
continue
except (
AttributeError,
io.UnsupportedOperation,
OSError,
): # Stream doesn't support reconfigure
pass # no-op
# Method 2: Wrap with TextIOWrapper for piped output
try:
if hasattr(_stream, "buffer"):
_new_stream = io.TextIOWrapper(
_stream.buffer,
encoding="utf-8",
errors="replace",
line_buffering=True,
)
setattr(sys, _stream_name, _new_stream)
except (
AttributeError,
io.UnsupportedOperation,
OSError,
): # Stream doesn't support wrapper
pass # no-op
# Clean up temporary variables
del _stream_name, _stream
if "_new_stream" in dir():
del _new_stream
DEFAULT_OLLAMA_URL = "http://localhost:11434"
# Minimum Ollama version required for newer embedding models (qwen3-embedding, etc.)
@@ -214,7 +177,7 @@ def fetch_ollama_api(base_url: str, endpoint: str, timeout: int = 5) -> dict | N
with urllib.request.urlopen(req, timeout=timeout) as response:
return json.loads(response.read().decode())
except urllib.error.URLError:
except urllib.error.URLError as e:
return None
except json.JSONDecodeError:
return None
@@ -541,7 +504,7 @@ def cmd_pull_model(args) -> None:
)
elif progress.get("status") == "success":
# Download complete
pass # no-op
pass
except json.JSONDecodeError:
continue
+19 -168
View File
@@ -12,44 +12,30 @@ from pathlib import Path
from typing import Literal, TypedDict
# Model shorthand to full model ID mapping
# Values must match apps/frontend/src/shared/constants/models.ts MODEL_ID_MAP
MODEL_ID_MAP: dict[str, str] = {
"opus": "claude-opus-4-6",
"opus-1m": "claude-opus-4-6",
"opus": "claude-opus-4-5-20251101",
"sonnet": "claude-sonnet-4-5-20250929",
"haiku": "claude-haiku-4-5-20251001",
}
# Model shorthand to required SDK beta headers
# Maps model shorthands that need special beta flags (e.g., 1M context window)
MODEL_BETAS_MAP: dict[str, list[str]] = {
"opus-1m": ["context-1m-2025-08-07"],
}
# Thinking level to budget tokens mapping
# Values must match apps/frontend/src/shared/constants/models.ts THINKING_BUDGET_MAP
THINKING_BUDGET_MAP: dict[str, int] = {
# Thinking level to budget tokens mapping (None = no extended thinking)
# Values must match auto-claude-ui/src/shared/constants/models.ts THINKING_BUDGET_MAP
THINKING_BUDGET_MAP: dict[str, int | None] = {
"none": None,
"low": 1024,
"medium": 4096, # Moderate analysis
"high": 16384, # Deep thinking for QA review
"ultrathink": 63999, # Maximum reasoning depth (API requires max_tokens >= budget + 1, so 63999 + 1 = 64000 limit)
}
# Effort level mapping for adaptive thinking models (e.g., Opus 4.6)
# These models support CLAUDE_CODE_EFFORT_LEVEL env var for effort-based routing
EFFORT_LEVEL_MAP: dict[str, str] = {"low": "low", "medium": "medium", "high": "high"}
# Models that support adaptive thinking via effort level (env var)
# These models get both max_thinking_tokens AND effort_level
ADAPTIVE_THINKING_MODELS: set[str] = {"claude-opus-4-6"}
# Spec runner phase-specific thinking levels
# Heavy phases use high for deep analysis
# Heavy phases use ultrathink for deep analysis
# Light phases use medium after compaction
SPEC_PHASE_THINKING_LEVELS: dict[str, str] = {
# Heavy phases - high (discovery, spec creation, self-critique)
"discovery": "high",
"spec_writing": "high",
"self_critique": "high",
# Heavy phases - ultrathink (discovery, spec creation, self-critique)
"discovery": "ultrathink",
"spec_writing": "ultrathink",
"self_critique": "ultrathink",
# Light phases - medium (after first invocation with compaction)
"requirements": "medium",
"research": "medium",
@@ -99,7 +85,6 @@ class TaskMetadataConfig(TypedDict, total=False):
phaseThinking: PhaseThinkingConfig
model: str
thinkingLevel: str
fastMode: bool
Phase = Literal["spec", "planning", "coding", "qa"]
@@ -127,7 +112,6 @@ def resolve_model_id(model: str) -> str:
"haiku": "ANTHROPIC_DEFAULT_HAIKU_MODEL",
"sonnet": "ANTHROPIC_DEFAULT_SONNET_MODEL",
"opus": "ANTHROPIC_DEFAULT_OPUS_MODEL",
"opus-1m": "ANTHROPIC_DEFAULT_OPUS_MODEL",
}
env_var = env_var_map.get(model)
if env_var:
@@ -142,31 +126,15 @@ def resolve_model_id(model: str) -> str:
return model
def get_model_betas(model_short: str) -> list[str]:
"""
Get required SDK beta headers for a model shorthand.
Some model configurations (e.g., opus-1m for 1M context window) require
passing beta headers to the Claude Agent SDK.
Args:
model_short: Model shorthand (e.g., 'opus', 'opus-1m', 'sonnet')
Returns:
List of beta header strings, or empty list if none required
"""
return MODEL_BETAS_MAP.get(model_short, [])
def get_thinking_budget(thinking_level: str) -> int:
def get_thinking_budget(thinking_level: str) -> int | None:
"""
Get the thinking budget for a thinking level.
Args:
thinking_level: Thinking level (low, medium, high)
thinking_level: Thinking level (none, low, medium, high, ultrathink)
Returns:
Token budget for extended thinking
Token budget or None for no extended thinking
"""
import logging
@@ -246,43 +214,6 @@ def get_phase_model(
return resolve_model_id(DEFAULT_PHASE_MODELS[phase])
def get_phase_model_betas(
spec_dir: Path,
phase: Phase,
cli_model: str | None = None,
) -> list[str]:
"""
Get required SDK beta headers for the model selected for a specific phase.
Uses the same priority logic as get_phase_model() to determine which model
shorthand is selected, then looks up any required beta headers.
Args:
spec_dir: Path to the spec directory
phase: Execution phase (spec, planning, coding, qa)
cli_model: Model from CLI argument (optional)
Returns:
List of beta header strings, or empty list if none required
"""
# Determine the model shorthand (before resolution to full ID)
if cli_model:
return get_model_betas(cli_model)
metadata = load_task_metadata(spec_dir)
if metadata:
if metadata.get("isAutoProfile") and metadata.get("phaseModels"):
phase_models = metadata["phaseModels"]
model_short = phase_models.get(phase, DEFAULT_PHASE_MODELS[phase])
return get_model_betas(model_short)
if metadata.get("model"):
return get_model_betas(metadata["model"])
return get_model_betas(DEFAULT_PHASE_MODELS[phase])
def get_phase_thinking(
spec_dir: Path,
phase: Phase,
@@ -330,7 +261,7 @@ def get_phase_thinking_budget(
spec_dir: Path,
phase: Phase,
cli_thinking: str | None = None,
) -> int:
) -> int | None:
"""
Get the thinking budget tokens for a specific execution phase.
@@ -340,7 +271,7 @@ def get_phase_thinking_budget(
cli_thinking: Thinking level from CLI argument (optional)
Returns:
Token budget for extended thinking
Token budget or None for no extended thinking
"""
thinking_level = get_phase_thinking(spec_dir, phase, cli_thinking)
return get_thinking_budget(thinking_level)
@@ -351,7 +282,7 @@ def get_phase_config(
phase: Phase,
cli_model: str | None = None,
cli_thinking: str | None = None,
) -> tuple[str, str, int]:
) -> tuple[str, str, int | None]:
"""
Get the full configuration for a specific execution phase.
@@ -371,87 +302,7 @@ def get_phase_config(
return model_id, thinking_level, thinking_budget
def is_adaptive_model(model_id: str) -> bool:
"""
Check if a model supports adaptive thinking via effort level.
Adaptive models support the CLAUDE_CODE_EFFORT_LEVEL environment variable
for effort-based routing in addition to max_thinking_tokens.
Args:
model_id: Full model ID (e.g., 'claude-opus-4-6')
Returns:
True if the model supports adaptive thinking
"""
return model_id in ADAPTIVE_THINKING_MODELS
def get_thinking_kwargs_for_model(model_id: str, thinking_level: str) -> dict:
"""
Get thinking-related kwargs for create_client() based on model type.
For adaptive models (Opus 4.6): returns both max_thinking_tokens and effort_level.
For other models (Sonnet, Haiku): returns only max_thinking_tokens.
Args:
model_id: Full model ID (e.g., 'claude-opus-4-6')
thinking_level: Thinking level string (low, medium, high)
Returns:
Dict with 'max_thinking_tokens' and optionally 'effort_level'
"""
kwargs: dict = {"max_thinking_tokens": get_thinking_budget(thinking_level)}
if is_adaptive_model(model_id):
kwargs["effort_level"] = EFFORT_LEVEL_MAP.get(thinking_level, "medium")
return kwargs
def get_phase_client_thinking_kwargs(
spec_dir: Path,
phase: Phase,
phase_model: str,
cli_thinking: str | None = None,
) -> dict:
"""
Get thinking kwargs for create_client() for a specific execution phase.
Combines get_phase_thinking() and get_thinking_kwargs_for_model() to produce
the correct kwargs dict based on phase config and model capabilities.
Args:
spec_dir: Path to the spec directory
phase: Execution phase (spec, planning, coding, qa)
phase_model: Resolved full model ID for this phase
cli_thinking: Thinking level from CLI argument (optional)
Returns:
Dict with 'max_thinking_tokens' and optionally 'effort_level'
"""
thinking_level = get_phase_thinking(spec_dir, phase, cli_thinking)
return get_thinking_kwargs_for_model(phase_model, thinking_level)
def get_fast_mode(spec_dir: Path) -> bool:
"""
Check if Fast Mode is enabled for this task.
Fast Mode provides faster Opus 4.6 output at higher cost.
Reads the fastMode flag from task_metadata.json.
Args:
spec_dir: Path to the spec directory
Returns:
True if Fast Mode is enabled, False otherwise
"""
metadata = load_task_metadata(spec_dir)
if metadata:
return bool(metadata.get("fastMode", False))
return False
def get_spec_phase_thinking_budget(phase_name: str) -> int:
def get_spec_phase_thinking_budget(phase_name: str) -> int | None:
"""
Get the thinking budget for a specific spec runner phase.
@@ -462,7 +313,7 @@ def get_spec_phase_thinking_budget(phase_name: str) -> int:
phase_name: Name of the spec phase (e.g., 'discovery', 'spec_writing')
Returns:
Token budget for extended thinking
Token budget for extended thinking, or None for no extended thinking
"""
thinking_level = SPEC_PHASE_THINKING_LEVELS.get(phase_name, "medium")
return get_thinking_budget(thinking_level)
+4 -4
View File
@@ -104,8 +104,8 @@ class ContextLoader:
)
if declared_type in _WORKFLOW_TYPE_MAPPING:
return _WORKFLOW_TYPE_MAPPING[declared_type]
except (json.JSONDecodeError, KeyError): # Invalid JSON; skip
pass # no-op: skip invalid files
except (json.JSONDecodeError, KeyError):
pass
# 2. Check complexity_assessment.json (AI's assessment)
assessment_file = self.spec_dir / "complexity_assessment.json"
@@ -118,8 +118,8 @@ class ContextLoader:
)
if declared_type in _WORKFLOW_TYPE_MAPPING:
return _WORKFLOW_TYPE_MAPPING[declared_type]
except (json.JSONDecodeError, KeyError): # Invalid JSON; skip
pass # no-op: skip invalid files
except (json.JSONDecodeError, KeyError):
pass
# 3. & 4. Fall back to spec content detection
return self._detect_workflow_type_from_spec(spec_content)
+3
View File
@@ -54,6 +54,7 @@ class FeaturePlanGenerator(PlanGenerator):
service_order = determine_service_order(files_by_service)
backend_phase = None
worker_phase = None
for service in service_order:
files = files_by_service[service]
@@ -114,6 +115,8 @@ class FeaturePlanGenerator(PlanGenerator):
# Track for dependencies
if service_type in ["backend", "api", "server"]:
backend_phase = phase_num
elif service_type in ["worker", "celery"]:
worker_phase = phase_num
# Add integration phase if multiple services
if len(service_order) > 1:
-38
View File
@@ -18,47 +18,9 @@ Usage:
python auto-claude/planner.py --spec-dir auto-claude/specs/001-feature/
"""
import io
import json
import sys
from pathlib import Path
# Configure safe encoding on Windows to handle Unicode characters in output
if sys.platform == "win32":
for _stream_name in ("stdout", "stderr"):
_stream = getattr(sys, _stream_name)
# Method 1: Try reconfigure (works for TTY)
if hasattr(_stream, "reconfigure"):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
continue
except (
AttributeError,
io.UnsupportedOperation,
OSError,
): # Stream doesn't support reconfigure
pass # no-op
# Method 2: Wrap with TextIOWrapper for piped output
try:
if hasattr(_stream, "buffer"):
_new_stream = io.TextIOWrapper(
_stream.buffer,
encoding="utf-8",
errors="replace",
line_buffering=True,
)
setattr(sys, _stream_name, _new_stream)
except (
AttributeError,
io.UnsupportedOperation,
OSError,
): # Stream doesn't support wrapper
pass # no-op
# Clean up temporary variables
del _stream_name, _stream
if "_new_stream" in dir():
del _new_stream
from implementation_plan import ImplementationPlan
from planner_lib.context import ContextLoader
from planner_lib.generators import get_plan_generator
-37
View File
@@ -10,47 +10,10 @@ Usage:
python prediction.py auto-claude/specs/001-feature/
"""
import io
import json
import sys
from pathlib import Path
# Configure safe encoding on Windows to handle Unicode characters in output
if sys.platform == "win32":
for _stream_name in ("stdout", "stderr"):
_stream = getattr(sys, _stream_name)
# Method 1: Try reconfigure (works for TTY)
if hasattr(_stream, "reconfigure"):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
continue
except (
AttributeError,
io.UnsupportedOperation,
OSError,
): # Stream doesn't support reconfigure
pass # no-op
# Method 2: Wrap with TextIOWrapper for piped output
try:
if hasattr(_stream, "buffer"):
_new_stream = io.TextIOWrapper(
_stream.buffer,
encoding="utf-8",
errors="replace",
line_buffering=True,
)
setattr(sys, _stream_name, _new_stream)
except (
AttributeError,
io.UnsupportedOperation,
OSError,
): # Stream doesn't support wrapper
pass # no-op
# Clean up temporary variables
del _stream_name, _stream
if "_new_stream" in dir():
del _new_stream
from prediction import generate_subtask_checklist
+9 -9
View File
@@ -23,18 +23,18 @@ tailored security profiles.
"""
# Re-export all command registries from the package
from .command_registry.base import (
from .command_registry import (
BASE_COMMANDS,
CLOUD_COMMANDS,
CODE_QUALITY_COMMANDS,
DATABASE_COMMANDS,
FRAMEWORK_COMMANDS,
INFRASTRUCTURE_COMMANDS,
LANGUAGE_COMMANDS,
PACKAGE_MANAGER_COMMANDS,
VALIDATED_COMMANDS,
VERSION_MANAGER_COMMANDS,
)
from .command_registry.cloud import CLOUD_COMMANDS
from .command_registry.code_quality import CODE_QUALITY_COMMANDS
from .command_registry.databases import DATABASE_COMMANDS
from .command_registry.frameworks import FRAMEWORK_COMMANDS
from .command_registry.infrastructure import INFRASTRUCTURE_COMMANDS
from .command_registry.languages import LANGUAGE_COMMANDS
from .command_registry.package_managers import PACKAGE_MANAGER_COMMANDS
from .command_registry.version_managers import VERSION_MANAGER_COMMANDS
__all__ = [
"BASE_COMMANDS",
+2 -2
View File
@@ -253,8 +253,8 @@ class StackDetector:
if "apiVersion:" in content and "kind:" in content:
self.stack.infrastructure.append("kubernetes")
break
except OSError: # Non-critical failure; continue
pass # no-op: skip inaccessible files
except OSError:
pass
# Helm
if self.parser.file_exists("Chart.yaml", "charts/"):
+8 -5
View File
@@ -56,9 +56,12 @@ class StructureAnalyzer:
if pkg and "scripts" in pkg:
self.custom_scripts.npm_scripts = list(pkg["scripts"].keys())
# If any npm scripts exist, allow the npm-related commands
if self.custom_scripts.npm_scripts:
self.script_commands.update(["npm", "yarn", "pnpm", "bun"])
# Add commands to run these scripts
for script in self.custom_scripts.npm_scripts:
self.script_commands.add("npm")
self.script_commands.add("yarn")
self.script_commands.add("pnpm")
self.script_commands.add("bun")
def _detect_makefile_targets(self) -> None:
"""Detect Makefile targets."""
@@ -100,8 +103,8 @@ class StructureAnalyzer:
def _detect_shell_scripts(self) -> None:
"""Detect shell scripts in root directory."""
for _ in ["*.sh", "*.bash"]:
for script_path in self.parser.glob_files(_):
for ext in ["*.sh", "*.bash"]:
for script_path in self.parser.glob_files(ext):
script_name = script_path.name
self.custom_scripts.shell_scripts.append(script_name)
# Allow executing these scripts
+1 -1
View File
@@ -1,3 +1,3 @@
"""Backward compatibility shim - import from prompts_pkg.prompt_generator instead."""
from prompts_pkg.prompt_generator import * # noqa: F401, F403
from prompts_pkg.prompt_generator import * # noqa: F403
+1 -1
View File
@@ -1,3 +1,3 @@
"""Backward compatibility shim - import from prompts_pkg.prompts instead."""
from prompts_pkg.prompts import * # noqa: F401, F403
from prompts_pkg.prompts import * # noqa: F403

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