chore(trigger): retire dead bulletin_created dashboard writer

The bulletin_created event handler propagated a 'bulletin_board' section into
every branch dashboard, but it was fully dead: nothing fired the event, its
BULLETINS.central.json store no longer exists, and prax already prunes
'bulletin_board' via DEPRECATED_SECTIONS. Archived the handler to
events/.archive/, removed its import + trigger.on() registration, dropped the
5 covering tests (558 pass). seedgo audit 100%. prax pruning left intact.
Closes td-102.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEQZXCtgnF3NQtcttTErpq
This commit is contained in:
AIOSAI
2026-06-24 06:12:31 -07:00
co-authored by Claude Opus 4.8
parent feecd263eb
commit c771a22771
4 changed files with 9 additions and 319 deletions
+8
View File
@@ -25,6 +25,14 @@ PyPI version — not the changelog header.
10 existing line-scoped entries across drone/memory/skills and dropped 3 dead
entries already pointing past EOF (td-009).
### Removed
- **Dead `bulletin_created` trigger handler** — the event handler that wrote a
`bulletin_board` section into every branch dashboard is retired: nothing fired
the event, its `BULLETINS.central.json` store no longer exists, and prax
already prunes `bulletin_board` as a deprecated section. Archived + unwired
from the event registry; prax's pruning stays (td-102).
## [2026-06-23]
The **2.6.0** release — a large `dev → main` merge spanning several weeks (68 commits).
@@ -1,246 +0,0 @@
# =================== AIPass ====================
# Name: bulletin_created.py
# Description: Bulletin created event handler for dashboard propagation
# Version: 1.0.0
# Created: 2026-01-31
# Modified: 2026-01-31
# =============================================
"""
Bulletin Created Event Handler
Handles bulletin_created events fired when a new bulletin is created.
Propagates the bulletin to all branch dashboards.
Event data expected:
- bulletin_id: ID of the created bulletin
- title: Bulletin title
- message: Bulletin content
- priority: Bulletin priority level
- created_by: Who created it
- timestamp: When created
"""
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List
from aipass.trigger.apps.config import TRIGGER_ROOT, atomic_write_json
from aipass.trigger.apps.handlers.json import json_handler
def _find_repo_root() -> Path:
"""Walk up from this file to find the repo root (contains AIPASS_REGISTRY.json)."""
current = Path(__file__).resolve().parent
for parent in [current] + list(current.parents):
if (parent / "AIPASS_REGISTRY.json").exists():
return parent
return Path.cwd()
_REPO_ROOT = _find_repo_root()
# Paths
BRANCH_REGISTRY = _REPO_ROOT / "AIPASS_REGISTRY.json"
BULLETINS_PATH = _REPO_ROOT / "BULLETINS.central.json"
_HANDLER_LOG = TRIGGER_ROOT / "logs" / "bulletin_handler.log"
def _log_warning(message: str) -> None:
"""Log warning to file (event handlers cannot import Prax logger - causes recursion)."""
try:
_HANDLER_LOG.parent.mkdir(parents=True, exist_ok=True)
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
with open(_HANDLER_LOG, "a", encoding="utf-8") as f:
f.write(f"{ts} | WARNING | {message}\n")
except Exception:
pass
def _load_branch_registry() -> List[Dict]:
"""
Load branch registry silently.
Returns:
List of branch dicts with name, path, status.
Empty list on any error.
"""
try:
if not BRANCH_REGISTRY.exists():
return []
data = json.loads(BRANCH_REGISTRY.read_text(encoding="utf-8"))
return data.get("branches", [])
except Exception as exc:
_log_warning(f"load branch registry failed: {exc}")
return []
def _load_bulletins() -> List[Dict]:
"""
Load bulletins from central storage.
Returns:
List of bulletin dicts. Empty list on error.
"""
try:
if not BULLETINS_PATH.exists():
return []
data = json.loads(BULLETINS_PATH.read_text())
return data.get("bulletins", [])
except Exception as exc:
_log_warning(f"load bulletins failed: {exc}")
return []
def _filter_active_bulletins(bulletins: List[Dict]) -> List[Dict]:
"""
Filter bulletins to only active ones.
Args:
bulletins: List of all bulletins
Returns:
List of active bulletins only
"""
return [b for b in bulletins if b.get("active", False)]
def _load_dashboard(branch_path: Path) -> Dict:
"""
Load existing dashboard or create minimal structure.
Args:
branch_path: Path to branch root
Returns:
Dashboard data dict (existing or minimal structure)
"""
dashboard_path = branch_path / "DASHBOARD.local.json"
if dashboard_path.exists():
try:
data = json.loads(dashboard_path.read_text())
if "sections" not in data:
data["sections"] = {}
if "bulletin_board" not in data["sections"]:
data["sections"]["bulletin_board"] = {"managed_by": "aipass", "active_bulletins": [], "pending_ack": []}
return data
except Exception as exc:
_log_warning(f"parse dashboard JSON for {branch_path}: {exc}")
# Create minimal dashboard structure
return {
"last_refreshed": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"sections": {"bulletin_board": {"managed_by": "aipass", "active_bulletins": [], "pending_ack": []}},
}
def _save_dashboard(branch_path: Path, dashboard: Dict) -> bool:
"""
Save dashboard to branch.
Args:
branch_path: Path to branch root
dashboard: Dashboard data to save
Returns:
True if saved, False on error
"""
try:
dashboard_path = branch_path / "DASHBOARD.local.json"
dashboard["last_refreshed"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
atomic_write_json(dashboard_path, dashboard)
return True
except Exception as exc:
_log_warning(f"save dashboard to {branch_path}: {exc}")
return False
def _propagate_bulletins_to_branches() -> None:
"""
Propagate active bulletins to all branch dashboards.
Loads active bulletins and updates each branch's dashboard
with the bulletin_board section.
Silent failure - catches all exceptions.
"""
try:
# Load branch registry
branches = _load_branch_registry()
if not branches:
return
# Load and filter active bulletins
all_bulletins = _load_bulletins()
active_bulletins = _filter_active_bulletins(all_bulletins)
# Update each branch dashboard
for branch in branches:
branch_path_str = branch.get("path")
if not branch_path_str:
continue
branch_path = Path(branch_path_str)
if not branch_path.exists():
continue
try:
# Load dashboard
dashboard = _load_dashboard(branch_path)
# Update bulletin_board section ONLY
if "sections" not in dashboard:
dashboard["sections"] = {}
dashboard["sections"]["bulletin_board"] = {
"managed_by": "aipass",
"active_bulletins": active_bulletins,
"pending_ack": [],
}
# Save dashboard
_save_dashboard(branch_path, dashboard)
except Exception as exc:
_log_warning(f"propagate to branch {branch_path_str}: {exc}")
continue
except Exception as exc:
_log_warning(f"bulletin propagation failed: {exc}")
def handle_bulletin_created(
_bulletin_id: str | None = None,
_title: str | None = None,
_message: str | None = None,
_priority: str | None = None,
_created_by: str | None = None,
_timestamp: str | None = None,
**_kwargs: Any,
) -> None:
"""
Handle bulletin_created event - propagate bulletin to all branch dashboards.
Event parameters are received but not used directly - we reload from
central storage to ensure consistency with any concurrent updates.
Args:
_bulletin_id: ID of the created bulletin (unused - reload from storage)
_title: Bulletin title (unused - reload from storage)
_message: Bulletin content (unused - reload from storage)
_priority: Bulletin priority level (unused - reload from storage)
_created_by: Who created it (unused - reload from storage)
_timestamp: When created (unused - reload from storage)
**_kwargs: Additional event data (ignored)
"""
try:
# Propagate bulletins to all branches
# We reload from central storage to ensure consistency
# (the newly created bulletin should already be saved there)
_propagate_bulletins_to_branches()
json_handler.log_operation("bulletin_event", {"success": True})
except Exception as exc:
_log_warning(f"handle_bulletin_created failed: {exc}")
@@ -61,7 +61,6 @@ def setup_handlers():
except ImportError:
_log_warning("ai_mail not available — error notifications won't send")
from .warning_logged import handle_warning_logged
from .bulletin_created import handle_bulletin_created
from .memory_template_updated import handle_memory_template_updated
# from .pr_status_sync import handle_pr_created, handle_pr_merged # TDPLAN-0007: status-sync decommissioned
@@ -74,7 +73,6 @@ def setup_handlers():
trigger.on("plan_file_moved", handle_plan_file_moved)
trigger.on("error_detected", handle_error_detected)
trigger.on("warning_logged", handle_warning_logged)
trigger.on("bulletin_created", handle_bulletin_created)
trigger.on("memory_template_updated", handle_memory_template_updated)
# trigger.on("pr_created", handle_pr_created) # TDPLAN-0007: status-sync decommissioned
# trigger.on("pr_merged", handle_pr_merged) # TDPLAN-0007: status-sync decommissioned
@@ -6,7 +6,7 @@
# Modified: 2026-04-25
# =============================================
"""Tests for cli, memory_template_updated, warning_logged, and bulletin_created event handlers."""
"""Tests for cli, memory_template_updated, and warning_logged event handlers."""
import sys
from pathlib import Path
@@ -41,7 +41,6 @@ def _mock_infrastructure(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Non
"aipass.trigger.apps.handlers.events.cli",
"aipass.trigger.apps.handlers.events.memory_template_updated",
"aipass.trigger.apps.handlers.events.warning_logged",
"aipass.trigger.apps.handlers.events.bulletin_created",
):
monkeypatch.delitem(sys.modules, mod_name, raising=False)
@@ -67,13 +66,6 @@ def _import_warning_logged():
return m
def _import_bulletin():
"""Import bulletin_created handler module fresh after mocking."""
import aipass.trigger.apps.handlers.events.bulletin_created as m
return m
# ---------------------------------------------------------------------------
# cli.py -- handle_cli_header_displayed
# ---------------------------------------------------------------------------
@@ -197,65 +189,3 @@ class TestHandleWarningLogged:
mod = _import_warning_logged()
result = mod.handle_warning_logged()
assert result is None
# ---------------------------------------------------------------------------
# bulletin_created.py -- handle_bulletin_created
# ---------------------------------------------------------------------------
class TestHandleBulletinCreated:
"""Tests for handle_bulletin_created from bulletin_created.py."""
def test_does_not_raise_when_files_missing(self) -> None:
"""Silently handles missing registry and bulletins files."""
mod = _import_bulletin()
mod.handle_bulletin_created()
def test_logs_operation_on_success(self) -> None:
"""Logs bulletin_event after successful propagation."""
mod = _import_bulletin()
mod._propagate_bulletins_to_branches = MagicMock()
from aipass.trigger.apps.handlers.json import json_handler
json_handler.log_operation.reset_mock() # type: ignore[union-attr]
mod.handle_bulletin_created()
mod._propagate_bulletins_to_branches.assert_called_once()
json_handler.log_operation.assert_called_once_with( # type: ignore[union-attr]
"bulletin_event", {"success": True}
)
def test_catches_propagation_exception(self) -> None:
"""Does not log operation when propagation raises."""
mod = _import_bulletin()
mod._propagate_bulletins_to_branches = MagicMock(side_effect=RuntimeError("propagation failed"))
from aipass.trigger.apps.handlers.json import json_handler
json_handler.log_operation.reset_mock() # type: ignore[union-attr]
mod.handle_bulletin_created()
json_handler.log_operation.assert_not_called() # type: ignore[union-attr]
def test_accepts_all_params(self) -> None:
"""Accepts all documented event parameters without error."""
mod = _import_bulletin()
mod._propagate_bulletins_to_branches = MagicMock()
mod.handle_bulletin_created(
_bulletin_id="b1",
_title="System update",
_message="Scheduled maintenance",
_priority="high",
_created_by="devpulse",
_timestamp="2026-04-25T10:00:00",
)
def test_returns_none(self) -> None:
"""Handler returns None."""
mod = _import_bulletin()
mod._propagate_bulletins_to_branches = MagicMock()
result = mod.handle_bulletin_created()
assert result is None