feat(system): registry glob discovery, drone stderr fix, pyright OOM fix (#38)
Registry discovery (drone, spawn, seedgo) now globs for *_REGISTRY.json with CWD-first walk-up — enabling project isolation without hardcoded filenames. Drone stderr crash fixed (Rich Console). Pyright OOM fixed via pyrightconfig excludes. AIPASS.md simplified for universal use. Global prompt updated with flow commands. DPLAN-003 added for AIPass-as-OS design vision. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
4f87b5c044
commit
8ab43a2cd0
@@ -35,6 +35,7 @@ When a task belongs to a specialist, send it there. Don't burn context doing the
|
||||
drone @ai_mail send @target "Subject" "Body" --dispatch # Task email
|
||||
drone @ai_mail dispatch wake @target # Wake branch
|
||||
drone @flow create . "Subject" # Create FPLAN
|
||||
drone @flow create . "Subject" dplan # Create DPLAN (dplan template)
|
||||
drone @flow list # Active plans
|
||||
drone systems # All branches
|
||||
```
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# DPLAN-003 Working Directory
|
||||
|
||||
Research, mapping, and planning files for "AIPass as Operating System."
|
||||
|
||||
Parent plan: `AIPass/DPLAN-003_aipass_as_operating_system_2026-03-13.md`
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose | Status |
|
||||
|------|---------|--------|
|
||||
| `registry_discovery_map.md` | Every find_registry() call, file, line number | Pending |
|
||||
| `registry_refactor_plan.md` | Shared commons function design, migration steps | Pending |
|
||||
| `aipass_init_spec.md` | What `aipass init` creates, CLI design, templates | Pending |
|
||||
| `drone_help_spec.md` | `drone aipass help` module design | Pending |
|
||||
| `portability_audit.md` | Full investigation results (session 24) | Done |
|
||||
@@ -0,0 +1,31 @@
|
||||
# Portability Audit — Session 24 Results
|
||||
|
||||
## Summary
|
||||
|
||||
| Tool | Registry Discovery | CWD-Aware | Portable | Hardcoded |
|
||||
|------|-------------------|-----------|----------|-----------|
|
||||
| Drone | Walk-up + env var | No (uses registry) | Yes | Registry filename |
|
||||
| Spawn | Walk-up + env var | No (uses registry) | Partial | Template location |
|
||||
| Prax | Walk-up (no env) | No (sys logs at repo) | Partial | System logs dir |
|
||||
| AI_Mail | Walk-up (no env) | No (inbox per branch) | Yes | Inbox location |
|
||||
| Flow | Walk-up (no env) | Yes (plan creation) | Hybrid | Plan registry |
|
||||
|
||||
## Key Findings
|
||||
|
||||
- All tools use walk-up strategy to find `AIPASS_REGISTRY.json`
|
||||
- Registry-relative path resolution already works (move registry + dirs = works)
|
||||
- `AIPASS_REGISTRY` env var supported by drone and spawn
|
||||
- System logs hardcoded to `{repo_root}/system_logs/`
|
||||
- Spawn templates hardcoded to `{spawn_package}/templates/`
|
||||
- Walk-up doesn't stop at project boundaries — finds nearest registry up the tree
|
||||
|
||||
## The Core Fix
|
||||
|
||||
Change `find_registry()` to:
|
||||
1. Walk up from CWD looking for `*_REGISTRY.json` (glob, not hardcoded name)
|
||||
2. Stop at first match — that's the project boundary
|
||||
3. If none found, return error ("No AIPass project. Run `aipass init`")
|
||||
|
||||
## Source
|
||||
|
||||
Full investigation transcript: background agent session 24, 42 tool calls across drone/spawn/ai_mail/flow/prax.
|
||||
@@ -16,8 +16,12 @@ Standard branch entry point (apps/drone.py pattern).
|
||||
import sys
|
||||
from typing import List
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
from aipass.prax import logger
|
||||
from aipass.cli.apps.modules import console
|
||||
|
||||
err_console = Console(stderr=True)
|
||||
from aipass.drone.apps.modules import BranchNotFoundError, CommandExecutionError
|
||||
from aipass.drone.apps.modules.discovery import get_help
|
||||
from aipass.drone.apps.modules.resolver import list_branches
|
||||
@@ -159,7 +163,7 @@ def _handle_module(name: str, args: List[str]) -> int:
|
||||
try:
|
||||
result = route_module_command(name, command, cmd_args)
|
||||
except (ImportError, AttributeError) as exc:
|
||||
console.print(f"drone: module @{name} is registered but not available: {exc}", stderr=True)
|
||||
err_console.print(f"drone: module @{name} is registered but not available: {exc}")
|
||||
return 1
|
||||
|
||||
if result.get("stdout"):
|
||||
@@ -184,15 +188,15 @@ def _handle_target(args: List[str]) -> int:
|
||||
try:
|
||||
result = route_command(target)
|
||||
except BranchNotFoundError as exc:
|
||||
console.print(f"drone: {exc}", stderr=True)
|
||||
err_console.print(f"drone: {exc}")
|
||||
return 1
|
||||
except CommandExecutionError as exc:
|
||||
console.print(f"drone: {exc}", stderr=True)
|
||||
err_console.print(f"drone: {exc}")
|
||||
return 1
|
||||
if result.stdout:
|
||||
console.print(result.stdout, end="", highlight=False)
|
||||
if result.stderr:
|
||||
console.print(result.stderr, end="", highlight=False, stderr=True)
|
||||
err_console.print(result.stderr, end="", highlight=False)
|
||||
return result.exit_code
|
||||
|
||||
# --help = show help
|
||||
@@ -204,10 +208,10 @@ def _handle_target(args: List[str]) -> int:
|
||||
else:
|
||||
console.print(f"No help available for {target}.")
|
||||
except BranchNotFoundError as exc:
|
||||
console.print(f"drone: {exc}", stderr=True)
|
||||
err_console.print(f"drone: {exc}")
|
||||
return 1
|
||||
except CommandExecutionError as exc:
|
||||
console.print(f"drone: {exc}", stderr=True)
|
||||
err_console.print(f"drone: {exc}")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
@@ -225,16 +229,16 @@ def _handle_target(args: List[str]) -> int:
|
||||
interactive=interactive,
|
||||
)
|
||||
except BranchNotFoundError as exc:
|
||||
console.print(f"drone: {exc}", stderr=True)
|
||||
err_console.print(f"drone: {exc}")
|
||||
return 1
|
||||
except CommandExecutionError as exc:
|
||||
console.print(f"drone: {exc}", stderr=True)
|
||||
err_console.print(f"drone: {exc}")
|
||||
return 1
|
||||
|
||||
if result.stdout:
|
||||
console.print(result.stdout, end="", highlight=False)
|
||||
if result.stderr:
|
||||
console.print(result.stderr, end="", highlight=False, stderr=True)
|
||||
err_console.print(result.stderr, end="", highlight=False)
|
||||
return result.exit_code
|
||||
|
||||
|
||||
@@ -272,8 +276,8 @@ def main() -> int:
|
||||
return _handle_target(args)
|
||||
|
||||
# Unknown command
|
||||
console.print(f"drone: unknown command '{command}'", stderr=True)
|
||||
console.print("Run 'drone --help' for usage.", stderr=True)
|
||||
err_console.print(f"drone: unknown command '{command}'")
|
||||
err_console.print("Run 'drone --help' for usage.")
|
||||
return 1
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"""
|
||||
Handler for registry file operations.
|
||||
|
||||
Handles loading, parsing, and normalizing AIPASS_REGISTRY.json.
|
||||
Handles loading, parsing, and normalizing *_REGISTRY.json files.
|
||||
All file I/O and data transformation for the registry lives here.
|
||||
"""
|
||||
|
||||
@@ -33,8 +33,18 @@ from .exceptions import (
|
||||
_registry_path: Optional[Path] = None
|
||||
|
||||
|
||||
def _first_registry_in(directory: Path) -> Optional[Path]:
|
||||
"""Return the first *_REGISTRY.json in *directory*, or None.
|
||||
|
||||
When multiple matches exist, the alphabetically-first name wins
|
||||
so the result is deterministic across platforms.
|
||||
"""
|
||||
matches = sorted(directory.glob("*_REGISTRY.json"))
|
||||
return matches[0] if matches else None
|
||||
|
||||
|
||||
def find_registry() -> Path:
|
||||
"""Find AIPASS_REGISTRY.json by walking up from this file's location.
|
||||
"""Find a *_REGISTRY.json by walking up from this file's location.
|
||||
|
||||
Search order:
|
||||
1. Explicitly set path via set_registry_path()
|
||||
@@ -42,23 +52,33 @@ def find_registry() -> Path:
|
||||
3. Walk up from drone package location
|
||||
4. Walk up from cwd
|
||||
5. Default: package-relative path
|
||||
"""
|
||||
# Walk up from this file (works for pip editable installs)
|
||||
current = Path(__file__).resolve().parent
|
||||
for parent in [current] + list(current.parents):
|
||||
candidate = parent / "AIPASS_REGISTRY.json"
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
|
||||
# Walk up from cwd (works for regular installs)
|
||||
The first directory that contains any *_REGISTRY.json is treated
|
||||
as the project boundary. If that directory holds more than one
|
||||
match, the alphabetically-first file is returned.
|
||||
"""
|
||||
# Walk up from cwd FIRST — this is where the user is working
|
||||
cwd = Path.cwd()
|
||||
for parent in [cwd] + list(cwd.parents):
|
||||
candidate = parent / "AIPASS_REGISTRY.json"
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
hit = _first_registry_in(parent)
|
||||
if hit is not None:
|
||||
return hit
|
||||
|
||||
# Fallback — use package-relative path (no filesystem assumptions)
|
||||
return Path(__file__).resolve().parents[4] / "AIPASS_REGISTRY.json"
|
||||
# Walk up from this file (fallback for pip editable installs)
|
||||
current = Path(__file__).resolve().parent
|
||||
for parent in [current] + list(current.parents):
|
||||
hit = _first_registry_in(parent)
|
||||
if hit is not None:
|
||||
return hit
|
||||
|
||||
# Fallback — use package-relative path; glob there too
|
||||
fallback_dir = Path(__file__).resolve().parents[4]
|
||||
hit = _first_registry_in(fallback_dir)
|
||||
if hit is not None:
|
||||
return hit
|
||||
# Ultimate fallback: return a conventional name so the caller
|
||||
# gets a clear "not found" path in the error message.
|
||||
return fallback_dir / "AIPASS_REGISTRY.json"
|
||||
|
||||
|
||||
def get_registry_path() -> Path:
|
||||
@@ -113,7 +133,7 @@ def load_registry() -> Dict[str, Any]:
|
||||
if not registry_path.exists():
|
||||
raise RegistryNotFoundError(
|
||||
f"Registry not found at {registry_path}. "
|
||||
"Create an AIPASS_REGISTRY.json in your project root."
|
||||
"Create a *_REGISTRY.json file in your project root."
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -51,14 +51,20 @@ def _is_branch_private(branch_name: str) -> bool:
|
||||
|
||||
def _find_registry() -> Path:
|
||||
"""
|
||||
Find AIPASS_REGISTRY.json by walking up from this file's location.
|
||||
Works regardless of install path depth.
|
||||
Find *_REGISTRY.json by walking up from this file's location.
|
||||
Uses glob to match any registry file (aligned with drone's registry_handler).
|
||||
"""
|
||||
current = Path(__file__).resolve().parent
|
||||
for parent in [current] + list(current.parents):
|
||||
candidate = parent / "AIPASS_REGISTRY.json"
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
matches = sorted(parent.glob("*_REGISTRY.json"))
|
||||
if matches:
|
||||
return matches[0]
|
||||
# Fallback: walk up from CWD (matches drone's search order)
|
||||
cwd = Path.cwd()
|
||||
for parent in [cwd] + list(cwd.parents):
|
||||
matches = sorted(parent.glob("*_REGISTRY.json"))
|
||||
if matches:
|
||||
return matches[0]
|
||||
return Path.cwd() / "AIPASS_REGISTRY.json"
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: registry.py
|
||||
# Description: AIPASS_REGISTRY.json CRUD operations
|
||||
# Description: *_REGISTRY.json discovery and CRUD operations
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-03-05
|
||||
# Modified: 2026-03-07
|
||||
# =============================================
|
||||
|
||||
"""AIPASS_REGISTRY.json CRUD operations."""
|
||||
"""*_REGISTRY.json discovery and CRUD operations."""
|
||||
|
||||
import json
|
||||
import os
|
||||
@@ -31,49 +31,56 @@ def _branches_as_list(branches):
|
||||
return []
|
||||
|
||||
|
||||
def _glob_registry(directory):
|
||||
"""Find the first *_REGISTRY.json in a directory (sorted for consistency).
|
||||
|
||||
Args:
|
||||
directory: Path to search in
|
||||
|
||||
Returns:
|
||||
Path to the registry file, or None if not found
|
||||
"""
|
||||
matches = sorted(directory.glob("*_REGISTRY.json"))
|
||||
return matches[0] if matches else None
|
||||
|
||||
|
||||
def find_registry(start_path=None):
|
||||
"""
|
||||
Find AIPASS_REGISTRY.json — consistent with drone's resolution.
|
||||
Find *_REGISTRY.json — walks up from __file__ and start_path/cwd.
|
||||
|
||||
The first *_REGISTRY.json found while walking up IS the project boundary.
|
||||
If multiple exist in the same directory, picks the first alphabetically.
|
||||
|
||||
Priority:
|
||||
1. AIPASS_REGISTRY environment variable
|
||||
2. Project root (directory with pyproject.toml or .git) — walk up from __file__
|
||||
3. Project root — walk up from start_path/cwd
|
||||
4. Walk up from __file__ for any existing registry
|
||||
5. Last resort: cwd
|
||||
2. Walk up from __file__ — first dir containing *_REGISTRY.json
|
||||
3. Walk up from start_path/cwd — first dir containing *_REGISTRY.json
|
||||
4. Last resort: cwd / AIPASS_REGISTRY.json (backwards compat)
|
||||
|
||||
Args:
|
||||
start_path: Directory to start searching from
|
||||
|
||||
Returns:
|
||||
Path to AIPASS_REGISTRY.json
|
||||
Path to *_REGISTRY.json
|
||||
"""
|
||||
# Check environment variable first (same as drone's config.py)
|
||||
env_path = os.environ.get("AIPASS_REGISTRY")
|
||||
if env_path:
|
||||
return Path(env_path)
|
||||
|
||||
# Walk up from package location — find project root first
|
||||
pkg_dir = Path(__file__).resolve().parent
|
||||
for parent in [pkg_dir] + list(pkg_dir.parents):
|
||||
if (parent / "pyproject.toml").exists() or (parent / ".git").exists():
|
||||
candidate = parent / "AIPASS_REGISTRY.json"
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
# Project root found but no registry — create here
|
||||
return candidate
|
||||
|
||||
# Walk up from start_path or cwd
|
||||
# Walk up from start_path or cwd FIRST — user's location takes priority
|
||||
current = Path(start_path).resolve() if start_path else Path.cwd()
|
||||
for parent in [current] + list(current.parents):
|
||||
if (parent / "pyproject.toml").exists() or (parent / ".git").exists():
|
||||
return parent / "AIPASS_REGISTRY.json"
|
||||
found = _glob_registry(parent)
|
||||
if found:
|
||||
return found
|
||||
|
||||
# Fallback: any existing registry walking up from package
|
||||
# Walk up from package location (fallback for editable installs)
|
||||
pkg_dir = Path(__file__).resolve().parent
|
||||
for parent in [pkg_dir] + list(pkg_dir.parents):
|
||||
candidate = parent / "AIPASS_REGISTRY.json"
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
found = _glob_registry(parent)
|
||||
if found:
|
||||
return found
|
||||
|
||||
# Last resort: cwd
|
||||
return Path.cwd() / "AIPASS_REGISTRY.json"
|
||||
|
||||
Reference in New Issue
Block a user