feat(slack): outbound notifier for approvals, digests and readiness
Closes the operator loop: instead of asking the system with /cmo, the system tells the marketing channel when something needs attention.
- slack_notifier.py:
- NotifierState persisted at runtime/slack_notifications.json (notified approval ids, last digest id, last readiness status).
- notify_pending_approvals: posts any new pending approval with a rich block (action, platform, risk, cost, id, resolve hint).
- notify_latest_digest: posts the most recent shadow-mode digest once, with a 260-char summary.
- notify_readiness_change: fires only when overall status transitions
bucket (ready
↔ attention↔ blocked) with per-check detail. - run_notifier: orchestrates the three, saves state atomically, idempotent across re-runs.
- CLI:
slack notify [--approvals --digest --readiness --all]for cron / ad-hoc runs, andslack notify-loop --interval Nused by the scheduler service. - docker-compose now runs a dedicated
notifiercontainer that loops every CMO_NOTIFIER_INTERVAL_SECONDS (default 300s). Sleeps gracefully when Slack env vars are missing so it's safe to ship pre-provisioning. - 11 new tests cover state roundtrip, formatter outputs, dedup behaviour (approvals/digests/readiness), failure-path state preservation, and end-to-end run_notifier idempotence.
Verified in production: the notifier service posted the first red readiness alert to #cmoagent immediately after deploy (Slack returned ok=true, ts preserved in state). 178/178 suite green.