feat: move autocoder runtime files into .autocoder/ subdirectory

Add centralized path resolution module (autocoder_paths.py) that
consolidates all autocoder-generated file paths behind a dual-path
strategy: check .autocoder/X first, fall back to root-level X for
backward compatibility, default to .autocoder/X for new projects.

Key changes:
- New autocoder_paths.py with dual-path resolution for features.db,
  assistant.db, lock files, settings, prompts dir, and progress cache
- migrate_project_layout() safely moves old-layout projects to new
  layout with SQLite WAL flush and integrity verification
- Updated 22 files to delegate path construction to autocoder_paths
- Reset/delete logic cleans both old and new file locations
- Orphan lock cleanup checks both locations per project
- Migration called automatically at agent start in autonomous_agent_demo.py
- Updated markdown commands/skills to reference .autocoder/prompts/
- CLAUDE.md documentation updated with new project structure

Files at project root that remain unchanged:
- CLAUDE.md (Claude SDK reads from cwd via setting_sources=["project"])
- app_spec.txt root copy (agent templates reference it via cat)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Auto
2026-02-01 11:32:06 +02:00
parent c4d0c6c9b2
commit dc5bcc4ae9
24 changed files with 532 additions and 86 deletions

View File

@@ -76,7 +76,8 @@ def get_system_prompt(project_name: str, project_dir: Path) -> str:
"""Generate the system prompt for the assistant with project context."""
# Try to load app_spec.txt for context
app_spec_content = ""
app_spec_path = project_dir / "prompts" / "app_spec.txt"
from autocoder_paths import get_prompts_dir
app_spec_path = get_prompts_dir(project_dir) / "app_spec.txt"
if app_spec_path.exists():
try:
app_spec_content = app_spec_path.read_text(encoding="utf-8")
@@ -235,7 +236,9 @@ class AssistantChatSession:
"allow": permissions_list,
},
}
settings_file = self.project_dir / ".claude_assistant_settings.json"
from autocoder_paths import get_claude_assistant_settings_path
settings_file = get_claude_assistant_settings_path(self.project_dir)
settings_file.parent.mkdir(parents=True, exist_ok=True)
with open(settings_file, "w") as f:
json.dump(security_settings, f, indent=2)

View File

@@ -63,7 +63,8 @@ class ConversationMessage(Base):
def get_db_path(project_dir: Path) -> Path:
"""Get the path to the assistant database for a project."""
return project_dir / "assistant.db"
from autocoder_paths import get_assistant_db_path
return get_assistant_db_path(project_dir)
def get_engine(project_dir: Path):

View File

@@ -24,6 +24,7 @@ from typing import Awaitable, Callable, Literal, Set
import psutil
from registry import list_registered_projects
from security import extract_commands, get_effective_commands, is_command_allowed
from server.utils.process_utils import kill_process_tree
logger = logging.getLogger(__name__)
@@ -114,7 +115,8 @@ class DevServerProcessManager:
self._callbacks_lock = threading.Lock()
# Lock file to prevent multiple instances (stored in project directory)
self.lock_file = self.project_dir / ".devserver.lock"
from autocoder_paths import get_devserver_lock_path
self.lock_file = get_devserver_lock_path(self.project_dir)
@property
def status(self) -> Literal["stopped", "running", "crashed"]:
@@ -304,6 +306,20 @@ class DevServerProcessManager:
if not self.project_dir.exists():
return False, f"Project directory does not exist: {self.project_dir}"
# Defense-in-depth: validate command against security allowlist
commands = extract_commands(command)
if not commands:
return False, "Could not parse command for security validation"
allowed_commands, blocked_commands = get_effective_commands(self.project_dir)
for cmd in commands:
if cmd in blocked_commands:
logger.warning("Blocked dev server command '%s' (in blocklist) for %s", cmd, self.project_name)
return False, f"Command '{cmd}' is blocked and cannot be used as a dev server command"
if not is_command_allowed(cmd, allowed_commands):
logger.warning("Rejected dev server command '%s' (not in allowlist) for %s", cmd, self.project_name)
return False, f"Command '{cmd}' is not in the allowed commands list"
self._command = command
self._detected_url = None # Reset URL detection
@@ -487,8 +503,18 @@ def cleanup_orphaned_devserver_locks() -> int:
if not project_path.exists():
continue
lock_file = project_path / ".devserver.lock"
if not lock_file.exists():
# Check both legacy and new locations for lock files
from autocoder_paths import get_autocoder_dir
lock_locations = [
project_path / ".devserver.lock",
get_autocoder_dir(project_path) / ".devserver.lock",
]
lock_file = None
for candidate in lock_locations:
if candidate.exists():
lock_file = candidate
break
if lock_file is None:
continue
try:

View File

@@ -128,7 +128,8 @@ class ExpandChatSession:
return
# Verify project has existing spec
spec_path = self.project_dir / "prompts" / "app_spec.txt"
from autocoder_paths import get_prompts_dir
spec_path = get_prompts_dir(self.project_dir) / "app_spec.txt"
if not spec_path.exists():
yield {
"type": "error",
@@ -166,7 +167,9 @@ class ExpandChatSession:
],
},
}
settings_file = self.project_dir / f".claude_settings.expand.{uuid.uuid4().hex}.json"
from autocoder_paths import get_expand_settings_path
settings_file = get_expand_settings_path(self.project_dir, uuid.uuid4().hex)
settings_file.parent.mkdir(parents=True, exist_ok=True)
self._settings_file = settings_file
with open(settings_file, "w", encoding="utf-8") as f:
json.dump(security_settings, f, indent=2)

View File

@@ -92,7 +92,8 @@ class AgentProcessManager:
self._callbacks_lock = threading.Lock()
# Lock file to prevent multiple instances (stored in project directory)
self.lock_file = self.project_dir / ".agent.lock"
from autocoder_paths import get_agent_lock_path
self.lock_file = get_agent_lock_path(self.project_dir)
@property
def status(self) -> Literal["stopped", "running", "paused", "crashed"]:
@@ -579,8 +580,18 @@ def cleanup_orphaned_locks() -> int:
if not project_path.exists():
continue
lock_file = project_path / ".agent.lock"
if not lock_file.exists():
# Check both legacy and new locations for lock files
from autocoder_paths import get_autocoder_dir
lock_locations = [
project_path / ".agent.lock",
get_autocoder_dir(project_path) / ".agent.lock",
]
lock_file = None
for candidate in lock_locations:
if candidate.exists():
lock_file = candidate
break
if lock_file is None:
continue
try:

View File

@@ -92,8 +92,9 @@ class SchedulerService:
async def _load_project_schedules(self, project_name: str, project_dir: Path) -> int:
"""Load schedules for a single project. Returns count of schedules loaded."""
from api.database import Schedule, create_database
from autocoder_paths import get_features_db_path
db_path = project_dir / "features.db"
db_path = get_features_db_path(project_dir)
if not db_path.exists():
return 0
@@ -567,8 +568,9 @@ class SchedulerService:
):
"""Check if a project should be started on server startup."""
from api.database import Schedule, ScheduleOverride, create_database
from autocoder_paths import get_features_db_path
db_path = project_dir / "features.db"
db_path = get_features_db_path(project_dir)
if not db_path.exists():
return

View File

@@ -125,7 +125,8 @@ class SpecChatSession:
# Delete app_spec.txt so Claude can create it fresh
# The SDK requires reading existing files before writing, but app_spec.txt is created new
# Note: We keep initializer_prompt.md so Claude can read and update the template
prompts_dir = self.project_dir / "prompts"
from autocoder_paths import get_prompts_dir
prompts_dir = get_prompts_dir(self.project_dir)
app_spec_path = prompts_dir / "app_spec.txt"
if app_spec_path.exists():
app_spec_path.unlink()
@@ -145,7 +146,9 @@ class SpecChatSession:
],
},
}
settings_file = self.project_dir / ".claude_settings.json"
from autocoder_paths import get_claude_settings_path
settings_file = get_claude_settings_path(self.project_dir)
settings_file.parent.mkdir(parents=True, exist_ok=True)
with open(settings_file, "w") as f:
json.dump(security_settings, f, indent=2)