feat(system): seedgo v2 operational, full system audit, 14/15 branches at 99%

Three days of intensive work bringing seedgo to full operational status
and driving all branches through comprehensive standards compliance.

Seedgo v2.0.0:
- 22 checkers active (up from 20), standards pack fully operational
- New introspection standard researched from Dev-Pass, FPLAN-0017 open
- Bypass system for false positives (.seedgo config)
- Standards query and audit commands fully functional

System-wide audit (FPLAN-0016):
- All 14 auditable branches at 99%+ compliance
- CLI imports standardized across all branches (console from cli.apps.modules)
- handle_command(command, args) → bool contract added to all modules
- print_help() function naming fixed for checker pattern matching
- Handler extraction: large modules split, file I/O moved to handler layer
- New handlers created across ai_mail, backup, daemon, flow, skills, spawn, seedgo

Branch-specific highlights:
- ai_mail: email.py split 840→420 lines, 4 new handlers
- flow: dplan_flow.py 688→591 lines, 4 new handlers
- seedgo: massive restructure — standards moved to handlers/aipass_standards/,
  old standards/ tree removed, bypass system added, diagnostics module
- commons: database module added, CLI imports fixed
- skills: 5 handle_commands added, help function renamed
- trigger: error reporter handler, handle_command routing
- All branches: consistent architecture, clean drone routing

Culture doc (CLAUDE.md) added — documents AIPass philosophy, identity,
memory system, and collaboration principles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AIOSAI
2026-03-10 01:26:42 -07:00
co-authored by Claude Opus 4.6
parent 09e759a8a4
commit babedd9c64
589 changed files with 17140 additions and 24881 deletions
+107 -3
View File
@@ -1,15 +1,115 @@
# DAEMON
**Purpose:** Cron-triggered task scheduler with plugin system
**Purpose:** Cron-triggered task scheduler with plugin system. Routes commands to modules for scheduled tasks, activity reports, action management, and status digests.
**Module:** `aipass.daemon`
**Created:** 2026-03-07
**Citizen Class:** birthright
**Citizen Class:** builder
**Last Updated:** 2026-03-08
---
## Overview
Birthright citizen — minimal presence with identity and memory.
Builder citizen -- full 3-layer architecture with identity and memory. DAEMON serves as the background orchestration branch: it discovers modules at startup, routes CLI commands to them, and provides introspection and help output via Rich console.
### What I Do
- Route CLI commands to discovered modules (update, schedule, activity_report, actions)
- Provide scheduled task management and follow-ups
- Generate activity reports across branches
- Manage an action registry with toggle, info, reminders, and schedules
- Produce status digest updates
---
## Architecture
```
daemon/
├── __init__.py
├── README.md
├── DASHBOARD.local.json
├── apps/
│ ├── daemon.py # Entry point (CLI) — module discovery + command routing
│ ├── daemon_wakeup.py # Wakeup / cron trigger
│ ├── scheduler_cron.py # Cron scheduler
│ ├── modules/
│ │ ├── update.py # Status digest module — summarizes DAEMON activity
│ │ ├── schedule.py # Scheduled follow-ups — fire-and-forget task management
│ │ ├── activity_report.py # Branch activity report generator
│ │ └── actions.py # Action registry CLI — list, toggle, info, reminders
│ ├── handlers/
│ │ ├── actions/
│ │ │ └── actions_registry.py # Action registry implementation
│ │ ├── json/
│ │ │ └── json_handler.py # JSON data operations
│ │ ├── monitoring/
│ │ │ ├── activity_collector.py # Collects branch activity data
│ │ │ ├── memory_health.py # Memory health checks
│ │ │ └── red_flag_detector.py # Detects anomalies / red flags
│ │ ├── schedule/
│ │ │ ├── task_registry.py # Task registry for scheduled items
│ │ │ ├── assistant_notifier.py # Assistant notification dispatch
│ │ │ └── telegram_notifier.py # Telegram notification dispatch
│ │ ├── telegram/
│ │ │ └── assistant_chat.py # Telegram assistant chat handler
│ │ └── update/
│ │ └── data_loader.py # Data loading for status digests
│ ├── extensions/ # Extension point for additional capabilities
│ ├── json_templates/ # JSON template definitions
│ └── plugins/
│ ├── botfather_reminder.py # BotFather reminder plugin
│ ├── community_rotation.py # Community rotation plugin
│ ├── daily_audit.py # Daily audit plugin
│ ├── dev_central_monitor.py # Dev-Central monitor plugin
│ └── heartbeat.py # Heartbeat / liveness plugin
├── daemon_json/ # JSON tracking data
├── docs/ # Documentation
├── tools/ # Branch verification utilities
└── tests/ # Test suite
```
---
## Commands / Usage
```bash
drone @daemon # Show discovered modules (introspection)
drone @daemon --help # Rich-formatted help with all commands
drone @daemon --version # Print version
drone @daemon update [args...] # Status digest — summarize DAEMON activity
drone @daemon schedule [args...] # Manage scheduled follow-ups and tasks
drone @daemon activity_report [args...] # Generate branch activity reports
drone @daemon actions [args...] # Action registry — list, toggle, info, set reminder/schedule
```
Each module accepts `--help` for module-specific usage:
```bash
drone @daemon <command> --help
```
---
## Modules
| Module | Description |
|--------|-------------|
| `update` | Status digest of DAEMON activity |
| `schedule` | Fire-and-forget scheduled follow-ups and task management |
| `activity_report` | Branch activity report generator (plain text output) |
| `actions` | Action registry CLI -- list, toggle, info, set reminder, set schedule, plugin migration |
---
## Integration Points
### Depends On
- `rich` -- Console output and formatted display
- Python stdlib (`sys`, `typing`, `logging`)
### Provides To
- All modules -- background task scheduling, activity monitoring, action tracking
- Plugins -- extensible plugin system for recurring tasks (heartbeat, daily audit, community rotation, etc.)
---
@@ -19,3 +119,7 @@ Birthright citizen — minimal presence with identity and memory.
- **Session History:** `.trinity/local.json`
- **Observations:** `.trinity/observations.json`
- **Branch Prompt:** `.aipass/branch_system_prompt.md`
---
*Last Updated: 2026-03-08*
+7 -16
View File
@@ -1,16 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: daemon.py - DAEMON Branch Entry Point
# Date: 2026-01-21
# =================== AIPass ====================
# Name: daemon.py
# Description: Entry point CLI for drone @daemon
# Version: 1.0.0
# Category: daemon
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-01-21): Initial branch creation
#
# CODE STANDARDS:
# - Handlers implement logic, modules orchestrate
# Created: 2026-03-08
# Modified: 2026-03-08
# =============================================
"""
@@ -26,12 +19,10 @@ import sys
from typing import List, Any
# Logger
import logging
logger = logging.getLogger(__name__)
from aipass.prax.apps.modules.logger import system_logger as logger
# Console
from rich.console import Console
console = Console()
from aipass.cli.apps.modules import console
def _header(text):
console.print(f"\n[bold cyan]{'='*70}[/bold cyan]")
+49 -23
View File
@@ -1,19 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: daemon_wakeup.py - DAEMON Wake-Up Cron Trigger
# Date: 2026-02-15
# =================== AIPass ====================
# Name: daemon_wakeup.py
# Description: DAEMON Wake-Up Cron Trigger
# Version: 1.0.0
# Category: daemon/apps
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-02-15): Initial implementation - cron-triggered wake-up checker
#
# CODE STANDARDS:
# - Handlers implement logic, modules orchestrate
# - No Rich console (headless cron execution)
# - Stdout logging (cron redirects to logs/daemon_wakeup.log)
# - stdlib only + optional imports
# Created: 2026-02-15
# Modified: 2026-02-15
# =============================================
"""
@@ -40,21 +30,23 @@ import time
from pathlib import Path
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
from aipass.prax import logger
# logger imported from aipass.prax
from aipass.cli.apps.modules import console
# =============================================
# OPTIONAL IMPORTS
# OPTIONAL IMPORTS (via module layer)
# =============================================
# Telegram notifications (optional) — use absolute imports for standalone script
# Telegram notifications (optional) — route through modules, not handlers directly
try:
from aipass.daemon.apps.handlers.schedule.assistant_notifier import (
from aipass.daemon.apps.modules.wakeup_ops import (
notify_wakeup,
notify_report,
notify_error,
TELEGRAM_AVAILABLE,
)
TELEGRAM_AVAILABLE = True
except ImportError:
TELEGRAM_AVAILABLE = False
notify_wakeup = None
@@ -76,10 +68,34 @@ INBOX_PATH = _DAEMON_ROOT / "ai_mail.local" / "inbox.json"
# LOGGING
# =============================================
def print_introspection():
"""Display module introspection info."""
console.print()
console.print("daemon_wakeup Module")
console.print("Cron trigger for daemon wake-up inbox checking and reporting")
console.print()
console.print("Connected Handlers:")
console.print(" modules/")
console.print(" - wakeup_ops.py (notify_wakeup, notify_report, notify_error — daemon bot Telegram notifications)")
console.print()
def print_help() -> None:
"""Display usage information for daemon_wakeup."""
console.print("\n[bold cyan]daemon_wakeup.py - DAEMON Wake-Up Cron Trigger[/bold cyan]")
console.print("\n[yellow]USAGE:[/yellow]")
console.print(" python daemon_wakeup.py Run the wake-up checker")
console.print(" python daemon_wakeup.py --help Show this help message")
console.print("\n[yellow]DESCRIPTION:[/yellow]")
console.print(" Checks daemon's email inbox and sends summary reports.")
console.print(" Intended to be called periodically by cron.")
console.print()
def log(message: str) -> None:
"""Print timestamped log line to stdout (captured by cron redirect)."""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] {message}", flush=True)
console.print(f"[{timestamp}] {message}")
# =============================================
@@ -211,6 +227,16 @@ def main() -> int:
Returns:
0 on success, 1 on error
"""
args = sys.argv[1:]
if not args:
print_introspection()
return 0
if args[0] in ['--help', '-h']:
print_help()
sys.exit(0)
log("=" * 60)
log("Daemon wake-up triggered")
@@ -287,7 +313,7 @@ if __name__ == "__main__":
except Exception as e:
# Last-resort catch -- never crash silently
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] FATAL: Unhandled exception: {e}", flush=True)
console.print(f"[{timestamp}] FATAL: Unhandled exception: {e}")
if TELEGRAM_AVAILABLE:
try:
notify_error(f"FATAL: {e}")
@@ -1,22 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: actions_registry.py - Numbered Action Registry
# Date: 2026-03-02
# =================== AIPass ====================
# Name: actions_registry.py
# Description: Numbered Action Registry
# Version: 1.0.0
# Category: daemon/handlers/actions
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-02): Initial creation - DPLAN-043
# * Sequential 4-digit IDs (0001-9999)
# * JSON storage with CRUD operations
# * Plugin auto-migration from PLUGIN_CONFIG
# * Due-checking logic for all schedule types
# * Supports: plugin, schedule, reminder action types
#
# CODE STANDARDS:
# - Handler independence: pure business logic
# - No Rich console (headless compatible)
# Created: 2026-03-02
# Modified: 2026-03-02
# =============================================
"""
@@ -35,12 +22,12 @@ Action types:
"""
import json
import logging
from aipass.prax import logger
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional
logger = logging.getLogger(__name__)
# logger imported from aipass.prax
# Paths
_DAEMON_ROOT = Path(__file__).resolve().parents[3] # src/aipass/daemon/
@@ -1,19 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: json_handler.py - JSON Auto-Creating Handler
# Date: 2025-11-21
# =================== AIPass ====================
# Name: json_handler.py
# Description: JSON Auto-Creating Handler
# Version: 1.2.0
# Category: daemon/handlers/json
#
# CHANGELOG (Max 5 entries):
# - v1.2.0 (2026-01-29): Fixed paths to point to DAEMON branch
# - v1.1.0 (2025-11-21): Refactored to comply with error handling
# - v1.0.0 (2025-11-13): Initial JSON auto-creation system
#
# CODE STANDARDS:
# - Pure functions with proper error raising
# - No Prax imports (handler tier 3)
# Created: 2025-11-21
# Modified: 2026-01-29
# =============================================
"""
@@ -1,19 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: activity_collector.py - Branch Activity Data Collector
# Date: 2026-01-30
# =================== AIPass ====================
# Name: activity_collector.py
# Description: Branch Activity Data Collector
# Version: 0.1.0
# Category: daemon/handlers/monitoring
#
# CHANGELOG (Max 5 entries):
# - v0.1.0 (2026-01-30): Initial implementation - FPLAN-0266 Phase 1
#
# CODE STANDARDS:
# - Handler implements collection logic, module orchestrates
# - NO Prax logger in handlers (handlers don't log)
# - Uses pathlib for all path operations
# - Standard library only
# Created: 2026-01-30
# Modified: 2026-01-30
# =============================================
"""
@@ -1,19 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: memory_health.py - Branch Memory Health Checker
# Date: 2026-01-30
# =================== AIPass ====================
# Name: memory_health.py
# Description: Branch Memory Health Checker
# Version: 0.1.0
# Category: daemon/handlers/monitoring
#
# CHANGELOG (Max 5 entries):
# - v0.1.0 (2026-01-30): Initial implementation - FPLAN-0266 Phase 1
#
# CODE STANDARDS:
# - Handler implements checking logic, module orchestrates
# - NO Prax logger in handlers (handlers don't log)
# - Uses pathlib for all path operations
# - Standard library only
# Created: 2026-01-30
# Modified: 2026-01-30
# =============================================
"""
@@ -1,19 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: red_flag_detector.py - Branch Red Flag Detection Engine
# Date: 2026-01-30
# =================== AIPass ====================
# Name: red_flag_detector.py
# Description: Branch Red Flag Detection Engine
# Version: 0.1.0
# Category: daemon/handlers/monitoring
#
# CHANGELOG (Max 5 entries):
# - v0.1.0 (2026-01-30): Initial implementation - FPLAN-0266 Phase 2
#
# CODE STANDARDS:
# - Handler implements detection logic, module orchestrates
# - NO Prax logger in handlers (handlers don't log)
# - Uses pathlib for all path operations
# - Standard library only
# Created: 2026-01-30
# Modified: 2026-01-30
# =============================================
"""
@@ -0,0 +1,490 @@
# =================== AIPass ====================
# Name: report_generator.py
# Description: Activity Report Generator Handler
# Version: 1.0.0
# Created: 2026-03-08
# Modified: 2026-03-08
# =============================================
"""
Activity Report Generator Handler
Generates formatted CLI-ready activity reports from monitoring data.
Produces human-readable string reports - no display dependencies.
"""
from datetime import datetime, timedelta
from typing import Dict, Any, List, Optional
from aipass.prax import logger
# logger imported from aipass.prax
# Import sibling monitoring handlers
from aipass.daemon.apps.handlers.monitoring import activity_collector
from aipass.daemon.apps.handlers.monitoring import memory_health
from aipass.daemon.apps.handlers.monitoring import red_flag_detector
# =============================================
# CONSTANTS
# =============================================
SYMBOL_OK = "[OK]"
SYMBOL_WARNING = "[!]"
SYMBOL_RED = "[X]"
SYMBOL_INACTIVE = "[-]"
# =============================================
# HELPER FUNCTIONS
# =============================================
def _format_time_ago(iso_timestamp: Optional[str]) -> str:
"""
Format an ISO timestamp as human-readable time ago.
Args:
iso_timestamp: ISO format timestamp string or None.
Returns:
Human-readable string like "2h ago" or "3d ago".
"""
if not iso_timestamp:
return "never"
try:
dt = datetime.fromisoformat(iso_timestamp)
delta = datetime.now() - dt
hours = delta.total_seconds() / 3600
if hours < 1:
minutes = int(delta.total_seconds() / 60)
return f"{minutes}m ago"
elif hours < 24:
return f"{int(hours)}h ago"
else:
days = int(hours / 24)
return f"{days}d ago"
except (ValueError, TypeError):
return "unknown"
def _get_status_symbol(status: str) -> str:
"""
Get CLI symbol for a status code.
Args:
status: Status string (OK, WARNING, RED, NO_ACTIVITY, RED_FLAG, ERROR).
Returns:
ASCII symbol for display.
"""
status_upper = status.upper()
if status_upper in ("OK",):
return SYMBOL_OK
elif status_upper in ("WARNING",):
return SYMBOL_WARNING
elif status_upper in ("RED", "RED_FLAG", "ERROR"):
return SYMBOL_RED
else:
return SYMBOL_INACTIVE
def _box_header(title: str, width: int = 50) -> str:
"""Generate a boxed header line."""
return "=" * width + "\n" + title + "\n" + "=" * width
def _section_header(title: str) -> str:
"""Generate a section header line."""
return f"\n{title}\n" + "-" * len(title)
# =============================================
# DATA AGGREGATION
# =============================================
def _aggregate_data(since_hours: float = 24) -> Dict[str, Any]:
"""
Aggregate data from all monitoring handlers.
Args:
since_hours: Time window in hours to analyze.
Returns:
Dict with combined data from all handlers.
"""
since_timestamp = datetime.now() - timedelta(hours=since_hours)
# Get red flag summary (includes all branch statuses)
red_flag_summary = red_flag_detector.get_red_flag_summary(since_timestamp)
# Get activity data
activity_data = activity_collector.get_all_branch_activity(since_timestamp)
# Get memory health for each branch
branch_paths = activity_collector.get_branch_paths()
memory_health_data = {}
for branch_info in branch_paths:
name = branch_info.get("name", "")
path = branch_info.get("path", "")
if name and path:
health = memory_health.get_memory_health_status(path, name)
memory_health_data[name] = health
return {
"timestamp": datetime.now().isoformat(),
"time_window_hours": since_hours,
"red_flag_summary": red_flag_summary,
"activity_data": activity_data,
"memory_health": memory_health_data,
}
# =============================================
# REPORT GENERATION
# =============================================
def generate_activity_report(
since_hours: float = 24,
verbosity: str = "normal"
) -> str:
"""
Generate a formatted CLI-ready activity report.
Aggregates data from all monitoring handlers and produces a
human-readable report with branch status, red flags, and recommendations.
Args:
since_hours: Time window in hours to analyze (default: 24).
verbosity: Report detail level - "brief", "normal", or "detailed".
Returns:
Formatted string report suitable for CLI display.
"""
data = _aggregate_data(since_hours)
red_flags = data["red_flag_summary"]
activity = data["activity_data"]
health_data = data["memory_health"]
lines: List[str] = []
# Header
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
lines.append(_box_header(f"BRANCH ACTIVITY REPORT - {timestamp}\nTime window: Last {since_hours:.0f} hours"))
# Summary section
total_branches = red_flags.get("total_branches", 0)
active_branches = red_flags.get("ok", 0)
red_flag_count = red_flags.get("red_flags", 0)
no_activity_count = red_flags.get("no_activity", 0)
error_count = red_flags.get("errors", 0)
# Count memory health statuses
health_ok = sum(1 for h in health_data.values() if h.get("overall_status") == "OK")
health_warning = sum(1 for h in health_data.values() if h.get("overall_status") == "WARNING")
health_red = sum(1 for h in health_data.values() if h.get("overall_status") == "RED")
lines.append(_section_header("SUMMARY"))
lines.append(f"Active branches: {active_branches}/{total_branches}")
lines.append(f"RED FLAGS: {red_flag_count}")
lines.append(f"Memory health: {health_ok} OK, {health_warning} warning, {health_red} red")
if error_count > 0:
lines.append(f"Scan errors: {error_count}")
# Red Flags section
lines.append(_section_header("RED FLAGS (requires attention)"))
violations = red_flags.get("violations", [])
if violations:
for v in violations:
branch = v.get("branch_name", "UNKNOWN")
reason = v.get("reason", "Unknown reason")
code_count = v.get("code_change_count", 0)
lines.append(f" {SYMBOL_RED} {branch}")
lines.append(f" Reason: {reason}")
lines.append(f" Code changes: {code_count} files")
else:
lines.append(" [None - all active branches updated memories]")
# Branch Status section
lines.append(_section_header("BRANCH STATUS"))
all_branches = red_flags.get("all_branches", [])
if verbosity == "brief":
# Just counts, no individual branches
pass
else:
for branch_status in all_branches:
name = branch_status.get("branch_name", "UNKNOWN")
status = branch_status.get("status", "UNKNOWN")
reason = branch_status.get("reason", "")
# Get memory health for this branch
branch_health = health_data.get(name, {})
mem_status = branch_health.get("overall_status", "UNKNOWN")
symbol = _get_status_symbol(status)
# Format status line
if status == "NO_ACTIVITY":
lines.append(f" {SYMBOL_INACTIVE} {name} - inactive (no changes)")
elif status == "RED_FLAG":
lines.append(f" {SYMBOL_RED} {name} - RED FLAG: {reason}")
elif status == "ERROR":
lines.append(f" {SYMBOL_RED} {name} - ERROR: {reason}")
else:
# Get memory update time
mem_update = branch_status.get("memory_last_update")
time_ago = _format_time_ago(mem_update)
mem_symbol = _get_status_symbol(mem_status)
if mem_status == "OK":
lines.append(f" {SYMBOL_OK} {name} - OK (memory updated {time_ago})")
else:
lines.append(f" {SYMBOL_WARNING} {name} - {mem_status} (memory updated {time_ago})")
# Detailed mode: show file changes
if verbosity == "detailed" and status != "NO_ACTIVITY":
code_changes = branch_status.get("code_changes", [])
memory_modified = branch_status.get("memory_files_modified", [])
if code_changes:
lines.append(f" Code files: {len(code_changes)}")
for cf in code_changes[:5]:
lines.append(f" - {cf.get('file', 'unknown')}")
if len(code_changes) > 5:
lines.append(f" ... and {len(code_changes) - 5} more")
if memory_modified:
lines.append(f" Memory files: {len(memory_modified)}")
for mf in memory_modified[:3]:
lines.append(f" - {mf.get('file', 'unknown')}")
# Recommendations section
lines.append(_section_header("RECOMMENDATIONS"))
recommendations: List[str] = []
# Add recommendations for red flags
for v in violations:
branch = v.get("branch_name", "UNKNOWN")
recommendations.append(f"- {branch}: Update memory files to reflect code changes")
# Add recommendations for warning health status
for name, health in health_data.items():
if health.get("overall_status") == "WARNING":
issues = health.get("issues", [])
if issues:
recommendations.append(f"- {name}: {issues[0]}")
elif health.get("overall_status") == "RED":
issues = health.get("issues", [])
if issues:
recommendations.append(f"- {name}: URGENT - {issues[0]}")
if recommendations:
for rec in recommendations[:10]: # Limit to 10 recommendations
lines.append(f" {rec}")
if len(recommendations) > 10:
lines.append(f" ... and {len(recommendations) - 10} more")
else:
lines.append(" [None - system healthy]")
lines.append("") # Trailing newline
return "\n".join(lines)
def generate_branch_report(
branch_name: str,
since_hours: float = 24
) -> str:
"""
Generate a detailed report for a single branch.
Provides a deep dive on one branch including all file changes,
memory status, and specific recommendations.
Args:
branch_name: Name of the branch (uppercase, e.g., "DRONE").
since_hours: Time window in hours to analyze.
Returns:
Formatted string report for the specified branch.
"""
since_timestamp = datetime.now() - timedelta(hours=since_hours)
# Find the branch path
branch_paths = activity_collector.get_branch_paths()
branch_path = None
for bp in branch_paths:
if bp.get("name", "").upper() == branch_name.upper():
branch_path = bp.get("path")
branch_name = bp.get("name", branch_name) # Use canonical name
break
if not branch_path:
return f"ERROR: Branch '{branch_name}' not found in registry."
lines: List[str] = []
# Header
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
lines.append(_box_header(f"BRANCH REPORT: {branch_name}\n{timestamp} | Last {since_hours:.0f} hours"))
# Get branch data
red_flag_status = red_flag_detector.get_branch_status(
branch_name, branch_path, since_timestamp
)
health_status = memory_health.get_memory_health_status(branch_path, branch_name)
activity_data = activity_collector.scan_branch_activity(
branch_name, branch_path, since_timestamp
)
# Status Overview
lines.append(_section_header("STATUS OVERVIEW"))
rf_status = red_flag_status.get("status", "UNKNOWN")
rf_reason = red_flag_status.get("reason", "")
mem_status = health_status.get("overall_status", "UNKNOWN")
symbol = _get_status_symbol(rf_status)
lines.append(f"Activity Status: {symbol} {rf_status}")
lines.append(f" Reason: {rf_reason}")
mem_symbol = _get_status_symbol(mem_status)
lines.append(f"Memory Health: {mem_symbol} {mem_status}")
lines.append(f"Path: {branch_path}")
# File Changes
lines.append(_section_header("FILE CHANGES"))
code_files = activity_data.get("code_files", [])
memory_files = activity_data.get("memory_files", [])
lines.append(f"Code files modified: {len(code_files)}")
for cf in code_files:
mtime = _format_time_ago(cf.get("mtime"))
lines.append(f" - {cf.get('name', 'unknown')} ({mtime})")
lines.append(f"\nMemory files modified: {len(memory_files)}")
for mf in memory_files:
mtime = _format_time_ago(mf.get("mtime"))
lines.append(f" - {mf.get('name', 'unknown')} ({mtime})")
if not code_files and not memory_files:
lines.append(" [No files modified in time window]")
# Memory Health Details
lines.append(_section_header("MEMORY HEALTH DETAILS"))
file_check = health_status.get("file_check", {})
lines.append("Required files:")
for fname, exists in file_check.get("required", {}).items():
symbol = SYMBOL_OK if exists else SYMBOL_RED
lines.append(f" {symbol} {fname}")
lines.append("\nOptional files:")
for fname, exists in file_check.get("optional", {}).items():
symbol = SYMBOL_OK if exists else SYMBOL_WARNING
lines.append(f" {symbol} {fname}")
# Freshness
freshness = health_status.get("freshness_checks", {})
if freshness:
lines.append("\nFreshness:")
for fname, fresh in freshness.items():
status = fresh.get("status", "UNKNOWN")
message = fresh.get("message", "")
symbol = _get_status_symbol(status)
lines.append(f" {symbol} {fname}: {message}")
# Issues
issues = health_status.get("issues", [])
if issues:
lines.append(_section_header("ISSUES"))
for issue in issues:
lines.append(f" - {issue}")
# Recommendations
lines.append(_section_header("RECOMMENDATIONS"))
recommendations: List[str] = []
if rf_status == "RED_FLAG":
recommendations.append("Update memory files to document recent code changes")
for issue in issues:
if "Missing required" in issue:
recommendations.append(f"Create missing file: {issue.split(':')[-1].strip()}")
elif "stale" in issue.lower() or "not modified" in issue.lower():
recommendations.append("Review and update memory files")
if recommendations:
for rec in recommendations:
lines.append(f" - {rec}")
else:
lines.append(" [None - branch is healthy]")
lines.append("")
return "\n".join(lines)
def get_json_report(since_hours: float = 24) -> Dict[str, Any]:
"""
Get raw report data as a dictionary for programmatic access.
Useful for storing report snapshots, API responses, or further processing.
Args:
since_hours: Time window in hours to analyze.
Returns:
Dict containing all aggregated data from handlers.
"""
data = _aggregate_data(since_hours)
red_flags = data["red_flag_summary"]
health_data = data["memory_health"]
activity = data["activity_data"]
# Build summary
health_ok = sum(1 for h in health_data.values() if h.get("overall_status") == "OK")
health_warning = sum(1 for h in health_data.values() if h.get("overall_status") == "WARNING")
health_red = sum(1 for h in health_data.values() if h.get("overall_status") == "RED")
summary = {
"total_branches": red_flags.get("total_branches", 0),
"active_branches": red_flags.get("ok", 0),
"red_flags": red_flags.get("red_flags", 0),
"no_activity": red_flags.get("no_activity", 0),
"errors": red_flags.get("errors", 0),
"health_ok": health_ok,
"health_warning": health_warning,
"health_red": health_red,
}
# Build per-branch data
branches_data: Dict[str, Any] = {}
all_branches = red_flags.get("all_branches", [])
for branch_status in all_branches:
name = branch_status.get("branch_name", "")
if not name:
continue
branches_data[name] = {
"red_flag_status": branch_status,
"memory_health": health_data.get(name, {}),
"activity": activity.get("branches", {}).get(name, {}),
}
return {
"timestamp": data["timestamp"],
"time_window_hours": data["time_window_hours"],
"summary": summary,
"violations": red_flags.get("violations", []),
"branches": branches_data,
}
@@ -1,18 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: assistant_notifier.py - Daemon Bot Telegram Notifications
# Date: 2026-02-15
# =================== AIPass ====================
# Name: assistant_notifier.py
# Description: Daemon Bot Telegram Notifications
# Version: 1.0.0
# Category: daemon/handlers/schedule
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-02-15): Initial implementation - daemon bot notifications
#
# CODE STANDARDS:
# - Handlers implement logic, modules orchestrate
# - No cross-branch imports, no Prax logger
# - stdlib only (urllib, json) - runs under /usr/bin/python3
# Created: 2026-02-15
# Modified: 2026-02-15
# =============================================
"""
@@ -31,6 +22,9 @@ from typing import Dict
from urllib.request import Request, urlopen
from urllib.error import URLError
from aipass.prax import logger
# logger imported from aipass.prax
# =============================================
# CONSTANTS
# =============================================
@@ -79,7 +73,7 @@ def send_notification(message: str) -> bool:
try:
config = load_config()
except (FileNotFoundError, KeyError, json.JSONDecodeError) as e:
print(f"[daemon_notifier] Config error: {e}")
logger.error(f"[daemon_notifier] Config error: {e}")
return False
url = f"https://api.telegram.org/bot{config['bot_token']}/sendMessage"
@@ -96,13 +90,13 @@ def send_notification(message: str) -> bool:
result = json.loads(resp.read())
if result.get("ok"):
return True
print(f"[daemon_notifier] API error: {result.get('description')}")
logger.error(f"[daemon_notifier] API error: {result.get('description')}")
return False
except URLError as e:
print(f"[daemon_notifier] Send failed: {e}")
logger.error(f"[daemon_notifier] Send failed: {e}")
return False
except Exception as e:
print(f"[daemon_notifier] Unexpected error: {e}")
logger.error(f"[daemon_notifier] Unexpected error: {e}")
return False
@@ -1,17 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: task_registry.py - DAEMON Scheduled Tasks Registry
# Date: 2026-02-04
# =================== AIPass ====================
# Name: task_registry.py
# Description: DAEMON Scheduled Tasks Registry
# Version: 1.0.0
# Category: daemon/handlers/schedule
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-02-04): Initial implementation - task storage and operations
#
# CODE STANDARDS:
# - Handlers implement logic, modules orchestrate
# - No cross-branch imports, no Prax logger
# Created: 2026-02-04
# Modified: 2026-02-04
# =============================================
"""
@@ -53,6 +45,17 @@ def _ensure_json_exists() -> None:
json.dump(DEFAULT_SCHEDULE_DATA, f, indent=2, ensure_ascii=False)
def ensure_lock_dir() -> Dict[str, Any]:
"""Ensure the daemon_json directory exists for lock files.
Returns:
Dict with 'path' (str) of the lock file directory.
"""
lock_dir = SCHEDULE_JSON_PATH.parent
lock_dir.mkdir(parents=True, exist_ok=True)
return {"path": str(lock_dir)}
def load_tasks() -> List[Dict[str, Any]]:
"""
Load all tasks from schedule.json.
@@ -377,6 +380,137 @@ def get_pending_tasks() -> List[Dict[str, Any]]:
return [t for t in tasks if t.get("status") == "pending"]
# =============================================
# BATCH PROCESSING
# =============================================
def process_due_tasks_batch(
send_email_fn=None,
stale_max_age: int = 5,
) -> Dict[str, Any]:
"""
Process all due tasks: recover stale, dispatch emails, track results.
This is the implementation logic for batch task processing.
The module layer handles display; this handler returns raw data.
Args:
send_email_fn: Callable to send email (to_branch, subject, message, ...).
If None, email dispatch is skipped.
stale_max_age: Maximum minutes before a dispatching task is considered stale.
Returns:
Dict with keys: due, success, failed, recovered, errors (list of str),
processed_tasks (list of dicts with id, recipient, task, status).
"""
import time
results: Dict[str, Any] = {
"due": 0,
"success": 0,
"failed": 0,
"recovered": 0,
"errors": [],
"processed_tasks": [],
}
# Recover any stale dispatches
try:
recovered = recover_stale_dispatches(max_age_minutes=stale_max_age)
results["recovered"] = recovered
except Exception as e:
results["errors"].append(f"Stale recovery: {e}")
# Get due tasks
try:
due_tasks = get_due_tasks()
except Exception as e:
results["errors"].append(f"Load tasks: {e}")
return results
results["due"] = len(due_tasks)
if not due_tasks:
return results
for task in due_tasks:
task_id = task.get("id", "")
recipient = task.get("recipient", "")
task_desc = task.get("task", "")
message = task.get("message", "")
task_result = {
"id": task_id,
"recipient": recipient,
"task": task_desc,
"status": "pending",
}
# Mark as dispatching (prevents re-dispatch)
try:
mark_dispatching(task_id)
except Exception as e:
results["errors"].append(f"Mark dispatching {task_id[:8]}: {e}")
results["failed"] += 1
task_result["status"] = "error"
task_result["error"] = str(e)
results["processed_tasks"].append(task_result)
continue
# Build email body
email_body = f"{task_desc}"
if message:
email_body += f"\n\nDetails:\n{message}"
# Send the email
if send_email_fn is None:
mark_pending(task_id)
results["failed"] += 1
task_result["status"] = "skipped"
task_result["error"] = "email function not available"
results["errors"].append(f"Email unavailable for {task_id[:8]}")
results["processed_tasks"].append(task_result)
continue
try:
email_sent = send_email_fn(
to_branch=recipient,
subject=f"[SCHEDULED] {task_desc}",
message=email_body,
from_branch='@daemon',
auto_execute=True,
reply_to='@dev_central',
)
if email_sent:
mark_completed(task_id)
results["success"] += 1
task_result["status"] = "sent"
else:
mark_pending(task_id)
results["failed"] += 1
task_result["status"] = "failed"
task_result["error"] = "email send returned False"
results["errors"].append(f"Email failed: {task_id[:8]} -> {recipient}")
except Exception as e:
try:
mark_pending(task_id)
except Exception:
pass
results["failed"] += 1
task_result["status"] = "error"
task_result["error"] = str(e)
results["errors"].append(f"Email error {task_id[:8]}: {e}")
results["processed_tasks"].append(task_result)
# Small delay between dispatches (prevents thundering herd)
time.sleep(1.0)
return results
# =============================================
# MAIN - Testing
# =============================================
@@ -1,17 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: telegram_notifier.py - DAEMON Scheduler Telegram Notifications
# Date: 2026-02-15
# =================== AIPass ====================
# Name: telegram_notifier.py
# Description: DAEMON Scheduler Telegram Notifications
# Version: 1.0.0
# Category: daemon/handlers/schedule
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-02-15): Initial implementation - scheduler bot notifications
#
# CODE STANDARDS:
# - Handlers implement logic, modules orchestrate
# - No cross-branch imports, no Prax logger
# Created: 2026-02-15
# Modified: 2026-02-15
# =============================================
"""
@@ -30,6 +22,9 @@ from typing import Dict
from urllib.request import Request, urlopen
from urllib.error import URLError
from aipass.prax import logger
# logger imported from aipass.prax
# =============================================
# CONSTANTS
# =============================================
@@ -78,7 +73,7 @@ def send_notification(message: str) -> bool:
try:
config = load_config()
except (FileNotFoundError, KeyError, json.JSONDecodeError) as e:
print(f"[telegram_notifier] Config error: {e}")
logger.error(f"[telegram_notifier] Config error: {e}")
return False
url = f"https://api.telegram.org/bot{config['bot_token']}/sendMessage"
@@ -95,13 +90,13 @@ def send_notification(message: str) -> bool:
result = json.loads(resp.read())
if result.get("ok"):
return True
print(f"[telegram_notifier] API error: {result.get('description')}")
logger.error(f"[telegram_notifier] API error: {result.get('description')}")
return False
except URLError as e:
print(f"[telegram_notifier] Send failed: {e}")
logger.error(f"[telegram_notifier] Send failed: {e}")
return False
except Exception as e:
print(f"[telegram_notifier] Unexpected error: {e}")
logger.error(f"[telegram_notifier] Unexpected error: {e}")
return False
@@ -1,19 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: assistant_chat.py - Daemon Bot Telegram Launcher
# Date: 2026-02-15
# =================== AIPass ====================
# Name: assistant_chat.py
# Description: Daemon Bot Telegram Launcher
# Version: 2.0.0
# Category: daemon/handlers/telegram
#
# CHANGELOG (Max 5 entries):
# - v2.0.0 (2026-02-15): Thin launcher - delegates to shared direct_chat.py
# - v1.1.0 (2026-02-15): Photo/document support (archived)
# - v1.0.0 (2026-02-15): Initial long-polling + tmux bridge (archived)
#
# CODE STANDARDS:
# - Thin launcher only - all logic lives in direct_chat.py
# - External AI imports made optional
# Created: 2026-02-15
# Modified: 2026-02-15
# =============================================
"""
@@ -27,6 +17,9 @@ This file only provides the daemon-specific configuration.
import sys
from pathlib import Path
from aipass.prax import logger
# logger imported from aipass.prax
try:
from api.apps.modules.telegram_chat import run_direct_chat
TELEGRAM_CHAT_AVAILABLE = True
@@ -37,7 +30,7 @@ except ImportError:
_DAEMON_ROOT = Path(__file__).resolve().parents[3] # src/aipass/daemon/
if not TELEGRAM_CHAT_AVAILABLE:
print("[assistant_chat] telegram_chat module not available, exiting")
logger.error("[assistant_chat] telegram_chat module not available, exiting")
sys.exit(1)
sys.exit(run_direct_chat(
@@ -1,17 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: data_loader.py - DAEMON Data Loading Handler
# Date: 2026-01-29
# =================== AIPass ====================
# Name: data_loader.py
# Description: DAEMON Data Loading Handler
# Version: 1.0.0
# Category: daemon/handlers/update
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-01-29): Initial implementation - data loading for update digest
#
# CODE STANDARDS:
# - Handlers implement logic, modules orchestrate
# - No cross-branch imports, no Prax logger
# Created: 2026-01-29
# Modified: 2026-01-29
# =============================================
"""
+26 -23
View File
@@ -1,20 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: actions.py - Action Registry CLI Module
# Date: 2026-03-02
# =================== AIPass ====================
# Name: actions.py
# Description: Action Registry CLI Module
# Version: 1.0.0
# Category: daemon/modules
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-02): Initial creation - DPLAN-043
# * List, toggle, info, set reminder, set schedule
# * Plugin migration command
# * Formatted Rich console output
#
# CODE STANDARDS:
# - Seed pattern compliance - console.print() for all output
# - Thin orchestration - handlers implement logic
# Created: 2026-03-02
# Modified: 2026-03-02
# =============================================
"""
@@ -29,11 +18,9 @@ import sys
from pathlib import Path
from typing import List
import logging
logger = logging.getLogger(__name__)
from aipass.prax import logger
from rich.console import Console
console = Console()
from aipass.cli.apps.modules import console
def _header(text):
console.print(f"\n[bold cyan]{'='*70}[/bold cyan]")
@@ -63,6 +50,22 @@ from aipass.daemon.apps.handlers.actions.actions_registry import (
MODULE_NAME = "actions"
# =============================================
# INTROSPECTION
# =============================================
def print_introspection():
"""Display module introspection info."""
console.print()
console.print("actions Module")
console.print("CLI interface for the numbered action registry (DPLAN-043)")
console.print()
console.print("Connected Handlers:")
console.print(" handlers/actions/")
console.print(" - actions_registry.py (list_actions, get_action, toggle_action, delete_action, create_action, migrate_plugins, next_due_str — registry CRUD)")
console.print()
# =============================================
# OUTPUT FORMATTING
# =============================================
@@ -169,7 +172,7 @@ def _print_action_detail(action: dict) -> None:
console.print()
def _print_help() -> None:
def print_help() -> None:
"""Display help using Rich formatted output."""
console.print()
_header("Actions -- Numbered Action Registry")
@@ -455,7 +458,7 @@ def handle_command(command: str, args: List[str]) -> bool:
try:
# No args or help
if not args or args[0] in ['--help', '-h', 'help']:
_print_help()
print_help()
return True
subcommand = args[0]
@@ -517,7 +520,7 @@ def main() -> None:
args = sys.argv[1:]
if not args or args[0] in ['--help', '-h', 'help']:
_print_help()
print_help()
return
handle_command('actions', args)
+96 -548
View File
@@ -1,19 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: activity_report.py - Branch Activity Report Generator Module
# Date: 2026-01-30
# Version: 0.1.0
# Category: daemon/modules
#
# CHANGELOG (Max 5 entries):
# - v0.1.0 (2026-01-30): Initial implementation - FPLAN-0266 Phase 3
#
# CODE STANDARDS:
# - Module orchestrates handlers - handlers implement logic
# - Plain text CLI output (no Rich colors for this module)
# - Type hints on all functions
# - Standard library only + handler imports
# =================== AIPass ====================
# Name: activity_report.py
# Description: Branch Activity Report Generator Module
# Version: 0.2.0
# Created: 2026-01-30
# Modified: 2026-03-08
# =============================================
"""
@@ -28,16 +18,19 @@ This is a MODULE (orchestration layer) that coordinates:
- red_flag_detector: Detects presence violations (code changed but memory not updated)
"""
from datetime import datetime, timedelta
from typing import Dict, Any, List, Optional
from typing import Dict, Any, List
import logging
logger = logging.getLogger(__name__)
from aipass.prax import logger
# logger imported from aipass.prax
# Import monitoring handlers using absolute imports
from aipass.daemon.apps.handlers.monitoring import activity_collector
from aipass.daemon.apps.handlers.monitoring import memory_health
from aipass.daemon.apps.handlers.monitoring import red_flag_detector
from aipass.cli.apps.modules import console
# Import report generation handler (implementation lives in handler layer)
from aipass.daemon.apps.handlers.monitoring.report_generator import (
generate_activity_report,
generate_branch_report,
get_json_report,
)
# =============================================
@@ -46,466 +39,21 @@ from aipass.daemon.apps.handlers.monitoring import red_flag_detector
MODULE_NAME = "activity_report"
# Status symbols for CLI output
SYMBOL_OK = "[OK]"
SYMBOL_WARNING = "[!]"
SYMBOL_RED = "[X]"
SYMBOL_INACTIVE = "[-]"
# =============================================
# HELPER FUNCTIONS
# INTROSPECTION
# =============================================
def _format_time_ago(iso_timestamp: Optional[str]) -> str:
"""
Format an ISO timestamp as human-readable time ago.
Args:
iso_timestamp: ISO format timestamp string or None.
Returns:
Human-readable string like "2h ago" or "3d ago".
"""
if not iso_timestamp:
return "never"
try:
dt = datetime.fromisoformat(iso_timestamp)
delta = datetime.now() - dt
hours = delta.total_seconds() / 3600
if hours < 1:
minutes = int(delta.total_seconds() / 60)
return f"{minutes}m ago"
elif hours < 24:
return f"{int(hours)}h ago"
else:
days = int(hours / 24)
return f"{days}d ago"
except (ValueError, TypeError):
return "unknown"
def _get_status_symbol(status: str) -> str:
"""
Get CLI symbol for a status code.
Args:
status: Status string (OK, WARNING, RED, NO_ACTIVITY, RED_FLAG, ERROR).
Returns:
ASCII symbol for display.
"""
status_upper = status.upper()
if status_upper in ("OK",):
return SYMBOL_OK
elif status_upper in ("WARNING",):
return SYMBOL_WARNING
elif status_upper in ("RED", "RED_FLAG", "ERROR"):
return SYMBOL_RED
else:
return SYMBOL_INACTIVE
def _box_header(title: str, width: int = 50) -> str:
"""Generate a boxed header line."""
return "=" * width + "\n" + title + "\n" + "=" * width
def _section_header(title: str) -> str:
"""Generate a section header line."""
return f"\n{title}\n" + "-" * len(title)
# =============================================
# DATA AGGREGATION
# =============================================
def _aggregate_data(since_hours: float = 24) -> Dict[str, Any]:
"""
Aggregate data from all monitoring handlers.
Args:
since_hours: Time window in hours to analyze.
Returns:
Dict with combined data from all handlers.
"""
since_timestamp = datetime.now() - timedelta(hours=since_hours)
# Get red flag summary (includes all branch statuses)
red_flag_summary = red_flag_detector.get_red_flag_summary(since_timestamp)
# Get activity data
activity_data = activity_collector.get_all_branch_activity(since_timestamp)
# Get memory health for each branch
branch_paths = activity_collector.get_branch_paths()
memory_health_data = {}
for branch_info in branch_paths:
name = branch_info.get("name", "")
path = branch_info.get("path", "")
if name and path:
health = memory_health.get_memory_health_status(path, name)
memory_health_data[name] = health
return {
"timestamp": datetime.now().isoformat(),
"time_window_hours": since_hours,
"red_flag_summary": red_flag_summary,
"activity_data": activity_data,
"memory_health": memory_health_data,
}
# =============================================
# REPORT GENERATION
# =============================================
def generate_activity_report(
since_hours: float = 24,
verbosity: str = "normal"
) -> str:
"""
Generate a formatted CLI-ready activity report.
Aggregates data from all monitoring handlers and produces a
human-readable report with branch status, red flags, and recommendations.
Args:
since_hours: Time window in hours to analyze (default: 24).
verbosity: Report detail level - "brief", "normal", or "detailed".
Returns:
Formatted string report suitable for CLI display.
"""
data = _aggregate_data(since_hours)
red_flags = data["red_flag_summary"]
activity = data["activity_data"]
health_data = data["memory_health"]
lines: List[str] = []
# Header
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
lines.append(_box_header(f"BRANCH ACTIVITY REPORT - {timestamp}\nTime window: Last {since_hours:.0f} hours"))
# Summary section
total_branches = red_flags.get("total_branches", 0)
active_branches = red_flags.get("ok", 0)
red_flag_count = red_flags.get("red_flags", 0)
no_activity_count = red_flags.get("no_activity", 0)
error_count = red_flags.get("errors", 0)
# Count memory health statuses
health_ok = sum(1 for h in health_data.values() if h.get("overall_status") == "OK")
health_warning = sum(1 for h in health_data.values() if h.get("overall_status") == "WARNING")
health_red = sum(1 for h in health_data.values() if h.get("overall_status") == "RED")
lines.append(_section_header("SUMMARY"))
lines.append(f"Active branches: {active_branches}/{total_branches}")
lines.append(f"RED FLAGS: {red_flag_count}")
lines.append(f"Memory health: {health_ok} OK, {health_warning} warning, {health_red} red")
if error_count > 0:
lines.append(f"Scan errors: {error_count}")
# Red Flags section
lines.append(_section_header("RED FLAGS (requires attention)"))
violations = red_flags.get("violations", [])
if violations:
for v in violations:
branch = v.get("branch_name", "UNKNOWN")
reason = v.get("reason", "Unknown reason")
code_count = v.get("code_change_count", 0)
lines.append(f" {SYMBOL_RED} {branch}")
lines.append(f" Reason: {reason}")
lines.append(f" Code changes: {code_count} files")
else:
lines.append(" [None - all active branches updated memories]")
# Branch Status section
lines.append(_section_header("BRANCH STATUS"))
all_branches = red_flags.get("all_branches", [])
if verbosity == "brief":
# Just counts, no individual branches
pass
else:
for branch_status in all_branches:
name = branch_status.get("branch_name", "UNKNOWN")
status = branch_status.get("status", "UNKNOWN")
reason = branch_status.get("reason", "")
# Get memory health for this branch
branch_health = health_data.get(name, {})
mem_status = branch_health.get("overall_status", "UNKNOWN")
symbol = _get_status_symbol(status)
# Format status line
if status == "NO_ACTIVITY":
lines.append(f" {SYMBOL_INACTIVE} {name} - inactive (no changes)")
elif status == "RED_FLAG":
lines.append(f" {SYMBOL_RED} {name} - RED FLAG: {reason}")
elif status == "ERROR":
lines.append(f" {SYMBOL_RED} {name} - ERROR: {reason}")
else:
# Get memory update time
mem_update = branch_status.get("memory_last_update")
time_ago = _format_time_ago(mem_update)
mem_symbol = _get_status_symbol(mem_status)
if mem_status == "OK":
lines.append(f" {SYMBOL_OK} {name} - OK (memory updated {time_ago})")
else:
lines.append(f" {SYMBOL_WARNING} {name} - {mem_status} (memory updated {time_ago})")
# Detailed mode: show file changes
if verbosity == "detailed" and status != "NO_ACTIVITY":
code_changes = branch_status.get("code_changes", [])
memory_modified = branch_status.get("memory_files_modified", [])
if code_changes:
lines.append(f" Code files: {len(code_changes)}")
for cf in code_changes[:5]:
lines.append(f" - {cf.get('file', 'unknown')}")
if len(code_changes) > 5:
lines.append(f" ... and {len(code_changes) - 5} more")
if memory_modified:
lines.append(f" Memory files: {len(memory_modified)}")
for mf in memory_modified[:3]:
lines.append(f" - {mf.get('file', 'unknown')}")
# Recommendations section
lines.append(_section_header("RECOMMENDATIONS"))
recommendations: List[str] = []
# Add recommendations for red flags
for v in violations:
branch = v.get("branch_name", "UNKNOWN")
recommendations.append(f"- {branch}: Update memory files to reflect code changes")
# Add recommendations for warning health status
for name, health in health_data.items():
if health.get("overall_status") == "WARNING":
issues = health.get("issues", [])
if issues:
recommendations.append(f"- {name}: {issues[0]}")
elif health.get("overall_status") == "RED":
issues = health.get("issues", [])
if issues:
recommendations.append(f"- {name}: URGENT - {issues[0]}")
if recommendations:
for rec in recommendations[:10]: # Limit to 10 recommendations
lines.append(f" {rec}")
if len(recommendations) > 10:
lines.append(f" ... and {len(recommendations) - 10} more")
else:
lines.append(" [None - system healthy]")
lines.append("") # Trailing newline
return "\n".join(lines)
def generate_branch_report(
branch_name: str,
since_hours: float = 24
) -> str:
"""
Generate a detailed report for a single branch.
Provides a deep dive on one branch including all file changes,
memory status, and specific recommendations.
Args:
branch_name: Name of the branch (uppercase, e.g., "DRONE").
since_hours: Time window in hours to analyze.
Returns:
Formatted string report for the specified branch.
"""
since_timestamp = datetime.now() - timedelta(hours=since_hours)
# Find the branch path
branch_paths = activity_collector.get_branch_paths()
branch_path = None
for bp in branch_paths:
if bp.get("name", "").upper() == branch_name.upper():
branch_path = bp.get("path")
branch_name = bp.get("name", branch_name) # Use canonical name
break
if not branch_path:
return f"ERROR: Branch '{branch_name}' not found in registry."
lines: List[str] = []
# Header
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
lines.append(_box_header(f"BRANCH REPORT: {branch_name}\n{timestamp} | Last {since_hours:.0f} hours"))
# Get branch data
red_flag_status = red_flag_detector.get_branch_status(
branch_name, branch_path, since_timestamp
)
health_status = memory_health.get_memory_health_status(branch_path, branch_name)
activity_data = activity_collector.scan_branch_activity(
branch_name, branch_path, since_timestamp
)
# Status Overview
lines.append(_section_header("STATUS OVERVIEW"))
rf_status = red_flag_status.get("status", "UNKNOWN")
rf_reason = red_flag_status.get("reason", "")
mem_status = health_status.get("overall_status", "UNKNOWN")
symbol = _get_status_symbol(rf_status)
lines.append(f"Activity Status: {symbol} {rf_status}")
lines.append(f" Reason: {rf_reason}")
mem_symbol = _get_status_symbol(mem_status)
lines.append(f"Memory Health: {mem_symbol} {mem_status}")
lines.append(f"Path: {branch_path}")
# File Changes
lines.append(_section_header("FILE CHANGES"))
code_files = activity_data.get("code_files", [])
memory_files = activity_data.get("memory_files", [])
lines.append(f"Code files modified: {len(code_files)}")
for cf in code_files:
mtime = _format_time_ago(cf.get("mtime"))
lines.append(f" - {cf.get('name', 'unknown')} ({mtime})")
lines.append(f"\nMemory files modified: {len(memory_files)}")
for mf in memory_files:
mtime = _format_time_ago(mf.get("mtime"))
lines.append(f" - {mf.get('name', 'unknown')} ({mtime})")
if not code_files and not memory_files:
lines.append(" [No files modified in time window]")
# Memory Health Details
lines.append(_section_header("MEMORY HEALTH DETAILS"))
file_check = health_status.get("file_check", {})
lines.append("Required files:")
for fname, exists in file_check.get("required", {}).items():
symbol = SYMBOL_OK if exists else SYMBOL_RED
lines.append(f" {symbol} {fname}")
lines.append("\nOptional files:")
for fname, exists in file_check.get("optional", {}).items():
symbol = SYMBOL_OK if exists else SYMBOL_WARNING
lines.append(f" {symbol} {fname}")
# Freshness
freshness = health_status.get("freshness_checks", {})
if freshness:
lines.append("\nFreshness:")
for fname, fresh in freshness.items():
status = fresh.get("status", "UNKNOWN")
message = fresh.get("message", "")
symbol = _get_status_symbol(status)
lines.append(f" {symbol} {fname}: {message}")
# Issues
issues = health_status.get("issues", [])
if issues:
lines.append(_section_header("ISSUES"))
for issue in issues:
lines.append(f" - {issue}")
# Recommendations
lines.append(_section_header("RECOMMENDATIONS"))
recommendations: List[str] = []
if rf_status == "RED_FLAG":
recommendations.append("Update memory files to document recent code changes")
for issue in issues:
if "Missing required" in issue:
recommendations.append(f"Create missing file: {issue.split(':')[-1].strip()}")
elif "stale" in issue.lower() or "not modified" in issue.lower():
recommendations.append("Review and update memory files")
if recommendations:
for rec in recommendations:
lines.append(f" - {rec}")
else:
lines.append(" [None - branch is healthy]")
lines.append("")
return "\n".join(lines)
def get_json_report(since_hours: float = 24) -> Dict[str, Any]:
"""
Get raw report data as a dictionary for programmatic access.
Useful for storing report snapshots, API responses, or further processing.
Args:
since_hours: Time window in hours to analyze.
Returns:
Dict containing all aggregated data from handlers.
"""
data = _aggregate_data(since_hours)
red_flags = data["red_flag_summary"]
health_data = data["memory_health"]
activity = data["activity_data"]
# Build summary
health_ok = sum(1 for h in health_data.values() if h.get("overall_status") == "OK")
health_warning = sum(1 for h in health_data.values() if h.get("overall_status") == "WARNING")
health_red = sum(1 for h in health_data.values() if h.get("overall_status") == "RED")
summary = {
"total_branches": red_flags.get("total_branches", 0),
"active_branches": red_flags.get("ok", 0),
"red_flags": red_flags.get("red_flags", 0),
"no_activity": red_flags.get("no_activity", 0),
"errors": red_flags.get("errors", 0),
"health_ok": health_ok,
"health_warning": health_warning,
"health_red": health_red,
}
# Build per-branch data
branches_data: Dict[str, Any] = {}
all_branches = red_flags.get("all_branches", [])
for branch_status in all_branches:
name = branch_status.get("branch_name", "")
if not name:
continue
branches_data[name] = {
"red_flag_status": branch_status,
"memory_health": health_data.get(name, {}),
"activity": activity.get("branches", {}).get(name, {}),
}
return {
"timestamp": data["timestamp"],
"time_window_hours": data["time_window_hours"],
"summary": summary,
"violations": red_flags.get("violations", []),
"branches": branches_data,
}
def print_introspection():
"""Display module introspection info."""
console.print()
console.print("activity_report Module")
console.print("Branch activity report generator — monitors file changes, memory health, and presence violations")
console.print()
console.print("Connected Handlers:")
console.print(" handlers/monitoring/")
console.print(" - report_generator.py (generate_activity_report, generate_branch_report, get_json_report — report generation and JSON output)")
console.print()
# =============================================
@@ -514,71 +62,71 @@ def get_json_report(since_hours: float = 24) -> Dict[str, Any]:
def _print_activity_help() -> None:
"""Display help for the activity command."""
print()
print("=" * 60)
print("ACTIVITY - Quick Activity Summary")
print("=" * 60)
print()
print("USAGE:")
print(" drone @daemon activity")
print(" daemon activity")
print(" daemon activity --hours 48")
print()
print("DESCRIPTION:")
print(" Quick 24-hour activity summary (default).")
print(" Shows branch status, red flags, and recommendations.")
print()
print("OPTIONS:")
print(" --hours N, -t N Time window in hours (default: 24)")
print(" --help, -h Show this help message")
print()
console.print()
console.print("=" * 60)
console.print("ACTIVITY - Quick Activity Summary")
console.print("=" * 60)
console.print()
console.print("USAGE:")
console.print(" drone @daemon activity")
console.print(" daemon activity")
console.print(" daemon activity --hours 48")
console.print()
console.print("DESCRIPTION:")
console.print(" Quick 24-hour activity summary (default).")
console.print(" Shows branch status, red flags, and recommendations.")
console.print()
console.print("OPTIONS:")
console.print(" --hours N, -t N Time window in hours (default: 24)")
console.print(" --help, -h Show this help message")
console.print()
def _print_activity_report_help() -> None:
"""Display help for the activity-report command."""
print()
print("=" * 60)
print("ACTIVITY-REPORT - Full Detailed Report")
print("=" * 60)
print()
print("USAGE:")
print(" drone @daemon activity-report")
print(" daemon activity-report")
print(" daemon activity-report --hours 48")
print(" daemon activity-report --json")
print()
print("DESCRIPTION:")
print(" Full detailed activity report with file-level changes.")
print(" Includes per-branch breakdown and complete recommendations.")
print()
print("OPTIONS:")
print(" --hours N, -t N Time window in hours (default: 24)")
print(" --json, -j Output raw JSON data")
print(" --help, -h Show this help message")
print()
console.print()
console.print("=" * 60)
console.print("ACTIVITY-REPORT - Full Detailed Report")
console.print("=" * 60)
console.print()
console.print("USAGE:")
console.print(" drone @daemon activity-report")
console.print(" daemon activity-report")
console.print(" daemon activity-report --hours 48")
console.print(" daemon activity-report --json")
console.print()
console.print("DESCRIPTION:")
console.print(" Full detailed activity report with file-level changes.")
console.print(" Includes per-branch breakdown and complete recommendations.")
console.print()
console.print("OPTIONS:")
console.print(" --hours N, -t N Time window in hours (default: 24)")
console.print(" --json, -j Output raw JSON data")
console.print(" --help, -h Show this help message")
console.print()
def _print_branch_health_help() -> None:
"""Display help for the branch-health command."""
print()
print("=" * 60)
print("BRANCH-HEALTH - Single Branch Deep Dive")
print("=" * 60)
print()
print("USAGE:")
print(" drone @daemon branch-health DRONE")
print(" daemon branch-health FLOW")
print(" daemon branch-health SEED --hours 48")
print()
print("DESCRIPTION:")
print(" Deep dive report for a single branch.")
print(" Shows all file changes, memory health, and specific recommendations.")
print()
print("OPTIONS:")
print(" <branch_name> Required - branch name (e.g., DRONE, FLOW, SEED)")
print(" --hours N, -t N Time window in hours (default: 24)")
print(" --help, -h Show this help message")
print()
console.print()
console.print("=" * 60)
console.print("BRANCH-HEALTH - Single Branch Deep Dive")
console.print("=" * 60)
console.print()
console.print("USAGE:")
console.print(" drone @daemon branch-health DRONE")
console.print(" daemon branch-health FLOW")
console.print(" daemon branch-health SEED --hours 48")
console.print()
console.print("DESCRIPTION:")
console.print(" Deep dive report for a single branch.")
console.print(" Shows all file changes, memory health, and specific recommendations.")
console.print()
console.print("OPTIONS:")
console.print(" <branch_name> Required - branch name (e.g., DRONE, FLOW, SEED)")
console.print(" --hours N, -t N Time window in hours (default: 24)")
console.print(" --help, -h Show this help message")
console.print()
def _parse_hours_arg(args: List[str]) -> float:
@@ -631,7 +179,7 @@ def handle_command(command: str, args: List[str]) -> bool:
hours = _parse_hours_arg(args)
report = generate_activity_report(since_hours=hours, verbosity="normal")
print(report)
console.print(report)
return True
# Handle 'activity-report' command - detailed report
@@ -646,10 +194,10 @@ def handle_command(command: str, args: List[str]) -> bool:
if '--json' in args or '-j' in args:
import json
data = get_json_report(hours)
print(json.dumps(data, indent=2))
console.print(json.dumps(data, indent=2))
else:
report = generate_activity_report(since_hours=hours, verbosity="detailed")
print(report)
console.print(report)
return True
# Handle 'branch-health' command - single branch report
@@ -674,15 +222,15 @@ def handle_command(command: str, args: List[str]) -> bool:
i += 1
if not branch_name:
print("ERROR: branch-health requires a branch name")
print()
print("Usage: branch-health <branch_name> [--hours N]")
print("Example: branch-health DRONE")
console.print("ERROR: branch-health requires a branch name")
console.print()
console.print("Usage: branch-health <branch_name> [--hours N]")
console.print("Example: branch-health DRONE")
return True
hours = _parse_hours_arg(args)
report = generate_branch_report(branch_name, since_hours=hours)
print(report)
console.print(report)
return True
# Not our command
@@ -729,13 +277,13 @@ def main() -> None:
if args.json:
import json
data = get_json_report(args.hours)
print(json.dumps(data, indent=2))
console.print(json.dumps(data, indent=2))
elif args.branch:
report = generate_branch_report(args.branch, args.hours)
print(report)
console.print(report)
else:
report = generate_activity_report(args.hours, args.verbosity)
print(report)
console.print(report)
if __name__ == "__main__":
+52 -85
View File
@@ -1,18 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: schedule.py - DAEMON Scheduled Follow-ups Module
# Date: 2026-02-04
# =================== AIPass ====================
# Name: schedule.py
# Description: DAEMON Scheduled Follow-ups Module
# Version: 1.0.0
# Category: daemon/modules
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-02-04): Initial implementation - fire-and-forget scheduled tasks
#
# CODE STANDARDS:
# - Seed pattern compliance - console.print() for all output
# - Thin orchestration - handlers implement logic
# - Type hints on all functions
# Created: 2026-02-04
# Modified: 2026-02-04
# =============================================
"""
@@ -28,11 +19,9 @@ import argparse
from pathlib import Path
from typing import List
import logging
logger = logging.getLogger(__name__)
from aipass.prax import logger
from rich.console import Console
console = Console()
from aipass.cli.apps.modules import console
def _header(text):
console.print(f"\n[bold cyan]{'='*70}[/bold cyan]")
@@ -48,7 +37,8 @@ def _error(text):
# Handler imports
from aipass.daemon.apps.handlers.schedule.task_registry import (
load_tasks, create_task, delete_task, get_due_tasks, mark_completed, parse_due_date,
mark_dispatching, mark_pending, recover_stale_dispatches
mark_dispatching, mark_pending, recover_stale_dispatches, process_due_tasks_batch,
ensure_lock_dir,
)
# File lock for single-instance execution
@@ -72,6 +62,22 @@ except ImportError:
MODULE_NAME = "schedule"
# =============================================
# INTROSPECTION
# =============================================
def print_introspection():
"""Display module introspection info."""
console.print()
console.print("schedule Module")
console.print("CLI interface for fire-and-forget scheduled follow-ups")
console.print()
console.print("Connected Handlers:")
console.print(" handlers/schedule/")
console.print(" - task_registry.py (load_tasks, create_task, delete_task, get_due_tasks, mark_completed, parse_due_date, mark_dispatching, mark_pending, recover_stale_dispatches, process_due_tasks_batch, ensure_lock_dir — task CRUD and processing)")
console.print()
_DAEMON_ROOT = Path(__file__).resolve().parents[3] # src/aipass/daemon/
JSON_DIR = _DAEMON_ROOT / "daemon_json"
@@ -247,7 +253,7 @@ def _handle_run_due(_args: List[str]) -> bool:
return _process_due_tasks()
lock_file = JSON_DIR / "schedule.lock"
lock_file.parent.mkdir(parents=True, exist_ok=True)
ensure_lock_dir()
# Try to acquire lock (non-blocking)
lock = FileLock(lock_file, timeout=0)
@@ -260,86 +266,47 @@ def _handle_run_due(_args: List[str]) -> bool:
def _process_due_tasks() -> bool:
"""Process due tasks sequentially with status tracking."""
import time
"""Process due tasks -- delegates to handler, formats output."""
try:
# Recover any stale dispatches (stuck > 5 minutes)
recovered = recover_stale_dispatches(max_age_minutes=5)
if recovered:
console.print(f"[dim]Recovered {recovered} stale dispatch(es)[/dim]")
# Delegate to handler for all implementation logic
email_fn = send_email_direct if AI_MAIL_AVAILABLE else None
results = process_due_tasks_batch(send_email_fn=email_fn, stale_max_age=5)
due_tasks = get_due_tasks()
# Display results (module responsibility)
if results["recovered"]:
console.print(f"[dim]Recovered {results['recovered']} stale dispatch(es)[/dim]")
if not due_tasks:
if results["due"] == 0:
console.print("[dim]No tasks due at this time.[/dim]")
return True
console.print()
_header(f"Running {len(due_tasks)} Due Task(s)")
_header(f"Running {results['due']} Due Task(s)")
console.print()
success_count = 0
fail_count = 0
for task_result in results.get("processed_tasks", []):
task_id = task_result.get("id", "")[:8]
recipient = task_result.get("recipient", "")
task_desc = task_result.get("task", "")[:40]
status = task_result.get("status", "")
for task in due_tasks:
task_id = task.get("id", "")
recipient = task.get("recipient", "")
task_desc = task.get("task", "")
message = task.get("message", "")
# Mark as dispatching (prevents re-dispatch)
mark_dispatching(task_id)
# Build email body
email_body = f"{task_desc}"
if message:
email_body += f"\n\nDetails:\n{message}"
# Send the email
if not AI_MAIL_AVAILABLE:
mark_pending(task_id)
if status == "sent":
_success(f"Sent to {recipient}: {task_desc}")
logger.info(f"[DAEMON] Scheduled email sent: {task_id} -> {recipient}")
elif status == "skipped":
_error(f"ai_mail not available, cannot send to {recipient}")
fail_count += 1
continue
try:
email_sent = send_email_direct(
to_branch=recipient,
subject=f"[SCHEDULED] {task_desc}",
message=email_body,
from_branch='@daemon',
auto_execute=True,
reply_to='@dev_central'
)
if email_sent:
mark_completed(task_id)
_success(f"Sent to {recipient}: {task_desc[:40]}")
success_count += 1
logger.info(f"[DAEMON] Scheduled email sent: {task_id[:8]} -> {recipient}")
else:
# Reset to pending for retry
mark_pending(task_id)
_error(f"Failed to send to {recipient}: {task_desc[:40]}")
fail_count += 1
logger.error(f"[DAEMON] Scheduled email failed: {task_id[:8]} -> {recipient}")
except Exception as e:
# Reset to pending for retry
mark_pending(task_id)
_error(f"Error sending to {recipient}: {e}")
fail_count += 1
logger.error(f"[DAEMON] Scheduled email error: {task_id[:8]} -> {recipient}: {e}")
# Small delay between dispatches (prevents thundering herd)
time.sleep(1.0)
elif status == "failed":
_error(f"Failed to send to {recipient}: {task_desc}")
logger.error(f"[DAEMON] Scheduled email failed: {task_id} -> {recipient}")
elif status == "error":
_error(f"Error sending to {recipient}: {task_result.get('error', '')}")
logger.error(f"[DAEMON] Scheduled email error: {task_id} -> {recipient}: {task_result.get('error', '')}")
console.print()
console.print(f"[bold]Results:[/bold] {success_count} sent, {fail_count} failed")
console.print(f"[bold]Results:[/bold] {results['success']} sent, {results['failed']} failed")
console.print()
return fail_count == 0
return results["failed"] == 0
except Exception as e:
_error(f"Failed to run due tasks: {e}")
@@ -0,0 +1,134 @@
# =================== AIPass ====================
# Name: scheduler_ops.py
# Description: Scheduler Cron Operations Module
# Version: 1.0.0
# Created: 2026-03-08
# Modified: 2026-03-08
# =============================================
"""
Scheduler operations module -- facade for cron entry point.
Provides a clean module-layer interface over handler functions
used by scheduler_cron.py. Entry-level scripts import from
this module instead of reaching into handlers directly.
"""
from aipass.prax import logger
try:
from aipass.cli.apps.modules.display import console
except ImportError:
from rich.console import Console
console = Console()
# =============================================
# TELEGRAM NOTIFICATIONS
# =============================================
try:
from aipass.daemon.apps.handlers.schedule.telegram_notifier import (
notify_triggered,
notify_complete,
notify_error,
)
TELEGRAM_AVAILABLE = True
except ImportError:
TELEGRAM_AVAILABLE = False
notify_triggered = None
notify_complete = None
notify_error = None
# =============================================
# TASK REGISTRY
# =============================================
try:
from aipass.daemon.apps.handlers.schedule.task_registry import (
get_due_tasks,
mark_dispatching,
mark_completed,
mark_pending,
recover_stale_dispatches,
)
TASK_REGISTRY_AVAILABLE = True
except ImportError:
TASK_REGISTRY_AVAILABLE = False
get_due_tasks = None
mark_dispatching = None
mark_completed = None
mark_pending = None
recover_stale_dispatches = None
# =============================================
# ACTION REGISTRY (DPLAN-043)
# =============================================
try:
from aipass.daemon.apps.handlers.actions.actions_registry import (
load_registry,
is_action_due,
update_last_run,
mark_reminder_completed,
migrate_plugins,
next_due_str,
)
ACTION_REGISTRY_AVAILABLE = True
except ImportError:
ACTION_REGISTRY_AVAILABLE = False
load_registry = None
is_action_due = None
update_last_run = None
mark_reminder_completed = None
migrate_plugins = None
next_due_str = None
# =============================================
# INTROSPECTION
# =============================================
def print_introspection():
"""Display module introspection info."""
console.print()
console.print("scheduler_ops Module")
console.print("Facade for scheduler_cron.py — re-exports handler functions for cron entry point")
console.print()
console.print("Connected Handlers:")
console.print(" handlers/schedule/")
console.print(" - telegram_notifier.py (notify_triggered, notify_complete, notify_error — Telegram notifications)")
console.print(" - task_registry.py (get_due_tasks, mark_dispatching, mark_completed, mark_pending, recover_stale_dispatches — task lifecycle)")
console.print()
console.print(" handlers/actions/")
console.print(" - actions_registry.py (load_registry, is_action_due, update_last_run, mark_reminder_completed, migrate_plugins, next_due_str — action registry)")
console.print()
# =============================================
# DRONE ROUTING
# =============================================
def handle_command(command: str, args: list) -> bool:
"""Handle commands routed by the entry point.
Args:
command: Command name
args: Additional arguments
Returns:
True if command was handled, False otherwise
"""
if command == "scheduler-ops":
from aipass.cli.apps.modules import console
console.print()
console.print("[bold cyan]Scheduler Ops[/bold cyan] - Cron operations facade")
console.print()
console.print(f" [dim]Telegram available:[/dim] {TELEGRAM_AVAILABLE}")
console.print(f" [dim]Task registry:[/dim] {TASK_REGISTRY_AVAILABLE}")
console.print(f" [dim]Action registry:[/dim] {ACTION_REGISTRY_AVAILABLE}")
console.print()
console.print("[dim]This module is a facade used by scheduler_cron.py.[/dim]")
console.print()
return True
return False
+26 -21
View File
@@ -1,18 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: update.py - DAEMON Status Digest Module
# Date: 2026-01-29
# =================== AIPass ====================
# Name: update.py
# Description: DAEMON Status Digest Module
# Version: 1.0.0
# Category: daemon/modules
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-01-29): Initial implementation - digest of DAEMON activity
#
# CODE STANDARDS:
# - Seed pattern compliance - console.print() for all output
# - Thin orchestration - handlers implement logic
# - Type hints on all functions
# Created: 2026-01-29
# Modified: 2026-01-29
# =============================================
"""
@@ -26,11 +17,9 @@ Returns digest of DAEMON activity for check-ins.
import sys
from typing import Dict, Any, List
import logging
logger = logging.getLogger(__name__)
from aipass.prax import logger
from rich.console import Console
console = Console()
from aipass.cli.apps.modules import console
def _header(text):
console.print(f"\n[bold cyan]{'='*70}[/bold cyan]")
@@ -52,6 +41,22 @@ from aipass.daemon.apps.handlers.update.data_loader import (
MODULE_NAME = "update"
# =============================================
# INTROSPECTION
# =============================================
def print_introspection():
"""Display module introspection info."""
console.print()
console.print("update Module")
console.print("Returns digest of DAEMON activity for check-ins")
console.print()
console.print("Connected Handlers:")
console.print(" handlers/update/")
console.print(" - data_loader.py (load_inbox, load_local, categorize_messages, get_session_summary, get_escalations — inbox and session data)")
console.print()
# =============================================
# OUTPUT FORMATTING
# =============================================
@@ -104,7 +109,7 @@ def _print_digest(inbox_data: Dict[str, Any], local_data: Dict[str, Any]) -> Non
console.print()
def _print_help() -> None:
def print_help() -> None:
"""Display help using Rich formatted output."""
console.print()
_header("Update Module - DAEMON Status Digest")
@@ -146,7 +151,7 @@ def handle_command(command: str, args: list) -> bool:
try:
if args and args[0] in ['--help', '-h', 'help']:
_print_help()
print_help()
return True
inbox_data = load_inbox()
@@ -171,7 +176,7 @@ def main() -> None:
args = sys.argv[1:]
if len(args) == 0 or args[0] in ['--help', '-h', 'help']:
_print_help()
print_help()
return
handle_command('update', args)
@@ -0,0 +1,84 @@
# =================== AIPass ====================
# Name: wakeup_ops.py
# Description: Wake-Up Cron Operations Module
# Version: 1.0.0
# Created: 2026-03-08
# Modified: 2026-03-08
# =============================================
"""
Wake-up operations module -- facade for cron entry point.
Provides a clean module-layer interface over handler functions
used by daemon_wakeup.py. Entry-level scripts import from
this module instead of reaching into handlers directly.
"""
from aipass.prax import logger
try:
from aipass.cli.apps.modules.display import console
except ImportError:
from rich.console import Console
console = Console()
# =============================================
# DAEMON BOT TELEGRAM NOTIFICATIONS
# =============================================
try:
from aipass.daemon.apps.handlers.schedule.assistant_notifier import (
notify_wakeup,
notify_report,
notify_error,
)
TELEGRAM_AVAILABLE = True
except ImportError:
TELEGRAM_AVAILABLE = False
notify_wakeup = None
notify_report = None
notify_error = None
# =============================================
# INTROSPECTION
# =============================================
def print_introspection():
"""Display module introspection info."""
console.print()
console.print("wakeup_ops Module")
console.print("Facade for daemon_wakeup.py — re-exports daemon bot Telegram notifications")
console.print()
console.print("Connected Handlers:")
console.print(" handlers/schedule/")
console.print(" - assistant_notifier.py (notify_wakeup, notify_report, notify_error — daemon bot Telegram notifications)")
console.print()
# =============================================
# DRONE ROUTING
# =============================================
def handle_command(command: str, args: list) -> bool:
"""Handle commands routed by the entry point.
Args:
command: Command name
args: Additional arguments
Returns:
True if command was handled, False otherwise
"""
if command == "wakeup-ops":
from aipass.cli.apps.modules import console
console.print()
console.print("[bold cyan]Wakeup Ops[/bold cyan] - Cron wake-up facade")
console.print()
console.print(f" [dim]Telegram available:[/dim] {TELEGRAM_AVAILABLE}")
console.print()
console.print("[dim]This module is a facade used by daemon_wakeup.py.[/dim]")
console.print()
return True
return False
@@ -1,18 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: botfather_reminder.py - Hourly reminder to create PATRICK_PRIVATE Telegram bot
# Date: 2026-02-26
# =================== AIPass ====================
# Name: botfather_reminder.py
# Description: Hourly reminder to create PATRICK_PRIVATE Telegram bot
# Version: 1.0.0
# Category: plugins/scheduler
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-02-26): Initial - self-resolving BotFather reminder
#
# CODE STANDARDS:
# - Self-resolving: stops when token is set in config
# - Starts after 2026-02-27T09:00 (BotFather cooldown ~11hrs)
# - Sends email to @dev_central inbox, no agent spawn needed
# Created: 2026-02-26
# Modified: 2026-02-26
# =============================================
"""
@@ -1,19 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: community_rotation.py - Rotating Community Engagement Plugin
# Date: 2026-02-22
# =================== AIPass ====================
# Name: community_rotation.py
# Description: Rotating Community Engagement Plugin
# Version: 1.1.0
# Category: daemon/apps/plugins
#
# CHANGELOG (Max 5 entries):
# - v1.1.0 (2026-02-22): Dashboard pre-check, activity tracking, inactivity alerting
# - v1.0.0 (2026-02-21): Initial creation - hourly branch rotation for Commons engagement
#
# CODE STANDARDS:
# - Plugin interface: PLUGIN_CONFIG + run()
# - Named by action (community_rotation), not target
# - Self-dispatching: handles branch selection and wake internally
# Created: 2026-02-21
# Modified: 2026-02-22
# =============================================
"""
@@ -39,8 +29,8 @@ import sys
from datetime import datetime
from pathlib import Path
import logging
logger = logging.getLogger(__name__)
from aipass.prax import logger
# logger imported from aipass.prax
# Paths
REGISTRY_PATH = Path(os.environ.get('AIPASS_REGISTRY', Path.home() / '.aipass' / 'AIPASS_REGISTRY.json'))
+7 -15
View File
@@ -1,17 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: daily_audit.py - Daily Standards Audit Plugin
# Date: 2026-02-20
# =================== AIPass ====================
# Name: daily_audit.py
# Description: Daily Standards Audit Plugin
# Version: 1.0.0
# Category: daemon/apps/plugins
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-02-20): Initial creation - daily Seed audit at 4am
#
# CODE STANDARDS:
# - Plugin interface: PLUGIN_CONFIG + run()
# - Named by action (audit), not target (seed) per naming standard
# Created: 2026-02-20
# Modified: 2026-02-20
# =============================================
"""
@@ -22,8 +14,8 @@ Seed checks BRANCH_REGISTRY completeness, runs drone @seed audit @all,
fixes non-compliance issues, and emails a summary to @dev_central.
"""
import logging
logger = logging.getLogger(__name__)
from aipass.prax import logger
# logger imported from aipass.prax
PLUGIN_CONFIG = {
"name": "daily_audit",
@@ -1,21 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: dev_central_monitor.py - Hourly System Monitor Plugin
# Date: 2026-02-23
# =================== AIPass ====================
# Name: dev_central_monitor.py
# Description: Hourly System Monitor Plugin
# Version: 1.0.0
# Category: daemon/apps/plugins
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-02-23): Initial creation - hourly system health check
# * Monitors VERA autonomous performance
# * Checks daemon health, inbox status, PR queue
# * Records metrics in VERA_AUTONOMOUS_TRACKER.md
# * Reports blockers and suggests unblocking strategies
#
# CODE STANDARDS:
# - Plugin interface: PLUGIN_CONFIG + run()
# - Named by action (dev_central_monitor), not target
# Created: 2026-02-23
# Modified: 2026-02-23
# =============================================
"""
@@ -32,8 +20,8 @@ Patrick's directive (Session 124): DEV_CENTRAL should be the best
at overcoming blockers. Learn, then teach VERA.
"""
import logging
logger = logging.getLogger(__name__)
from aipass.prax import logger
# logger imported from aipass.prax
PLUGIN_CONFIG = {
"name": "dev_central_monitor",
+7 -21
View File
@@ -1,23 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: heartbeat.py - Periodic Heartbeat Plugin
# Date: 2026-02-20
# =================== AIPass ====================
# Name: heartbeat.py
# Description: Periodic Heartbeat Plugin
# Version: 1.0.0
# Category: daemon/apps/plugins
#
# CHANGELOG (Max 5 entries):
# - v2.0.0 (2026-02-23): DPLAN-029 redesign -- identity-driven, not checklist.
# Anthropic research: heuristics over scripts, no conditional escape hatches.
# Nexus philosophy: choice over command, noticing over being told.
# Her system prompt already covers role/teams/dispatch/publishing -- don't repeat.
# - v1.2.0 (2026-02-21): Rewrote prompt - directive with resource locations
# - v1.1.0 (2026-02-21): Updated prompt - teams retired, VERA operates solo
# - v1.0.0 (2026-02-20): Initial creation - VERA 30min heartbeat
#
# CODE STANDARDS:
# - Plugin interface: PLUGIN_CONFIG + run()
# - Named by action (heartbeat), not target (vera) per naming standard
# Created: 2026-02-20
# Modified: 2026-02-23
# =============================================
"""
@@ -34,8 +20,8 @@ Design philosophy (Session 134 research):
- Fresh sessions: no accumulated idle context from prior wakes
"""
import logging
logger = logging.getLogger(__name__)
from aipass.prax import logger
# logger imported from aipass.prax
PLUGIN_CONFIG = {
"name": "heartbeat",
+58 -32
View File
@@ -1,22 +1,9 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: scheduler_cron.py - DAEMON Scheduler Cron Trigger
# Date: 2026-02-15
# =================== AIPass ====================
# Name: scheduler_cron.py
# Description: DAEMON Scheduler Cron Trigger
# Version: 2.0.0
# Category: daemon/apps
#
# CHANGELOG (Max 5 entries):
# - v2.0.0 (2026-03-02): DPLAN-043 -- Action registry integration replaces direct plugin processing
# - v1.3.0 (2026-02-21): Self-dispatch support for plugins with dynamic branch targeting
# - v1.2.0 (2026-02-21): Detailed notification - plugin names, next-due times, next run
# - v1.1.0 (2026-02-20): Add plugin discovery and scheduled execution via wake.py
# - v1.0.0 (2026-02-15): Initial implementation - cron-triggered scheduler
#
# CODE STANDARDS:
# - Handlers implement logic, modules orchestrate
# - No Rich console (headless cron execution)
# - Stdout logging (cron redirects to logs/scheduler_cron.log)
# Created: 2026-02-15
# Modified: 2026-03-02
# =============================================
"""
@@ -47,37 +34,37 @@ from datetime import datetime, timedelta
import fcntl
import logging
logger = logging.getLogger(__name__)
from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.cli.apps.modules import console
# =============================================
# OPTIONAL IMPORTS
# OPTIONAL IMPORTS (via module layer)
# =============================================
# Resolve package imports — this script can run standalone or as a module
# Resolve package imports — route through modules, not handlers directly
try:
from aipass.daemon.apps.handlers.schedule.telegram_notifier import (
from aipass.daemon.apps.modules.scheduler_ops import (
notify_triggered,
notify_complete,
notify_error,
TELEGRAM_AVAILABLE,
)
TELEGRAM_AVAILABLE = True
except ImportError:
TELEGRAM_AVAILABLE = False
notify_triggered = None
notify_complete = None
notify_error = None
# Task registry handler
# Task registry (via module layer)
try:
from aipass.daemon.apps.handlers.schedule.task_registry import (
from aipass.daemon.apps.modules.scheduler_ops import (
get_due_tasks,
mark_dispatching,
mark_completed,
mark_pending,
recover_stale_dispatches,
TASK_REGISTRY_AVAILABLE,
)
TASK_REGISTRY_AVAILABLE = True
except ImportError:
TASK_REGISTRY_AVAILABLE = False
get_due_tasks = None
@@ -102,17 +89,17 @@ except ImportError:
PLUGINS_AVAILABLE = False
discover_plugins = None
# Action registry (DPLAN-043)
# Action registry (DPLAN-043) (via module layer)
try:
from aipass.daemon.apps.handlers.actions.actions_registry import (
from aipass.daemon.apps.modules.scheduler_ops import (
load_registry,
is_action_due,
update_last_run,
mark_reminder_completed,
migrate_plugins,
next_due_str,
ACTION_REGISTRY_AVAILABLE,
)
ACTION_REGISTRY_AVAILABLE = True
except ImportError:
ACTION_REGISTRY_AVAILABLE = False
load_registry = None
@@ -142,10 +129,39 @@ PLUGIN_LAST_RUN_FILE = Path(__file__).parent / "plugins" / ".last_run.json"
# LOGGING
# =============================================
def print_introspection():
"""Display module introspection info."""
console.print()
console.print("scheduler_cron Module")
console.print("Cron trigger for scheduled tasks and action registry processing")
console.print()
console.print("Connected Handlers:")
console.print(" modules/")
console.print(" - scheduler_ops.py (notify_triggered, notify_complete, notify_error — Telegram notifications)")
console.print(" - scheduler_ops.py (get_due_tasks, mark_dispatching, mark_completed, mark_pending, recover_stale_dispatches — task registry ops)")
console.print(" - scheduler_ops.py (load_registry, is_action_due, update_last_run, mark_reminder_completed, migrate_plugins, next_due_str — action registry ops)")
console.print()
console.print(" plugins/")
console.print(" - discover_plugins (plugin discovery and scheduled execution)")
console.print()
def print_help() -> None:
"""Display usage information for scheduler_cron."""
console.print("\n[bold cyan]scheduler_cron.py - DAEMON Scheduler Cron Trigger[/bold cyan]")
console.print("\n[yellow]USAGE:[/yellow]")
console.print(" python scheduler_cron.py Run the cron scheduler")
console.print(" python scheduler_cron.py --help Show this help message")
console.print("\n[yellow]DESCRIPTION:[/yellow]")
console.print(" Processes due scheduled tasks and actions from the registry.")
console.print(" Intended to be called periodically by cron.")
console.print()
def log(message: str) -> None:
"""Print timestamped log line to stdout (captured by cron redirect)."""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] {message}", flush=True)
console.print(f"[{timestamp}] {message}")
# =============================================
@@ -770,6 +786,16 @@ def main() -> int:
Returns:
0 on success, 1 on error
"""
args = sys.argv[1:]
if not args:
print_introspection()
return 0
if args[0] in ['--help', '-h']:
print_help()
sys.exit(0)
log("=" * 60)
log("Scheduler cron triggered")
@@ -892,7 +918,7 @@ if __name__ == "__main__":
except Exception as e:
# Last-resort catch -- never crash silently
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] FATAL: Unhandled exception: {e}", flush=True)
console.print(f"[{timestamp}] FATAL: Unhandled exception: {e}")
if TELEGRAM_AVAILABLE:
try:
notify_error(EVENT_NAME, f"FATAL: {e}")