diff --git a/src/aipass/ai_mail/apps/handlers/email/delivery.py b/src/aipass/ai_mail/apps/handlers/email/delivery.py index 43fd53eb..813d5127 100644 --- a/src/aipass/ai_mail/apps/handlers/email/delivery.py +++ b/src/aipass/ai_mail/apps/handlers/email/delivery.py @@ -90,7 +90,11 @@ def get_all_branches() -> List[Dict]: registry_data = json.load(f) # Parse branch entries from JSON structure - for branch in registry_data.get("branches", []): + # Handle both formats: list of dicts or dict keyed by name + raw_branches = registry_data.get("branches", []) + if isinstance(raw_branches, dict): + raw_branches = list(raw_branches.values()) + for branch in raw_branches: branch_name = branch.get("name", "") path = branch.get("path", "") diff --git a/src/aipass/ai_mail/apps/handlers/users/branch_detection.py b/src/aipass/ai_mail/apps/handlers/users/branch_detection.py index 95685658..ace28d90 100644 --- a/src/aipass/ai_mail/apps/handlers/users/branch_detection.py +++ b/src/aipass/ai_mail/apps/handlers/users/branch_detection.py @@ -44,6 +44,20 @@ def _find_repo_root() -> Path: BRANCH_REGISTRY_PATH = _find_repo_root() / "AIPASS_REGISTRY.json" + +def _get_branches_list(registry: dict) -> list: + """Normalize branches from registry to a list of dicts. + + Handles both formats: + - List: [{"name": "DEVPULSE", ...}, ...] + - Dict: {"devpulse": {"name": "devpulse", ...}, ...} + """ + branches = registry.get("branches", []) + if isinstance(branches, dict): + return list(branches.values()) + return branches + + # ============================================= # BRANCH DETECTION FUNCTIONS # ============================================= @@ -67,7 +81,14 @@ def detect_branch_from_pwd() -> Dict | None: None if no branch detected """ try: - # Use caller's CWD if passed by drone, otherwise fall back to process CWD + # Primary: use explicit branch name passed by drone (works in Docker + local) + caller_branch = os.environ.get("AIPASS_CALLER_BRANCH") + if caller_branch: + branch_info = _lookup_branch_by_name(caller_branch) + if branch_info: + return branch_info + + # Fallback: use caller's CWD for path-based detection (local only) caller_cwd = os.environ.get("AIPASS_CALLER_CWD") cwd = Path(caller_cwd) if caller_cwd else Path.cwd() @@ -87,6 +108,38 @@ def detect_branch_from_pwd() -> Dict | None: return None +def _lookup_branch_by_name(branch_name: str) -> Dict | None: + """ + Look up branch in the registry by name (case-insensitive). + + Handles both registry formats: + - List format: {"branches": [{"name": "DEVPULSE", ...}, ...]} + - Dict format: {"branches": {"devpulse": {"name": "devpulse", ...}, ...}} + + Args: + branch_name: Branch name (e.g., "DEVPULSE", "devpulse") + + Returns: + Dict with branch info from registry, or None if not found + """ + if not BRANCH_REGISTRY_PATH.exists(): + return None + + try: + with open(BRANCH_REGISTRY_PATH, 'r', encoding='utf-8') as f: + registry = json.load(f) + + name_lower = branch_name.lower() + for branch in _get_branches_list(registry): + if branch.get("name", "").lower() == name_lower: + return branch + + return None + + except Exception: + return None + + def find_branch_root(start_path: Path) -> Path | None: """ Walk up directory tree to find branch root. @@ -138,7 +191,7 @@ def get_branch_info_from_registry(branch_path: Path) -> Dict | None: branch_path_resolved = branch_path.resolve() # Search registry for matching path - for branch in registry.get("branches", []): + for branch in _get_branches_list(registry): reg_path = Path(branch["path"]) # Resolve relative paths against registry location, not CWD if not reg_path.is_absolute(): diff --git a/src/aipass/ai_mail/apps/handlers/users/user.py b/src/aipass/ai_mail/apps/handlers/users/user.py index 93e8574b..e7fd2d9d 100644 --- a/src/aipass/ai_mail/apps/handlers/users/user.py +++ b/src/aipass/ai_mail/apps/handlers/users/user.py @@ -107,10 +107,9 @@ def get_user_by_email(email: str) -> Dict | None: Returns: Dict containing user info, or None if not found """ - from .branch_detection import get_branch_info_from_registry + from .branch_detection import BRANCH_REGISTRY_PATH, _get_branches_list # Use registry lookup - from .branch_detection import BRANCH_REGISTRY_PATH registry_path = BRANCH_REGISTRY_PATH if not registry_path.exists(): return None @@ -120,7 +119,7 @@ def get_user_by_email(email: str) -> Dict | None: with open(registry_path, 'r', encoding='utf-8') as f: registry = json.load(f) - for branch in registry.get("branches", []): + for branch in _get_branches_list(registry): if branch.get("email") == email: branch_path = Path(branch.get("path", "")) return { @@ -141,7 +140,7 @@ def get_all_users() -> Dict[str, Dict]: Returns: Dict mapping branch emails to user info dicts """ - from .branch_detection import BRANCH_REGISTRY_PATH + from .branch_detection import BRANCH_REGISTRY_PATH, _get_branches_list registry_path = BRANCH_REGISTRY_PATH if not registry_path.exists(): return {} @@ -152,7 +151,7 @@ def get_all_users() -> Dict[str, Dict]: registry = json.load(f) users = {} - for branch in registry.get("branches", []): + for branch in _get_branches_list(registry): email = branch.get("email", "") if email: branch_path = Path(branch.get("path", "")) diff --git a/src/aipass/drone/apps/modules/router.py b/src/aipass/drone/apps/modules/router.py index da7a533e..8527647d 100644 --- a/src/aipass/drone/apps/modules/router.py +++ b/src/aipass/drone/apps/modules/router.py @@ -5,6 +5,7 @@ Routes commands to branch entry points by resolving symbolic @branch names, locating the branch's apps/{name}.py entry point, and executing via subprocess. """ +import json import logging import sys from pathlib import Path @@ -27,6 +28,31 @@ def _find_entry_point(branch_path: str, branch_name: str) -> Path: return entry_point +def _detect_caller_branch_name(cwd: Path) -> str | None: + """Walk up from cwd to find .trinity/passport.json and extract branch name.""" + current = cwd.resolve() + for _ in range(10): + passport = current / ".trinity" / "passport.json" + if passport.exists(): + try: + with open(passport, "r", encoding="utf-8") as f: + data = json.load(f) + # Handle both passport formats: + # v1: branch_info.branch_name (local/full passport) + # v2: identity.name (Docker/minimal passport) + name = data.get("branch_info", {}).get("branch_name") + if not name: + name = data.get("identity", {}).get("name") + return name + except Exception: + return None + parent = current.parent + if parent == current: + break + current = parent + return None + + def route_command( target: str, command: str, @@ -57,6 +83,11 @@ def route_command( # Pass caller's CWD so target branches can detect who invoked them caller_env = {"AIPASS_CALLER_CWD": str(Path.cwd())} + # Detect caller branch name from passport.json and pass it explicitly + caller_branch = _detect_caller_branch_name(Path.cwd()) + if caller_branch: + caller_env["AIPASS_CALLER_BRANCH"] = caller_branch + result = execute_command( executable=sys.executable, args=cmd_args, diff --git a/src/aipass/flow/FPLAN-0011_fix_ai_mail_caller_identity_detection_fo_2026-03-08.md b/src/aipass/flow/FPLAN-0011_fix_ai_mail_caller_identity_detection_fo_2026-03-08.md new file mode 100644 index 00000000..c7c4ca16 --- /dev/null +++ b/src/aipass/flow/FPLAN-0011_fix_ai_mail_caller_identity_detection_fo_2026-03-08.md @@ -0,0 +1,281 @@ +# FPLAN-0011 - Fix ai_mail caller identity detection for Docker and local + +**Created**: 2026-03-08 +**Branch**: flow +**Status**: Active +**Type**: Standard Plan + +--- + +## What Are Flow Plans? + +Flow Plans (FPLANs) are for **BUILDING** - autonomous construction of systems, features, modules. + +**This is NOT for:** +- Research or exploration (use agents directly) +- Quick fixes (just do it) +- Discussion or planning (that happens before creating the FPLAN) + +**This IS for:** +- Building features or modules +- Single focused construction tasks +- Sub-plans within a master plan + +--- + +## When to Use This vs Master Plan + +| This (Default) | Master Plan | +|----------------|-------------| +| Single focused task | 3+ phases, complex build | +| Self-contained | Roadmap + multiple sub-plans | +| Quick build | Multi-session project | +| One phase of a master | Entire branch/system build | + +**Need a master plan?** `drone @flow create "subject" master` + +--- + +## Branch Directory Structure + +Use dedicated directories - don't scatter files: + +| Directory | Purpose | +|-----------|---------| +| `apps/` | Code (modules/, handlers/) | +| `tests/` | All test files | +| `tools/` | Utility scripts | +| `artifacts/` | Agent outputs | +| `docs/` | Documentation | + +--- + +## Critical: Branch Manager Role + +**You are the ORCHESTRATOR, not the builder.** + +Your 200k context is precious. Burning it on file reads and code writing risks compaction during autonomous work. Agents have clean context - use them for ALL building. + +| You Do (Orchestrator) | Agents Do (Builders) | +|-----------------------|----------------------| +| Create plans | Write code | +| Give instructions | Run tests | +| Review output | Read/modify files | +| Course correct | Research/exploration | +| Update memories | Heavy lifting | +| Send status emails | Single-task execution | + +**Pattern:** Instruct agent → Wait for completion → Review output → Next step + +--- + +## Seek Branch Expertise + +Don't figure everything out alone. Other branches are domain experts - ask them first. + +**Before building anything that touches another branch's domain:** +```bash +ai_mail send @branch "Question: [topic]" "I'm working on X and need guidance on Y. What's the best approach?" +``` + +**Common examples:** +- Building something with email? Ask @ai_mail how delivery works +- Need routing or @ resolution? Ask @drone +- Unsure about standards? Ask @seedgo for reference code +- Need persistent storage or search? Ask @memory_bank +- Event-driven behavior? Ask @trigger about their event system +- Dashboard integration? Ask @devpulse about update_section() + +They have deep memory on their systems. A 1-email question saves you hours of guessing. + +--- + +## Notepad + +Keep `notepad.md` in your branch directory as a shared scratchpad during the build. Use it for: +- **Status updates** - Quick progress lines so the user can glance without asking +- **Questions for the user** - Non-urgent questions that can wait for the next check-in +- **Notes to self** - Decisions made, things to revisit, gotchas discovered + +Update it as you work - lightweight, not formal. The user checks it when they want to, skips it when busy. + +--- + +## Command Reference + +When unsure about syntax, use `--help`: + +```bash +# Flow - Plan management +drone @flow create . "subject" # Create plan (. = current dir) +drone @flow close FPLAN-XXXX # Close plan +drone @flow list # List active plans +drone @flow --help # Full help + +# Seedgo - Quality gates +drone @seedgo checklist # 10-point check on file +drone @seedgo audit @branch # Full branch audit +drone @seedgo --help # Full help + +# AI_Mail - Status updates +drone @ai_mail send @devpulse "Subject" "Message" +drone @ai_mail --help # Full help + +# Discovery +drone systems # All available modules +drone list @branch # Commands for branch +``` + +--- + +## Planning Phase + +### Goal +ai_mail caller identity detection works in both Docker containers and local environments. When devpulse sends an email via `drone @ai_mail send @spawn "Subject" "Body"`, ai_mail correctly identifies devpulse as the sender — regardless of mount paths. + +### Problem +Drone routes commands to ai_mail via subprocess with `cwd=ai_mail_dir`. Drone passes `AIPASS_CALLER_CWD` env var with the original caller's directory. ai_mail's `branch_detection.py` reads this CWD and walks up looking for `.trinity/passport.json`, then matches against the registry. + +In Docker: registry paths use container paths (`/home/coder/workspace/AIPass/...`) which are generated fresh. The CWD-to-registry path resolution fails when paths don't match exactly. + +### Approach +Two-branch fix — drone and ai_mail cooperate: + +**Phase 1 — Drone side** (`src/aipass/drone/apps/modules/router.py`): +- Detect caller branch name from CWD (walk up to find `.trinity/passport.json`, read `identity.name`) +- Pass as `AIPASS_CALLER_BRANCH` env var alongside existing `AIPASS_CALLER_CWD` +- Keep CWD — other branches may need it for different purposes + +**Phase 2 — ai_mail side** (`src/aipass/ai_mail/apps/handlers/users/branch_detection.py`): +- Check `AIPASS_CALLER_BRANCH` first — direct name lookup in registry (path-independent) +- Fall back to `AIPASS_CALLER_CWD` path resolution (existing behavior) +- Fall back to `Path.cwd()` (original behavior) + +**Phase 3 — Test in Docker**: +- Copy fixed files into `aipass-fresh-test` container +- Run `cd src/aipass/devpulse && drone @ai_mail send @spawn "Test" "Test body"` +- Verify email lands in spawn's inbox +- Verify sender is "@devpulse" + +**Phase 4 — Test locally**: +- Run same command from local devpulse +- Verify existing ai_mail functionality still works + +### Reference Documents +- `src/aipass/drone/apps/modules/router.py` — lines 57-66, AIPASS_CALLER_CWD +- `src/aipass/drone/apps/handlers/executor.py` — lines 46-50, env merge +- `src/aipass/ai_mail/apps/handlers/users/branch_detection.py` — lines 51-87, detect_branch_from_pwd() +- `src/aipass/ai_mail/apps/handlers/users/user.py` — lines 37-97, get_current_user() + +--- + +## Agent Preparation (Before Deploying) + +Agents can't work blind. They need context before they build. + +**Your Prep Work (as orchestrator):** +1. [ ] Know where agent will work (branch path, key directories) +2. [ ] Identify files agent needs to reference or modify +3. [ ] Gather any specs, planning docs, or examples to include +4. [ ] Prepare COMPLETE instructions (agents are stateless) + +**Agent's First Task (context building):** +- Agent should explore/read relevant files BEFORE writing code +- "First, read X and Y to understand the current structure" +- "Look at Z for the pattern to follow" +- Context-first, build-second + +**What Agents DON'T Have:** +- No prior conversation history +- No memory files loaded automatically +- No knowledge of other branches +- Only what you put in their instructions + +**Your instructions determine success - be thorough and specific.** + +--- + +## Agent Instructions Template +``` +You are working at [BRANCH_PATH]. + +TASK: [Specific single task] + +CONTEXT: +- [What they need to know] +- Reference: [planning docs, existing code to study] +- First, READ the relevant files to understand current structure + +DELIVERABLES: +- [Specific file or output expected] +- Tests → tests/ +- Reports/logs → artifacts/reports/ or artifacts/logs/ + +CONSTRAINTS: +- Follow Seedgo standards (3-layer architecture) +- Do NOT modify files outside your task scope +- CROSS-BRANCH: Never modify other branches' files unless explicitly authorized by the user +- 2-ATTEMPT RULE: If something fails twice, note the issue and move on +- Do NOT go down rabbit holes debugging + +WHEN COMPLETE: +- Verify code runs without syntax errors +- List files created/modified +- Note any issues encountered (with what was attempted) +``` + +--- + +## Execution Log + +### 2026-03-08 +- [ ] Created FPLAN-0011 +- [ ] Agent deployed for: [task] +- [ ] Agent completed: [outcome] +- [ ] Seedgo checklist passed: [file] +- [ ] Memories updated + +**Log Pattern:** Task → Agent → Outcome → Quality check → Next + +**If production stops (critical blocker):** +```bash +drone @ai_mail send @devpulse "PRODUCTION STOPPED: FPLAN-0011" "Issue: [description]. Attempted: [what was tried]. Awaiting guidance." +``` + +--- + +## Notes + +[Working notes, issues encountered, decisions made] + +--- + +## Completion Checklist + +### Before Closing + +- [ ] All goals achieved +- [ ] Agent output reviewed and verified +- [ ] Seedgo checklist on new code: `drone @seedgo checklist ` +- [ ] Branch memories updated: + - [ ] `BRANCH.local.json` - session/work log + - [ ] `BRANCH.observations.json` - patterns learned (if any) +- [ ] README.md updated (if build changed status/capabilities) +- [ ] Status email sent to @devpulse: + ```bash + drone @ai_mail send @devpulse "FPLAN-0011 Complete" "Summary of what was done, any issues, outcomes" + ``` + +**Completion Order:** Memories → README → Email (README before email - don't report complete with stale docs) + +### Definition of Done +[What specifically defines complete for this plan?] + +--- + +## Close Command + +When all boxes checked: +```bash +drone @flow close FPLAN-0011 +```