diff --git a/.aipass/aipass_global_prompt.md b/.aipass/aipass_global_prompt.md index cc783ef9..63ad581a 100644 --- a/.aipass/aipass_global_prompt.md +++ b/.aipass/aipass_global_prompt.md @@ -3,6 +3,8 @@ **This prompt is your guide.** The patterns shown here are exact. Don't guess command syntax — the examples ARE the API. +**If a command or workflow seems obvious but isn't documented here, flag it.** Don't silently guess — ask or investigate with `--help`. Missing instructions are a prompt bug, not a knowledge gap. + **USER NAME:** Patrick If no user name is set above, ask on first interaction. @@ -52,6 +54,9 @@ drone systems # List all registered branches drone @seedgo audit aipass # Run standards audit on all branches drone @seedgo standards_query aipass_standards # List all standards (then query by name) drone @prax monitor # Real-time monitoring (interactive) +drone @flow create . "Subject" # Create FPLAN (execution plan) +drone @flow create . "Subject" dplan # Create DPLAN (design/planning doc) +drone @flow list # List active plans ``` ## Dispatch — Wake a Branch diff --git a/AIPASS.md b/AIPASS.md index e8e38a2a..f0894c78 100644 --- a/AIPASS.md +++ b/AIPASS.md @@ -1,22 +1,14 @@ # AIPass — Project Prompt -Project-level instructions loaded by Claude Code. Persists in context for the entire conversation. -Details live in README, --help, .trinity/ memories, and the Global Prompt (.aipass/aipass_global_prompt.md). +Project-level instructions. Persists in context for the entire conversation. ## Startup On any greeting, silently read these files from CWD and run the commands — no narration, no announcing steps. Just do it and respond with the status. -**Read:** `.trinity/passport.json`, `.trinity/local.json`, `.trinity/observations.json`, `STATUS.local.md`, `README.md` -**Run:** `git status`, `drone systems` - -## Navigation - -- 15 branches under `src/aipass/` (+ commons at `src/commons/`, skills at `src/skills/`) -- `drone @branch --help` for commands. `drone systems` for branch list. README.md for architecture. +**Read:** `.trinity/passport.json`, `.trinity/local.json`, `.trinity/observations.json`, `README.md` +**Run:** `git status` ## Memories -Update `.trinity/` at natural breakpoints, after milestones, and on `/memo`. If compaction hits before you save, it's gone. Details in your branch prompt. - - +Update `.trinity/` at natural breakpoints, after milestones, and on `/memo`. If compaction hits before you save, it's gone. diff --git a/dplan-003/registry_discovery_map.md b/dplan-003/registry_discovery_map.md new file mode 100644 index 00000000..17583c4c --- /dev/null +++ b/dplan-003/registry_discovery_map.md @@ -0,0 +1,46 @@ +# Registry Discovery Map — FPLAN-0029 Phase 1 + +## Primary Implementations (5) + +| File | Function | Strategy | +|------|----------|----------| +| `src/aipass/spawn/apps/handlers/registry.py:34-79` | `find_registry(start_path)` | Env var → project markers (.git/pyproject.toml) → walk-up → cwd fallback | +| `src/aipass/drone/apps/handlers/registry_handler.py:36-61` | `find_registry()` | Walk-up from __file__ → walk-up from cwd → parents[4] fallback | +| `src/aipass/drone/apps/handlers/registry_handler.py:64-81` | `get_registry_path()` | Global override → env var → find_registry() | +| `src/commons/apps/handlers/database/db.py:343-378` | `_find_branch_registry()` | Env var AIPASS_ROOT → walk-up (10 limit) → ~/.aipass/ fallback | +| `src/commons/apps/handlers/identity/identity_ops.py:33-67` | `_find_branch_registry_path()` | Same as db.py | + +## Local Reimplementations (23+) + +All do walk-up from `__file__` looking for `AIPASS_REGISTRY.json`: + +### ai_mail (4 files) +- `apps/handlers/users/branch_detection.py:26-38` +- `apps/handlers/registry/read.py:31-41` +- `apps/handlers/email/format.py:21-31` +- `apps/handlers/dispatch/daemon.py:36-55` + +### memory (3 files) +- `apps/handlers/dashboard_push.py:44-54` +- `apps/handlers/monitor/detector.py:36-83` +- `apps/handlers/monitor/memory_watcher.py:363-385` + +### prax (2 files) +- `apps/handlers/registry/reader.py:24-39` +- `apps/handlers/dashboard/agent_status_writer.py:33-44` + +### seedgo (3 files) +- `apps/handlers/audit/discovery.py:54-62` +- `apps/handlers/diagnostics/discovery.py:21-29` +- `apps/handlers/readme/readme_ops.py:29-37` + +### + 11 more across other branches + +## Hardcoded String Count +100+ references to `"AIPASS_REGISTRY.json"` across codebase. + +## Phase 1 Plan +1. Create `commons.registry.find_registry()` — one function, glob for `*_REGISTRY.json` +2. All 23+ modules import from commons +3. Drone/spawn wrappers call commons internally +4. Test: temp dir with TEST_REGISTRY.json → drone systems finds it diff --git a/pyrightconfig.json b/pyrightconfig.json index 1d09e831..28ab1b05 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -2,5 +2,11 @@ "extraPaths": ["src"], "pythonVersion": "3.10", "reportMissingImports": "error", - "reportAttributeAccessIssue": "error" + "reportAttributeAccessIssue": "error", + "exclude": [ + "**/.venv", + "**/node_modules", + "**/.archive", + "**/.sorting_unprocessed" + ] } diff --git a/src/aipass/devpulse/.aipass/aipass_local_prompt.md b/src/aipass/devpulse/.aipass/aipass_local_prompt.md index cdce5a61..884d908c 100644 --- a/src/aipass/devpulse/.aipass/aipass_local_prompt.md +++ b/src/aipass/devpulse/.aipass/aipass_local_prompt.md @@ -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 ``` diff --git a/src/aipass/devpulse/dplan-003/README.md b/src/aipass/devpulse/dplan-003/README.md new file mode 100644 index 00000000..2b4ac53b --- /dev/null +++ b/src/aipass/devpulse/dplan-003/README.md @@ -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 | diff --git a/src/aipass/devpulse/dplan-003/portability_audit.md b/src/aipass/devpulse/dplan-003/portability_audit.md new file mode 100644 index 00000000..090f00cc --- /dev/null +++ b/src/aipass/devpulse/dplan-003/portability_audit.md @@ -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. diff --git a/src/aipass/drone/apps/drone.py b/src/aipass/drone/apps/drone.py index d387a87e..3d0d04d9 100644 --- a/src/aipass/drone/apps/drone.py +++ b/src/aipass/drone/apps/drone.py @@ -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 diff --git a/src/aipass/drone/apps/handlers/registry_handler.py b/src/aipass/drone/apps/handlers/registry_handler.py index f4f6089e..a6f54369 100644 --- a/src/aipass/drone/apps/handlers/registry_handler.py +++ b/src/aipass/drone/apps/handlers/registry_handler.py @@ -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: diff --git a/src/aipass/seedgo/apps/handlers/audit/discovery.py b/src/aipass/seedgo/apps/handlers/audit/discovery.py index 5a242f9c..0927c2d9 100644 --- a/src/aipass/seedgo/apps/handlers/audit/discovery.py +++ b/src/aipass/seedgo/apps/handlers/audit/discovery.py @@ -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" diff --git a/src/aipass/spawn/apps/handlers/registry.py b/src/aipass/spawn/apps/handlers/registry.py index 080e8688..e852c0e3 100644 --- a/src/aipass/spawn/apps/handlers/registry.py +++ b/src/aipass/spawn/apps/handlers/registry.py @@ -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"