feat(server): say at startup when alerting is unconfigured (#107) #140

Merged
mathias merged 1 commits from fix/107-loud-unconfigured-alerting-rebased into main 2026-09-23 13:02:40 +00:00
Owner

Closes #107. Replaces PR #108 (stale, predates the tertiary-LLM router and CLAUDE_INGEST_SKIP_PROJECTS features that landed on main since — merging it as-is would have silently reverted both).

What this is

Rebase of PR #108's original code (agent/107-loud-unconfigured-alerting, never previously CI-tested — workflow_run_list showed zero runs on that branch) onto current main. No functional change to the original alertingWarnings implementation.

alertingWarnings(getenv func(string) string) []string — pure, env-injected, called once in main() right after the logger is built. Warns once at startup when BRAIN_NTFY_URL is unset, naming what won't fire: brain_edit_root governance rewrites and capture audit degradation. WARN, not fatal — deliberately: this is a degraded-observability case (the unconfigured-homelab path is legitimate), not a bypassed gate.

BRAIN_LOKI_URL — deliberately not added

The original issue's checklist optionally asked for the same treatment on BRAIN_LOKI_URL. Not done here: unlike BRAIN_NTFY_URL unset (alerts silently never fire — a real silent failure), BRAIN_LOKI_URL unset already has a fully working, non-silent fallback — buildAuditSink logs "capture audit: slog sink (BRAIN_LOKI_URL unset)" at Info level and returns a fully functional local sink. Treating it identically to the ntfy case would misrepresent it as a silent failure when it isn't one. The issue said "optionally" for exactly this reason — happy to add a WARN-level echo of the existing Info log if wanted, but didn't want to manufacture false severity to tick a checklist box.

Test plan

  • TestAlertingWarnings (pre-existing, ported through the rebase): unconfigured case names BRAIN_NTFY_URL/brain_edit_root/audit; configured case is silent.
  • Verified the tertiary-LLM router block and CLAUDE_INGEST_SKIP_PROJECTS veto-list feature (both landed on main after this branch was originally cut) survived the rebase intact.
  • Full module gate: go build ./..., go vet ./..., go test ./... -race -count=1 — 31 packages green.
  • task check green.

🤖 Generated with Claude Code


Created via git-mcp on behalf of @mathias

Closes #107. Replaces PR #108 (stale, predates the tertiary-LLM router and `CLAUDE_INGEST_SKIP_PROJECTS` features that landed on main since — merging it as-is would have silently reverted both). ## What this is Rebase of PR #108's original code (`agent/107-loud-unconfigured-alerting`, never previously CI-tested — `workflow_run_list` showed zero runs on that branch) onto current `main`. No functional change to the original `alertingWarnings` implementation. `alertingWarnings(getenv func(string) string) []string` — pure, env-injected, called once in `main()` right after the logger is built. Warns once at startup when `BRAIN_NTFY_URL` is unset, naming what won't fire: `brain_edit_root` governance rewrites and capture audit degradation. WARN, not fatal — deliberately: this is a degraded-observability case (the unconfigured-homelab path is legitimate), not a bypassed gate. ## BRAIN_LOKI_URL — deliberately not added The original issue's checklist optionally asked for the same treatment on `BRAIN_LOKI_URL`. Not done here: unlike `BRAIN_NTFY_URL` unset (alerts silently never fire — a real silent failure), `BRAIN_LOKI_URL` unset already has a fully working, non-silent fallback — `buildAuditSink` logs `"capture audit: slog sink (BRAIN_LOKI_URL unset)"` at Info level and returns a fully functional local sink. Treating it identically to the ntfy case would misrepresent it as a silent failure when it isn't one. The issue said "optionally" for exactly this reason — happy to add a WARN-level echo of the existing Info log if wanted, but didn't want to manufacture false severity to tick a checklist box. ## Test plan - [x] `TestAlertingWarnings` (pre-existing, ported through the rebase): unconfigured case names `BRAIN_NTFY_URL`/`brain_edit_root`/`audit`; configured case is silent. - [x] Verified the tertiary-LLM router block and `CLAUDE_INGEST_SKIP_PROJECTS` veto-list feature (both landed on `main` after this branch was originally cut) survived the rebase intact. - [x] Full module gate: `go build ./...`, `go vet ./...`, `go test ./... -race -count=1` — 31 packages green. - [x] `task check` green. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- _Created via git-mcp on behalf of @mathias_
mathias added 1 commit 2026-09-23 12:59:31 +00:00
feat(server): say at startup when alerting is unconfigured (#107)
CI / Lint / Test / Vet (pull_request) Successful in 36s
CI / Mirror to GitHub (pull_request) Skipped
67c3039448
buildNotifier returns a nil interface when BRAIN_NTFY_URL is unset, and
every consumer skips a nil notifier silently. That is right for an
unconfigured homelab and wrong for an alert whose entire value is that
someone finds out: "configured and working" and "never configured" were
byte-identical in behaviour — no error, no warning, only a message that
never arrives, which nobody notices because nobody is waiting for an alert
they have never seen.

Two alerts were dark in production for exactly that reason (#100): the
governance-rewrite alert from #97, and the audit-degradation alert that
reports the audit trail is down. Both were found by firing a real event
and watching nothing happen, not by reading anything.

WARN rather than fatal, deliberately. The brain rule
`empty-credential-must-refuse-to-start-not-switch-modes` says an empty
credential must be a startup error rather than a silent mode switch — but
that is about a control being removed. Here observability degrades, no gate
is bypassed, and refusing to boot would break the unconfigured-homelab case
the nil notifier exists to support. What that rule demands and this
provides is that the mode switch stops being silent.

buildAuditSink already announces its own fallback ("capture audit: slog
sink (BRAIN_LOKI_URL unset)"), so only the notifier needed this.

alertingWarnings is pure and env-injected: the reason this gap survived is
that nothing about it was observable, so the fix is tested rather than
asserted.

Refs #107, #100, #97.
mathias merged commit c6341f020a into main 2026-09-23 13:02:40 +00:00
Sign in to join this conversation.