feat(system): STATUS board, flow DPLAN fixes, prompt architecture, dev.local.md (#32)

STATUS board: per-branch STATUS.local.md with central aggregation via
drone @prax status sync. Prax handler scans AIPASS_REGISTRY.json and
builds collapsible STATUS.md at repo root (14 operational, 1 in-progress).

Flow: migrated all 10 DPLAN handler files from Dev-Pass paths to
Path(__file__)-based resolution. Fixed FPLAN template detection
(is_template_content was deleting plans with real content).

Prompts: breadcrumbs philosophy in global prompt, dev.local.md awareness,
devpulse branch prompt revised (expertise table, working habits, git hygiene).

ai_mail: sender identity fix propagated, email.py handler refactoring.

Prax: status sync handler, dashboard command collision fix, central/reader.py.

dev.local.md seeded across all 15 branches + spawn template. Gitignore
updated (dev.local.md, STATUS.local.md, CLOSED_PLANS.local.json, backup_data).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AIPass
2026-03-10 13:36:05 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 0cfb440ae3
commit fc0de2a76f
75 changed files with 1982 additions and 338 deletions
+23
View File
@@ -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 <topic>` — 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).
+5 -5
View File
@@ -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."
+4 -1
View File
@@ -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/
+1 -11
View File
@@ -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).
+346
View File
@@ -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
---
<details><summary><strong>@ai_mail</strong> — Operational (2026-03-10)</summary>
# @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)
</details>
<details><summary><strong>@api</strong> — Operational (2026-03-10)</summary>
# @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
</details>
<details><summary><strong>@backup</strong> — Operational (2026-03-10)</summary>
# @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/
</details>
<details><summary><strong>@cli</strong> — Operational (2026-03-10)</summary>
# @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
</details>
<details><summary><strong>@commons</strong> — In Progress (2026-03-10)</summary>
# @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
</details>
<details><summary><strong>@daemon</strong> — Operational (2026-03-10)</summary>
# @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
</details>
<details><summary><strong>@devpulse</strong> — Operational (2026-03-10)</summary>
# @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
</details>
<details><summary><strong>@drone</strong> — Operational (2026-03-10)</summary>
# @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
</details>
<details><summary><strong>@flow</strong> — Operational (2026-03-10)</summary>
# @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)
</details>
<details><summary><strong>@memory</strong> — Operational (2026-03-10)</summary>
# @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
</details>
<details><summary><strong>@prax</strong> — Operational (2026-03-10)</summary>
# @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)
</details>
<details><summary><strong>@seedgo</strong> — Operational (2026-03-10)</summary>
# @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
</details>
<details><summary><strong>@skills</strong> — Operational (2026-03-10)</summary>
# @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
</details>
<details><summary><strong>@spawn</strong> — Operational (2026-03-10)</summary>
# @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
</details>
<details><summary><strong>@trigger</strong> — Operational (2026-03-10)</summary>
# @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
</details>
+3 -1
View File
@@ -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
@@ -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)
@@ -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
@@ -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
@@ -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
@@ -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)
@@ -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
@@ -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)
# =============================================================================
@@ -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:
@@ -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
@@ -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
@@ -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:
@@ -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,
+5 -1
View File
@@ -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
)
@@ -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)
@@ -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
@@ -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 {}
+5 -2
View File
@@ -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
+15
View File
@@ -0,0 +1,15 @@
# dev.local.md - ai_mail
```
Branch: src/aipass/ai_mail
Created: 2026-03-07
```
## Issues
-
---
## Todos
-
+1 -5
View File
@@ -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 =
+636
View File
@@ -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"
+15
View File
@@ -0,0 +1,15 @@
# dev.local.md - API
```
Branch: /home/coder/workspace/AIPass/src/aipass/api
Created: 2026-03-07
```
## Issues
-
---
## Todos
-
@@ -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
@@ -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
@@ -0,0 +1,8 @@
{
"module_name": "{{MODULE_NAME}}",
"version": "1.0.0",
"config": {
"enabled": true,
"max_log_entries": 100
}
}
@@ -0,0 +1,4 @@
{
"created": "{{TIMESTAMP}}",
"last_updated": "{{TIMESTAMP}}"
}
@@ -0,0 +1 @@
[]
+15
View File
@@ -0,0 +1,15 @@
# dev.local.md - BACKUP
```
Branch: /home/patrick/Projects/AIPass/src/aipass/backup
Created: 2026-03-07
```
## Issues
-
---
## Todos
-
+15
View File
@@ -0,0 +1,15 @@
# dev.local.md - CLI
```
Branch: /home/coder/workspace/AIPass/src/aipass/cli
Created: 2026-03-07
```
## Issues
-
---
## Todos
-
+15
View File
@@ -0,0 +1,15 @@
# dev.local.md - DAEMON
```
Branch: /home/patrick/Projects/AIPass/src/aipass/daemon
Created: 2026-03-07
```
## Issues
-
---
## Todos
-
@@ -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.
+56
View File
@@ -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
+15
View File
@@ -0,0 +1,15 @@
# dev.local.md - DRONE
```
Branch: /home/coder/workspace/AIPass/src/aipass/drone
Created: 2026-03-07
```
## Issues
-
---
## Todos
-
+25
View File
@@ -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"
}
]
}
+38 -23
View File
@@ -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 <command> [args...][/dim]")
console.print(" [dim]python3 flow.py --help[/dim]")
console.print(" [dim]drone @flow <command> [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 <command> --help[/dim]")
console.print(" [dim]drone @flow <command> --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
@@ -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
"""
+4 -8
View File
@@ -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"
# =============================================================================
@@ -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"
# =============================================================================
@@ -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"}
@@ -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"
# =============================================================================
@@ -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"
# =============================================================================
+17 -26
View File
@@ -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 <subcommand> [options]
drone @flow plan <subcommand> [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]",
""
]
+3 -7
View File
@@ -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")
@@ -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:
@@ -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"
# =============================================================================
@@ -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"]
@@ -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}}
+35 -10
View File
@@ -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:
+14 -16
View File
@@ -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():
@@ -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()
+34
View File
@@ -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)
@@ -0,0 +1,6 @@
{
"DPLAN": {
"next_number": 2
},
"next_number": 2
}
@@ -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}}*
+1 -1
View File
@@ -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()
+2 -1
View File
@@ -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
+1 -1
View File
@@ -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()
+15
View File
@@ -0,0 +1,15 @@
# dev.local.md - MEMORY
```
Branch: /home/patrick/Projects/AIPass/src/aipass/memory
Created: 2026-03-07
```
## Issues
-
---
## Todos
-
@@ -0,0 +1,3 @@
from .reader import read_all_centrals
__all__ = ["read_all_centrals"]
@@ -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
@@ -0,0 +1,3 @@
from .sync import sync_status
__all__ = ["sync_status"]
@@ -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
<details> 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"<details><summary><strong>{branch_email}</strong> — {state} ({last_update})</summary>"
)
lines.append("")
lines.append(content.rstrip())
lines.append("")
lines.append("</details>")
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,
}
+17 -10
View File
@@ -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():
+49 -13
View File
@@ -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()
+15
View File
@@ -0,0 +1,15 @@
# dev.local.md - PRAX
```
Branch: /home/coder/workspace/AIPass/src/aipass/prax
Created: 2026-03-07
```
## Issues
-
---
## Todos
-
+35
View File
@@ -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)
+15
View File
@@ -0,0 +1,15 @@
# dev.local.md - spawn
```
Branch: src/aipass/spawn
Created: 2026-03-05
```
## Issues
- None currently
---
## Todos
- None currently
@@ -0,0 +1,23 @@
# dev.local.md - {{BRANCHNAME}}
```
Branch: {{CWD}}
Created: {{DATE}}
```
## Active Work
-
## Issues
-
---
## Todos
-
## Notepad
-
+15
View File
@@ -0,0 +1,15 @@
# dev.local.md - TRIGGER
```
Branch: /home/coder/workspace/AIPass/src/aipass/trigger
Created: 2026-03-07
```
## Issues
-
---
## Todos
-
+15
View File
@@ -0,0 +1,15 @@
# dev.local.md - commons
```
Branch: src/commons
Created: 2026-03-10
```
## Issues
-
---
## Todos
-
+15
View File
@@ -0,0 +1,15 @@
# dev.local.md - skills
```
Branch: src/skills
Created: 2026-03-10
```
## Issues
-
---
## Todos
-