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:
co-authored by
Claude Opus 4.6
parent
09e759a8a4
commit
babedd9c64
+107
-3
@@ -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*
|
||||
|
||||
@@ -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]")
|
||||
|
||||
@@ -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
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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
|
||||
@@ -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'))
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user