Files
AIPass/src/aipass/spawn/apps/handlers/delete_ops.py
T
AIOSAIandClaude Opus 4.6 babedd9c64 feat(system): seedgo v2 operational, full system audit, 14/15 branches at 99%
Three days of intensive work bringing seedgo to full operational status
and driving all branches through comprehensive standards compliance.

Seedgo v2.0.0:
- 22 checkers active (up from 20), standards pack fully operational
- New introspection standard researched from Dev-Pass, FPLAN-0017 open
- Bypass system for false positives (.seedgo config)
- Standards query and audit commands fully functional

System-wide audit (FPLAN-0016):
- All 14 auditable branches at 99%+ compliance
- CLI imports standardized across all branches (console from cli.apps.modules)
- handle_command(command, args) → bool contract added to all modules
- print_help() function naming fixed for checker pattern matching
- Handler extraction: large modules split, file I/O moved to handler layer
- New handlers created across ai_mail, backup, daemon, flow, skills, spawn, seedgo

Branch-specific highlights:
- ai_mail: email.py split 840→420 lines, 4 new handlers
- flow: dplan_flow.py 688→591 lines, 4 new handlers
- seedgo: massive restructure — standards moved to handlers/aipass_standards/,
  old standards/ tree removed, bypass system added, diagnostics module
- commons: database module added, CLI imports fixed
- skills: 5 handle_commands added, help function renamed
- trigger: error reporter handler, handle_command routing
- All branches: consistent architecture, clean drone routing

Culture doc (CLAUDE.md) added — documents AIPass philosophy, identity,
memory system, and collaboration principles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 01:26:42 -07:00

202 lines
6.7 KiB
Python

# =================== AIPass ====================
# Name: delete_ops.py
# Description: Delete handler — implementation logic for branch deletion
# Version: 1.0.0
# Created: 2026-03-07
# Modified: 2026-03-07
# =============================================
"""Delete handler implementation for branch lifecycle management.
Contains the core deletion logic: resolving branch paths, archiving branch
directories, removing from registry, and safety checks for protected branches.
"""
import shutil
from datetime import datetime
from pathlib import Path
from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.spawn.apps.handlers.registry import (
find_registry,
load_registry,
save_registry,
_branches_as_list,
)
# Repo root — resolved from spawn package location
_REPO_ROOT = Path(__file__).parents[5] # handlers/apps/spawn/aipass/src/AIPass
# Branches that cannot be deleted (critical infrastructure)
_PROTECTED_BRANCHES = {"spawn", "devpulse", "drone"}
# =============================================================================
# PUBLIC API
# =============================================================================
def delete_branch(
branch_name: str,
confirm: bool = True,
dry_run: bool = False,
) -> dict:
"""Archive and deregister a branch.
Workflow:
1. Resolve branch path from AIPASS_REGISTRY.json
2. Verify branch exists on filesystem
3. If confirm=True -> print what will happen, prompt "Are you sure? (y/N)"
4. If dry_run -> print summary, return
5. Create archive directory: {repo_root}/.archive/deleted_branches/{name}_{timestamp}/
6. Copy entire branch directory to archive (shutil.copytree)
7. Remove branch from AIPASS_REGISTRY.json
8. Remove branch directory (shutil.rmtree)
9. Log and return summary
SAFETY:
- Cannot delete spawn (self-protection)
- Cannot delete devpulse (orchestration hub protection)
- Cannot delete drone (routing infrastructure protection)
- Archive ALWAYS created before deletion
Returns:
Dict with deletion results.
"""
# Safety: check protected branches
if branch_name.lower() in _PROTECTED_BRANCHES:
msg = f"Cannot delete '{branch_name}' — protected branch ({', '.join(sorted(_PROTECTED_BRANCHES))})"
logger.warning(f"[delete] {msg}")
return {
"branch": branch_name,
"success": False,
"archive_path": "",
"registry_updated": False,
"error": msg,
}
# 1. Resolve branch path from registry
registry_path = find_registry()
registry = load_registry(registry_path)
branch_entry = None
branch_dir = None
for entry in _branches_as_list(registry.get("branches", [])):
if entry.get("name", "").lower() == branch_name.lower():
branch_entry = entry
rel_path = entry.get("path", "")
if rel_path:
branch_dir = (_REPO_ROOT / rel_path).resolve()
break
if branch_entry is None:
msg = f"Branch '{branch_name}' not found in registry"
logger.warning(f"[delete] {msg}")
return {
"branch": branch_name,
"success": False,
"archive_path": "",
"registry_updated": False,
"error": msg,
}
# 2. Verify branch exists on filesystem
if branch_dir is None or not branch_dir.is_dir():
msg = f"Branch directory does not exist: {branch_dir}"
logger.warning(f"[delete] {msg}")
return {
"branch": branch_name,
"success": False,
"archive_path": "",
"registry_updated": False,
"error": msg,
}
# 3. Confirmation prompt
if confirm and not dry_run:
try:
answer = input("Are you sure? (y/N): ").strip().lower()
except (EOFError, KeyboardInterrupt):
answer = ""
if answer != "y":
return {
"branch": branch_name,
"success": False,
"archive_path": "",
"registry_updated": False,
"error": "Cancelled by user",
}
# 4. Dry run — report and return
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
archive_dir = _REPO_ROOT / ".archive" / "deleted_branches" / f"{branch_name}_{timestamp}"
if dry_run:
logger.info(f"[delete] Dry run: would archive {branch_name} to {archive_dir}")
return {
"branch": branch_name,
"success": True,
"archive_path": str(archive_dir),
"registry_updated": False,
"dry_run": True,
}
# 5. Create archive
archive_dir.parent.mkdir(parents=True, exist_ok=True)
try:
shutil.copytree(str(branch_dir), str(archive_dir))
logger.info(f"[delete] Archived {branch_name} to {archive_dir}")
except Exception as exc:
msg = f"Failed to create archive: {exc}"
logger.error(f"[delete] {msg}")
return {
"branch": branch_name,
"success": False,
"archive_path": "",
"registry_updated": False,
"error": msg,
}
# 6. Remove from registry
branches = registry.get("branches", [])
if isinstance(branches, dict):
# Dict format: remove by key (try both cases)
for key in list(branches.keys()):
if key.lower() == branch_name.lower() or branches[key].get("name", "").lower() == branch_name.lower():
del branches[key]
registry["branches"] = branches
else:
registry["branches"] = [
b for b in branches
if b.get("name", "").lower() != branch_name.lower()
]
registry["metadata"]["total_branches"] = len(_branches_as_list(registry["branches"]))
registry_updated = save_registry(registry_path, registry)
if registry_updated:
logger.info(f"[delete] Removed {branch_name} from registry")
else:
logger.error(f"[delete] Failed to update registry after removing {branch_name}")
# 7. Remove branch directory
try:
shutil.rmtree(str(branch_dir))
logger.info(f"[delete] Removed branch directory: {branch_dir}")
except Exception as exc:
msg = f"Archive created but failed to remove directory: {exc}"
logger.error(f"[delete] {msg}")
return {
"branch": branch_name,
"success": False,
"archive_path": str(archive_dir),
"registry_updated": registry_updated,
"error": msg,
}
return {
"branch": branch_name,
"success": True,
"archive_path": str(archive_dir),
"registry_updated": registry_updated,
}