Skip to content

feat(tools): Slack/Discord alerts webhook (stateless, advisory, fail-open by design) - #746

Open
matt82198 wants to merge 5 commits into
mainfrom
feat/alerts-webhook
Open

matt82198 wants to merge 5 commits into
mainfrom
feat/alerts-webhook

Conversation

@matt82198

Copy link
Copy Markdown
Owner

Summary

Implements stateless one-shot tool that reads on-disk signals and POSTs compact status payloads to configured Slack/Discord webhooks.

Payload Composition

Reads from disk only:

  • Orchestrator heartbeat age (staleness detection)
  • Merge queue state
  • Recent exceptions (tail from JSONL)
  • Open PR count via gh (skipped silently if unavailable, never credential-hunts)

Composes Slack blocks or Discord embeds with fleet status summary.

Fail-Open Design

Advisory tool: all errors exit 0 cleanly:

  • Missing webhook_url = skip gracefully
  • Network errors/timeouts = warn and exit 0
  • gh command unavailable = silent skip (no credential hunting)

Implementation Details

  • CLI: python tools/alerts_webhook.py [--config PATH] [--dry-run]
  • Payload shapes: Slack blocks (type=header/section/context) and Discord embeds
  • Timeouts enforced (10s default) on every request
  • UTF-8 encoding throughout (config reads, network POSTs)
  • Stdlib-only (json, urllib, pathlib, time, sys, subprocess, os)
  • ASCII source, deterministic output

Testing

19 comprehensive TDD tests covering:

  • Payload composition from fixtures (heartbeat, exceptions, queue, PRs)
  • Slack vs Discord output formats
  • Missing config graceful skip
  • Network error tolerance (mock urlopen failures)
  • --dry-run mode (print, no network)
  • Timeout enforcement (10s parameter present)
  • UTF-8 encoding validation
  • gh command skip if unavailable

All tests pass. Secret scan clean. Tool gating: claudemd_lint OK, test suite count +19.

🚫 Follow-up PRs (deferred to daemons/ wiring layer):

  • Daemon invocation (when to trigger webhook posts)
  • Configuration in aesop.config.json schema docs
  • Integration with heartbeat/exception monitoring

📍 Generated with Claude Code

matt82198 and others added 2 commits August 3, 2026 12:14
…open by design)

Implements stateless one-shot tool that reads on-disk signals (heartbeat
age, merge-queue state, exception count, open PR count) and POSTs compact
status payloads to configured Slack/Discord webhooks. Fail-open design:
missing config, network errors, and unavailable tools all exit 0 cleanly.

Supports Slack blocks and Discord embeds output formats. Never logs or
exposes webhook URLs. All timeouts enforced (10s default). Skips gh
command silently if unavailable (no credential hunting).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@matt82198 matt82198 added the merge-queue Queued for the merge-queue advancer daemon label Aug 3, 2026
matt82198 and others added 3 commits August 3, 2026 14:16
…: lines

#751 moved the per-tool index out of tools/CLAUDE.md into each tool's own
INDEX: header line, from which tools/INDEX.md is generated, because the inline
list was the top merge-queue conflict surface. This branch was cut before that
and still carried its documentation update as an edit to the old inline list.

Resolved by taking main's 33-line tools/CLAUDE.md and porting this branch's
updated description(s) into the tool's own INDEX: line, then regenerating
tools/INDEX.md: alerts_webhook.py (new tool, INDEX: line added)

No documentation content lost; only the obsolete inline index lines are gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tools/alerts_webhook.py opened state/.orchestrator-heartbeat directly,
which is an unbaselined stateapi_lint violation (the read must go through
the sanctioned state_store.read_api facade). Fixed properly rather than
baselined: baselining a brand-new file would weaken the ratchet.

- read_heartbeat_age() -> read_heartbeat_stalled(), delegating to
  ReadAPI.check_heartbeat_fresh(".orchestrator-heartbeat", threshold).
  The filename is now an identifier passed TO the facade, which owns the
  file access and the staleness contract; the tool never opens it.
- Adopts the facade's fail-closed semantic: a missing or unreadable
  heartbeat now reports as stalled instead of the previous "active
  (age: unknown)", which silently swallowed the exact alarm condition.
- Facade import guarded; unavailable/erroring facade degrades to an
  "unknown" heartbeat status so the alert path stays fail-open.
- Payload text reports fresh/stalled/unknown rather than a raw age,
  since the facade exposes freshness, not age.
- Drops the now-unused time import; tools/INDEX.md regenerated.

Tests: 7 new regression tests pin the facade delegation (call args),
the fail-closed missing-heartbeat path, the unknown/degraded path, and
a source guard asserting no direct heartbeat open remains.

stateapi_lint: PASS, All 40 violations match baseline (0 new; baseline
file untouched).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-queue Queued for the merge-queue advancer daemon

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant