"""Bridge governance agents into the unified Agent Event Bus. This module emits normalized governance events for requirement changes, architecture reviews, user acceptance, test gates and version decisions. It uses duck typing so it can work with the unified AgentEventBus when available, while remaining testable without a running bus. """ from __future__ import annotations from dataclasses import asdict, dataclass, field from datetime import datetime, timezone from typing import Any, Protocol from uuid import uuid4 def utc_now() -> str: return datetime.now(timezone.utc).isoformat() class EventPublisher(Protocol): def publish( self, kind: str, source: str, target: str, summary: str, payload: dict[str, Any], correlation_id: str | None = None, ) -> dict[str, Any]: ... @dataclass(frozen=True) class GovernanceEventEnvelope: id: str kind: str source: str target: str summary: str payload: dict[str, Any] correlation_id: str | None = None created_at: str = field(default_factory=utc_now) def to_dict(self) -> dict[str, Any]: return asdict(self) class InMemoryGovernanceEventBus: """Small fallback bus for tests and offline governance runs.""" def __init__(self) -> None: self.events: list[dict[str, Any]] = [] def publish( self, kind: str, source: str, target: str, summary: str, payload: dict[str, Any], correlation_id: str | None = None, ) -> dict[str, Any]: event = GovernanceEventEnvelope( id=f"evt_{uuid4().hex[:12]}", kind=kind, source=source, target=target, summary=summary, payload=payload, correlation_id=correlation_id, ).to_dict() self.events.append(event) return event class GovernanceEventBridge: """Publishes governance lifecycle events to the Agent Event Bus.""" def __init__(self, bus: EventPublisher | None = None): self.bus = bus or InMemoryGovernanceEventBus() def requirement_changed(self, requirement: dict[str, Any]) -> dict[str, Any]: return self.bus.publish( kind="requirement_changed", source=requirement.get("source", "unknown"), target="architect-agent", summary=requirement.get("requirement", "requirement changed"), payload=requirement, correlation_id=requirement.get("id"), ) def architecture_review_started(self, proposal: dict[str, Any]) -> dict[str, Any]: return self.bus.publish( kind="architecture_review_started", source="architect-agent", target="architecture-review-agent", summary=proposal.get("title", "architecture review started"), payload=proposal, correlation_id=proposal.get("change_id"), ) def architecture_review_completed(self, decision: dict[str, Any]) -> dict[str, Any]: return self.bus.publish( kind="architecture_review_completed", source="architecture-review-agent", target="change-governance", summary=f"architecture review {decision.get('status', 'completed')}", payload=decision, correlation_id=decision.get("change_id"), ) def user_acceptance_completed(self, report: dict[str, Any]) -> dict[str, Any]: return self.bus.publish( kind="user_acceptance_completed", source="user-acceptance-agent", target="change-governance", summary=f"user acceptance {report.get('decision', 'completed')}", payload=report, correlation_id=report.get("change_id"), ) def test_gate_completed(self, result: dict[str, Any]) -> dict[str, Any]: return self.bus.publish( kind="test_gate_completed", source="test-gate", target="change-governance", summary=f"test gate {result.get('status', 'completed')}", payload=result, correlation_id=result.get("change_id"), ) def version_decision_created(self, decision: dict[str, Any]) -> dict[str, Any]: return self.bus.publish( kind="version_decision_created", source="architect-agent", target="owner-gate", summary=f"version bump recommendation: {decision.get('version_bump', 'unknown')}", payload=decision, correlation_id=decision.get("change_id"), ) __all__ = [ "GovernanceEventEnvelope", "InMemoryGovernanceEventBus", "GovernanceEventBridge", ]