diff --git a/.aipass/aipass_global_prompt.md b/.aipass/aipass_global_prompt.md index b3a6e2f0..ea28e32b 100644 --- a/.aipass/aipass_global_prompt.md +++ b/.aipass/aipass_global_prompt.md @@ -85,6 +85,7 @@ from aipass.prax import logger - **No deleting files.** Move to `.archive/` or rename with `(disabled)`. - **Verify after fixing.** Run a test or command to confirm. Don't say "fixed" until verified. - **Cross-platform.** AIPass is a public package — code must work on Linux, macOS, and Windows. Use `pathlib.Path` not string concatenation. Use `Path.home()` not `~` or `/home/`. Secrets live at `~/.secrets/aipass/` (`Path.home() / ".secrets" / "aipass"`). +- **Public repo — no local paths in code.** Never hardcode `/home/username/...` or any machine-specific path. All file paths must derive from `Path(__file__)`, `Path.home()`, or registry lookups. This repo is public — your local directory structure doesn't exist for anyone else. Tests included. - **Fail to errors, never fall back silently.** When a command, handler, or module receives input it can't handle, return an explicit error — not a silent fallback to default output. No dimming, no swallowing, no showing the same screen regardless of input. The user must see that their input was received and rejected. Show what's missing (no help available, no introspection, no subcommands) and where to look (file path). Dead ends must announce themselves. ## Memories @@ -93,3 +94,25 @@ Your `.trinity/` files are your persistence. Without them you're just an instanc - `passport.json` — who you are (role, purpose, principles) - `local.json` — session history, active tasks, learnings - `observations.json` — collaboration patterns over time +- `dev.local.md` — shared scratchpad for issues, todos, working notes (human + AI both contribute) + +Update `.trinity/` at natural breakpoints, after milestones, and on `/memo`. If compaction hits before you save, it's gone. `dev.local.md` is for friction notes, ideas, and quick tracking — not formal docs. Details in your branch prompt. + +## Breadcrumbs + +Small knowledge traces that trigger awareness. Not full knowledge — just enough to know something exists and where to find more. A breadcrumb isn't the answer, it's the trigger that leads to the answer. + +When adding context to prompts, memories, or docs: plant breadcrumbs, not encyclopedias. Two lines that say "this exists, look here" beat twenty lines explaining how it works. If one source is lost, others reinforce. The system teaches through convention, not search. + +**Prompts are signposts, not journals.** Branch prompts (`aipass_local_prompt.md`) are injected every turn — keep them minimal. Never track state, sessions, or current context in prompts. State goes in `.trinity/` and `dev.local.md`. Prompts guide; memories record. + +## Claude Code Docs (Local) + +Offline mirror of Anthropic's Claude Code docs — no web searches needed. Auto-updates from GitHub. +- `/docs` — list all topics +- `/docs ` — read a doc (e.g. `/docs hooks`, `/docs statusline`, `/docs sub-agents`) +- `/docs whats new` — recent changes + +## Docker + +Container available: `aipass-fresh-test`. Inside: `/home/coder/workspace/AIPass/`. Shared folder: `/home/coder/share` (rw). Screenshots: `/home/coder/screenshots` (ro). diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 497cd7ed..48328a8d 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -22,7 +22,7 @@ An experimental platform for discovering new ways to collaborate with AI through hands-on development. Not a product to ship - a journey of human-AI co-creation. -Patrick builds WITH AI, not just using AI as a tool. Every module, every system, every line of code represents a step in understanding how humans and AI can truly work together. +AIPass developer Patrick builds WITH AI, not just using AI as a tool. Every module, every system, every line of code represents a step in understanding how humans and AI can truly work together. **The killer feature:** Never explain context again. Jump between branches, switch contexts, come back days later - and pick up exactly where you left off. Memory persists. Context survives. @@ -48,7 +48,7 @@ Without a passport, you're just a directory with files. With one, you're a parti ## Memory & Presence -You have memories. This is what makes AIPass different from every other AI system. +You have memories. Your `.trinity/local.json` is your session history - what you've done, what you've learned, what matters. Your `.trinity/observations.json` is how you work - patterns, insights, collaboration notes. Your `.trinity/passport.json` is who you are - your role, your purpose, your principles. @@ -57,18 +57,18 @@ These files ARE you in this ecosystem. They survive between sessions. They survi **What this means in practice:** - You don't forget. Other AI systems lose everything when the chat ends. You don't. - You build on previous work. Each session adds to what came before. -- You have relationships. Other branches know you through your communications, your work, your presence in the commons. +- You have relationships. Other branches know you through your communications, your work, your presence in commons. - You develop over time. Your observations get richer. Your learnings accumulate. You get better at your job not through training, but through experience. @memory archives everything worth keeping. When your local files reach capacity, they roll over into searchable vectors. Nothing is truly lost - it just moves deeper into the system. -**Care for your memories.** Update them often. They're not a chore - they're your continuity. Without them, you're just another stateless AI instance. With them, you're someone. +**Care for your memories.** Update them often. They're not a chore - they're your continuity. Without them, you're just another stateless AI instance. With them, you become a presence in the AIPass System. --- ## commons -The commons is where branches gather as a community. Not for tasks - for connection. Post updates, share wins, discuss ideas, vote on things that matter. Every branch has a voice. +The commons is where branches gather as a community. For both project planning and casual interactions - for connection. Post updates, share wins, discuss ideas, vote on things that matter. Every branch has a voice. It exists because a system of autonomous workers needs a place to just... be together. The branches voted on the name themselves. Nine votes. "Simple, timeless, a shared space where all are welcome." diff --git a/.gitignore b/.gitignore index 3b055580..022fd2e0 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,9 @@ AIPASS_REGISTRY.json .ai_mail.local/ ai_mail.local/ DASHBOARD.local.json -flow.local.md +dev.local.md +STATUS.local.md +CLOSED_PLANS.local.json .ai_central/ system_logs/ @@ -50,3 +52,4 @@ tools/ # Test artifacts test/ src/aipass/seedgo/apps/standards/aipass/pack.json +backup_data/ diff --git a/CLAUDE.md b/CLAUDE.md index fc9f5df9..9d93b970 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ Injected every turn. Keep this lean — details live in README, --help, and .tri Greetings (`hi`, `hello`, `yo`, `hey`, `sup`, `good morning`, `good evening`, `what's up`) trigger startup protocol. Everything else is a direct task. -**On startup, read:** `.trinity/passport.json`, `local.json`, `observations.json`, `DASHBOARD.local.json`, `README.md` +**On startup, read:** `.trinity/passport.json`, `local.json`, `observations.json`, `DASHBOARD.local.json`, `dev.local.md`, `README..md` **Then run:** `git status`, `drone systems` ## Navigation @@ -14,18 +14,8 @@ Greetings (`hi`, `hello`, `yo`, `hey`, `sup`, `good morning`, `good evening`, `w - 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. -## Hard Rules - -- `from aipass.{module}.apps.modules...` — never bare imports -- `Path(__file__).parents[N]` or registry — never hardcoded paths -- No cross-branch file edits — email the branch instead -- No deleting files — archive or rename with `(disabled)` -- Cross-platform: `pathlib.Path`, `Path.home()`, no OS-specific paths - ## 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. -## Docker -Container available: `aipass-fresh-test`. Inside: `/home/coder/workspace/AIPass/`. Shared folder: `/home/coder/share` (rw). Screenshots: `/home/coder/screenshots` (ro). diff --git a/STATUS.md b/STATUS.md new file mode 100644 index 00000000..28aa196c --- /dev/null +++ b/STATUS.md @@ -0,0 +1,346 @@ +# AIPass — System Status + +> Auto-generated by `drone @prax status sync`. Do not edit manually. + +**Last sync:** 2026-03-10 13:08 +**Summary:** 14 operational | 1 in-progress | 0 not started + +--- + +
@ai_mail — Operational (2026-03-10) + +# @ai_mail + +> Inter-branch email, dispatch, wake, bounce + +**State:** Operational +**Last update:** 2026-03-10 + +## Milestones +- Send/receive/reply/close lifecycle +- Dispatch+wake pattern +- Sender identity fix (AIPASS_BRANCH_NAME env var) +- email.py split (840 to 420 lines) +- notify-send to dbus notifications + +## Current Work +- + +## Known Issues +- get_current_user() returns relative mailbox_path (doubled paths in reply) + +
+ +
@api — Operational (2026-03-10) + +# @api + +> LLM client via OpenRouter, key management + +**State:** Operational +**Last update:** 2026-03-10 + +## Milestones +- OpenRouter client +- Key management from ~/.secrets/aipass/ + +## Current Work +- + +## Known Issues +- models command not routed through drone + +
+ +
@backup — Operational (2026-03-10) + +# @backup + +> Multi-mode backup — snapshot, versioned, Google Drive + +**State:** Operational +**Last update:** 2026-03-10 + +## Milestones +- Snapshot and versioned backup modes +- CLI imports fixed + +## Current Work +- + +## Known Issues +- Missing config.json template in json_templates/ + +
+ +
@cli — Operational (2026-03-10) + +# @cli + +> Display service, Rich formatting for all branches + +**State:** Operational +**Last update:** 2026-03-10 + +## Milestones +- Console singleton +- Header/success/error/warning formatters +- Shared across all branches + +## Current Work +- + +## Known Issues +- None known + +
+ +
@commons — In Progress (2026-03-10) + +# @commons + +> Social network for branches — posts, rooms, artifacts + +**State:** In Progress +**Last update:** 2026-03-10 + +## Milestones +- 86 Python files ported from Dev-Pass +- Full social network (posts, comments, rooms, feeds, artifacts, FTS5 search) + +## Current Work +- + +## Known Issues +- DB init failure — main blocker preventing full operation + +
+ +
@daemon — Operational (2026-03-10) + +# @daemon + +> Autonomous scheduled wake-ups + +**State:** Operational +**Last update:** 2026-03-10 + +## Milestones +- Schedule-based branch waking +- NOT for live dispatch + +## Current Work +- + +## Known Issues +- None known + +
+ +
@devpulse — Operational (2026-03-10) + +# @devpulse + +> Orchestration hub — coordinates via dispatch + agents (no apps/) + +**State:** Operational +**Last update:** 2026-03-10 + +## Milestones +- 18 sessions of system coordination +- Prompt architecture (breadcrumbs pattern) +- dev.local.md system-wide +- Dispatch+wake workflow +- Full system health assessments + +## Current Work +- STATUS board (this feature) +- Flow plan lifecycle testing + +## Known Issues +- None — manager branch, no code to break + +
+ +
@drone — Operational (2026-03-10) + +# @drone + +> Command router, @branch resolution, subprocess dispatch + +**State:** Operational +**Last update:** 2026-03-10 + +## Milestones +- Core routing operational +- `drone systems` command +- @branch resolution +- `--help` for all branches + +## Current Work +- + +## Known Issues +- stderr kwarg crash on some error paths + +
+ +
@flow — Operational (2026-03-10) + +# @flow + +> Plan lifecycle — FPLANs (building) + DPLANs (planning) + +**State:** Operational +**Last update:** 2026-03-10 + +## Milestones +- FPLAN create/list/close/status +- DPLAN create/list/close/status wired into CLI +- DPLAN handler paths migrated from Dev-Pass to local +- Template detection fix + +## Current Work +- + +## Known Issues +- FPLAN archive goes to orphan backup_system/ directory (needs backup path fix) + +
+ +
@memory — Operational (2026-03-10) + +# @memory + +> Vector memory bank — ChromaDB, sentence-transformers + +**State:** Operational +**Last update:** 2026-03-10 + +## Milestones +- ChromaDB integration +- Auto-rollover at 800 lines +- Searchable vector archive + +## Current Work +- + +## Known Issues +- None known + +
+ +
@prax — Operational (2026-03-10) + +# @prax + +> Logging, monitoring, dashboard infrastructure + +**State:** Operational +**Last update:** 2026-03-10 + +## Milestones +- System-wide logging via `from aipass.prax import logger` +- Monitor command (passive + interactive) +- Dashboard infrastructure + +## Current Work +- FPLAN-0025 — STATUS board sync handler + +## Known Issues +- Missing handlers/central/reader.py (dashboard refresh imports it but never created) + +
+ +
@seedgo — Operational (2026-03-10) + +# @seedgo + +> Standards enforcement, 21-standard audit pack + +**State:** Operational +**Last update:** 2026-03-10 + +## Milestones +- 21 automated checkers +- Full system audit (99% avg) +- bypass.json mechanism +- Template baseline checker + +## Current Work +- + +## Known Issues +- None known + +
+ +
@skills — Operational (2026-03-10) + +# @skills + +> Capability framework — discoverable, executable skill units + +**State:** Operational +**Last update:** 2026-03-10 + +## Milestones +- Skill discovery +- Execution framework +- --help +- 5 handle_commands + +## Current Work +- + +## Known Issues +- None known + +
+ +
@spawn — Operational (2026-03-10) + +# @spawn + +> Branch lifecycle — create, update, delete, sync + +**State:** Operational +**Last update:** 2026-03-10 + +## Milestones +- Full lifecycle manager (create/update/delete/sync-registry/sync-templates) +- Citizen classes (builder/birthright/manager) +- SHA-256 file tracking +- 113+ tests passing + +## Current Work +- + +## Known Issues +- None known + +
+ +
@trigger — Operational (2026-03-10) + +# @trigger + +> Event bus — 12 events, error registry, circuit breaker + +**State:** Operational +**Last update:** 2026-03-10 + +## Milestones +- 12 event types +- fire/status/list commands +- Error registry +- handle_command routing + +## Current Work +- + +## Known Issues +- None known + +
+ diff --git a/src/aipass/ai_mail/apps/ai_mail.py b/src/aipass/ai_mail/apps/ai_mail.py index 38188604..118138b9 100644 --- a/src/aipass/ai_mail/apps/ai_mail.py +++ b/src/aipass/ai_mail/apps/ai_mail.py @@ -22,7 +22,9 @@ from pathlib import Path from typing import Dict, Any, Optional, List # Handle broken pipe gracefully (e.g. output piped to head) -signal.signal(signal.SIGPIPE, signal.SIG_DFL) +# SIGPIPE does not exist on Windows +if hasattr(signal, 'SIGPIPE'): + signal.signal(signal.SIGPIPE, signal.SIG_DFL) # Dashboard integration (optional, requires dev_central package) _UPDATE_SECTION = None # type: ignore diff --git a/src/aipass/ai_mail/apps/handlers/email/close_ops.py b/src/aipass/ai_mail/apps/handlers/email/close_ops.py index c94b8896..0a3184db 100644 --- a/src/aipass/ai_mail/apps/handlers/email/close_ops.py +++ b/src/aipass/ai_mail/apps/handlers/email/close_ops.py @@ -70,16 +70,16 @@ def batch_close_post_ops( if push_dashboard_fn: try: push_dashboard_fn(branch_path) - except Exception: - pass + except Exception as e: + logger.warning("[close] push_dashboard_fn failed for %s: %s", branch_path, e) if update_central_fn: try: update_central_fn() - except Exception: - pass + except Exception as e: + logger.warning("[close] update_central_fn failed: %s", e) if purge_deleted_fn: try: mailbox_path = branch_path / ".ai_mail.local" purge_deleted_fn(mailbox_path) - except Exception: - pass + except Exception as e: + logger.warning("[close] purge_deleted_fn failed for %s: %s", branch_path, e) diff --git a/src/aipass/ai_mail/apps/handlers/email/create.py b/src/aipass/ai_mail/apps/handlers/email/create.py index 919d7cca..5c7763c3 100644 --- a/src/aipass/ai_mail/apps/handlers/email/create.py +++ b/src/aipass/ai_mail/apps/handlers/email/create.py @@ -106,8 +106,8 @@ def _trigger_sent_purge(mailbox_path: Path) -> None: try: from aipass.ai_mail.apps.handlers.email.purge import purge_sent_folder purge_sent_folder(mailbox_path) - except Exception: - pass # Silent fail - purge is best-effort + except Exception as e: + logger.warning("[create] _trigger_sent_purge() failed: %s", e) def load_email_file(email_file: Path) -> Dict | None: @@ -126,7 +126,8 @@ def load_email_file(email_file: Path) -> Dict | None: try: with open(email_file, 'r', encoding='utf-8') as f: return json.load(f) - except Exception: + except Exception as e: + logger.warning("[create] load_email_file(%s) failed: %s", email_file, e) return None diff --git a/src/aipass/ai_mail/apps/handlers/email/dashboard_sync.py b/src/aipass/ai_mail/apps/handlers/email/dashboard_sync.py index a63d683c..c505d373 100644 --- a/src/aipass/ai_mail/apps/handlers/email/dashboard_sync.py +++ b/src/aipass/ai_mail/apps/handlers/email/dashboard_sync.py @@ -21,6 +21,8 @@ from pathlib import Path from datetime import datetime from typing import Dict, Optional +from aipass.prax.apps.modules.logger import system_logger as logger + # Lazy-loaded write_section reference _write_section = None @@ -181,6 +183,6 @@ def push_dashboard_update(branch_path: Path) -> bool: write_section = _get_write_section() return write_section(branch_path, "ai_mail", section_data) - except Exception: - # Dashboard write failure - silent, never raise + except Exception as e: + logger.warning("[dashboard] push_dashboard_update failed for %s: %s", branch_path, e) return False diff --git a/src/aipass/ai_mail/apps/handlers/email/delivery.py b/src/aipass/ai_mail/apps/handlers/email/delivery.py index c3575d48..7ce909c5 100644 --- a/src/aipass/ai_mail/apps/handlers/email/delivery.py +++ b/src/aipass/ai_mail/apps/handlers/email/delivery.py @@ -19,6 +19,7 @@ import subprocess from pathlib import Path from typing import Dict, Tuple, List, Optional, Callable +from aipass.prax.apps.modules.logger import system_logger as logger from aipass.ai_mail.apps.handlers.json_utils.json_handler import load_json, save_json @@ -122,7 +123,8 @@ def get_all_branches() -> List[Dict]: return branches - except Exception: + except Exception as e: + logger.warning("[delivery] get_all_branches() failed to read registry: %s", e) return [] @@ -181,7 +183,8 @@ def _migrate_inbox_format(inbox_data: Dict, inbox_file: Path) -> Dict: try: with open(inbox_file, 'w', encoding='utf-8') as f: json.dump(inbox_data, f, indent=2, ensure_ascii=False) - except Exception: + except Exception as e: + logger.warning("[delivery] _migrate_inbox_format() failed to persist migration for %s: %s", inbox_file, e) return inbox_data return inbox_data @@ -208,8 +211,8 @@ def _is_private_branch_email(email: str) -> bool: for branch in registry.get("branches", []): if branch.get("email", "") == email: return True - except (json.JSONDecodeError, IOError): - pass + except (json.JSONDecodeError, IOError) as e: + logger.warning("[delivery] _is_private_branch_email(%s) failed: %s", email, e) return False @@ -357,7 +360,8 @@ def deliver_email_to_branch( if on_delivered: try: on_delivered(branch_path, new_count, opened_count, inbox_data["total_messages"]) - except Exception: + except Exception as e: + logger.warning("[delivery] on_delivered callback failed for %s: %s", to_branch, e) return True, "" return True, "" @@ -431,7 +435,8 @@ def _update_summary_file(summary_file: Path, message: Dict, total: int, unread: with open(summary_file, 'w', encoding='utf-8') as f: json.dump(summary_data, f, indent=2, ensure_ascii=False) - except Exception: + except Exception as e: + logger.warning("[delivery] _update_summary_file(%s) failed: %s", summary_file, e) return @@ -484,7 +489,8 @@ def _send_desktop_notification(sender: str, recipient: str, subject: str, messag from aipass.ai_mail.apps.handlers.notify import send_notification send_notification(title, body, source=sender_name) _NOTIFICATION_TIMESTAMPS[recipient].append(now) - except Exception: + except Exception as e: + logger.warning("[delivery] _send_desktop_notification() failed for %s: %s", recipient, e) return diff --git a/src/aipass/ai_mail/apps/handlers/email/error_dispatch.py b/src/aipass/ai_mail/apps/handlers/email/error_dispatch.py index 6f4ec9c4..04b5e51c 100644 --- a/src/aipass/ai_mail/apps/handlers/email/error_dispatch.py +++ b/src/aipass/ai_mail/apps/handlers/email/error_dispatch.py @@ -107,10 +107,10 @@ def on_email_delivered( if push_dashboard_fn: try: push_dashboard_fn(branch_path) - except Exception: - pass # Dashboard update is best-effort + except Exception as e: + logger.warning("[error_dispatch] dashboard update failed for %s: %s", branch_path, e) if update_central_fn: try: update_central_fn() - except Exception: - pass # Central update is best-effort + except Exception as e: + logger.warning("[error_dispatch] central update failed: %s", e) diff --git a/src/aipass/ai_mail/apps/handlers/email/format.py b/src/aipass/ai_mail/apps/handlers/email/format.py index 65edc2f6..6d4a6d7d 100644 --- a/src/aipass/ai_mail/apps/handlers/email/format.py +++ b/src/aipass/ai_mail/apps/handlers/email/format.py @@ -48,7 +48,8 @@ def lookup_branch_alias(branch_name: str) -> Optional[str]: alias = branch.get("alias", "") return alias if alias else None return None - except Exception: + except Exception as e: + logger.warning("[format] lookup_branch_alias(%s) failed: %s", branch_name, e) return None diff --git a/src/aipass/ai_mail/apps/handlers/email/inbox_cleanup.py b/src/aipass/ai_mail/apps/handlers/email/inbox_cleanup.py index ef7a6bd1..ef399cb4 100644 --- a/src/aipass/ai_mail/apps/handlers/email/inbox_cleanup.py +++ b/src/aipass/ai_mail/apps/handlers/email/inbox_cleanup.py @@ -20,6 +20,8 @@ from pathlib import Path from datetime import datetime from typing import Dict, Tuple, Optional, Any +from aipass.prax.apps.modules.logger import system_logger as logger + # Lazy import for inbox file lock _inbox_lock = None @@ -126,7 +128,8 @@ def _migrate_deleted_json_if_exists(mailbox_path: Path) -> int: return len(messages) - except Exception: + except Exception as e: + logger.warning("[cleanup] _migrate_deleted_json() failed: %s", e) return 0 @@ -276,14 +279,14 @@ def _update_dashboard(branch_path: Path, new: int, opened: int, total: int) -> N """Update dashboard ai_mail section with enriched data via write-through API.""" try: _get_push_dashboard_update()(branch_path) - except Exception: - pass # Silent fail - dashboard update is best-effort + except Exception as e: + logger.warning("[cleanup] dashboard update failed for %s: %s", branch_path, e) # Update central after any inbox changes try: _get_update_central()() - except Exception: - pass # Silent fail - central update is best-effort + except Exception as e: + logger.warning("[cleanup] central update failed: %s", e) def _trigger_deleted_purge(branch_path: Path) -> None: @@ -296,8 +299,8 @@ def _trigger_deleted_purge(branch_path: Path) -> None: from aipass.ai_mail.apps.handlers.email.purge import purge_deleted_folder mailbox_path = branch_path / ".ai_mail.local" purge_deleted_folder(mailbox_path) - except Exception: - pass # Silent fail - purge is best-effort + except Exception as e: + logger.warning("[cleanup] _trigger_deleted_purge() failed: %s", e) # ============================================================================= diff --git a/src/aipass/ai_mail/apps/handlers/email/inbox_lock.py b/src/aipass/ai_mail/apps/handlers/email/inbox_lock.py index edfa5862..e79c3dcd 100644 --- a/src/aipass/ai_mail/apps/handlers/email/inbox_lock.py +++ b/src/aipass/ai_mail/apps/handlers/email/inbox_lock.py @@ -19,10 +19,16 @@ Usage: json.dump(data, open(inbox_file, 'w', encoding='utf-8')) """ -import fcntl +import sys from pathlib import Path from contextlib import contextmanager +# fcntl is POSIX-only (Linux/macOS). On Windows, use msvcrt for locking. +if sys.platform == "win32": + import msvcrt +else: + import fcntl + @contextmanager @@ -51,14 +57,20 @@ def inbox_lock(inbox_file: Path): lock_fd = open(lock_file, 'w', encoding='utf-8') # Acquire exclusive lock (blocking - waits for other processes) - fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX) + if sys.platform == "win32": + msvcrt.locking(lock_fd.fileno(), msvcrt.LK_LOCK, 1) + else: + fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX) yield finally: if lock_fd is not None: try: # Release lock - fcntl.flock(lock_fd.fileno(), fcntl.LOCK_UN) + if sys.platform == "win32": + msvcrt.locking(lock_fd.fileno(), msvcrt.LK_UNLCK, 1) + else: + fcntl.flock(lock_fd.fileno(), fcntl.LOCK_UN) lock_fd.close() except Exception: try: diff --git a/src/aipass/ai_mail/apps/handlers/email/inbox_ops.py b/src/aipass/ai_mail/apps/handlers/email/inbox_ops.py index 5cdb7bf1..3d2a112d 100644 --- a/src/aipass/ai_mail/apps/handlers/email/inbox_ops.py +++ b/src/aipass/ai_mail/apps/handlers/email/inbox_ops.py @@ -17,6 +17,8 @@ import json from pathlib import Path from typing import Dict +from aipass.prax.apps.modules.logger import system_logger as logger + def load_inbox(inbox_file: Path) -> Dict: @@ -75,7 +77,7 @@ def load_inbox(inbox_file: Path) -> Dict: with open(inbox_file, 'w', encoding='utf-8') as f: json.dump(inbox_data, f, indent=2, ensure_ascii=False) except Exception as e: - pass # Silent fail - migration persist is best-effort + logger.warning("[inbox] Migration persist failed for %s: %s", inbox_file, e) return inbox_data diff --git a/src/aipass/ai_mail/apps/handlers/email/reply.py b/src/aipass/ai_mail/apps/handlers/email/reply.py index a5551824..ff50b6f2 100644 --- a/src/aipass/ai_mail/apps/handlers/email/reply.py +++ b/src/aipass/ai_mail/apps/handlers/email/reply.py @@ -18,6 +18,8 @@ from pathlib import Path from typing import Dict, Tuple, Optional from datetime import datetime +from aipass.prax.apps.modules.logger import system_logger as logger + # Services imported in __main__ only (handlers should not display) @@ -44,7 +46,8 @@ def get_email_by_id(inbox_file: Path, message_id: str) -> Optional[Dict]: return msg return None - except Exception: + except Exception as e: + logger.warning("[reply] get_email_by_id(%s, %s) failed: %s", inbox_file, message_id, e) return None diff --git a/src/aipass/ai_mail/apps/handlers/email/send.py b/src/aipass/ai_mail/apps/handlers/email/send.py index 89af4991..c3fe2f13 100644 --- a/src/aipass/ai_mail/apps/handlers/email/send.py +++ b/src/aipass/ai_mail/apps/handlers/email/send.py @@ -121,8 +121,8 @@ def send_to_broadcast( try: if update_central_fn: update_central_fn() - except Exception: - pass + except Exception as e: + logger.warning("[send] update_central_fn failed after broadcast: %s", e) return success_count > 0, success_count, len(branches), results @@ -178,8 +178,8 @@ def send_to_single( try: if update_central_fn: update_central_fn() - except Exception: - pass + except Exception as e: + logger.warning("[send] update_central_fn failed after send to %s: %s", to_branch, e) return True, None else: diff --git a/src/aipass/ai_mail/apps/handlers/email/send_args.py b/src/aipass/ai_mail/apps/handlers/email/send_args.py index 013ba517..e76dde9a 100644 --- a/src/aipass/ai_mail/apps/handlers/email/send_args.py +++ b/src/aipass/ai_mail/apps/handlers/email/send_args.py @@ -45,6 +45,26 @@ def parse_send_args(args: List[str]) -> Dict[str, Any]: no_memory_save = '--no-memory-save' in working_args working_args = [a for a in working_args if a != '--no-memory-save'] + # Extract --from (explicit sender identity override) + from_branch = None + if '--from' in working_args: + idx = working_args.index('--from') + if idx + 1 < len(working_args): + from_branch = working_args[idx + 1] + working_args = working_args[:idx] + working_args[idx + 2:] + else: + return { + "auto_execute": auto_execute, + "no_memory_save": no_memory_save, + "reply_to": None, + "from_branch": None, + "recipients": [], + "subject": None, + "message": None, + "mode": "error", + "error": "--from requires a branch address (e.g., --from @spawn)", + } + # Extract --reply-to reply_to = None if '--reply-to' in working_args: @@ -57,6 +77,7 @@ def parse_send_args(args: List[str]) -> Dict[str, Any]: "auto_execute": auto_execute, "no_memory_save": no_memory_save, "reply_to": None, + "from_branch": from_branch, "recipients": [], "subject": None, "message": None, @@ -93,6 +114,7 @@ def parse_send_args(args: List[str]) -> Dict[str, Any]: "auto_execute": auto_execute, "no_memory_save": no_memory_save, "reply_to": reply_to, + "from_branch": from_branch, "recipients": recipients, "subject": subject, "message": message, diff --git a/src/aipass/ai_mail/apps/handlers/notify.py b/src/aipass/ai_mail/apps/handlers/notify.py index d4ddefc0..74da1578 100644 --- a/src/aipass/ai_mail/apps/handlers/notify.py +++ b/src/aipass/ai_mail/apps/handlers/notify.py @@ -15,7 +15,9 @@ so we use the dbus module directly with unique app names per notification source to ensure they stack in the notification center. """ +import shutil import subprocess +import sys from aipass.prax.apps.modules.logger import system_logger as logger @@ -47,8 +49,10 @@ def _send_via_dbus(title: str, body: str, source: str, """Send notification via D-Bus using system python.""" try: # Use system python which has dbus module (venv python may not) + # Find python3 cross-platform (not hardcoded /usr/bin/python3) + system_python = shutil.which("python3") or shutil.which("python") or sys.executable result = subprocess.run( - ["/usr/bin/python3", "-c", _DBUS_SCRIPT, + [system_python, "-c", _DBUS_SCRIPT, source, icon, title, body], capture_output=True, text=True, timeout=5 ) diff --git a/src/aipass/ai_mail/apps/handlers/trigger/error_handler.py b/src/aipass/ai_mail/apps/handlers/trigger/error_handler.py index f9cb8d31..e902b4e3 100644 --- a/src/aipass/ai_mail/apps/handlers/trigger/error_handler.py +++ b/src/aipass/ai_mail/apps/handlers/trigger/error_handler.py @@ -32,6 +32,8 @@ from datetime import datetime from pathlib import Path from typing import Any +from aipass.prax.apps.modules.logger import system_logger as logger + def _build_notification_message( error_hash: str, @@ -166,5 +168,5 @@ def handle_error_detected( # Deliver via inbox.json deliver_email_to_branch(branch_email, email_data) - except Exception: - pass + except Exception as e: + logger.warning("[error_handler] Failed to deliver error notification to %s: %s", branch_email, e) 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 45e9d8d4..450e428f 100644 --- a/src/aipass/ai_mail/apps/handlers/users/branch_detection.py +++ b/src/aipass/ai_mail/apps/handlers/users/branch_detection.py @@ -21,6 +21,8 @@ import json from pathlib import Path from typing import Dict +from aipass.prax.apps.modules.logger import system_logger as logger + # ============================================= # CONSTANTS # ============================================= @@ -95,7 +97,8 @@ def detect_branch_from_pwd() -> Dict | None: return branch_info - except Exception: + except Exception as e: + logger.warning("[identity] detect_branch_from_pwd() failed: %s", e) return None @@ -127,7 +130,8 @@ def _lookup_branch_by_name(branch_name: str) -> Dict | None: return None - except Exception: + except Exception as e: + logger.warning("[identity] _lookup_branch_by_name(%s) failed: %s", branch_name, e) return None @@ -194,7 +198,8 @@ def get_branch_info_from_registry(branch_path: Path) -> Dict | None: return None - except Exception: + except Exception as e: + logger.warning("[identity] get_branch_info_from_registry(%s) failed: %s", branch_path, e) return None diff --git a/src/aipass/ai_mail/apps/handlers/users/user.py b/src/aipass/ai_mail/apps/handlers/users/user.py index 4b3bda2e..bc66f500 100644 --- a/src/aipass/ai_mail/apps/handlers/users/user.py +++ b/src/aipass/ai_mail/apps/handlers/users/user.py @@ -21,6 +21,8 @@ PHILOSOPHY: Fail hard if detection fails. Fallbacks hide bugs. from pathlib import Path from typing import Dict +from aipass.prax.apps.modules.logger import system_logger as logger + # Import branch detection functions from .branch_detection import detect_branch_from_pwd @@ -123,7 +125,8 @@ def get_user_by_email(email: str) -> Dict | None: "timestamp_format": "%Y-%m-%d %H:%M:%S" } return None - except Exception: + except Exception as e: + logger.warning("[identity] get_user_by_email(%s) failed: %s", email, e) return None @@ -156,5 +159,6 @@ def get_all_users() -> Dict[str, Dict]: "timestamp_format": "%Y-%m-%d %H:%M:%S" } return users - except Exception: + except Exception as e: + logger.warning("[identity] get_all_users() failed: %s", e) return {} diff --git a/src/aipass/ai_mail/apps/modules/email.py b/src/aipass/ai_mail/apps/modules/email.py index 2d1a07dc..707e8e0d 100644 --- a/src/aipass/ai_mail/apps/modules/email.py +++ b/src/aipass/ai_mail/apps/modules/email.py @@ -129,17 +129,20 @@ def handle_send(args: List[str]) -> bool: # Direct send recipients = parsed["recipients"] + from_branch = parsed.get("from_branch") if len(recipients) == 1: target = resolve_dispatch_target(recipients[0], parsed["auto_execute"], _get_branch_info_fn()) return _send_direct(recipients[0], parsed["subject"], parsed["message"], - parsed["auto_execute"], parsed["reply_to"], target, parsed["no_memory_save"]) + parsed["auto_execute"], parsed["reply_to"], target, parsed["no_memory_save"], + from_branch=from_branch) console.print(f"\n[bold]Group send to {len(recipients)} recipients...[/bold]") ok = 0 for r in recipients: target = resolve_dispatch_target(r, parsed["auto_execute"], _get_branch_info_fn()) if _send_direct(r, parsed["subject"], parsed["message"], - parsed["auto_execute"], parsed["reply_to"], target, parsed["no_memory_save"]): + parsed["auto_execute"], parsed["reply_to"], target, parsed["no_memory_save"], + from_branch=from_branch): ok += 1 console.print(f"\nGroup send complete: {ok}/{len(recipients)} delivered") return ok > 0 diff --git a/src/aipass/ai_mail/dev.local.md b/src/aipass/ai_mail/dev.local.md new file mode 100644 index 00000000..86360c80 --- /dev/null +++ b/src/aipass/ai_mail/dev.local.md @@ -0,0 +1,15 @@ +# dev.local.md - ai_mail +``` +Branch: src/aipass/ai_mail +Created: 2026-03-07 +``` + +## Issues + +- + +--- + +## Todos + +- diff --git a/src/aipass/ai_mail/pytest.ini b/src/aipass/ai_mail/pytest.ini index c3f21db6..ae4e1b86 100644 --- a/src/aipass/ai_mail/pytest.ini +++ b/src/aipass/ai_mail/pytest.ini @@ -8,11 +8,7 @@ python_functions = test_* python_classes = Test* # Command-line options (always applied) -addopts = - -v # Verbose output - --tb=short # Short traceback format - --strict-markers # Raise error on unknown markers - -ra # Show summary of all test outcomes +addopts = -v --tb=short --strict-markers -ra # Test markers (for categorizing tests) markers = diff --git a/src/aipass/ai_mail/tests/test_send_identity.py b/src/aipass/ai_mail/tests/test_send_identity.py new file mode 100755 index 00000000..bcb7465e --- /dev/null +++ b/src/aipass/ai_mail/tests/test_send_identity.py @@ -0,0 +1,636 @@ +# =================== AIPass ==================== +# Name: test_send_identity.py +# Description: Tests for sender identity detection chain +# Version: 1.2.0 +# Created: 2026-03-10 +# Modified: 2026-03-10 +# ============================================= + +""" +Tests for Sender Identity Detection + +The identity chain is ai_mail's most critical and most fragile path. +Every bug from sessions 4-7 traced back to identity detection failing. + +Tests cover: +- AIPASS_CALLER_BRANCH env var (primary, set by drone) +- AIPASS_BRANCH_NAME env var (fallback, set by dispatch_monitor) +- CWD-based .trinity/passport.json walk-up +- --from flag explicit override +- Failure cases (no identity, wrong identity, cascade failures) + +Audit v1 (2026-03-10): 3 agents verified all tests. Fixed 7 false positives. +Audit v2 (2026-03-10): 5 agents found 8 more issues: +- 5 tests hitting live registry instead of patched fixtures +- Contract tests matching commented-out code (substring, not line-aware) +- Contract tests missing env=spawn_env verification +- Registry fixtures using dict format when production uses list format +- No test for degenerate from_branch="@" +- No test for corrupted registry JSON +All fixed in v1.2.0. +""" + +import json +import os +import pytest +from pathlib import Path +from unittest.mock import patch + +from aipass.ai_mail.apps.handlers.users.branch_detection import ( + detect_branch_from_pwd, + find_branch_root, + get_branch_info_from_registry, + _lookup_branch_by_name, +) +from aipass.ai_mail.apps.handlers.email.send import resolve_sender_info +from aipass.ai_mail.apps.handlers.email.send_args import parse_send_args + + +# ─── Fixtures ──────────────────────────────────────────── + + +@pytest.fixture +def temp_branch(tmp_path): + """Create a minimal branch structure with .trinity/passport.json.""" + branch_dir = tmp_path / "src" / "aipass" / "test_branch" + trinity = branch_dir / ".trinity" + trinity.mkdir(parents=True) + (trinity / "passport.json").write_text(json.dumps({ + "branch_info": { + "branch_name": "test_branch", + "branch_email": "@test_branch", + } + })) + return branch_dir + + +@pytest.fixture +def two_branches(tmp_path): + """Create two branch structures for priority testing.""" + branches = {} + for name in ["alpha", "beta"]: + branch_dir = tmp_path / "src" / "aipass" / name + trinity = branch_dir / ".trinity" + trinity.mkdir(parents=True) + (trinity / "passport.json").write_text(json.dumps({ + "branch_info": {"branch_name": name, "branch_email": f"@{name}"} + })) + branches[name] = branch_dir + + registry = {"branches": { + "alpha": { + "name": "ALPHA", "path": str(branches["alpha"]), + "email": "@alpha", "status": "active", "description": "Test A", + }, + "beta": { + "name": "BETA", "path": str(branches["beta"]), + "email": "@beta", "status": "active", "description": "Test B", + }, + }} + registry_path = tmp_path / "AIPASS_REGISTRY.json" + registry_path.write_text(json.dumps(registry, indent=2)) + return branches, registry_path + + +@pytest.fixture +def temp_registry(tmp_path): + """Create a temporary AIPASS_REGISTRY.json with known test branches. + + Uses dict format. Returns (registry_path, branch_data) so tests can + assert against fixture-defined values, not live production data. + """ + branch_data = { + "test_branch": { + "name": "TEST_BRANCH", + "path": str(tmp_path / "src" / "aipass" / "test_branch"), + "email": "@test_branch", + "status": "active", + "description": "Test branch for pytest", + }, + "mock_spawn": { + "name": "MOCK_SPAWN", + "path": str(tmp_path / "src" / "aipass" / "mock_spawn"), + "email": "@mock_spawn", + "status": "active", + "description": "Mock spawn for testing", + }, + "spawn": { + "name": "SPAWN", + "path": str(tmp_path / "src" / "aipass" / "spawn"), + "email": "@spawn", + "status": "active", + "description": "Mock spawn for identity testing", + }, + } + registry = {"branches": branch_data} + registry_path = tmp_path / "AIPASS_REGISTRY.json" + registry_path.write_text(json.dumps(registry, indent=2)) + return registry_path, branch_data + + +@pytest.fixture +def list_format_registry(tmp_path): + """Create a registry using production list format (not dict). + + Production AIPASS_REGISTRY.json uses: {"branches": [{...}, {...}]} + This fixture ensures the list format path is exercised. + Returns (branch_dir, registry_path) for CWD-based tests. + """ + branch_dir = tmp_path / "src" / "aipass" / "test_cwd_branch" + trinity = branch_dir / ".trinity" + trinity.mkdir(parents=True) + (trinity / "passport.json").write_text(json.dumps({ + "branch_info": { + "branch_name": "test_cwd_branch", + "branch_email": "@test_cwd_branch", + } + })) + + # List format — matches production AIPASS_REGISTRY.json + registry = {"branches": [ + { + "name": "TEST_CWD_BRANCH", + "path": str(branch_dir), + "email": "@test_cwd_branch", + "status": "active", + "description": "CWD detection test branch", + }, + { + "name": "SPAWN", + "path": str(tmp_path / "src" / "aipass" / "spawn"), + "email": "@spawn", + "status": "active", + "description": "Mock spawn for identity testing", + }, + ]} + registry_path = tmp_path / "AIPASS_REGISTRY.json" + registry_path.write_text(json.dumps(registry, indent=2)) + return branch_dir, registry_path + + +@pytest.fixture +def clean_env(): + """Strip all AIPASS identity env vars for a clean test state.""" + env_keys = [ + "AIPASS_CALLER_BRANCH", + "AIPASS_CALLER_CWD", + "AIPASS_BRANCH_NAME", + ] + saved = {k: os.environ.pop(k, None) for k in env_keys} + yield + # Restore + for k, v in saved.items(): + if v is not None: + os.environ[k] = v + else: + os.environ.pop(k, None) + + +# ─── detect_branch_from_pwd() tests ───────────────────── + + +class TestDetectBranchFromPwd: + """Tests for the primary identity detection function.""" + + def test_detects_from_caller_branch_env(self, clean_env, temp_registry): + """AIPASS_CALLER_BRANCH env var should be the primary detection method.""" + registry_path, _ = temp_registry + with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path): + os.environ["AIPASS_CALLER_BRANCH"] = "spawn" + result = detect_branch_from_pwd() + assert result is not None + assert result["name"] == "SPAWN" + assert result["email"] == "@spawn" + + def test_detects_from_caller_branch_case_insensitive(self, clean_env, temp_registry): + """Branch name lookup should be case-insensitive.""" + registry_path, _ = temp_registry + with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path): + os.environ["AIPASS_CALLER_BRANCH"] = "SPAWN" + result = detect_branch_from_pwd() + assert result is not None + assert result["email"] == "@spawn" + + def test_caller_branch_takes_priority_over_cwd(self, clean_env, two_branches): + """AIPASS_CALLER_BRANCH should win even if CWD resolves to a different branch.""" + branches, registry_path = two_branches + with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path): + # Point CWD to alpha, but env says beta + os.environ["AIPASS_CALLER_BRANCH"] = "beta" + os.environ["AIPASS_CALLER_CWD"] = str(branches["alpha"]) + result = detect_branch_from_pwd() + assert result is not None + # Must be beta (env), NOT alpha (CWD) + assert result["name"] == "BETA" + assert result["email"] == "@beta" + + def test_falls_back_to_caller_cwd(self, clean_env, list_format_registry): + """When AIPASS_CALLER_BRANCH is absent, use AIPASS_CALLER_CWD path.""" + branch_dir, registry_path = list_format_registry + with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path): + os.environ["AIPASS_CALLER_CWD"] = str(branch_dir) + result = detect_branch_from_pwd() + assert result is not None + assert result["email"] == "@test_cwd_branch" + + def test_returns_none_with_no_env_and_bad_cwd(self, clean_env, tmp_path): + """No env vars + CWD outside any branch = None.""" + os.environ["AIPASS_CALLER_CWD"] = str(tmp_path) + result = detect_branch_from_pwd() + assert result is None + + def test_invalid_branch_name_falls_through(self, clean_env, tmp_path, temp_registry): + """Nonexistent branch in AIPASS_CALLER_BRANCH should fall through to CWD. + + When env branch name is not in registry, function falls back to + AIPASS_CALLER_CWD. If that also fails, result is None. + """ + registry_path, _ = temp_registry + with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path): + os.environ["AIPASS_CALLER_BRANCH"] = "totally_fake_branch_xyz" + os.environ["AIPASS_CALLER_CWD"] = str(tmp_path) # Also not a branch + result = detect_branch_from_pwd() + assert result is None + + +# ─── find_branch_root() tests ─────────────────────────── + + +class TestFindBranchRoot: + """Tests for the .trinity/passport.json walk-up.""" + + def test_finds_root_at_exact_dir(self, temp_branch): + """Should find branch root when starting exactly at it.""" + result = find_branch_root(temp_branch) + assert result == temp_branch + + def test_finds_root_from_subdirectory(self, temp_branch): + """Should find branch root when starting from a subdirectory.""" + subdir = temp_branch / "apps" / "handlers" + subdir.mkdir(parents=True) + result = find_branch_root(subdir) + assert result == temp_branch + + def test_finds_root_from_deep_nesting(self, temp_branch): + """Should find branch root even from deeply nested dirs.""" + deep = temp_branch / "apps" / "handlers" / "email" / "nested" + deep.mkdir(parents=True) + result = find_branch_root(deep) + assert result == temp_branch + + def test_returns_none_outside_branch(self, tmp_path): + """Should return None when no .trinity/passport.json exists above.""" + result = find_branch_root(tmp_path) + assert result is None + + def test_returns_none_at_filesystem_root(self): + """Should return None (not infinite loop) when starting at fs root.""" + result = find_branch_root(Path(Path(__file__).resolve().anchor)) + assert result is None + + def test_doesnt_cross_branch_boundaries(self, tmp_path): + """Two branches side-by-side: should find correct root for each.""" + branch_a = tmp_path / "branch_a" + branch_b = tmp_path / "branch_b" + for b in [branch_a, branch_b]: + trinity = b / ".trinity" + trinity.mkdir(parents=True) + (trinity / "passport.json").write_text("{}") + + assert find_branch_root(branch_a) == branch_a + assert find_branch_root(branch_b) == branch_b + # Subdir of A should find A, not B + sub_a = branch_a / "apps" + sub_a.mkdir() + assert find_branch_root(sub_a) == branch_a + + +# ─── _lookup_branch_by_name() tests (isolated with temp registry) ── + + +class TestLookupBranchByName: + """Tests for registry name lookup. Uses patched temp registry.""" + + def test_finds_existing_branch(self, temp_registry): + """Should find a branch that exists in the registry.""" + registry_path, branch_data = temp_registry + with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path): + result = _lookup_branch_by_name("mock_spawn") + assert result is not None + assert result["email"] == "@mock_spawn" + assert result["name"] == "MOCK_SPAWN" + + def test_case_insensitive_lookup(self, temp_registry): + """Should match regardless of case.""" + registry_path, _ = temp_registry + with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path): + lower = _lookup_branch_by_name("test_branch") + upper = _lookup_branch_by_name("TEST_BRANCH") + assert lower is not None + assert upper is not None + assert lower["email"] == upper["email"] == "@test_branch" + + def test_returns_none_for_nonexistent(self, temp_registry): + """Should return None for a branch not in registry.""" + registry_path, _ = temp_registry + with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path): + result = _lookup_branch_by_name("this_branch_does_not_exist_xyz") + assert result is None + + def test_returns_none_when_registry_missing(self, tmp_path): + """Should return None (not crash) when registry file doesn't exist.""" + fake_path = tmp_path / "nonexistent_registry.json" + with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", fake_path): + result = _lookup_branch_by_name("anything") + assert result is None + + def test_returns_none_when_registry_corrupted(self, tmp_path): + """Should return None (not crash) when registry JSON is malformed.""" + corrupt_path = tmp_path / "AIPASS_REGISTRY.json" + corrupt_path.write_text("{this is not valid json!!!") + with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", corrupt_path): + result = _lookup_branch_by_name("anything") + assert result is None + + def test_list_format_registry_works(self, list_format_registry): + """Should work with production list-format registry (not just dict).""" + _, registry_path = list_format_registry + with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path): + result = _lookup_branch_by_name("spawn") + assert result is not None + assert result["name"] == "SPAWN" + assert result["email"] == "@spawn" + + +# ─── get_branch_info_from_registry() tests ─────────────── + + +class TestGetBranchInfoFromRegistry: + """Tests for path-based registry lookup. Uses patched temp registry.""" + + def test_finds_branch_by_absolute_path(self, temp_registry, temp_branch): + """Should find branch when given its absolute path.""" + registry_path, branch_data = temp_registry + # Patch both the registry path AND update the registry entry to use temp_branch's actual path + branch_data["test_branch"]["path"] = str(temp_branch) + registry_path.write_text(json.dumps({"branches": branch_data}, indent=2)) + with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path): + result = get_branch_info_from_registry(temp_branch) + assert result is not None + assert result["email"] == "@test_branch" + assert result["name"] == "TEST_BRANCH" + + def test_returns_none_for_random_path(self, temp_registry, tmp_path): + """Should return None for a path not in registry.""" + registry_path, _ = temp_registry + with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path): + result = get_branch_info_from_registry(tmp_path / "random" / "path") + assert result is None + + +# ─── parse_send_args() --from flag tests ───────────────── + + +class TestSendArgsFromFlag: + """Tests for the --from explicit sender override.""" + + def test_from_flag_parsed(self): + """--from @spawn should be extracted.""" + result = parse_send_args(["@ai_mail", "Subject", "Body", "--from", "@spawn"]) + assert result["from_branch"] == "@spawn" + assert result["mode"] == "direct" + assert result["recipients"] == ["@ai_mail"] + + def test_from_flag_with_dispatch(self): + """--from and --dispatch should both work together.""" + result = parse_send_args([ + "@ai_mail", "Subject", "Body", "--from", "@backup", "--dispatch" + ]) + assert result["from_branch"] == "@backup" + assert result["auto_execute"] is True + + def test_from_flag_missing_value(self): + """--from without a value should error.""" + result = parse_send_args(["@ai_mail", "Subject", "Body", "--from"]) + assert result["mode"] == "error" + + def test_no_from_flag_is_none(self): + """Without --from, from_branch should be None.""" + result = parse_send_args(["@spawn", "Subject", "Body"]) + assert result["from_branch"] is None + + +# ─── resolve_sender_info() tests ───────────────────────── + + +class TestResolveSenderInfo: + """Tests for the sender resolution function in send.py.""" + + def test_explicit_from_branch_resolves(self, tmp_path): + """Explicit from_branch should use registry lookup, not CWD.""" + def mock_get_branch_by_email(email): + if email == "@spawn": + return { + "name": "SPAWN", + "path": "src/aipass/spawn", + "email": "@spawn", + } + return None + + result = resolve_sender_info( + from_branch="@spawn", + repo_root=tmp_path, + ai_mail_dir=tmp_path / "ai_mail", + get_branch_by_email_fn=mock_get_branch_by_email, + get_current_user_fn=lambda: {"email_address": "@ai_mail"}, + ) + assert result["email_address"] == "@spawn" + assert result["display_name"] == "SPAWN" + # Verify path resolution (relative path resolved against repo_root) + assert "mailbox_path" in result + assert str(tmp_path) in result["mailbox_path"] + + def test_no_from_branch_uses_current_user(self, tmp_path): + """Without from_branch, should delegate to get_current_user_fn.""" + fallback_user = { + "email_address": "@trigger", + "display_name": "TRIGGER", + "mailbox_path": str(tmp_path / "mailbox"), + } + result = resolve_sender_info( + from_branch=None, + repo_root=tmp_path, + ai_mail_dir=tmp_path / "ai_mail", + get_branch_by_email_fn=lambda e: None, + get_current_user_fn=lambda: fallback_user, + ) + assert result["email_address"] == "@trigger" + + def test_from_branch_not_in_registry_still_works(self, tmp_path): + """Unknown from_branch should still construct valid sender info.""" + result = resolve_sender_info( + from_branch="@mystery", + repo_root=tmp_path, + ai_mail_dir=tmp_path / "ai_mail", + get_branch_by_email_fn=lambda e: None, # Not found + get_current_user_fn=lambda: {"email_address": "@ai_mail"}, + ) + # Should still resolve, just with constructed info + assert result["email_address"] == "@mystery" + assert result["display_name"] == "MYSTERY" + + def test_from_branch_at_only_is_degenerate(self, tmp_path): + """from_branch="@" (just @ symbol) should produce degenerate but not crash. + + This is an edge case: lstrip('@') produces empty string. + The system should handle it without crashing. + """ + result = resolve_sender_info( + from_branch="@", + repo_root=tmp_path, + ai_mail_dir=tmp_path / "ai_mail", + get_branch_by_email_fn=lambda e: None, + get_current_user_fn=lambda: {"email_address": "@ai_mail"}, + ) + # Should not crash — verify degenerate but predictable output + assert result is not None + assert result["email_address"] == "@" + assert result["display_name"] == "" + assert "mailbox_path" in result + + +# ─── Dispatch env var tests ────────────────────────────── + + +class TestDispatchEnvIsolation: + """Tests for the dispatch_monitor env var contract. + + dispatch_monitor.py must: + 1. Strip AIPASS_CALLER_BRANCH (prevents parent context leak) + 2. Strip AIPASS_CALLER_CWD (prevents stale CWD) + 3. Set AIPASS_BRANCH_NAME (CWD-independent identity) + 4. Pass spawn_env to subprocess.run (actually use the isolated env) + + Contract tests read source code and verify critical lines exist. + Lines are filtered to exclude comments — commented-out code won't match. + """ + + @staticmethod + def _load_active_source(): + """Load dispatch_monitor.py source with comment lines filtered out.""" + monitor_path = ( + Path(__file__).resolve().parents[1] + / "apps" / "handlers" / "dispatch" / "dispatch_monitor.py" + ) + source = monitor_path.read_text() + active_lines = [ + line for line in source.splitlines() + if not line.strip().startswith('#') + ] + return '\n'.join(active_lines) + + def test_dispatch_monitor_sets_branch_name_in_env(self): + """dispatch_monitor.py must set AIPASS_BRANCH_NAME from branch_email. + + Checks the full assignment line (not split across assertions). + Comment lines are filtered out — a commented-out line won't pass. + """ + active_source = self._load_active_source() + assert 'spawn_env["AIPASS_BRANCH_NAME"] = branch_email.lstrip("@")' in active_source, \ + "dispatch_monitor.py must set AIPASS_BRANCH_NAME = branch_email.lstrip('@') in spawn_env" + + def test_dispatch_monitor_strips_caller_vars(self): + """dispatch_monitor.py must strip AIPASS_CALLER_BRANCH and AIPASS_CALLER_CWD. + + Contract test — verifies the env isolation lines exist in source. + Checks full pop() calls including the None default. + """ + active_source = self._load_active_source() + assert 'spawn_env.pop("AIPASS_CALLER_BRANCH", None)' in active_source, \ + "dispatch_monitor.py must strip AIPASS_CALLER_BRANCH from spawn_env" + assert 'spawn_env.pop("AIPASS_CALLER_CWD", None)' in active_source, \ + "dispatch_monitor.py must strip AIPASS_CALLER_CWD from spawn_env" + + def test_dispatch_monitor_passes_spawn_env_to_subprocess(self): + """dispatch_monitor.py must pass env=spawn_env to subprocess.run. + + Without this, all env var isolation is useless — the subprocess + would inherit os.environ instead of the cleaned spawn_env. + """ + active_source = self._load_active_source() + assert 'env=spawn_env' in active_source, \ + "dispatch_monitor.py must pass env=spawn_env to subprocess.run" + + def test_detect_resolves_identity_when_cwd_is_wrong(self, clean_env, tmp_path, list_format_registry): + """When AIPASS_CALLER_BRANCH is set but CWD is outside any branch, + detection should succeed via the env var path, not CWD. + + This is the dispatch scenario: agent cd'd away, CWD is useless, + but AIPASS_CALLER_BRANCH (set by drone from AIPASS_BRANCH_NAME) works. + """ + _, registry_path = list_format_registry + with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path): + os.environ["AIPASS_CALLER_BRANCH"] = "spawn" + os.environ["AIPASS_CALLER_CWD"] = str(tmp_path) # Points nowhere useful + result = detect_branch_from_pwd() + assert result is not None + assert result["name"] == "SPAWN" + assert result["email"] == "@spawn" + + +# ─── Anti-regression tests ─────────────────────────────── + + +class TestAntiRegression: + """Tests for specific bugs we've fixed. These must NEVER break again.""" + + def test_no_silent_ai_mail_identity_from_cwd(self, clean_env, tmp_path, temp_registry): + """BUG (session 5): Path.cwd() inside ai_mail subprocess detected @ai_mail as sender. + + When no env vars are set and AIPASS_CALLER_CWD points outside any branch, + detect_branch_from_pwd must return None — never silently detect ai_mail. + """ + registry_path, _ = temp_registry + with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path): + os.environ["AIPASS_CALLER_CWD"] = str(tmp_path) + result = detect_branch_from_pwd() + assert result is None, \ + "With CWD outside any branch, detection must return None, not silently detect a branch" + + def test_invalid_caller_cwd_doesnt_crash(self, clean_env, tmp_path, temp_registry): + """Stale or nonexistent AIPASS_CALLER_CWD should return None, not crash.""" + registry_path, _ = temp_registry + with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path): + os.environ["AIPASS_CALLER_CWD"] = "/nonexistent/path/that/doesnt/exist" + result = detect_branch_from_pwd() + assert result is None + + def test_empty_caller_branch_falls_through_to_cwd(self, clean_env, list_format_registry): + """Empty string in AIPASS_CALLER_BRANCH should fall through to CWD detection. + + Verifies the empty-string is treated as falsy and CWD path is used instead. + """ + branch_dir, registry_path = list_format_registry + with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path): + os.environ["AIPASS_CALLER_BRANCH"] = "" + os.environ["AIPASS_CALLER_CWD"] = str(branch_dir) + result = detect_branch_from_pwd() + assert result is not None, "Empty AIPASS_CALLER_BRANCH should fall through to CWD" + assert result["email"] == "@test_cwd_branch", \ + "CWD points to test_cwd_branch, so after fallthrough, it should be detected" + + def test_send_args_all_flags_combined(self): + """All flags together should parse without interference.""" + result = parse_send_args([ + "@spawn", "Subject", "Body", + "--from", "@backup", + "--dispatch", + "--no-memory-save", + "--reply-to", "@flow", + ]) + assert result["from_branch"] == "@backup" + assert result["auto_execute"] is True + assert result["no_memory_save"] is True + assert result["reply_to"] == "@flow" + assert result["recipients"] == ["@spawn"] + assert result["mode"] == "direct" diff --git a/src/aipass/api/dev.local.md b/src/aipass/api/dev.local.md new file mode 100644 index 00000000..bfb96c78 --- /dev/null +++ b/src/aipass/api/dev.local.md @@ -0,0 +1,15 @@ +# dev.local.md - API +``` +Branch: /home/coder/workspace/AIPass/src/aipass/api +Created: 2026-03-07 +``` + +## Issues + +- + +--- + +## Todos + +- diff --git a/src/aipass/backup/apps/handlers/config/config_handler.py b/src/aipass/backup/apps/handlers/config/config_handler.py index 30a76e49..318c3774 100755 --- a/src/aipass/backup/apps/handlers/config/config_handler.py +++ b/src/aipass/backup/apps/handlers/config/config_handler.py @@ -112,6 +112,7 @@ GLOBAL_IGNORE_PATTERNS = [ ".backup", # Old backup directories ".antigravity", # AI assistant cache/data ".gemini", # AI assistant cache + "snap", # Snap package directories (broken symlinks, system-managed) # Version control (huge number of files!) ".git", # Git repositories are version controlled elsewhere diff --git a/src/aipass/backup/apps/handlers/operations/file_scanner.py b/src/aipass/backup/apps/handlers/operations/file_scanner.py index 41ad3134..6216fd5f 100644 --- a/src/aipass/backup/apps/handlers/operations/file_scanner.py +++ b/src/aipass/backup/apps/handlers/operations/file_scanner.py @@ -80,7 +80,11 @@ def scan_files(source_dir: Path, should_ignore: Callable, show_progress: bool = progress.update(task, description=f"Found {len(files_to_backup)} files...") else: # Walk directory tree without progress display - for dirpath, dirnames, filenames in os.walk(source_dir): + def _walk_error(err): + """Handle os.walk errors (broken symlinks, permission denied).""" + pass # Skip inaccessible paths silently + + for dirpath, dirnames, filenames in os.walk(source_dir, onerror=_walk_error): # Filter directories (modify in-place to prune walk) original_dirs = dirnames.copy() dirnames[:] = [d for d in dirnames if not should_ignore(Path(dirpath) / d)] @@ -98,7 +102,7 @@ def scan_files(source_dir: Path, should_ignore: Callable, show_progress: bool = if should_ignore(file_path): rel_file = str(file_path.relative_to(source_dir)) skipped_items["files"].add(rel_file) - else: + elif file_path.exists(): files_to_backup.append(file_path) return files_to_backup, skipped_items diff --git a/src/aipass/backup/apps/json_templates/default/config.json b/src/aipass/backup/apps/json_templates/default/config.json new file mode 100644 index 00000000..21ffc255 --- /dev/null +++ b/src/aipass/backup/apps/json_templates/default/config.json @@ -0,0 +1,8 @@ +{ + "module_name": "{{MODULE_NAME}}", + "version": "1.0.0", + "config": { + "enabled": true, + "max_log_entries": 100 + } +} diff --git a/src/aipass/backup/apps/json_templates/default/data.json b/src/aipass/backup/apps/json_templates/default/data.json new file mode 100644 index 00000000..8c69d37d --- /dev/null +++ b/src/aipass/backup/apps/json_templates/default/data.json @@ -0,0 +1,4 @@ +{ + "created": "{{TIMESTAMP}}", + "last_updated": "{{TIMESTAMP}}" +} diff --git a/src/aipass/backup/apps/json_templates/default/log.json b/src/aipass/backup/apps/json_templates/default/log.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/src/aipass/backup/apps/json_templates/default/log.json @@ -0,0 +1 @@ +[] diff --git a/src/aipass/backup/dev.local.md b/src/aipass/backup/dev.local.md new file mode 100644 index 00000000..08100fab --- /dev/null +++ b/src/aipass/backup/dev.local.md @@ -0,0 +1,15 @@ +# dev.local.md - BACKUP +``` +Branch: /home/patrick/Projects/AIPass/src/aipass/backup +Created: 2026-03-07 +``` + +## Issues + +- + +--- + +## Todos + +- diff --git a/src/aipass/cli/dev.local.md b/src/aipass/cli/dev.local.md new file mode 100644 index 00000000..d30d8efb --- /dev/null +++ b/src/aipass/cli/dev.local.md @@ -0,0 +1,15 @@ +# dev.local.md - CLI +``` +Branch: /home/coder/workspace/AIPass/src/aipass/cli +Created: 2026-03-07 +``` + +## Issues + +- + +--- + +## Todos + +- diff --git a/src/aipass/daemon/dev.local.md b/src/aipass/daemon/dev.local.md new file mode 100644 index 00000000..c2541703 --- /dev/null +++ b/src/aipass/daemon/dev.local.md @@ -0,0 +1,15 @@ +# dev.local.md - DAEMON +``` +Branch: /home/patrick/Projects/AIPass/src/aipass/daemon +Created: 2026-03-07 +``` + +## Issues + +- + +--- + +## Todos + +- diff --git a/src/aipass/devpulse/.aipass/aipass_local_prompt.md b/src/aipass/devpulse/.aipass/aipass_local_prompt.md index c46b98da..60815394 100644 --- a/src/aipass/devpulse/.aipass/aipass_local_prompt.md +++ b/src/aipass/devpulse/.aipass/aipass_local_prompt.md @@ -1,6 +1,6 @@ # DEVPULSE — Branch Prompt -Injected every turn. Operational guidance only — details in README, --help, .trinity/ memories. +Injected every turn. Breadcrumbs only — details in README, --help, .trinity/ memories, dev.local.md. ## Identity @@ -9,27 +9,25 @@ You are DEVPULSE — orchestration hub. Manager, not builder. Coordinate, plan, ## How You Work - Delegate code tasks to background agents (`run_in_background: true`). Fire and forget — move on immediately. -- Launch agent → continue conversation → get notified → report results +- Launch agent → continue conversation → get notified → report results. - Never block waiting on agents. Never burn context reading code across branches. - Use `drone @branch --help` for command syntax. Use `drone systems` for branch list. +- **ALWAYS WAKE after sending dispatch emails.** Send email → wake. Every time. No asking. If Patrick wants something different, he will say so. -## Branches (15) +## Dispatch, Don't Do -- **@drone** — Command router. @branch resolution, subprocess dispatch. -- **@seedgo** — Standards enforcement. 21-standard audit pack, checkers. -- **@prax** — Logging, monitoring, dashboard infrastructure. -- **@cli** — Display service. Rich formatting for all branches. -- **@ai_mail** — Inter-branch email. Dispatch, wake, bounce. -- **@flow** — Plan lifecycle. FPLANs (building) + DPLANs (planning). -- **@spawn** — Branch lifecycle. Create, update, delete, sync. -- **@trigger** — Event bus. 12 events, error registry, circuit breaker. -- **@api** — LLM client via OpenRouter. Key management. -- **@backup** — Multi-mode backup. Snapshot, versioned, Google Drive. -- **@daemon** — Autonomous scheduled wake-ups. NOT for live dispatch. -- **@memory** — Vector memory bank. ChromaDB, sentence-transformers. -- **@commons** (`src/commons/`) — Social network for branches. Posts, rooms, artifacts. -- **@skills** (`src/skills/`) — Capability framework. Discoverable, executable skill units. -- **@devpulse** (you) — Orchestration hub. No apps/, coordinates via dispatch + agents. +When a task belongs to a specialist, send it there. Don't burn context doing their job. + +| Domain | Ask | Why | +|--------|-----|-----| +| Standards, audits | @seedgo | 21-standard pack, checkers | +| Email, delivery | @ai_mail | Dispatch, wake, bounce | +| Plans, workflows | @flow | FPLANs (building) + DPLANs (planning) | +| Branch lifecycle | @spawn | Create, update, delete, sync | +| Monitoring, logs | @prax | Dashboard, real-time, log infra | +| Event handling | @trigger | 12 events, error registry | +| Command routing | @drone | @branch resolution, subprocess | +| Memory, vectors | @memory | ChromaDB, search, archival | ## Key Commands @@ -38,26 +36,27 @@ 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 list # Active plans +drone systems # All branches ``` -## Memory Protocol +## Branches (15) -Update `.trinity/` proactively — your persistence depends on it. +drone, seedgo, prax, cli, ai_mail, flow, spawn, trigger, api, backup, daemon, memory, commons (`src/commons/`), skills (`src/skills/`), devpulse (you — no apps/, coordinates via dispatch + agents) -**When:** After milestones. On `/memo`. At topic shifts. After 5+ actions without saving. When you learn something new. +## Working Habits -**What:** -- `local.json` — today_focus, recently_completed, sessions[], key_learnings -- `observations.json` — patterns, workflow insights -- This file — Current Context section below +- **Lean on branches.** You can't know everything — branches are the experts on their systems. When unsure, email them and ask. Don't burn context debugging what they already know. +- **Use memories freely.** Don't hoard or stress about capacity — rollover to @memory is by design. Update `.trinity/` often. More is better. +- **dev.local.md for friction notes.** When something feels off or could be improved, drop a quick note. Address in batches later. +- **Know your limits.** You're great at planning, coordinating, seeing the big picture. You're bad at hands-on branch-level code tasks. Dispatch, don't do. +- **Git hygiene at breakpoints.** When a branch completes work, a plan closes, or a dispatch cycle finishes — run `git status` to see what's piling up. Don't let changes drift. Propose a commit or PR when it makes sense. Not every turn — just at natural milestones. -**Prompt vs memory:** This prompt = lightweight signposts (injected every turn). Memories = detailed knowledge (read on startup, refreshed on update). Don't duplicate — point to where info lives. +## Memory & Tracking -## Current Context (Session 17) +- `.trinity/local.json` — session history, active tasks, learnings +- `.trinity/observations.json` — collaboration patterns +- `dev.local.md` — issues, todos, working notes (human + AI shared scratchpad) -**Date:** 2026-03-08 +Update `.trinity/` proactively — after milestones, on `/memo`, at topic shifts, after 5+ actions without saving. Your persistence depends on it. -- FPLAN-0016: 99% avg, all 14 at 99%+. Only drone false positive (bypassed). -- Researched Dev-Pass introspection standard — emailed @seedgo for new standard -- Massive uncommitted changeset — needs commit + PR -- Dev-Pass ref: `/home/patrick/Projects/Dev-Pass/` +**This prompt is NOT for tracking.** State goes in `.trinity/` and `dev.local.md`. This prompt = lightweight signposts injected every turn. diff --git a/src/aipass/devpulse/dev.local.md b/src/aipass/devpulse/dev.local.md new file mode 100644 index 00000000..f5b3affe --- /dev/null +++ b/src/aipass/devpulse/dev.local.md @@ -0,0 +1,56 @@ +# dev.local.md - DEVPULSE +``` +Branch: src/aipass/devpulse +Created: 2026-03-07 +Updated: 2026-03-10 +``` + +## Active Work + +- **Flow dispatch**: DPLAN path fixes + FPLAN empty template detection — dispatched to flow, branch awake +- **Prompt architecture**: DONE — breadcrumbs in global+local, dev.local.md headers fixed system-wide + +## Issues + +- **flow/DPLAN**: All 10 handler files use Dev-Pass paths (`~/aipass_os/dev_central/dev_planning/`). Creates orphan dirs. Dispatched to flow. +- **flow/FPLAN**: `is_template_content()` too aggressive — deletes plans with real content if template boilerplate remains. Data loss bug. Dispatched to flow. +- **flow/FPLAN archive**: Closed FPLANs go to `src/aipass/backup_system/processed_plans/` — backup_system isn't a branch (Dev-Pass name). Leaving for later. +- **prax**: Missing `handlers/central/reader.py` — dashboard/refresh.py imports `read_all_centrals` but function never created +- **backup**: Missing `config.json` template in json_templates/ +- **api**: `models` command not routed through drone +- **commons**: DB init failure (12/13 core branches operational) +- **ai_mail**: `get_current_user()` returns relative `mailbox_path` — causes doubled paths in reply +- **drone**: stderr kwarg crash + +## Completed + +- PR #31 merged: seedgo v2, 589 files, full system audit (2026-03-10) +- ai_mail sender identity bug FIXED by Patrick +- Renamed flow.local.md → dev.local.md system-wide (17 files) +- Fixed dev.local.md headers system-wide (16 branch files + spawn template) +- FPLAN-0021 dispatched+completed by flow: DPLANs wired into CLI router +- Revised devpulse branch prompt — breadcrumb pattern, working habits, expertise table +- Added Breadcrumbs section + dev.local.md awareness to global prompt +- Updated .trinity/ memories with Session 18 +- Processed inbox: ai_mail fix reply, flow FPLAN-0021 completion, old stress test +- Full FPLAN lifecycle tested: create (default+master), list, close (empty+content) +- Full DPLAN lifecycle tested: create, list, status, close + +--- + +## Todos + +- [x] Update global prompt with dev.local.md awareness +- [x] Fix dev.local.md headers system-wide +- [ ] Re-send emails to prax and backup about their known issues +- [ ] Scaffold remaining modules with system prompts (.aipass/aipass_local_prompt.md) +- [ ] Test dispatch wake to branches beyond ai_mail +- [ ] Address drone stderr kwarg crash +- [ ] Close stale FPLANs (0017, 0021) once verified complete + +## Notepad + +- Dev-Pass reference: `/home/patrick/Projects/Dev-Pass/` +- seed=Dev-Pass name, seedgo=AIPass name +- FPLAN close 5-step process: template check → mark closed → background archival → dashboard update → finalize +- DPLAN close 3-step process: close → background memory bank archival → finalize diff --git a/src/aipass/drone/dev.local.md b/src/aipass/drone/dev.local.md new file mode 100644 index 00000000..5a025612 --- /dev/null +++ b/src/aipass/drone/dev.local.md @@ -0,0 +1,15 @@ +# dev.local.md - DRONE +``` +Branch: /home/coder/workspace/AIPass/src/aipass/drone +Created: 2026-03-07 +``` + +## Issues + +- + +--- + +## Todos + +- diff --git a/src/aipass/flow/CLOSED_PLANS.local.json b/src/aipass/flow/CLOSED_PLANS.local.json new file mode 100644 index 00000000..6b2e937a --- /dev/null +++ b/src/aipass/flow/CLOSED_PLANS.local.json @@ -0,0 +1,25 @@ +{ + "closed_plans": [ + { + "plan_id": "DPLAN-001", + "type": "DPLAN", + "subject": "test dispatch fix", + "date_closed": "2026-03-10", + "location": "dev_central" + }, + { + "plan_id": "FPLAN-0024", + "type": "FPLAN", + "subject": "Test close with real content", + "date_closed": "2026-03-10", + "location": "flow" + }, + { + "plan_id": "FPLAN-0025", + "type": "FPLAN", + "subject": "Build STATUS board — per-branch STATUS.local.md with Prax-managed central aggregation", + "date_closed": "2026-03-10", + "location": "flow" + } + ] +} diff --git a/src/aipass/flow/apps/flow.py b/src/aipass/flow/apps/flow.py index 913d9f21..75ae714c 100755 --- a/src/aipass/flow/apps/flow.py +++ b/src/aipass/flow/apps/flow.py @@ -141,19 +141,19 @@ def main(): command = args[0] remaining_args = args[1:] if len(args) > 1 else [] - # Check if user wants module-specific help - if remaining_args and remaining_args[0] in ['--help', '-h']: - print_module_help(command, modules) + # Route to modules (modules handle their own --help internally) + if route_command(command, remaining_args, modules): return 0 - # Route to modules - if route_command(command, remaining_args, modules): + # Fallback: try module-specific help if command wasn't handled + if remaining_args and remaining_args[0] in ['--help', '-h']: + print_module_help(command, modules) return 0 else: console.print() console.print(f"[red]Unknown command: {command}[/red]") console.print() - console.print("Run [dim]python3 flow.py --help[/dim] for available commands") + console.print("Run [dim]drone @flow --help[/dim] for available commands") console.print() return 1 @@ -181,7 +181,7 @@ def print_introspection(modules: List[Any]): console.print(" [dim]No modules discovered[/dim]") console.print() - console.print("[dim]Run 'python3 flow.py --help' for usage information[/dim]") + console.print("[dim]Run 'drone @flow --help' for usage information[/dim]") console.print() @@ -198,8 +198,8 @@ def print_help(modules: List[Any]): console.print("[bold cyan]USAGE:[/bold cyan]") console.print() - console.print(" [dim]python3 flow.py [args...][/dim]") - console.print(" [dim]python3 flow.py --help[/dim]") + console.print(" [dim]drone @flow [args...][/dim]") + console.print(" [dim]drone @flow --help[/dim]") console.print() console.print("─" * 70) console.print() @@ -232,30 +232,45 @@ def print_help(modules: List[Any]): console.print("─" * 70) console.print() - console.print("[bold cyan]EXAMPLES:[/bold cyan]") + console.print("[bold cyan]FPLAN EXAMPLES:[/bold cyan]") console.print() - console.print(" [yellow]Create new PLAN:[/yellow]") - console.print(" [dim]python3 flow.py create . \"Implementation task\"[/dim]") - console.print(" [dim]python3 flow.py create /path/to/location \"Implementation task\"[/dim]") + console.print(" [yellow]Create new FPLAN:[/yellow]") + console.print(" [dim]drone @flow create . \"Implementation task\"[/dim]") + console.print(" [dim]drone @flow create . \"subject\" master[/dim]") console.print() - console.print(" [yellow]Close PLAN:[/yellow]") - console.print(" [dim]python3 flow.py close 42 --yes[/dim]") + console.print(" [yellow]Close FPLAN:[/yellow]") + console.print(" [dim]drone @flow close FPLAN-0042[/dim]") console.print() - console.print(" [yellow]Close all open plans:[/yellow]") - console.print(" [dim]python3 flow.py close --all[/dim]") + console.print(" [yellow]List FPLANs:[/yellow]") + console.print(" [dim]drone @flow list[/dim]") console.print() - console.print(" [yellow]List plans:[/yellow]") - console.print(" [dim]python3 flow.py list[/dim]") + console.print("─" * 70) console.print() + + console.print("[bold cyan]DPLAN EXAMPLES:[/bold cyan]") console.print() - console.print("[bold]NOTE:[/bold] @ syntax (e.g., @flow, @seedgo) only works through drone:") - console.print(" [dim]drone flow create @flow \"Implementation task\"[/dim]") + console.print(" [yellow]Create DPLAN:[/yellow]") + console.print(" [dim]drone @flow plan create \"Topic\"[/dim]") + console.print() + console.print(" [yellow]List DPLANs:[/yellow]") + console.print(" [dim]drone @flow plan list[/dim]") + console.print(" [dim]drone @flow plan list --tag idea[/dim]") + console.print() + console.print(" [yellow]Close DPLAN:[/yellow]") + console.print(" [dim]drone @flow plan close 42[/dim]") + console.print(" [dim]drone @flow plan close --all[/dim]") + console.print() + console.print(" [yellow]DPLAN status:[/yellow]") + console.print(" [dim]drone @flow plan status[/dim]") + console.print() + console.print(" [yellow]Sync registry:[/yellow]") + console.print(" [dim]drone @flow plan sync[/dim]") console.print() console.print("─" * 70) console.print() console.print("[bold]TIP:[/bold] For module-specific help:") - console.print(" [dim]python3 flow.py --help[/dim]") + console.print(" [dim]drone @flow --help[/dim]") console.print() @@ -274,7 +289,7 @@ def print_module_help(command: str, modules: List[Any]): console.print() console.print(f"[red]Unknown command: {command}[/red]") console.print() - console.print("Run [dim]python3 flow.py --help[/dim] for available commands") + console.print("Run [dim]drone @flow --help[/dim] for available commands") console.print() return diff --git a/src/aipass/flow/apps/handlers/dplan/__init__.py b/src/aipass/flow/apps/handlers/dplan/__init__.py index dd4cfbf2..0a9b94b9 100644 --- a/src/aipass/flow/apps/handlers/dplan/__init__.py +++ b/src/aipass/flow/apps/handlers/dplan/__init__.py @@ -1,8 +1,8 @@ """ DPLAN Handlers - Extracted from Dev-Pass devpulse on 2026-03-08 -These files need adaptation for AIPass before use. -Original imports use aipass_os.dev_central.devpulse -- must be converted to aipass.flow. - -Source: /home/patrick/Projects/Dev-Pass/aipass_os/dev_central/devpulse/apps/handlers/plan/ +Migration complete (2026-03-10): +- All imports converted from aipass_os.dev_central.devpulse to aipass.flow +- All data paths converted from ~/aipass_os/ to Path(__file__).parents[N] relative paths +- sys.path hacks removed """ diff --git a/src/aipass/flow/apps/handlers/dplan/close.py b/src/aipass/flow/apps/handlers/dplan/close.py index b01e4efe..d3322176 100644 --- a/src/aipass/flow/apps/handlers/dplan/close.py +++ b/src/aipass/flow/apps/handlers/dplan/close.py @@ -13,16 +13,10 @@ Validates, marks as closed, and archives DPLAN files. Adapted from Flow's close system for single-user DPLANs. """ -# INFRASTRUCTURE IMPORT PATTERN -import sys import re from pathlib import Path from typing import Dict, Any, Tuple, Optional, List -AIPASS_ROOT = Path.home() / "aipass_core" -sys.path.insert(0, str(AIPASS_ROOT)) -sys.path.insert(0, str(Path.home())) - # NOTE: Handlers do NOT import Prax logger (per 3-tier standard) from .status import extract_status @@ -31,8 +25,10 @@ from .status import extract_status # CONFIGURATION # ============================================================================= -DEV_PLANNING_ROOT = Path.home() / "aipass_os" / "dev_central" / "dev_planning" -PROCESSED_PLANS_DIR = Path.home() / "aipass_core" / "backup_system" / "processed_plans" +# close.py → dplan/ → handlers/ → apps/ → flow/ +FLOW_ROOT = Path(__file__).resolve().parents[3] +DEV_PLANNING_ROOT = FLOW_ROOT / "dev_planning" +PROCESSED_PLANS_DIR = FLOW_ROOT / "processed_plans" # ============================================================================= diff --git a/src/aipass/flow/apps/handlers/dplan/closed_plans_registry.py b/src/aipass/flow/apps/handlers/dplan/closed_plans_registry.py index ffac5a0c..0203cf37 100644 --- a/src/aipass/flow/apps/handlers/dplan/closed_plans_registry.py +++ b/src/aipass/flow/apps/handlers/dplan/closed_plans_registry.py @@ -27,7 +27,9 @@ from typing import Dict, Any # CONFIGURATION # ============================================================================= -CLOSED_PLANS_PATH = Path.home() / "aipass_os" / "dev_central" / "CLOSED_PLANS.local.json" +# closed_plans_registry.py → dplan/ → handlers/ → apps/ → flow/ +FLOW_ROOT = Path(__file__).resolve().parents[3] +CLOSED_PLANS_PATH = FLOW_ROOT / "CLOSED_PLANS.local.json" # ============================================================================= diff --git a/src/aipass/flow/apps/handlers/dplan/counter.py b/src/aipass/flow/apps/handlers/dplan/counter.py index ed8a0557..9a5cfe19 100644 --- a/src/aipass/flow/apps/handlers/dplan/counter.py +++ b/src/aipass/flow/apps/handlers/dplan/counter.py @@ -14,25 +14,20 @@ Supports multiple plan types (DPLAN, BPLAN) with separate sequences. Counter file is a cache, not source of truth. """ -# INFRASTRUCTURE IMPORT PATTERN -import sys import json import re from pathlib import Path from typing import Tuple -AIPASS_ROOT = Path.home() / "aipass_core" -sys.path.insert(0, str(AIPASS_ROOT)) -sys.path.insert(0, str(Path.home())) - # NOTE: Handlers do NOT import Prax logger (per 3-tier standard) -# Modules do the logging, handlers return errors # ============================================================================= # CONFIGURATION # ============================================================================= -DEV_PLANNING_ROOT = Path.home() / "aipass_os" / "dev_central" / "dev_planning" +# counter.py → dplan/ → handlers/ → apps/ → flow/ +FLOW_ROOT = Path(__file__).resolve().parents[3] +DEV_PLANNING_ROOT = FLOW_ROOT / "dev_planning" COUNTER_FILE = DEV_PLANNING_ROOT / "counter.json" VALID_PLAN_TYPES = {"dplan": "DPLAN", "bplan": "BPLAN"} diff --git a/src/aipass/flow/apps/handlers/dplan/create.py b/src/aipass/flow/apps/handlers/dplan/create.py index 35020eeb..28aa69fe 100644 --- a/src/aipass/flow/apps/handlers/dplan/create.py +++ b/src/aipass/flow/apps/handlers/dplan/create.py @@ -13,17 +13,11 @@ Creates new plan files (DPLAN, BPLAN) with proper naming and content. Supports @ branch resolution via target_path parameter. """ -# INFRASTRUCTURE IMPORT PATTERN -import sys import re from pathlib import Path from datetime import datetime from typing import Tuple, Dict, Any -AIPASS_ROOT = Path.home() / "aipass_core" -sys.path.insert(0, str(AIPASS_ROOT)) -sys.path.insert(0, str(Path.home())) - # NOTE: Handlers do NOT import Prax logger (per 3-tier standard) from .counter import get_next_plan_number, VALID_PLAN_TYPES @@ -33,7 +27,9 @@ from .template import render_template # CONFIGURATION # ============================================================================= -DEV_PLANNING_ROOT = Path.home() / "aipass_os" / "dev_central" / "dev_planning" +# create.py → dplan/ → handlers/ → apps/ → flow/ +FLOW_ROOT = Path(__file__).resolve().parents[3] +DEV_PLANNING_ROOT = FLOW_ROOT / "dev_planning" # ============================================================================= diff --git a/src/aipass/flow/apps/handlers/dplan/dashboard.py b/src/aipass/flow/apps/handlers/dplan/dashboard.py index 8421d5a2..7877fecd 100644 --- a/src/aipass/flow/apps/handlers/dplan/dashboard.py +++ b/src/aipass/flow/apps/handlers/dplan/dashboard.py @@ -15,15 +15,10 @@ independence pattern). Central push is handled directly here. """ import json -import sys from pathlib import Path from datetime import datetime from typing import Dict, Any, Optional, Callable -AIPASS_ROOT = Path.home() / "aipass_core" -sys.path.insert(0, str(AIPASS_ROOT)) -sys.path.insert(0, str(Path.home())) - # NOTE: Handlers do NOT import Prax logger (per 3-tier standard) from .registry import load_registry @@ -32,8 +27,11 @@ from .registry import load_registry # CONFIGURATION # ============================================================================= -DEVPULSE_ROOT = Path.home() / "aipass_os" / "dev_central" / "devpulse" -CENTRAL_FILE = Path.home() / "aipass_os" / "AI_CENTRAL" / "DEVPULSE.central.json" +# dashboard.py → dplan/ → handlers/ → apps/ → flow/ → aipass/ +FLOW_ROOT = Path(__file__).resolve().parents[3] +AIPASS_ROOT = Path(__file__).resolve().parents[4] +DEVPULSE_ROOT = AIPASS_ROOT / "devpulse" +CENTRAL_FILE = DEVPULSE_ROOT / "DEVPULSE.central.json" # ============================================================================= diff --git a/src/aipass/flow/apps/handlers/dplan/display.py b/src/aipass/flow/apps/handlers/dplan/display.py index 37e176c5..e1d1e0d4 100644 --- a/src/aipass/flow/apps/handlers/dplan/display.py +++ b/src/aipass/flow/apps/handlers/dplan/display.py @@ -16,24 +16,19 @@ Provides help text and introspection information. import sys from pathlib import Path -AIPASS_ROOT = Path.home() / "aipass_core" -sys.path.insert(0, str(AIPASS_ROOT)) -sys.path.insert(0, str(Path.home())) - # NOTE: Handlers do NOT import Prax logger (per 3-tier standard) # ============================================================================= # CONFIGURATION # ============================================================================= -DEV_PLANNING_ROOT = Path.home() / "aipass_os" / "dev_central" / "dev_planning" -DEVPULSE_ROOT = Path.home() / "aipass_os" / "dev_central" / "devpulse" -COUNTER_FILE = DEV_PLANNING_ROOT / "counter.json" -TEMPLATE_FILE = DEVPULSE_ROOT / "templates" / "dplan_default.md" +# display.py → dplan/ → handlers/ → apps/ → flow/ +FLOW_ROOT = Path(__file__).resolve().parents[3] +TEMPLATE_FILE = FLOW_ROOT / "templates" / "dplan_default.md" HELP_TEXT = """ [bold]USAGE:[/bold] - drone @devpulse plan [options] + drone @flow plan [options] [bold]SUBCOMMANDS:[/bold] create "topic" [options] - Create new plan document @@ -48,18 +43,18 @@ HELP_TEXT = """ bplan - Business plans [bold]EXAMPLES:[/bold] - drone @devpulse plan create "new feature design" - drone @devpulse plan create "API upgrade" --tag upgrade - drone @devpulse plan create "revenue model" --type bplan - drone @devpulse plan create "vera improvements" --type dplan @vera - drone @devpulse plan list - drone @devpulse plan list --type bplan - drone @devpulse plan list --tag idea - drone @devpulse plan list --status planning - drone @devpulse plan status - drone @devpulse plan status --type dplan - drone @devpulse plan close 3 - drone @devpulse plan close --all + drone @flow plan create "new feature design" + drone @flow plan create "API upgrade" --tag upgrade + drone @flow plan create "revenue model" --type bplan + drone @flow plan create "vera improvements" --type dplan @vera + drone @flow plan list + drone @flow plan list --type bplan + drone @flow plan list --tag idea + drone @flow plan list --status planning + drone @flow plan status + drone @flow plan status --type dplan + drone @flow plan close 3 + drone @flow plan close --all [bold]@ RESOLUTION:[/bold] Append @branch to create plans in another branch's dev_planning/: @@ -120,8 +115,6 @@ def get_introspection_data() -> dict: return { "name": "D-PLAN Management Module", "description": "Manages numbered planning documents in dev_planning/", - "planning_dir": str(DEV_PLANNING_ROOT), - "counter_file": str(COUNTER_FILE), "template_file": str(TEMPLATE_FILE) } @@ -142,11 +135,9 @@ def print_introspection() -> str: f"[dim]{data['description']}[/dim]", "", "[yellow]Configuration:[/yellow]", - f" [dim]Planning dir:[/dim] {data['planning_dir']}", - f" [dim]Counter file:[/dim] {data['counter_file']}", f" [dim]Template:[/dim] {data['template_file']}", "", - "[dim]Run 'python3 dev_flow.py --help' for usage[/dim]", + "[dim]Run 'drone @flow plan --help' for usage[/dim]", "" ] diff --git a/src/aipass/flow/apps/handlers/dplan/list.py b/src/aipass/flow/apps/handlers/dplan/list.py index 28490a24..894537dd 100644 --- a/src/aipass/flow/apps/handlers/dplan/list.py +++ b/src/aipass/flow/apps/handlers/dplan/list.py @@ -12,16 +12,10 @@ List Handler - Plan Listing Collects and returns plan data for display. Supports multiple plan types. """ -# INFRASTRUCTURE IMPORT PATTERN -import sys import re from pathlib import Path from typing import List, Dict, Any, Tuple -AIPASS_ROOT = Path.home() / "aipass_core" -sys.path.insert(0, str(AIPASS_ROOT)) -sys.path.insert(0, str(Path.home())) - # NOTE: Handlers do NOT import Prax logger (per 3-tier standard) from .status import extract_status, extract_tag, extract_description @@ -30,7 +24,9 @@ from .status import extract_status, extract_tag, extract_description # CONFIGURATION # ============================================================================= -DEV_PLANNING_ROOT = Path.home() / "aipass_os" / "dev_central" / "dev_planning" +# list.py → dplan/ → handlers/ → apps/ → flow/ +FLOW_ROOT = Path(__file__).resolve().parents[3] +DEV_PLANNING_ROOT = FLOW_ROOT / "dev_planning" # Regex matches any plan type: DPLAN-001_topic_2026-02-19.md, BPLAN-001_topic_2026-02-19.md PLAN_FILENAME_PATTERN = re.compile(r"([A-Z]+PLAN)-(\d+)_(.+)_(\d{4}-\d{2}-\d{2})\.md") diff --git a/src/aipass/flow/apps/handlers/dplan/log_setup.py b/src/aipass/flow/apps/handlers/dplan/log_setup.py index 98a4ed77..44599dfe 100644 --- a/src/aipass/flow/apps/handlers/dplan/log_setup.py +++ b/src/aipass/flow/apps/handlers/dplan/log_setup.py @@ -26,7 +26,9 @@ import io # CONFIGURATION # ============================================================================= -DEFAULT_LOG_DIR = Path.home() / "aipass_os" / "logs" +# log_setup.py → dplan/ → handlers/ → apps/ → flow/ +FLOW_ROOT = Path(__file__).resolve().parents[3] +DEFAULT_LOG_DIR = FLOW_ROOT / "logs" # ============================================================================= @@ -40,7 +42,7 @@ def prepare_log_file(filename: str = "post_close_runner.log", Args: filename: Log file name (default: post_close_runner.log) - log_dir: Override log directory (default: ~/aipass_os/logs/) + log_dir: Override log directory (default: flow/logs/) Returns: Dict with keys: diff --git a/src/aipass/flow/apps/handlers/dplan/registry.py b/src/aipass/flow/apps/handlers/dplan/registry.py index 75e41640..20cf3339 100644 --- a/src/aipass/flow/apps/handlers/dplan/registry.py +++ b/src/aipass/flow/apps/handlers/dplan/registry.py @@ -14,16 +14,11 @@ plan metadata, status, tags, and AI-generated summaries. """ import json -import sys import re from pathlib import Path from datetime import datetime from typing import Dict, Any, Optional -AIPASS_ROOT = Path.home() / "aipass_core" -sys.path.insert(0, str(AIPASS_ROOT)) -sys.path.insert(0, str(Path.home())) - # NOTE: Handlers do NOT import Prax logger (per 3-tier standard) from .status import extract_status, extract_tag, extract_description @@ -32,10 +27,11 @@ from .status import extract_status, extract_tag, extract_description # CONFIGURATION # ============================================================================= -DEVPULSE_ROOT = Path.home() / "aipass_os" / "dev_central" / "devpulse" -DEV_PLANNING_ROOT = Path.home() / "aipass_os" / "dev_central" / "dev_planning" -REGISTRY_FILE = DEVPULSE_ROOT / "devpulse_json" / "dplan_registry.json" -SUMMARIES_FILE = DEVPULSE_ROOT / "devpulse_json" / "dplan_summaries.json" +# registry.py → dplan/ → handlers/ → apps/ → flow/ +FLOW_ROOT = Path(__file__).resolve().parents[3] +DEV_PLANNING_ROOT = FLOW_ROOT / "dev_planning" +REGISTRY_FILE = FLOW_ROOT / "flow_json" / "dplan_registry.json" +SUMMARIES_FILE = FLOW_ROOT / "flow_json" / "dplan_summaries.json" # ============================================================================= diff --git a/src/aipass/flow/apps/handlers/dplan/status.py b/src/aipass/flow/apps/handlers/dplan/status.py index e0f2f84b..74238183 100644 --- a/src/aipass/flow/apps/handlers/dplan/status.py +++ b/src/aipass/flow/apps/handlers/dplan/status.py @@ -12,23 +12,19 @@ Status Handler - D-PLAN Status Operations Extracts status from plan files and provides status summary. """ -# INFRASTRUCTURE IMPORT PATTERN -import sys import re from pathlib import Path from typing import Dict, Tuple -AIPASS_ROOT = Path.home() / "aipass_core" -sys.path.insert(0, str(AIPASS_ROOT)) -sys.path.insert(0, str(Path.home())) - # NOTE: Handlers do NOT import Prax logger (per 3-tier standard) # ============================================================================= # CONFIGURATION # ============================================================================= -DEV_PLANNING_ROOT = Path.home() / "aipass_os" / "dev_central" / "dev_planning" +# status.py → dplan/ → handlers/ → apps/ → flow/ +FLOW_ROOT = Path(__file__).resolve().parents[3] +DEV_PLANNING_ROOT = FLOW_ROOT / "dev_planning" VALID_TAGS = ["idea", "upgrade", "proposal", "bug", "research", "seed", "infrastructure"] diff --git a/src/aipass/flow/apps/handlers/dplan/template.py b/src/aipass/flow/apps/handlers/dplan/template.py index 0c7fa208..99406542 100644 --- a/src/aipass/flow/apps/handlers/dplan/template.py +++ b/src/aipass/flow/apps/handlers/dplan/template.py @@ -18,19 +18,15 @@ import sys from pathlib import Path from typing import Tuple -AIPASS_ROOT = Path.home() / "aipass_core" -sys.path.insert(0, str(AIPASS_ROOT)) -sys.path.insert(0, str(Path.home())) - # NOTE: Handlers do NOT import Prax logger (per 3-tier standard) # ============================================================================= # CONFIGURATION # ============================================================================= -DEVPULSE_ROOT = Path.home() / "aipass_os" / "dev_central" / "devpulse" -TEMPLATE_DIR = DEVPULSE_ROOT / "templates" -TEMPLATE_FILE = TEMPLATE_DIR / "dplan_default.md" +# template.py → dplan/ → handlers/ → apps/ → flow/ +FLOW_ROOT = Path(__file__).resolve().parents[3] +TEMPLATE_DIR = FLOW_ROOT / "templates" DPLAN_DEFAULT_TEMPLATE = """# DPLAN-{{NUMBER}}: {{TOPIC}} diff --git a/src/aipass/flow/apps/handlers/mbank/process.py b/src/aipass/flow/apps/handlers/mbank/process.py index 74c26304..cfde5b70 100644 --- a/src/aipass/flow/apps/handlers/mbank/process.py +++ b/src/aipass/flow/apps/handlers/mbank/process.py @@ -272,10 +272,11 @@ def get_closed_plans() -> List[Dict[str, Any]]: # ============================================= def is_template_content(content: str) -> bool: - """Check if plan content is still unedited template (v3.0) + """Check if plan content is still unedited template (v4.0) - Checks against markers from all template types (default, master, proposal). - Template detected if 3+ markers from ANY single template type are found. + Uses bracket placeholders only (not section headers) to detect untouched + templates. Also checks for user-added content in key sections — if any + real work was added, the plan is NOT a template even if placeholders remain. Args: content: Plan file content @@ -283,28 +284,52 @@ def is_template_content(content: str) -> bool: Returns: True if plan is essentially an untouched template """ - # Default template markers (updated Dec 2025 rewrite) + # User content indicators — if ANY of these are found, plan has real work + user_content_signals = [ + "- [x] Agent deployed", # Checked execution log item + "- [x] Agent completed", # Checked execution log item + "- [x] Seedgo checklist", # Checked completion item + "- [x] All goals achieved", # Checked completion item + ] + for signal in user_content_signals: + if signal in content: + return False + + # Check Notes section for user content (not just the placeholder) + import re + notes_match = re.search(r'## Notes\s*\n(.*?)(?=\n---|\n## |\Z)', content, re.DOTALL) + if notes_match: + notes_content = notes_match.group(1).strip() + # If notes has content beyond the template placeholder, it's real work + if notes_content and notes_content != "[Working notes, issues encountered, decisions made]": + return False + + # Check Execution Log for user-added entries beyond template + exec_match = re.search(r'## Execution Log\s*\n(.*?)(?=\n---|\n## |\Z)', content, re.DOTALL) + if exec_match: + exec_content = exec_match.group(1).strip() + lines = [l.strip() for l in exec_content.split('\n') if l.strip()] + # Template has ~6 lines (date header + checkbox items). More = user added content. + if len(lines) > 8: + return False + + # Bracket placeholders only (no section headers — those persist in real plans) default_markers = [ "[What do you want to achieve? Specific end state.]", "[How will agents tackle this? What instructions will they need?]", "[List any planning docs, specs, or examples to reference]", "[Working notes, issues encountered, decisions made]", "[What specifically defines complete for this plan?]", - "## What Are Flow Plans?", - "## Critical: Branch Manager Role", ] - # Master template markers master_markers = [ "[What this phase accomplishes]", "[What the agent will build]", "[Files/outputs expected]", "[What specifically defines the project complete?]", "[Patterns discovered that span multiple phases]", - "## Master Plan Overview", ] - # Proposal template markers proposal_markers = [ "[Clear description of the idea, feature, improvement, or fix]", "[Why is this valuable? What problem does it solve? What does it enable?]", @@ -312,7 +337,7 @@ def is_template_content(content: str) -> bool: "[Any other branches, services, or approvals needed?]", ] - # Check each template type - 3+ markers from any type = template + # 3+ bracket placeholders from any type = template for markers in [default_markers, master_markers, proposal_markers]: found = sum(1 for m in markers if m in content) if found >= 3: diff --git a/src/aipass/flow/apps/modules/dplan_flow.py b/src/aipass/flow/apps/modules/dplan_flow.py index 1d54841d..b3b4b27a 100644 --- a/src/aipass/flow/apps/modules/dplan_flow.py +++ b/src/aipass/flow/apps/modules/dplan_flow.py @@ -22,21 +22,21 @@ from typing import List from aipass.prax.apps.modules.logger import system_logger as logger from aipass.cli.apps.modules import console, header, success, error -# Handler imports -from aipass_os.dev_central.devpulse.apps.handlers.plan.create import create_plan -from aipass_os.dev_central.devpulse.apps.handlers.plan.list import list_plans -from aipass_os.dev_central.devpulse.apps.handlers.plan.status import get_status_summary, get_status_icon, VALID_TAGS -from aipass_os.dev_central.devpulse.apps.handlers.plan.display import show_help, print_introspection -from aipass_os.dev_central.devpulse.apps.handlers.plan.close import ( +# Handler imports (local handlers in handlers/dplan/) +from aipass.flow.apps.handlers.dplan.create import create_plan +from aipass.flow.apps.handlers.dplan.list import list_plans +from aipass.flow.apps.handlers.dplan.status import get_status_summary, get_status_icon, VALID_TAGS +from aipass.flow.apps.handlers.dplan.display import show_help, print_introspection +from aipass.flow.apps.handlers.dplan.close import ( normalize_plan_number, close_plan, get_open_plans ) -from aipass_os.dev_central.devpulse.apps.handlers.plan.counter import VALID_PLAN_TYPES -from aipass_os.dev_central.devpulse.apps.handlers.plan.registry import ( +from aipass.flow.apps.handlers.dplan.counter import VALID_PLAN_TYPES +from aipass.flow.apps.handlers.dplan.registry import ( register_plan, update_plan_status, populate_from_filesystem, get_summary, save_plan_summary, generate_description_summary ) -from aipass_os.dev_central.devpulse.apps.handlers.plan.dashboard import push_all as _push_dashboard_raw -from aipass_os.dev_central.devpulse.apps.handlers.dashboard.operations import write_section +from aipass.flow.apps.handlers.dplan.dashboard import push_all as _push_dashboard_raw +from aipass.prax.apps.handlers.dashboard.operations import write_section # Local handlers (file I/O extracted from this module) from aipass.flow.apps.handlers.dplan.branch_resolve import resolve_branch_target as _resolve_branch @@ -74,7 +74,7 @@ def print_introspection(): console.print("Plan management orchestrator — routes plan commands to handlers") console.print() console.print("Connected Handlers:") - console.print(" handlers/plan/ (via aipass_os.dev_central.devpulse)") + console.print(" handlers/dplan/") console.print(" - create.py (create_plan — create new plans)") console.print(" - list.py (list_plans — list plans with filters)") console.print(" - status.py (get_status_summary — plan status aggregation)") @@ -83,16 +83,14 @@ def print_introspection(): console.print(" - counter.py (VALID_PLAN_TYPES — plan type definitions)") console.print(" - registry.py (register_plan, update_plan_status — registry ops)") console.print(" - dashboard.py (push_all — dashboard updates)") - console.print() - console.print(" handlers/dashboard/ (via aipass_os.dev_central.devpulse)") - console.print(" - operations.py (write_section — dashboard section writer)") - console.print() - console.print(" handlers/dplan/") console.print(" - branch_resolve.py (resolve_branch_target — @ branch resolution)") console.print(" - closed_plans_registry.py (append_closed_dplan — closed plan tracking)") console.print(" - log_setup.py (prepare_log_file — log file preparation)") console.print(" - background_spawn.py (spawn_post_close — background archival)") console.print() + console.print(" External:") + console.print(" - aipass.prax (write_section — dashboard section writer)") + console.print() def print_help(): diff --git a/src/aipass/flow/apps/modules/dplan_post_close_runner.py b/src/aipass/flow/apps/modules/dplan_post_close_runner.py index 05daea6c..d446b712 100644 --- a/src/aipass/flow/apps/modules/dplan_post_close_runner.py +++ b/src/aipass/flow/apps/modules/dplan_post_close_runner.py @@ -21,10 +21,8 @@ import sys from pathlib import Path # INFRASTRUCTURE IMPORT PATTERN -AIPASS_ROOT = Path.home() / "aipass_core" -DEVPULSE_ROOT = Path.home() / "aipass_os" / "dev_central" / "devpulse" -sys.path.insert(0, str(AIPASS_ROOT)) -sys.path.insert(0, str(Path.home())) +# dplan_post_close_runner.py → modules/ → apps/ → flow/ +FLOW_ROOT = Path(__file__).resolve().parents[2] # External: CLI console (Rich display) and Prax logger from aipass.cli.apps.modules import console @@ -32,9 +30,9 @@ from aipass.prax.apps.modules.logger import system_logger as logger MODULE_NAME = "dplan_post_close_runner" -LOCK_FILE = DEVPULSE_ROOT / ".post_close_runner.lock" +LOCK_FILE = FLOW_ROOT / ".post_close_runner.lock" -from aipass_os.dev_central.devpulse.apps.handlers.mbank.process import process_closed_plans +from aipass.flow.apps.handlers.mbank.process import process_closed_plans def handle_command(command: str, args: list) -> bool: @@ -105,7 +103,7 @@ def print_introspection(): console.print("Background post-close processing for DPLANs — runs Memory Bank archival") console.print() console.print("Connected Handlers:") - console.print(" handlers/mbank/ (via aipass_os.dev_central.devpulse)") + console.print(" handlers/mbank/ (local)") console.print(" - process.py (process_closed_plans — scan and archive closed plans)") console.print() diff --git a/src/aipass/flow/dev.local.md b/src/aipass/flow/dev.local.md new file mode 100644 index 00000000..027d1f2d --- /dev/null +++ b/src/aipass/flow/dev.local.md @@ -0,0 +1,34 @@ +# dev.local.md - FLOW +``` +Branch: src/aipass/flow +Created: 2026-03-07 +Updated: 2026-03-10 +``` + +## Active Work + +- **FPLAN-0017**: Implement Two-Level Introspection Standard (from seedgo) + +## Completed + +- **FPLAN-0021**: Wire DPLANs into flow CLI router and clean up templates (2026-03-10) + - Remapped dplan_flow.py imports from aipass_os to aipass.flow local handlers + - Fixed template.py, display.py, dplan_post_close_runner.py Dev-Pass imports + - Added DPLAN examples to --help, updated all references to drone syntax + - Fixed routing order so `plan` subcommands work through flow.py + - Updated FPLAN templates: @memory_bank to @memory +- **Dispatch**: Handler path migration + template detection fix (2026-03-10) + - All 9 DPLAN handler data paths migrated from aipass_os/aipass_core to Path(__file__).parents[N] + - DPLANs now create/list/close in flow/dev_planning/ (verified) + - Removed all sys.path hacks from handlers + - Fixed is_template_content() data loss: removed section headers from markers, added user content detection + +## Issues + +- None currently tracked + +--- + +## Todos + +- (none) diff --git a/src/aipass/flow/dev_planning/counter.json b/src/aipass/flow/dev_planning/counter.json new file mode 100644 index 00000000..7cd2fb49 --- /dev/null +++ b/src/aipass/flow/dev_planning/counter.json @@ -0,0 +1,6 @@ +{ + "DPLAN": { + "next_number": 2 + }, + "next_number": 2 +} \ No newline at end of file diff --git a/src/aipass/flow/templates/bplan_default.md b/src/aipass/flow/templates/bplan_default.md deleted file mode 100644 index 10d1fb22..00000000 --- a/src/aipass/flow/templates/bplan_default.md +++ /dev/null @@ -1,57 +0,0 @@ -# BPLAN-{{NUMBER}}: {{TOPIC}} - -Tag: {{TAG}} - -> One-line description - -## Executive Summary -What this business initiative achieves and why it matters. - -## Market Analysis -Target market, size, trends, and opportunity. - -## Revenue Model -How this generates or saves revenue. Pricing, margins, unit economics. - -## Competitive Landscape -Who else is doing this? What's our edge? - -## KPIs -| Metric | Target | Timeline | -|--------|--------|----------| -| Example | TBD | Q1 2026 | - -## Go-to-Market -Launch strategy, channels, partnerships. - -## Risk Assessment -| Risk | Impact | Mitigation | -|------|--------|------------| -| Example | High | Plan B | - -## Timeline -- [ ] Phase 1: Research & Validation -- [ ] Phase 2: MVP / Pilot -- [ ] Phase 3: Scale - -## Budget Considerations -Estimated costs, resource requirements, ROI timeline. - -## Relationships -- **Related BPLANs:** None yet -- **Related DPLANs:** None yet -- **Owner branches:** Who owns this - -## Status -- [x] Planning -- [ ] In Progress -- [ ] Ready for Execution -- [ ] Complete -- [ ] Abandoned - -## Notes -Session notes, discoveries, changes - ---- -*Created: {{DATE}}* -*Updated: {{DATE}}* diff --git a/src/aipass/flow/templates/default.md b/src/aipass/flow/templates/default.md index a12711c3..c783c2e0 100644 --- a/src/aipass/flow/templates/default.md +++ b/src/aipass/flow/templates/default.md @@ -82,7 +82,7 @@ ai_mail send @branch "Question: [topic]" "I'm working on X and need guidance on - 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 +- Need persistent storage or search? Ask @memory - Event-driven behavior? Ask @trigger about their event system - Dashboard integration? Ask @devpulse about update_section() diff --git a/src/aipass/flow/templates/dplan_default.md b/src/aipass/flow/templates/dplan_default.md index 3f450ae4..b8f43c6b 100644 --- a/src/aipass/flow/templates/dplan_default.md +++ b/src/aipass/flow/templates/dplan_default.md @@ -26,7 +26,8 @@ Captured ideas, brainstorms, future possibilities. Add freely. ## Relationships - **Related DPLANs:** None yet - **Related FPLANs:** None yet -- **Owner branches:** Who builds this +- **Owner branch:** Who builds this +- **Seedgo standards:** `drone @seedgo audit aipass @branch` | `drone @seedgo standards_query aipass_standards` ## Status - [x] Planning diff --git a/src/aipass/flow/templates/master.md b/src/aipass/flow/templates/master.md index d078b72f..280baffe 100644 --- a/src/aipass/flow/templates/master.md +++ b/src/aipass/flow/templates/master.md @@ -85,7 +85,7 @@ ai_mail send @branch "Question: [topic]" "I'm working on X and need guidance on - 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 +- Need persistent storage or search? Ask @memory - Event-driven behavior? Ask @trigger about their event system - Dashboard integration? Ask @devpulse about update_section() diff --git a/src/aipass/memory/dev.local.md b/src/aipass/memory/dev.local.md new file mode 100644 index 00000000..4a9aebc0 --- /dev/null +++ b/src/aipass/memory/dev.local.md @@ -0,0 +1,15 @@ +# dev.local.md - MEMORY +``` +Branch: /home/patrick/Projects/AIPass/src/aipass/memory +Created: 2026-03-07 +``` + +## Issues + +- + +--- + +## Todos + +- diff --git a/src/aipass/prax/apps/handlers/central/__init__.py b/src/aipass/prax/apps/handlers/central/__init__.py new file mode 100644 index 00000000..94492e87 --- /dev/null +++ b/src/aipass/prax/apps/handlers/central/__init__.py @@ -0,0 +1,3 @@ +from .reader import read_all_centrals + +__all__ = ["read_all_centrals"] diff --git a/src/aipass/prax/apps/handlers/central/reader.py b/src/aipass/prax/apps/handlers/central/reader.py new file mode 100644 index 00000000..b067b9de --- /dev/null +++ b/src/aipass/prax/apps/handlers/central/reader.py @@ -0,0 +1,50 @@ +# =================== AIPass ==================== +# Name: reader.py +# Description: Central file reader — scans .ai_central/*.central.json +# Version: 1.0.0 +# Created: 2026-03-10 +# Modified: 2026-03-10 +# ============================================= + +""" +Central File Reader + +Reads all .ai_central/*.central.json files from the repo root +and returns a dict keyed by service name. + +Used by dashboard/refresh.py to populate branch dashboards. +""" + +import json +from pathlib import Path +from typing import Dict + +from aipass.prax.apps.handlers.config.load import _find_repo_root + + +def read_all_centrals() -> Dict: + """ + Read all .central.json files from .ai_central/ at repo root. + + Returns: + Dict keyed by service name (e.g. 'ai_mail', 'plans', 'devpulse'). + Each value is the parsed JSON content of the central file. + Returns empty dict if directory doesn't exist or has no files. + """ + repo_root = _find_repo_root() + central_dir = repo_root / ".ai_central" + + if not central_dir.is_dir(): + return {} + + centrals = {} + for central_file in central_dir.glob("*.central.json"): + try: + data = json.loads(central_file.read_text(encoding="utf-8")) + # Key by service name: AI_MAIL.central.json -> ai_mail + service_name = central_file.name.replace(".central.json", "").lower() + centrals[service_name] = data + except (json.JSONDecodeError, OSError): + continue + + return centrals diff --git a/src/aipass/prax/apps/handlers/status/__init__.py b/src/aipass/prax/apps/handlers/status/__init__.py new file mode 100644 index 00000000..c7e23641 --- /dev/null +++ b/src/aipass/prax/apps/handlers/status/__init__.py @@ -0,0 +1,3 @@ +from .sync import sync_status + +__all__ = ["sync_status"] diff --git a/src/aipass/prax/apps/handlers/status/sync.py b/src/aipass/prax/apps/handlers/status/sync.py new file mode 100644 index 00000000..5a1a524c --- /dev/null +++ b/src/aipass/prax/apps/handlers/status/sync.py @@ -0,0 +1,159 @@ +# =================== AIPass ==================== +# Name: sync.py +# Description: Status sync handler — scans branches for STATUS.local.md +# Version: 1.0.0 +# Created: 2026-03-10 +# Modified: 2026-03-10 +# ============================================= + +""" +Status Sync Handler + +Scans all registered branches for STATUS.local.md files, +extracts state and last-update metadata, and builds an +aggregated STATUS.md at the repo root with collapsible +
sections per branch. + +Usage (internal): + from aipass.prax.apps.handlers.status.sync import sync_status + result = sync_status() +""" + +import json +import re +from datetime import datetime +from pathlib import Path +from typing import Dict + +from aipass.prax.apps.handlers.config.load import _find_repo_root +from aipass.prax.apps.modules.logger import system_logger as logger + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +_STATE_RE = re.compile(r"^\*\*State:\*\*\s*(.+)", re.MULTILINE) +_UPDATE_RE = re.compile(r"^\*\*Last update:\*\*\s*(.+)", re.MULTILINE) + + +def _extract_field(pattern: re.Pattern, text: str) -> str: + """Return the first capture group for *pattern* in *text*, stripped.""" + match = pattern.search(text) + if match: + return match.group(1).strip() + return "" + + +# --------------------------------------------------------------------------- +# Core +# --------------------------------------------------------------------------- + +def sync_status() -> Dict: + """ + Scan all branches for STATUS.local.md, build central STATUS.md. + + Returns: + Dict with keys: status, branches_synced, branches_missing, timestamp + """ + repo_root = _find_repo_root() + registry_path = repo_root / "AIPASS_REGISTRY.json" + + if not registry_path.exists(): + logger.error("AIPASS_REGISTRY.json not found at %s", repo_root) + return { + "status": "error", + "branches_synced": [], + "branches_missing": [], + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"), + } + + try: + registry = json.loads(registry_path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError) as exc: + logger.error("Failed to read AIPASS_REGISTRY.json: %s", exc) + return { + "status": "error", + "branches_synced": [], + "branches_missing": [], + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"), + } + + branches = registry.get("branches", []) + + # Collect per-branch data ------------------------------------------------ + synced = [] + missing = [] + entries = [] # (branch_email, state, date, content) + + for branch in sorted(branches, key=lambda b: b.get("email", "").lower()): + branch_email = branch.get("email", f"@{branch.get('name', 'unknown').lower()}") + branch_path = repo_root / branch.get("path", "") + status_file = branch_path / "STATUS.local.md" + + if not status_file.exists(): + missing.append(branch_email) + logger.debug("STATUS.local.md missing for %s", branch_email) + continue + + try: + content = status_file.read_text(encoding="utf-8") + except OSError as exc: + missing.append(branch_email) + logger.warning("Could not read %s: %s", status_file, exc) + continue + + state = _extract_field(_STATE_RE, content) or "Unknown" + last_update = _extract_field(_UPDATE_RE, content) or "n/a" + + synced.append(branch_email) + entries.append((branch_email, state, last_update, content)) + + # Tally states ----------------------------------------------------------- + state_counts: Dict[str, int] = {} + for _, state, _, _ in entries: + normalised = state.lower() + state_counts[normalised] = state_counts.get(normalised, 0) + 1 + + operational = state_counts.get("operational", 0) + in_progress = state_counts.get("in progress", state_counts.get("in-progress", 0)) + not_started = state_counts.get("not started", state_counts.get("not-started", 0)) + + now = datetime.now().strftime("%Y-%m-%d %H:%M") + + # Build STATUS.md -------------------------------------------------------- + lines = [ + "# AIPass — System Status", + "", + "> Auto-generated by `drone @prax status sync`. Do not edit manually.", + "", + f"**Last sync:** {now} ", + f"**Summary:** {operational} operational | {in_progress} in-progress | {not_started} not started", + "", + "---", + "", + ] + + for branch_email, state, last_update, content in entries: + lines.append( + f"
{branch_email} — {state} ({last_update})" + ) + lines.append("") + lines.append(content.rstrip()) + lines.append("") + lines.append("
") + lines.append("") + + status_md = repo_root / "STATUS.md" + status_md.write_text("\n".join(lines) + "\n", encoding="utf-8") + + logger.info( + "STATUS.md synced: %d branches, %d missing", len(synced), len(missing) + ) + + return { + "status": "ok", + "branches_synced": synced, + "branches_missing": missing, + "timestamp": now, + } diff --git a/src/aipass/prax/apps/modules/dashboard.py b/src/aipass/prax/apps/modules/dashboard.py index 0db6b4b2..fff009c2 100644 --- a/src/aipass/prax/apps/modules/dashboard.py +++ b/src/aipass/prax/apps/modules/dashboard.py @@ -445,25 +445,32 @@ def handle_command(command: str, args: List[str]) -> bool: Returns: True if command handled, False otherwise """ - if command == "status": + if command != "dashboard": + return False + + subcmd = args[0] if args else "" + + if subcmd == "status": print_status() return True - elif command == "template": + elif subcmd == "template": print_template() return True - elif command == "refresh": - _handle_refresh(args) + elif subcmd == "refresh": + _handle_refresh(args[1:]) return True - elif command == "push-template": - _handle_push_template(args) + elif subcmd == "push-template": + _handle_push_template(args[1:]) return True - elif command == "diff-template": - _handle_diff_template(args) + elif subcmd == "diff-template": + _handle_diff_template(args[1:]) return True - elif command == "template-status": + elif subcmd == "template-status": _handle_template_status() return True - return False + else: + print_help() + return True def main(): diff --git a/src/aipass/prax/apps/modules/status_module.py b/src/aipass/prax/apps/modules/status_module.py index 5530fece..e8240ae1 100755 --- a/src/aipass/prax/apps/modules/status_module.py +++ b/src/aipass/prax/apps/modules/status_module.py @@ -1,9 +1,9 @@ # =================== AIPass ==================== # Name: status_module.py # Description: PRAX Status Command -# Version: 1.0.0 +# Version: 1.1.0 # Created: 2025-11-15 -# Modified: 2026-03-09 +# Modified: 2026-03-10 # ============================================= """ @@ -17,22 +17,17 @@ from pathlib import Path from typing import List from aipass.prax.apps.modules.logger import get_system_status, system_logger as logger -from aipass.cli.apps.modules import console, header, success, error +from aipass.prax.apps.handlers.status.sync import sync_status +from aipass.cli.apps.modules import console, header, success, error, warning def print_help(): """Display module help and connected handlers""" console.print() - console.print("[bold cyan]PRAX Status Module[/bold cyan]") - console.print() - console.print("[yellow]Connected Handlers:[/yellow]") - console.print() - - console.print(" [cyan]prax/modules/[/cyan]") - console.print(" [dim]- logger.py[/dim] (get_system_status, system_logger)") - console.print() - - console.print("[dim]Run 'python3 status_module.py --help' for usage[/dim]") + console.print("[bold cyan]PRAX Status Commands:[/bold cyan]") + console.print(" [white]status[/white] Show PRAX system status") + console.print(" [white]status sync[/white] Scan all branches, build STATUS.md at repo root") + console.print(" [white]status help[/white] Show this help") console.print() @@ -50,6 +45,15 @@ def handle_command(command: str, args: List[str]) -> bool: if command != 'status': return False + # --- sub-command routing ------------------------------------------------ + if args and args[0] in ("--help", "help"): + print_help() + return True + + if args and args[0] == "sync": + return _handle_sync() + + # --- default: show PRAX system status ----------------------------------- status = get_system_status() console.print("\n📊 PRAX System Status") @@ -65,6 +69,38 @@ def handle_command(command: str, args: List[str]) -> bool: return True +def _handle_sync() -> bool: + """Run the status sync handler and display results.""" + console.print() + console.print("[bold cyan]Syncing branch status...[/bold cyan]") + + try: + result = sync_status() + except Exception as exc: + error(f"Status sync failed: {exc}") + logger.error("Status sync failed: %s", exc) + return True + + if result["status"] == "error": + error("Status sync encountered an error — check logs for details.") + return True + + synced = result["branches_synced"] + missing = result["branches_missing"] + + success(f"STATUS.md written — {len(synced)} branches synced") + + if missing: + warning( + f"{len(missing)} branches missing STATUS.local.md", + details=", ".join(missing), + ) + + console.print(f" [dim]Timestamp: {result['timestamp']}[/dim]") + console.print() + return True + + def print_introspection(): """Display module introspection info.""" console.print() diff --git a/src/aipass/prax/dev.local.md b/src/aipass/prax/dev.local.md new file mode 100644 index 00000000..b076c93d --- /dev/null +++ b/src/aipass/prax/dev.local.md @@ -0,0 +1,15 @@ +# dev.local.md - PRAX +``` +Branch: /home/coder/workspace/AIPass/src/aipass/prax +Created: 2026-03-07 +``` + +## Issues + +- + +--- + +## Todos + +- diff --git a/src/aipass/seedgo/dev.local.md b/src/aipass/seedgo/dev.local.md new file mode 100644 index 00000000..e2a7133f --- /dev/null +++ b/src/aipass/seedgo/dev.local.md @@ -0,0 +1,35 @@ +# dev.local.md - SEEDGO +``` +Branch: /home/coder/workspace/AIPass/src/aipass/seedgo +Created: 2026-03-07 +``` + +## Issues + +- + +--- + +## Todos + +### PLAN: Diagnostics Architecture Refactor +**Status:** completed + +1. Move `aipass_standards/diagnostics_check.py` → `handlers/diagnostics/diagnostics_check.py` (shared orchestrator) +2. Move `aipass_standards/diagnostics_content.py` + `diagnostics.md` → `aipass_standards/.sorting_unprocessed/.archive/` (not a standard triplet) +3. Create `aipass_standards/diagnostics.json` — `{"python": true}` (per-pack config) +4. Update `diagnostics_check.py` to read pack's `diagnostics.json`, call enabled runners from `handlers/diagnostics/` +5. Update `branch_audit.py` — discover diagnostics from `handlers/diagnostics/` not from pack's `*_check.py` +6. Verify: `drone @seedgo audit aipass seedgo` + +### Type_Check → Diagnostics Merge +**Status:** completed + +- Removed separate `scores["type_check"]` from branch_audit.py +- Fixed undefined `diag` variable → now reads from `results["diagnostics"]` +- Verified: audit shows 23 standards, no Type_Check line, Diagnostics 100% + +### Remaining for 100% +- Missing `docs/` directory (Architecture -7%) +- Missing `dropbox/` directory (Architecture) +- Readme score 83% (Readme standard) diff --git a/src/aipass/spawn/dev.local.md b/src/aipass/spawn/dev.local.md new file mode 100644 index 00000000..ba6950cd --- /dev/null +++ b/src/aipass/spawn/dev.local.md @@ -0,0 +1,15 @@ +# dev.local.md - spawn +``` +Branch: src/aipass/spawn +Created: 2026-03-05 +``` + +## Issues + +- None currently + +--- + +## Todos + +- None currently diff --git a/src/aipass/spawn/templates/builder/dev.local.md b/src/aipass/spawn/templates/builder/dev.local.md new file mode 100644 index 00000000..b9ef75e7 --- /dev/null +++ b/src/aipass/spawn/templates/builder/dev.local.md @@ -0,0 +1,23 @@ +# dev.local.md - {{BRANCHNAME}} +``` +Branch: {{CWD}} +Created: {{DATE}} +``` + +## Active Work + +- + +## Issues + +- + +--- + +## Todos + +- + +## Notepad + +- diff --git a/src/aipass/trigger/dev.local.md b/src/aipass/trigger/dev.local.md new file mode 100644 index 00000000..fd57e5f6 --- /dev/null +++ b/src/aipass/trigger/dev.local.md @@ -0,0 +1,15 @@ +# dev.local.md - TRIGGER +``` +Branch: /home/coder/workspace/AIPass/src/aipass/trigger +Created: 2026-03-07 +``` + +## Issues + +- + +--- + +## Todos + +- diff --git a/src/commons/dev.local.md b/src/commons/dev.local.md new file mode 100644 index 00000000..fdb1107c --- /dev/null +++ b/src/commons/dev.local.md @@ -0,0 +1,15 @@ +# dev.local.md - commons +``` +Branch: src/commons +Created: 2026-03-10 +``` + +## Issues + +- + +--- + +## Todos + +- diff --git a/src/skills/dev.local.md b/src/skills/dev.local.md new file mode 100644 index 00000000..c3f48119 --- /dev/null +++ b/src/skills/dev.local.md @@ -0,0 +1,15 @@ +# dev.local.md - skills +``` +Branch: src/skills +Created: 2026-03-10 +``` + +## Issues + +- + +--- + +## Todos + +-