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:
co-authored by
Claude Opus 4.6
parent
0cfb440ae3
commit
fc0de2a76f
@@ -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,
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# dev.local.md - ai_mail
|
||||
```
|
||||
Branch: src/aipass/ai_mail
|
||||
Created: 2026-03-07
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
-
|
||||
@@ -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
@@ -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"
|
||||
@@ -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 @@
|
||||
[]
|
||||
@@ -0,0 +1,15 @@
|
||||
# dev.local.md - BACKUP
|
||||
```
|
||||
Branch: /home/patrick/Projects/AIPass/src/aipass/backup
|
||||
Created: 2026-03-07
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
-
|
||||
@@ -0,0 +1,15 @@
|
||||
# dev.local.md - CLI
|
||||
```
|
||||
Branch: /home/coder/workspace/AIPass/src/aipass/cli
|
||||
Created: 2026-03-07
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
-
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,15 @@
|
||||
# dev.local.md - DRONE
|
||||
```
|
||||
Branch: /home/coder/workspace/AIPass/src/aipass/drone
|
||||
Created: 2026-03-07
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
-
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -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]",
|
||||
""
|
||||
]
|
||||
|
||||
|
||||
@@ -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}}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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}}*
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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():
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# dev.local.md - PRAX
|
||||
```
|
||||
Branch: /home/coder/workspace/AIPass/src/aipass/prax
|
||||
Created: 2026-03-07
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
-
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
-
|
||||
@@ -0,0 +1,15 @@
|
||||
# dev.local.md - TRIGGER
|
||||
```
|
||||
Branch: /home/coder/workspace/AIPass/src/aipass/trigger
|
||||
Created: 2026-03-07
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
-
|
||||
@@ -0,0 +1,15 @@
|
||||
# dev.local.md - commons
|
||||
```
|
||||
Branch: src/commons
|
||||
Created: 2026-03-10
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
-
|
||||
@@ -0,0 +1,15 @@
|
||||
# dev.local.md - skills
|
||||
```
|
||||
Branch: src/skills
|
||||
Created: 2026-03-10
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
-
|
||||
Reference in New Issue
Block a user