diff --git a/.gitignore b/.gitignore index 92b39197..7dbc0d52 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,20 @@ aipass_json/ .claude/ # setup-workspace.sh — tracked, needed for Docker build +src/aipass/trigger/trigger_json/trigger_data.json + + +# Sorting/working + +*config.json +*data.json +*inbox.json +*log.json +*sent.json +.ai_mail.local +ai_mail.local +*_json +dropbox +system_logs +AIPASS_REGISTRY.json +docker.md diff --git a/CLAUDE.md b/CLAUDE.md index 16af8f64..57dcb62d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -66,6 +66,14 @@ apps/ ## DevPulse DevPulse (`src/aipass/devpulse/`) is the orchestration hub for this repo. It coordinates work across modules, tracks status, and manages dev notes. It is to AIPass what DEV_CENTRAL is to Dev-Pass. +## Container Mounts +When running inside the Docker container (code-server at localhost:8080): +- **Shared folder**: `/home/coder/share` (read-write) — dropbox for files between host and container +- **Screenshots**: `/home/coder/screenshots` (read-only) — host screenshots are accessible here +- Use the `Read` tool to view screenshot images (PNG, JPG) — Claude is multimodal and can interpret them +- When user pastes host paths like `file:///home/aipass/Pictures/Screenshots/...`, translate to `/home/coder/screenshots/...` +- Never use host paths (`/home/aipass/...`) directly — they don't exist inside the container + ## Core Principles - Code is truth - if it doesn't run, it's not real - Test in Docker for isolation verification diff --git a/Dockerfile b/Dockerfile index 9dc26551..ee6297cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,18 +20,11 @@ RUN python3 -m venv /opt/venv \ && chown -R 1000:1000 /opt/venv ENV PATH="/opt/venv/bin:$PATH" -# Startup script in /etc so it's NOT shadowed by /home/coder bind mount -RUN mkdir -p /etc/entrypoint.d -COPY setup-workspace.sh /etc/entrypoint.d/setup-workspace.sh -RUN chmod +x /etc/entrypoint.d/setup-workspace.sh - -# Override ENTRYPOINTD to our non-shadowed location -ENV ENTRYPOINTD=/etc/entrypoint.d +# Empty workspace — clone your own repo after boot +RUN mkdir -p /home/coder/workspace && chown 1000:1000 /home/coder/workspace USER 1000 ENV PATH="/opt/venv/bin:$PATH" EXPOSE 8080 -# Use the official entrypoint (already in the base image at /usr/bin/entrypoint.sh) -# It runs fixuid -> entrypoint.d scripts -> dumb-init code-server -ENTRYPOINT ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "--auth", "none", "/home/coder/workspace/AIPass"] +ENTRYPOINT ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "--auth", "none", "/home/coder/workspace"] diff --git a/aipass_os/AI_CENTRAL/AI_MAIL.central.json b/aipass_os/AI_CENTRAL/AI_MAIL.central.json new file mode 100644 index 00000000..bcca2fb4 --- /dev/null +++ b/aipass_os/AI_CENTRAL/AI_MAIL.central.json @@ -0,0 +1,22 @@ +{ + "service": "ai_mail", + "last_updated": "2026-03-07", + "branch_stats": { + "SPAWN": { + "unread": 1, + "total": 1 + }, + "DEVPULSE": { + "unread": 1, + "total": 1 + }, + "AI_MAIL": { + "unread": 0, + "total": 3 + } + }, + "system_totals": { + "total_unread": 2, + "total_messages": 5 + } +} \ No newline at end of file diff --git a/src/aipass/ai_mail/apps/handlers/dispatch/wake.py b/src/aipass/ai_mail/apps/handlers/dispatch/wake.py index 3cd977d4..a710d9ff 100644 --- a/src/aipass/ai_mail/apps/handlers/dispatch/wake.py +++ b/src/aipass/ai_mail/apps/handlers/dispatch/wake.py @@ -303,7 +303,7 @@ def _check_pid_alive(pid: int) -> bool: # ─── Branch Resolution ────────────────────────────────── def resolve_branch(branch_email: str) -> Optional[Tuple[Path, str]]: - """Resolve a branch email to its filesystem path.""" + """Resolve a branch email to its absolute filesystem path.""" email = f"@{branch_email.lstrip('@').lower()}" registry = _read_json(BRANCH_REGISTRY) if registry is None: @@ -311,6 +311,8 @@ def resolve_branch(branch_email: str) -> Optional[Tuple[Path, str]]: for branch in registry.get("branches", []): if branch.get("email", "").lower() == email: path = Path(branch.get("path", "")) + if not path.is_absolute(): + path = _REPO_ROOT / path if path.exists(): return path, email return None diff --git a/src/aipass/devpulse/apps/extensions/__init__.py b/src/aipass/devpulse/apps/extensions/__init__.py new file mode 100644 index 00000000..95322c94 --- /dev/null +++ b/src/aipass/devpulse/apps/extensions/__init__.py @@ -0,0 +1 @@ +# Extensions package - Drop-in extensions for branch functionality diff --git a/src/aipass/devpulse/apps/handlers/json/__init__.py b/src/aipass/devpulse/apps/handlers/json/__init__.py new file mode 100644 index 00000000..6852b736 --- /dev/null +++ b/src/aipass/devpulse/apps/handlers/json/__init__.py @@ -0,0 +1 @@ +"""JSON handler package for devpulse.""" diff --git a/src/aipass/devpulse/apps/handlers/json/json_handler.py b/src/aipass/devpulse/apps/handlers/json/json_handler.py new file mode 100644 index 00000000..37753fe1 --- /dev/null +++ b/src/aipass/devpulse/apps/handlers/json/json_handler.py @@ -0,0 +1,54 @@ + +# ===================AIPASS==================== +# META DATA HEADER +# Name: json_handler.py - JSON Handler +# Date: 2026-03-07 +# Version: 1.0.0 +# Category: devpulse/handlers/json +# +# CHANGELOG (Max 5 entries): +# - v1.0.0 (2026-03-07): Initial implementation +# +# CODE STANDARDS: +# - Satisfies Seed architecture standard for apps/handlers/json/ path +# ============================================= + +""" +JSON Handler for DevPulse + +Handles JSON read/write operations for devpulse_json/ storage. +""" + +import json +from pathlib import Path +from typing import Any, Optional + +# Infrastructure paths (package-relative) +_DEVPULSE_ROOT = Path(__file__).resolve().parents[3] # devpulse/ +DEVPULSE_JSON_DIR = _DEVPULSE_ROOT / "devpulse_json" + + +def load_json(file_path: Path) -> Any: + """Load and return JSON data from file.""" + with open(file_path, 'r', encoding='utf-8') as f: + return json.load(f) + + +def save_json(file_path: Path, data: Any, indent: int = 2) -> None: + """Save data as JSON to file.""" + file_path.parent.mkdir(parents=True, exist_ok=True) + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=indent, ensure_ascii=False) + f.write('\n') + + +def ensure_json_exists(file_path: Path, default: Optional[Any] = None) -> Path: + """Ensure a JSON file exists, creating with default content if missing.""" + if not file_path.exists(): + save_json(file_path, default if default is not None else {}) + return file_path + + +def get_json_path(filename: str) -> Path: + """Get the path to a JSON file in devpulse_json/.""" + return DEVPULSE_JSON_DIR / filename diff --git a/src/aipass/devpulse/apps/json_templates/__init__.py b/src/aipass/devpulse/apps/json_templates/__init__.py new file mode 100644 index 00000000..5d00b535 --- /dev/null +++ b/src/aipass/devpulse/apps/json_templates/__init__.py @@ -0,0 +1 @@ +# JSON Templates package - Default JSON file templates diff --git a/src/aipass/drone/apps/drone.py b/src/aipass/drone/apps/drone.py index d646f1af..ac34e299 100644 --- a/src/aipass/drone/apps/drone.py +++ b/src/aipass/drone/apps/drone.py @@ -190,8 +190,15 @@ def _handle_target(args: List[str]) -> int: command = rest[0] cmd_args = rest[1:] + # Long-running interactive commands bypass capture + timeout + interactive = command in ("monitor",) + try: - result = route_command(target, command, args=cmd_args if cmd_args else None) + result = route_command( + target, command, + args=cmd_args if cmd_args else None, + interactive=interactive, + ) except BranchNotFoundError as exc: print(f"drone: {exc}", file=sys.stderr) return 1 diff --git a/src/aipass/drone/apps/handlers/executor.py b/src/aipass/drone/apps/handlers/executor.py index 66f26024..b3570683 100644 --- a/src/aipass/drone/apps/handlers/executor.py +++ b/src/aipass/drone/apps/handlers/executor.py @@ -29,10 +29,15 @@ def execute_command( cwd: str, timeout: int = 30, env: dict | None = None, + interactive: bool = False, ) -> CommandResult: """Execute a command via subprocess with safety guards. Never uses shell=True to prevent shell injection attacks. + + Args: + interactive: If True, inherit stdio (no capture, no timeout). + Used for long-running commands like prax monitor. """ import os @@ -45,14 +50,23 @@ def execute_command( run_env.update(env) try: - result = subprocess.run( - full_cmd, - cwd=cwd, - capture_output=True, - timeout=timeout, - shell=False, - env=run_env, - ) + if interactive: + # Inherit stdin/stdout/stderr for live interaction + result = subprocess.run( + full_cmd, + cwd=cwd, + shell=False, + env=run_env, + ) + else: + result = subprocess.run( + full_cmd, + cwd=cwd, + capture_output=True, + timeout=timeout, + shell=False, + env=run_env, + ) except subprocess.TimeoutExpired as e: raise CommandExecutionError( f"Command timed out after {timeout}s: {' '.join(full_cmd)}" @@ -66,6 +80,15 @@ def execute_command( f"OS error executing command: {e}" ) from e + if interactive: + return CommandResult( + stdout="", + stderr="", + exit_code=result.returncode, + branch="", + command="", + ) + stdout = result.stdout.decode("utf-8", errors="replace") stderr = result.stderr.decode("utf-8", errors="replace") diff --git a/src/aipass/drone/apps/modules/router.py b/src/aipass/drone/apps/modules/router.py index 2c27fd26..da7a533e 100644 --- a/src/aipass/drone/apps/modules/router.py +++ b/src/aipass/drone/apps/modules/router.py @@ -32,12 +32,16 @@ def route_command( command: str, args: Optional[List[str]] = None, timeout: int = 30, + interactive: bool = False, ) -> CommandResult: """Route a command to a branch's entry point. Resolves @target to an absolute path, locates the branch entry point at {path}/apps/{branch_name}.py, then executes: python3 apps/{name}.py {command} [args...] + + Args: + interactive: If True, inherit stdio for long-running/interactive commands. """ if args is None: args = [] @@ -59,6 +63,7 @@ def route_command( cwd=branch_path, timeout=timeout, env=caller_env, + interactive=interactive, ) return CommandResult( diff --git a/src/aipass/prax/apps/modules/monitor_module.py b/src/aipass/prax/apps/modules/monitor_module.py index bb2d4a74..4bddf9be 100755 --- a/src/aipass/prax/apps/modules/monitor_module.py +++ b/src/aipass/prax/apps/modules/monitor_module.py @@ -240,6 +240,7 @@ def handle_command(command: str, args: List[str]) -> bool: _monitoring_active = True # Parse args for initial branch filters + _is_tty = sys.stdin.isatty() if args: if args[0] == 'all': # Watch all branches @@ -250,13 +251,20 @@ def handle_command(command: str, args: List[str]) -> bool: initial_branches = args[0].split(',') _filter_state.watched_branches = set(normalize_branch_arg(b.strip()) for b in initial_branches) _filter_state.show_info = True + elif not _is_tty: + # No TTY and no args — default to watching all (can't type 'watch all') + _filter_state.show_all = True + _filter_state.show_info = True # Display header console.print() header("PRAX Mission Control - Unified Monitoring") console.print() console.print("[green]Monitoring system starting...[/green]") - console.print("[yellow]Quiet mode active - type 'help' for commands[/yellow]") + if _is_tty: + console.print("[yellow]Quiet mode active - type 'help' for commands[/yellow]") + else: + console.print("[yellow]Passive mode - no TTY detected, watching all (Ctrl+C to stop)[/yellow]") console.print() # Start monitoring threads + Telegram relay @@ -629,9 +637,19 @@ def _log_watcher_worker(): def _interactive_loop(): - """Interactive command loop - handles user input""" + """Interactive command loop - handles user input, or passive loop if no TTY""" global _monitoring_active, _filter_state, _event_queue + # Non-TTY mode: no interactive input available, just keep alive + if not sys.stdin.isatty(): + logger.info("[monitor] No TTY detected - running in passive mode (Ctrl+C to stop)") + try: + while _monitoring_active: + time.sleep(0.5) + except KeyboardInterrupt: + console.print("\n[yellow]Stopping monitoring...[/yellow]") + return + from aipass.prax.apps.handlers.monitoring.interactive_filter import ( parse_command, apply_filter, diff --git a/src/aipass/spawn/README.md b/src/aipass/spawn/README.md index 67752585..d3fa704a 100644 --- a/src/aipass/spawn/README.md +++ b/src/aipass/spawn/README.md @@ -69,7 +69,14 @@ spawn/ ## Integration Points ### Depends On -- Python stdlib only (`pathlib`, `json`, `shutil`, `hashlib`, `re`) +- `aipass.prax` — Logging via `system_logger` +- `aipass.cli` — Console output and headers +- Python stdlib (`pathlib`, `json`, `shutil`, `hashlib`, `re`) ### Provides To -- All modules — agent scaffolding and lifecycle setup +- All modules — agent creation service and lifecycle setup +- Registry: Reads/writes `AIPASS_REGISTRY.json` + +--- + +*Last Updated: 2026-03-07* diff --git a/src/aipass/spawn/__init__.py b/src/aipass/spawn/__init__.py index 0ac8a55d..7b2a50c6 100644 --- a/src/aipass/spawn/__init__.py +++ b/src/aipass/spawn/__init__.py @@ -6,6 +6,6 @@ Usage: result = spawn_agent("/path/to/new/agent", role="My Role", purpose="My Purpose") """ -from aipass.spawn.apps.modules.core import spawn_agent +from aipass.spawn.apps.modules.core import _spawn_agent as spawn_agent __all__ = ["spawn_agent"] diff --git a/src/aipass/spawn/apps/extensions/__init__.py b/src/aipass/spawn/apps/extensions/__init__.py new file mode 100644 index 00000000..65f92220 --- /dev/null +++ b/src/aipass/spawn/apps/extensions/__init__.py @@ -0,0 +1 @@ +# Extensions package diff --git a/src/aipass/spawn/apps/handlers/file_ops.py b/src/aipass/spawn/apps/handlers/file_ops.py index 673a4db6..a4b3ad14 100644 --- a/src/aipass/spawn/apps/handlers/file_ops.py +++ b/src/aipass/spawn/apps/handlers/file_ops.py @@ -1,5 +1,15 @@ +# =================== META ==================== +# Name: file_ops.py +# Description: Template copy and file operations +# Version: 1.0.0 +# Created: 2026-03-05 +# Modified: 2026-03-07 +# ============================================= + """Template copy and file rename operations.""" +import hashlib +import json import shutil from pathlib import Path @@ -9,6 +19,11 @@ from aipass.spawn.apps.handlers.placeholders import replace_placeholders SKIP_NAMES = {"__pycache__", ".git", ".template_registry.json"} +def ensure_directory(path): + """Create directory and parents if they don't exist.""" + Path(path).mkdir(parents=True, exist_ok=True) + + def copy_template(template_dir, target_dir, replacements): """ Recursively copy template to target, replacing placeholders in text files. @@ -123,3 +138,77 @@ def _replace_path_placeholders(rel_path, replacements): new_part = replace_placeholders(part, replacements) parts.append(new_part) return Path(*parts) if parts else rel_path + + +def regenerate_template_registry(target_dir): + """ + Regenerate .template_registry.json with fresh SHA-256 hashes. + + Scans all files in the agent directory and builds a new registry + with accurate content hashes. + + Args: + target_dir: Path to the spawned agent directory + """ + target_dir = Path(target_dir) + agent_dir = target_dir / ".agent" + if not agent_dir.exists(): + return + + registry_file = agent_dir / ".template_registry.json" + + files = {} + directories = {} + file_idx = 1 + dir_idx = 1 + + for item in sorted(target_dir.rglob("*")): + rel = item.relative_to(target_dir) + + # Skip .agent internal files and __pycache__ + if ".agent" in rel.parts or "__pycache__" in rel.parts: + continue + + if item.is_dir(): + dir_id = f"d{dir_idx:03d}" + directories[dir_id] = { + "path": str(rel), + "name": item.name, + } + dir_idx += 1 + elif item.is_file(): + file_id = f"f{file_idx:03d}" + try: + content = item.read_bytes() + content_hash = hashlib.sha256(content).hexdigest()[:16] + except (IOError, PermissionError): + content_hash = "unreadable" + + has_placeholder = False + try: + text = item.read_text(encoding="utf-8") + has_placeholder = "{{" in text and "}}" in text + except (UnicodeDecodeError, IOError): + pass + + files[file_id] = { + "path": str(rel), + "name": item.name, + "content_hash": content_hash, + "has_branch_placeholder": has_placeholder, + } + file_idx += 1 + + registry = { + "metadata": { + "description": "Template registry for tracking files", + "generated": True, + }, + "files": files, + "directories": directories, + } + + registry_file.write_text( + json.dumps(registry, indent=2, ensure_ascii=False) + "\n", + encoding="utf-8", + ) diff --git a/src/aipass/spawn/apps/handlers/json/__init__.py b/src/aipass/spawn/apps/handlers/json/__init__.py new file mode 100644 index 00000000..056876c7 --- /dev/null +++ b/src/aipass/spawn/apps/handlers/json/__init__.py @@ -0,0 +1 @@ +# JSON handlers package diff --git a/src/aipass/spawn/apps/handlers/json/json_handler.py b/src/aipass/spawn/apps/handlers/json/json_handler.py new file mode 100644 index 00000000..9595c321 --- /dev/null +++ b/src/aipass/spawn/apps/handlers/json/json_handler.py @@ -0,0 +1,31 @@ +# =================== META ==================== +# Name: json_handler.py +# Description: JSON file read/write operations for spawn +# Version: 1.0.0 +# Created: 2026-03-07 +# Modified: 2026-03-07 +# ============================================= + +"""JSON handler for spawn module.""" + +import json +from pathlib import Path +from typing import Any, Optional + + +def read_json(file_path: Path) -> Optional[dict]: + """Read and parse a JSON file.""" + try: + return json.loads(file_path.read_text()) + except (json.JSONDecodeError, FileNotFoundError): + return None + + +def write_json(file_path: Path, data: Any, indent: int = 2) -> bool: + """Write data to a JSON file.""" + try: + file_path.parent.mkdir(parents=True, exist_ok=True) + file_path.write_text(json.dumps(data, indent=indent) + "\n") + return True + except OSError: + return False diff --git a/src/aipass/spawn/apps/handlers/metadata.py b/src/aipass/spawn/apps/handlers/metadata.py index 3fd3e246..8e918dda 100644 --- a/src/aipass/spawn/apps/handlers/metadata.py +++ b/src/aipass/spawn/apps/handlers/metadata.py @@ -1,3 +1,11 @@ +# =================== META ==================== +# Name: metadata.py +# Description: Branch name extraction and profile detection +# Version: 1.0.0 +# Created: 2026-03-05 +# Modified: 2026-03-07 +# ============================================= + """Branch name extraction and profile detection.""" from pathlib import Path diff --git a/src/aipass/spawn/apps/handlers/placeholders.py b/src/aipass/spawn/apps/handlers/placeholders.py index 369fc7f2..b3221c30 100644 --- a/src/aipass/spawn/apps/handlers/placeholders.py +++ b/src/aipass/spawn/apps/handlers/placeholders.py @@ -1,3 +1,11 @@ +# =================== META ==================== +# Name: placeholders.py +# Description: Placeholder replacement engine +# Version: 1.0.0 +# Created: 2026-03-05 +# Modified: 2026-03-07 +# ============================================= + """Placeholder replacement engine for agent templates.""" import re diff --git a/src/aipass/spawn/apps/handlers/registry.py b/src/aipass/spawn/apps/handlers/registry.py index 5c06ef48..5292ae91 100644 --- a/src/aipass/spawn/apps/handlers/registry.py +++ b/src/aipass/spawn/apps/handlers/registry.py @@ -1,3 +1,11 @@ +# =================== META ==================== +# Name: registry.py +# Description: AIPASS_REGISTRY.json CRUD operations +# Version: 1.0.0 +# Created: 2026-03-05 +# Modified: 2026-03-07 +# ============================================= + """AIPASS_REGISTRY.json CRUD operations.""" import json @@ -119,6 +127,19 @@ def save_registry(registry_path, data): return False +def get_next_citizen_number(registry_path): + """Get next citizen number from registry (count of existing branches + 1). + + Args: + registry_path: Path to AIPASS_REGISTRY.json + + Returns: + int: Next citizen number + """ + data = load_registry(registry_path) + return len(data.get("branches", [])) + 1 + + def add_to_registry(registry_path, branch_name, branch_path, profile, email, purpose=""): """ Add a new branch entry to the registry. diff --git a/src/aipass/spawn/apps/json_templates/__init__.py b/src/aipass/spawn/apps/json_templates/__init__.py new file mode 100644 index 00000000..defbf8bc --- /dev/null +++ b/src/aipass/spawn/apps/json_templates/__init__.py @@ -0,0 +1 @@ +# JSON templates package diff --git a/src/aipass/spawn/apps/modules/core.py b/src/aipass/spawn/apps/modules/core.py index 11d24620..dcba6a9d 100644 --- a/src/aipass/spawn/apps/modules/core.py +++ b/src/aipass/spawn/apps/modules/core.py @@ -1,3 +1,11 @@ +# =================== META ==================== +# Name: core.py +# Description: Main orchestrator for agent spawning +# Version: 1.0.0 +# Created: 2026-03-05 +# Modified: 2026-03-07 +# ============================================= + """ Spawn Module — Create new AIPass agents from templates. @@ -11,21 +19,64 @@ Orchestrates the full agent creation workflow: 7. Validate no unreplaced placeholders remain """ -import hashlib -import json from pathlib import Path +from typing import List +from aipass.prax import logger from aipass.spawn.apps.handlers.metadata import get_branch_name, normalize_branch_name, detect_profile from aipass.spawn.apps.handlers.placeholders import build_replacements_dict, validate_no_placeholders -from aipass.spawn.apps.handlers.file_ops import copy_template, rename_placeholder_paths -from aipass.spawn.apps.handlers.registry import find_registry, add_to_registry +from aipass.spawn.apps.handlers.file_ops import copy_template, rename_placeholder_paths, regenerate_template_registry, ensure_directory +from aipass.spawn.apps.handlers.registry import find_registry, add_to_registry, get_next_citizen_number # Default template location (relative to spawn package root) DEFAULT_TEMPLATE = Path(__file__).parents[2] / "templates" / "agent.template" -def spawn_agent(target_path, role="", traits="", purpose="", profile=None, - template_dir=None, registry_path=None): +def handle_command(command: str, args: List[str]) -> bool: + """ + Route spawn commands to implementation. + + Args: + command: The command string (e.g. "create") + args: List of arguments for the command + + Returns: + True if command succeeded, False otherwise + """ + if command == "create": + if not args: + logger.error("spawn create requires a target path") + return False + target_path = args[0] + kwargs = {} + i = 1 + while i < len(args): + if args[i] == "--role" and i + 1 < len(args): + kwargs["role"] = args[i + 1] + i += 2 + elif args[i] == "--traits" and i + 1 < len(args): + kwargs["traits"] = args[i + 1] + i += 2 + elif args[i] == "--purpose" and i + 1 < len(args): + kwargs["purpose"] = args[i + 1] + i += 2 + elif args[i] == "--template" and i + 1 < len(args): + kwargs["template_dir"] = args[i + 1] + i += 2 + elif args[i] == "--registry" and i + 1 < len(args): + kwargs["registry_path"] = args[i + 1] + i += 2 + else: + i += 1 + result = _spawn_agent(target_path, **kwargs) + return result["success"] + else: + logger.error(f"Unknown spawn command: {command}") + return False + + +def _spawn_agent(target_path, role="", traits="", purpose="", profile=None, + template_dir=None, registry_path=None): """ Create a new AIPass agent from template. @@ -65,7 +116,7 @@ def spawn_agent(target_path, role="", traits="", purpose="", profile=None, # Determine citizen number from registry reg_path = Path(registry_path) if registry_path else find_registry(target.parent) - citizen_number = _get_next_citizen_number(reg_path) + citizen_number = get_next_citizen_number(reg_path) # Build placeholder replacements replacements = build_replacements_dict( @@ -75,14 +126,14 @@ def spawn_agent(target_path, role="", traits="", purpose="", profile=None, ) # Step 1: Copy template with placeholder replacement in content - target.mkdir(parents=True, exist_ok=True) + ensure_directory(target) copied, skipped = copy_template(template, target, replacements) # Step 2: Rename any {{BRANCH}} dirs/files that weren't caught by path replacement renamed = rename_placeholder_paths(target, folder_name) # Step 3: Regenerate .template_registry.json with fresh hashes - _regenerate_template_registry(target) + regenerate_template_registry(target) # Step 4: Register in AIPASS_REGISTRY.json registry_updated = add_to_registry( @@ -108,89 +159,6 @@ def spawn_agent(target_path, role="", traits="", purpose="", profile=None, } -def _get_next_citizen_number(registry_path): - """Get next citizen number from registry (count of existing branches + 1).""" - registry_path = Path(registry_path) - if not registry_path.exists(): - return 1 - try: - data = json.loads(registry_path.read_text(encoding="utf-8")) - return len(data.get("branches", [])) + 1 - except (json.JSONDecodeError, IOError): - return 1 - - -def _regenerate_template_registry(target_dir): - """ - Regenerate .template_registry.json with fresh SHA-256 hashes. - - Scans all files in the agent directory and builds a new registry - with accurate content hashes. - """ - target_dir = Path(target_dir) - agent_dir = target_dir / ".agent" - if not agent_dir.exists(): - return - - registry_file = agent_dir / ".template_registry.json" - - files = {} - directories = {} - file_idx = 1 - dir_idx = 1 - - for item in sorted(target_dir.rglob("*")): - rel = item.relative_to(target_dir) - - # Skip .agent internal files and __pycache__ - if ".agent" in rel.parts or "__pycache__" in rel.parts: - continue - - if item.is_dir(): - dir_id = f"d{dir_idx:03d}" - directories[dir_id] = { - "path": str(rel), - "name": item.name, - } - dir_idx += 1 - elif item.is_file(): - file_id = f"f{file_idx:03d}" - try: - content = item.read_bytes() - content_hash = hashlib.sha256(content).hexdigest()[:16] - except (IOError, PermissionError): - content_hash = "unreadable" - - has_placeholder = False - try: - text = item.read_text(encoding="utf-8") - has_placeholder = "{{" in text and "}}" in text - except (UnicodeDecodeError, IOError): - pass - - files[file_id] = { - "path": str(rel), - "name": item.name, - "content_hash": content_hash, - "has_branch_placeholder": has_placeholder, - } - file_idx += 1 - - registry = { - "metadata": { - "description": "Template registry for tracking files", - "generated": True, - }, - "files": files, - "directories": directories, - } - - registry_file.write_text( - json.dumps(registry, indent=2, ensure_ascii=False) + "\n", - encoding="utf-8", - ) - - def _error(message): """Return error result dict.""" return { diff --git a/src/aipass/spawn/apps/spawn.py b/src/aipass/spawn/apps/spawn.py index e9d43417..0e902e53 100644 --- a/src/aipass/spawn/apps/spawn.py +++ b/src/aipass/spawn/apps/spawn.py @@ -1,3 +1,11 @@ +# =================== META ==================== +# Name: spawn.py +# Description: Entry point CLI for drone @spawn +# Version: 1.0.0 +# Created: 2026-03-05 +# Modified: 2026-03-07 +# ============================================= + """ SPAWN Branch - Agent Creation System @@ -40,7 +48,7 @@ def print_help(): def handle_create(args): """Handle the create command.""" - from aipass.spawn.apps.modules.core import spawn_agent + from aipass.spawn.apps.modules.core import _spawn_agent as spawn_agent if not args: console.print("[red]Error: target path required[/red]") diff --git a/src/aipass/spawn/tests/test_spawn.py b/src/aipass/spawn/tests/test_spawn.py index bf773ffd..cfef6169 100644 --- a/src/aipass/spawn/tests/test_spawn.py +++ b/src/aipass/spawn/tests/test_spawn.py @@ -1,3 +1,11 @@ +# =================== META ==================== +# Name: test_spawn.py +# Description: Test suite for spawn module +# Version: 1.0.0 +# Created: 2026-03-05 +# Modified: 2026-03-07 +# ============================================= + """Tests for the aipass.spawn module.""" import json diff --git a/src/aipass/trigger/trigger_json/trigger_data.json b/src/aipass/trigger/trigger_json/trigger_data.json index 37ecdbbf..45f3d573 100644 --- a/src/aipass/trigger/trigger_json/trigger_data.json +++ b/src/aipass/trigger/trigger_json/trigger_data.json @@ -1,14 +1,14 @@ { "error_catchup": { - "last_scan_timestamp": "2026-03-06T14:27:27.869613", + "last_scan_timestamp": "2026-03-07T04:46:08.802277", "processed_hashes": [ - "ace01637", - "69de737b", - "c7edc03b", - "4095ef97", "68dd0ab6", + "4095ef97", + "69de737b", + "ace01637", + "02d93909", "541e0b9b", - "02d93909" + "c7edc03b" ], "max_hashes": 500, "max_lookback_hours": 24 diff --git a/your b/your new file mode 100644 index 00000000..35241f92 --- /dev/null +++ b/your @@ -0,0 +1 @@ +export PATH="$HOME/.local/bin:$PATH" shell config file