Warden's scheduled triggers analyze the full codebase on a cron. Today, findings go to a single tracking issue per skill that gets overwritten each run. There's no way to:
- Get notified in Slack when new findings appear
- Mark findings as false positives so they don't recur
- Track individual findings across runs
- Per-finding GitHub issues with semantic dedup across runs
- Slack webhook notifications for new findings
- Suppression file for false positives (rule-based pre-filtering)
- Replace the single-tracking-issue approach with a provider-based notification layer
[[notifications]]
type = "github-issues"
labels = ["warden"]
[[notifications]]
type = "slack"
webhookUrl = "$SLACK_WEBHOOK_URL"Top-level [[notifications]] array. Each provider receives all non-suppressed findings independently. Environment variables expanded at runtime via $VAR syntax.
The [[notifications]] section replaces the existing schedule.issueTitle tracking-issue approach. The createOrUpdateIssue code path and schedule.issueTitle config are removed. schedule.createFixPR and schedule.fixBranchPrefix remain (fix PRs are orthogonal to notifications).
Located at .agents/warden/suppressions.yaml:
suppressions:
- skill: "security-audit"
paths: ["src/legacy/**"]
reason: "Legacy code, not worth fixing"
- skill: "security-audit"
paths: ["src/admin/query.ts"]
title: "SQL injection"
reason: "Uses parameterized queries, false positive"Rules match on:
- skill (required): Exact skill name
- paths (required): Glob patterns matched against finding location path
- title (optional): Substring match against finding title
- reason (required): Human-readable justification
Loaded once per workflow run, applied before any provider receives findings.
interface NotificationProvider {
readonly name: string;
notify(context: NotificationContext): Promise<NotificationResult>;
}
interface NotificationContext {
findings: Finding[];
reports: SkillReport[];
repository: { owner: string; name: string };
commitSha: string;
}
interface NotificationResult {
provider: string;
sent: number;
skipped: number;
errors: string[];
}Skill Report -> Apply Suppressions -> All Providers (each gets same findings)
|-- github-issues (semantic dedup, creates/skips per finding)
|-- slack (sends all findings it receives)
- Creates one issue per unique finding
- Dedup: two-tier
- Hash match via
<!-- warden:SHA256 -->marker in issue body (cheap, catches identical text) - Semantic match via Haiku for same-file findings with no hash match (handles LLM variation)
- Hash match via
- Also checks closed issues with
warden:false-positivelabel (treated as suppressed) - Issue title:
[Warden] {finding.title} - Labels: configurable base labels +
warden:{skillName} - Body: severity, description, location with code link, suggested fix, hash marker
- Config:
labels(string array, default["warden"])
- Posts to incoming webhook URL using Block Kit formatting
- Sends all non-suppressed findings it receives
- Message: repo, commit, severity summary, up to 10 findings with details
- Skips notification if findings array is empty
- Config:
webhookUrl(string, supports$ENV_VARexpansion)
In src/action/workflow/schedule.ts, the createOrUpdateIssue call is replaced with:
const dispatcher = new NotificationDispatcher(providers, suppressions);
const result = await dispatcher.dispatch({
findings: report.findings,
reports: [report],
repository: { owner, name: repo },
commitSha: headSha,
skillName: resolved.name,
});- Persistent storage for "previously seen" finding tracking across all providers
autoCloseconfig flag on GitHub Issues provider (close issues when findings disappear)- CLI command for managing suppressions (
warden suppress add) - PR trigger integration (generic enough, but PR comments already have sophisticated dedup)