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.
- {devloop-0.3.1 → devloop-0.3.2}/PKG-INFO +1 -1
- {devloop-0.3.1 → devloop-0.3.2}/pyproject.toml +1 -1
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/main.py +85 -4
- devloop-0.3.2/src/devloop/cli/templates/git_hooks/pre-commit +68 -0
- devloop-0.3.2/src/devloop/cli/templates/git_hooks/pre-commit-checks +72 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/agent.py +29 -15
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/auto_fix.py +151 -10
- devloop-0.3.2/src/devloop/core/backup_manager.py +409 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/config.py +93 -2
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/manager.py +121 -3
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/performance.py +114 -0
- {devloop-0.3.1 → devloop-0.3.2}/LICENSE +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/README.md +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/__init__.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/__init__.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/agent_health_monitor.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/ci_monitor.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/code_rabbit.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/doc_lifecycle.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/echo.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/file_logger.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/formatter.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/git_commit_assistant.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/linter.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/performance_profiler.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/sandbox_helper.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/security_scanner.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/snyk.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/test_runner.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/agents/type_checker.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/__init__.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/commands/__init__.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/commands/custom_agents.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/commands/feedback.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/commands/summary.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/main_v1.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/pyodide_installer.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/templates/claude_commands/README.md +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/templates/claude_commands/agent-summary.md +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/templates/claude_commands/devloop-findings.md +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/cli/templates/claude_commands/devloop-status.md +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/collectors/__init__.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/collectors/base.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/collectors/filesystem.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/collectors/git.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/collectors/manager.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/collectors/process.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/collectors/system.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/__init__.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/agent_template.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/amp_integration.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/context.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/context_store.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/contextual_feedback.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/custom_agent.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/debug_trace.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/event.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/event_store.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/feedback.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/learning.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/operational_health.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/proactive_feedback.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/summary_formatter.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/core/summary_generator.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/security/__init__.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/security/audit_logger.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/security/bubblewrap_sandbox.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/security/cgroups_helper.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/security/factory.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/security/no_sandbox.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/security/package.json +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/security/pyodide_runner.js +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/security/pyodide_sandbox.py +0 -0
- {devloop-0.3.1 → devloop-0.3.2}/src/devloop/security/sandbox.py +0 -0
|
@@ -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
|
-
#
|
|
293
|
-
|
|
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(
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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)
|