devloop 0.3.1__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. {devloop-0.3.1 → devloop-0.3.2}/PKG-INFO +1 -1
  2. {devloop-0.3.1 → devloop-0.3.2}/pyproject.toml +1 -1
  3. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/main.py +85 -4
  4. devloop-0.3.2/src/devloop/cli/templates/git_hooks/pre-commit +68 -0
  5. devloop-0.3.2/src/devloop/cli/templates/git_hooks/pre-commit-checks +72 -0
  6. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/agent.py +29 -15
  7. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/auto_fix.py +151 -10
  8. devloop-0.3.2/src/devloop/core/backup_manager.py +409 -0
  9. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/config.py +93 -2
  10. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/manager.py +121 -3
  11. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/performance.py +114 -0
  12. {devloop-0.3.1 → devloop-0.3.2}/LICENSE +0 -0
  13. {devloop-0.3.1 → devloop-0.3.2}/README.md +0 -0
  14. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/__init__.py +0 -0
  15. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/__init__.py +0 -0
  16. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/agent_health_monitor.py +0 -0
  17. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/ci_monitor.py +0 -0
  18. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/code_rabbit.py +0 -0
  19. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/doc_lifecycle.py +0 -0
  20. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/echo.py +0 -0
  21. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/file_logger.py +0 -0
  22. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/formatter.py +0 -0
  23. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/git_commit_assistant.py +0 -0
  24. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/linter.py +0 -0
  25. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/performance_profiler.py +0 -0
  26. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/sandbox_helper.py +0 -0
  27. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/security_scanner.py +0 -0
  28. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/snyk.py +0 -0
  29. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/test_runner.py +0 -0
  30. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/type_checker.py +0 -0
  31. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/__init__.py +0 -0
  32. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/commands/__init__.py +0 -0
  33. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/commands/custom_agents.py +0 -0
  34. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/commands/feedback.py +0 -0
  35. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/commands/summary.py +0 -0
  36. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/main_v1.py +0 -0
  37. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/pyodide_installer.py +0 -0
  38. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/templates/claude_commands/README.md +0 -0
  39. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/templates/claude_commands/agent-summary.md +0 -0
  40. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/templates/claude_commands/devloop-findings.md +0 -0
  41. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/templates/claude_commands/devloop-status.md +0 -0
  42. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/collectors/__init__.py +0 -0
  43. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/collectors/base.py +0 -0
  44. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/collectors/filesystem.py +0 -0
  45. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/collectors/git.py +0 -0
  46. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/collectors/manager.py +0 -0
  47. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/collectors/process.py +0 -0
  48. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/collectors/system.py +0 -0
  49. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/__init__.py +0 -0
  50. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/agent_template.py +0 -0
  51. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/amp_integration.py +0 -0
  52. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/context.py +0 -0
  53. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/context_store.py +0 -0
  54. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/contextual_feedback.py +0 -0
  55. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/custom_agent.py +0 -0
  56. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/debug_trace.py +0 -0
  57. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/event.py +0 -0
  58. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/event_store.py +0 -0
  59. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/feedback.py +0 -0
  60. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/learning.py +0 -0
  61. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/operational_health.py +0 -0
  62. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/proactive_feedback.py +0 -0
  63. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/summary_formatter.py +0 -0
  64. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/summary_generator.py +0 -0
  65. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/security/__init__.py +0 -0
  66. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/security/audit_logger.py +0 -0
  67. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/security/bubblewrap_sandbox.py +0 -0
  68. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/security/cgroups_helper.py +0 -0
  69. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/security/factory.py +0 -0
  70. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/security/no_sandbox.py +0 -0
  71. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/security/package.json +0 -0
  72. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/security/pyodide_runner.js +0 -0
  73. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/security/pyodide_sandbox.py +0 -0
  74. {devloop-0.3.1 → devloop-0.3.2}/src/devloop/security/sandbox.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devloop
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Intelligent background agents for development workflow automation
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "devloop"
3
- version = "0.3.1"
3
+ version = "0.3.2"
4
4
  description = "Intelligent background agents for development workflow automation"
5
5
  authors = ["DevLoop Contributors <devloop@example.com>"]
6
6
  license = "MIT"
@@ -69,7 +69,7 @@ def amp_context():
69
69
  from pathlib import Path
70
70
 
71
71
  # Try to read the context index
72
- context_dir = Path(".devloop/context")
72
+ context_dir = Path.cwd() / ".devloop/context"
73
73
  index_file = context_dir / "index.json"
74
74
 
75
75
  if index_file.exists():
@@ -289,8 +289,13 @@ async def watch_async(path: Path, config_path: Path | None):
289
289
  console.print(f"[dim]Context store: {context_store.context_dir}[/dim]")
290
290
  console.print(f"[dim]Event store: {event_store.db_path}[/dim]")
291
291
 
292
- # Create agent manager with project directory
293
- agent_manager = AgentManager(event_bus, project_dir=path)
292
+ # Get global config for resource limits
293
+ global_config = config.get_global_config()
294
+
295
+ # Create agent manager with project directory and resource limits
296
+ agent_manager = AgentManager(
297
+ event_bus, project_dir=path, resource_limits=global_config.resource_limits
298
+ )
294
299
 
295
300
  # Create filesystem collector
296
301
  fs_config = {"watch_paths": [str(path)]}
@@ -558,10 +563,37 @@ This project uses background agents and Beads for task management.
558
563
  commands_copied.append(template_file.stem)
559
564
 
560
565
  if commands_copied:
561
- console.print(f"\n[green]✓[/green] Created Claude Code slash commands:")
566
+ console.print("\n[green]✓[/green] Created Claude Code slash commands:")
562
567
  for cmd in commands_copied:
563
568
  console.print(f" • /{cmd}")
564
569
 
570
+ # Install git hooks if this is a git repository
571
+ git_dir = path / ".git"
572
+ if git_dir.exists() and git_dir.is_dir():
573
+ hooks_template_dir = Path(__file__).parent / "templates" / "git_hooks"
574
+ hooks_dest_dir = git_dir / "hooks"
575
+
576
+ if hooks_template_dir.exists():
577
+ hooks_installed = []
578
+ for template_file in hooks_template_dir.iterdir():
579
+ if template_file.is_file():
580
+ dest_file = hooks_dest_dir / template_file.name
581
+
582
+ # Backup existing hook if present
583
+ if dest_file.exists():
584
+ backup_file = hooks_dest_dir / f"{template_file.name}.backup"
585
+ shutil.copy2(dest_file, backup_file)
586
+
587
+ # Install new hook
588
+ shutil.copy2(template_file, dest_file)
589
+ dest_file.chmod(0o755) # Make executable
590
+ hooks_installed.append(template_file.name)
591
+
592
+ if hooks_installed:
593
+ console.print("\n[green]✓[/green] Installed git hooks:")
594
+ for hook in hooks_installed:
595
+ console.print(f" • {hook}")
596
+
565
597
  console.print("\n[green]✓[/green] Initialized!")
566
598
  console.print("\nNext steps:")
567
599
  console.print(f" 1. Review/edit: [cyan]{claude_dir / 'agents.json'}[/cyan]")
@@ -633,5 +665,54 @@ def version():
633
665
  console.print(f"DevLoop v{__version__}")
634
666
 
635
667
 
668
+ @app.command()
669
+ def update_hooks(path: Path = typer.Argument(Path.cwd(), help="Project directory")):
670
+ """Update git hooks from latest templates."""
671
+ import shutil
672
+
673
+ git_dir = path / ".git"
674
+
675
+ if not git_dir.exists() or not git_dir.is_dir():
676
+ console.print(
677
+ f"[red]✗[/red] Not a git repository: {path}\n"
678
+ "[yellow]Git hooks can only be installed in git repositories.[/yellow]"
679
+ )
680
+ return
681
+
682
+ hooks_template_dir = Path(__file__).parent / "templates" / "git_hooks"
683
+ hooks_dest_dir = git_dir / "hooks"
684
+
685
+ if not hooks_template_dir.exists():
686
+ console.print(f"[red]✗[/red] Hook templates not found at: {hooks_template_dir}")
687
+ return
688
+
689
+ hooks_dest_dir.mkdir(parents=True, exist_ok=True)
690
+ hooks_updated = []
691
+
692
+ for template_file in hooks_template_dir.iterdir():
693
+ if template_file.is_file():
694
+ dest_file = hooks_dest_dir / template_file.name
695
+
696
+ # Backup existing hook if present
697
+ if dest_file.exists():
698
+ backup_file = hooks_dest_dir / f"{template_file.name}.backup"
699
+ shutil.copy2(dest_file, backup_file)
700
+ console.print(
701
+ f"[dim] Backed up existing hook: {template_file.name} -> {template_file.name}.backup[/dim]"
702
+ )
703
+
704
+ # Install new hook
705
+ shutil.copy2(template_file, dest_file)
706
+ dest_file.chmod(0o755) # Make executable
707
+ hooks_updated.append(template_file.name)
708
+
709
+ if hooks_updated:
710
+ console.print("\n[green]✓[/green] Updated git hooks:")
711
+ for hook in hooks_updated:
712
+ console.print(f" • {hook}")
713
+ else:
714
+ console.print("[yellow]No hooks found to update[/yellow]")
715
+
716
+
636
717
  if __name__ == "__main__":
637
718
  app()
@@ -0,0 +1,68 @@
1
+ #!/bin/sh
2
+ #
3
+ # Combined pre-commit hook
4
+ #
5
+ # Checks:
6
+ # 1. Poetry lock file sync with pyproject.toml
7
+ # 2. Code quality checks (Black, Ruff, mypy, pytest)
8
+ # 3. bd (beads) pre-commit flush
9
+ # 4. Any existing pre-commit.old hook
10
+ #
11
+
12
+ # Check if pyproject.toml changed but poetry.lock didn't
13
+ if git diff --cached --name-only | grep -q "pyproject.toml"; then
14
+ if ! git diff --cached --name-only | grep -q "poetry.lock"; then
15
+ echo "ERROR: pyproject.toml changed but poetry.lock not updated"
16
+ echo "This will cause CI failure: 'poetry.lock was last generated' error"
17
+ echo ""
18
+ echo "Fix: Run 'poetry lock' and stage both files"
19
+ echo " poetry lock"
20
+ echo " git add poetry.lock"
21
+ exit 1
22
+ fi
23
+ fi
24
+
25
+ # Run type checks and tests (only if there are Python changes)
26
+ if git diff --cached --name-only | grep -qE '\.py$'; then
27
+ if [ -x ".git/hooks/pre-commit-checks" ]; then
28
+ ".git/hooks/pre-commit-checks"
29
+ EXIT_CODE=$?
30
+ if [ $EXIT_CODE -ne 0 ]; then
31
+ exit $EXIT_CODE
32
+ fi
33
+ fi
34
+ fi
35
+
36
+ # Run existing hook if present
37
+ if [ -x ".git/hooks/pre-commit.old" ]; then
38
+ ".git/hooks/pre-commit.old" "$@"
39
+ EXIT_CODE=$?
40
+ if [ $EXIT_CODE -ne 0 ]; then
41
+ exit $EXIT_CODE
42
+ fi
43
+ fi
44
+
45
+ # Check if bd is available
46
+ if ! command -v bd >/dev/null 2>&1; then
47
+ echo "Warning: bd command not found, skipping pre-commit flush" >&2
48
+ exit 0
49
+ fi
50
+
51
+ # Check if we're in a bd workspace
52
+ if [ ! -d .beads ]; then
53
+ exit 0
54
+ fi
55
+
56
+ # Flush pending changes to JSONL
57
+ if ! bd sync --flush-only >/dev/null 2>&1; then
58
+ echo "Error: Failed to flush bd changes to JSONL" >&2
59
+ echo "Run 'bd sync --flush-only' manually to diagnose" >&2
60
+ exit 1
61
+ fi
62
+
63
+ # If the JSONL file was modified, stage it
64
+ if [ -f .beads/issues.jsonl ]; then
65
+ git add .beads/issues.jsonl 2>/dev/null || true
66
+ fi
67
+
68
+ exit 0
@@ -0,0 +1,72 @@
1
+ #!/bin/bash
2
+ #
3
+ # Pre-commit checks: Formatting, linting, type checking, and tests
4
+ # Prevents committing code that has formatting/linting/type errors or failing tests
5
+ #
6
+ # This is called from the main pre-commit hook after lock file checks
7
+ #
8
+
9
+ set -e
10
+
11
+ RED='\033[0;31m'
12
+ GREEN='\033[0;32m'
13
+ YELLOW='\033[1;33m'
14
+ NC='\033[0m'
15
+
16
+ echo -e "${YELLOW}[Pre-commit] Running code quality checks and tests...${NC}"
17
+
18
+ # Check if poetry is available
19
+ if ! command -v poetry &> /dev/null; then
20
+ echo -e "${YELLOW}[Pre-commit] poetry not found, skipping checks${NC}"
21
+ exit 0
22
+ fi
23
+
24
+ # Navigate to src directory where pyproject.toml is located
25
+ if [ ! -d "src" ]; then
26
+ echo -e "${YELLOW}[Pre-commit] src directory not found, skipping checks${NC}"
27
+ exit 0
28
+ fi
29
+
30
+ # Run Black formatter check
31
+ echo -e "${YELLOW}[Pre-commit] Checking code formatting (Black)...${NC}"
32
+ if ! poetry run black --check src/ > /tmp/black-output.txt 2>&1; then
33
+ echo -e "${RED}[Pre-commit] ❌ Black formatting check failed:${NC}"
34
+ cat /tmp/black-output.txt
35
+ echo -e "${YELLOW}Fix with: poetry run black src/${NC}"
36
+ exit 1
37
+ fi
38
+ echo -e "${GREEN}[Pre-commit] ✅ Code formatting passed${NC}"
39
+
40
+ # Run Ruff linting
41
+ echo -e "${YELLOW}[Pre-commit] Running linter (Ruff)...${NC}"
42
+ if ! poetry run ruff check src/ > /tmp/ruff-output.txt 2>&1; then
43
+ echo -e "${RED}[Pre-commit] ❌ Ruff linting failed:${NC}"
44
+ cat /tmp/ruff-output.txt
45
+ echo -e "${YELLOW}Fix with: poetry run ruff check src/ --fix${NC}"
46
+ exit 1
47
+ fi
48
+ echo -e "${GREEN}[Pre-commit] ✅ Linting passed${NC}"
49
+
50
+ cd src
51
+
52
+ # Run mypy
53
+ echo -e "${YELLOW}[Pre-commit] Type checking (mypy)...${NC}"
54
+ if ! poetry run mypy devloop/core/ devloop/agents/ > /tmp/mypy-output.txt 2>&1; then
55
+ echo -e "${RED}[Pre-commit] ❌ Type check failed:${NC}"
56
+ cat /tmp/mypy-output.txt
57
+ exit 1
58
+ fi
59
+ echo -e "${GREEN}[Pre-commit] ✅ Type checks passed${NC}"
60
+
61
+ # Run pytest (from parent directory where tests/ exists)
62
+ echo -e "${YELLOW}[Pre-commit] Running tests (pytest)...${NC}"
63
+ cd ..
64
+ if ! poetry run pytest tests/ -q > /tmp/pytest-output.txt 2>&1; then
65
+ echo -e "${RED}[Pre-commit] ❌ Tests failed:${NC}"
66
+ # Show last 50 lines of output
67
+ tail -50 /tmp/pytest-output.txt
68
+ exit 1
69
+ fi
70
+ echo -e "${GREEN}[Pre-commit] ✅ All tests passed${NC}"
71
+
72
+ exit 0
@@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional
11
11
 
12
12
  from .event import Event, EventBus
13
13
  from .feedback import FeedbackAPI
14
- from .performance import PerformanceMonitor
14
+ from .performance import AgentResourceTracker, PerformanceMonitor
15
15
 
16
16
 
17
17
  @dataclass
@@ -79,12 +79,14 @@ class Agent(ABC):
79
79
  event_bus: EventBus,
80
80
  feedback_api: Optional[FeedbackAPI] = None,
81
81
  performance_monitor: Optional[PerformanceMonitor] = None,
82
+ resource_tracker: Optional[AgentResourceTracker] = None,
82
83
  ):
83
84
  self.name = name
84
85
  self.triggers = triggers
85
86
  self.event_bus = event_bus
86
87
  self.feedback_api = feedback_api
87
88
  self.performance_monitor = performance_monitor
89
+ self.resource_tracker = resource_tracker
88
90
  self.enabled = True
89
91
  self.logger = logging.getLogger(f"agent.{name}")
90
92
  self._running = False
@@ -139,21 +141,33 @@ class Agent(ABC):
139
141
  try:
140
142
  operation_name = f"agent.{self.name}.handle"
141
143
 
142
- if self.performance_monitor:
143
- async with self.performance_monitor.monitor_operation(
144
- operation_name,
145
- metadata={"event_type": event.type, "agent_name": self.name},
146
- ) as metrics:
144
+ # Mark agent as active for resource tracking
145
+ if self.resource_tracker:
146
+ self.resource_tracker.mark_agent_active(self.name)
147
+
148
+ try:
149
+ if self.performance_monitor:
150
+ async with self.performance_monitor.monitor_operation(
151
+ operation_name,
152
+ metadata={
153
+ "event_type": event.type,
154
+ "agent_name": self.name,
155
+ },
156
+ ) as metrics:
157
+ result = await self.handle(event)
158
+ metrics.complete(result.success, result.error)
159
+
160
+ # Update result duration from metrics
161
+ if metrics.duration:
162
+ result.duration = metrics.duration
163
+ else:
164
+ start_time = time.time()
147
165
  result = await self.handle(event)
148
- metrics.complete(result.success, result.error)
149
-
150
- # Update result duration from metrics
151
- if metrics.duration:
152
- result.duration = metrics.duration
153
- else:
154
- start_time = time.time()
155
- result = await self.handle(event)
156
- result.duration = time.time() - start_time
166
+ result.duration = time.time() - start_time
167
+ finally:
168
+ # Mark agent as inactive after handling
169
+ if self.resource_tracker:
170
+ self.resource_tracker.mark_agent_inactive(self.name)
157
171
 
158
172
  # Update performance store if available
159
173
  if self.feedback_api:
@@ -2,8 +2,9 @@
2
2
 
3
3
  import logging
4
4
  from pathlib import Path
5
- from typing import Dict
5
+ from typing import Dict, List, Optional
6
6
 
7
+ from devloop.core.backup_manager import get_backup_manager
7
8
  from devloop.core.config import AutonomousFixesConfig, config
8
9
  from devloop.core.context_store import Finding, context_store
9
10
 
@@ -11,14 +12,29 @@ logger = logging.getLogger(__name__)
11
12
 
12
13
 
13
14
  class AutoFix:
14
- """Automatically applies safe fixes based on agent findings."""
15
-
16
- def __init__(self):
17
- self._fix_history = set() # Track applied fixes to avoid duplicates
18
-
19
- async def apply_safe_fixes(self) -> Dict[str, int]:
15
+ """Automatically applies safe fixes based on agent findings.
16
+
17
+ Features:
18
+ - Pre-modification backups for all changes
19
+ - Atomic operations with rollback support
20
+ - Safety level enforcement
21
+ - Opt-in configuration
22
+ - Comprehensive audit trail
23
+ """
24
+
25
+ def __init__(self, project_root: Optional[Path] = None):
26
+ self._fix_history: set[str] = set() # Track applied fixes to avoid duplicates
27
+ self._backup_manager = get_backup_manager(project_root or Path.cwd())
28
+ self._applied_fixes: List[Dict] = [] # Track all applied fixes in session
29
+
30
+ async def apply_safe_fixes(
31
+ self, require_confirmation: bool = True
32
+ ) -> Dict[str, int]:
20
33
  """Apply all safe fixes from agent findings.
21
34
 
35
+ Args:
36
+ require_confirmation: If True, user must explicitly confirm before applying
37
+
22
38
  Returns:
23
39
  Dict mapping agent types to number of fixes applied
24
40
  """
@@ -31,8 +47,31 @@ class AutoFix:
31
47
  logger.info("Autonomous fixes are disabled in configuration")
32
48
  return {}
33
49
 
50
+ # Check for explicit opt-in (CRITICAL SECURITY REQUIREMENT)
51
+ if not global_config.autonomous_fixes.opt_in:
52
+ logger.warning(
53
+ "Autonomous fixes require explicit opt-in. "
54
+ "Set 'autonomousFixes.opt_in: true' in .devloop/agents.json"
55
+ )
56
+ return {}
57
+
34
58
  findings = await context_store.get_findings()
35
59
  actionable_findings = [f for f in findings if f.auto_fixable]
60
+
61
+ if not actionable_findings:
62
+ logger.info("No auto-fixable findings to apply")
63
+ return {}
64
+
65
+ # User confirmation if required
66
+ if require_confirmation:
67
+ logger.info(
68
+ f"Found {len(actionable_findings)} auto-fixable findings. "
69
+ "User confirmation required before applying."
70
+ )
71
+ # In a real implementation, this would prompt the user
72
+ # For now, we return empty to be safe
73
+ return {}
74
+
36
75
  applied_fixes: Dict[str, int] = {}
37
76
 
38
77
  for finding in actionable_findings:
@@ -51,7 +90,13 @@ class AutoFix:
51
90
  finding: Finding,
52
91
  autonomous_fixes_config: AutonomousFixesConfig,
53
92
  ) -> bool:
54
- """Apply a single fix if it's safe to do so."""
93
+ """Apply a single fix if it's safe to do so.
94
+
95
+ This method:
96
+ 1. Creates a backup before modification
97
+ 2. Applies the fix
98
+ 3. Tracks the change for audit/rollback
99
+ """
55
100
  if finding.id in self._fix_history:
56
101
  return False # Already applied
57
102
 
@@ -64,10 +109,40 @@ class AutoFix:
64
109
  )
65
110
  return False
66
111
 
112
+ # CRITICAL: Create backup before any modification
113
+ file_path = finding.file
114
+ if file_path:
115
+ backup_id = self._backup_manager.create_backup(
116
+ file_path=Path(file_path),
117
+ fix_type=agent_type,
118
+ description=finding.message,
119
+ metadata={
120
+ "finding_id": finding.id,
121
+ "severity": finding.severity,
122
+ "safety_level": autonomous_fixes_config.safety_level,
123
+ "context": finding.context,
124
+ },
125
+ )
126
+
127
+ if not backup_id:
128
+ logger.error(f"Failed to create backup for {file_path}, aborting fix")
129
+ return False
130
+
131
+ logger.info(f"Created backup {backup_id} before applying fix")
132
+
67
133
  # Apply the fix
68
134
  success = await self._execute_fix(agent_type, finding)
69
135
  if success:
70
136
  self._fix_history.add(finding.id)
137
+ self._applied_fixes.append(
138
+ {
139
+ "finding_id": finding.id,
140
+ "agent_type": agent_type,
141
+ "file": file_path,
142
+ "backup_id": backup_id if file_path else None,
143
+ "message": finding.message,
144
+ }
145
+ )
71
146
  logger.info(f"Applied {agent_type} fix: {finding.message}")
72
147
 
73
148
  return success
@@ -213,12 +288,78 @@ class AutoFix:
213
288
  # Could use autopep8 or similar
214
289
  return False
215
290
 
291
+ def rollback_last(self) -> bool:
292
+ """Rollback the last applied fix.
293
+
294
+ Returns:
295
+ True if rollback successful
296
+ """
297
+ if not self._applied_fixes:
298
+ logger.warning("No fixes to rollback in current session")
299
+ return False
300
+
301
+ last_fix = self._applied_fixes[-1]
302
+ backup_id = last_fix.get("backup_id")
303
+
304
+ if not backup_id:
305
+ logger.error("No backup ID found for last fix")
306
+ return False
307
+
308
+ success = self._backup_manager.rollback(backup_id)
309
+ if success:
310
+ self._applied_fixes.pop()
311
+ logger.info(f"Rolled back fix: {last_fix['message']}")
312
+
313
+ return success
314
+
315
+ def rollback_all_session(self) -> int:
316
+ """Rollback all fixes from current session.
317
+
318
+ Returns:
319
+ Number of fixes successfully rolled back
320
+ """
321
+ rolled_back = 0
322
+
323
+ for fix in reversed(self._applied_fixes):
324
+ backup_id = fix.get("backup_id")
325
+ if backup_id and self._backup_manager.rollback(backup_id):
326
+ rolled_back += 1
327
+
328
+ if rolled_back > 0:
329
+ self._applied_fixes.clear()
330
+ logger.info(f"Rolled back {rolled_back} fixes from current session")
331
+
332
+ return rolled_back
333
+
334
+ def get_applied_fixes(self) -> List[Dict]:
335
+ """Get list of fixes applied in current session."""
336
+ return self._applied_fixes.copy()
337
+
338
+ def get_change_history(self, limit: Optional[int] = None) -> List[Dict]:
339
+ """Get change history from backup manager."""
340
+ return self._backup_manager.get_change_history(limit=limit)
341
+
216
342
 
217
343
  # Global instance
218
344
  auto_fix = AutoFix()
219
345
 
220
346
 
221
- async def apply_safe_fixes():
347
+ async def apply_safe_fixes(require_confirmation: bool = True):
222
348
  """Convenience function to apply safe fixes."""
223
- results = await auto_fix.apply_safe_fixes()
349
+ results = await auto_fix.apply_safe_fixes(require_confirmation=require_confirmation)
224
350
  return results
351
+
352
+
353
+ def rollback_last() -> bool:
354
+ """Convenience function to rollback last fix."""
355
+ return auto_fix.rollback_last()
356
+
357
+
358
+ def rollback_all_session() -> int:
359
+ """Convenience function to rollback all session fixes."""
360
+ return auto_fix.rollback_all_session()
361
+
362
+
363
+ def get_change_history(limit: Optional[int] = None) -> List[Dict]:
364
+ """Convenience function to get change history."""
365
+ return auto_fix.get_change_history(limit=limit)