{"id":51224816,"url":"https://github.com/saagpatel/notification-hub","last_synced_at":"2026-06-28T10:03:13.759Z","repository":{"id":351981083,"uuid":"1213297630","full_name":"saagpatel/notification-hub","owner":"saagpatel","description":"Local notification daemon for AI-tool events","archived":false,"fork":false,"pushed_at":"2026-06-19T08:14:41.000Z","size":1696,"stargazers_count":0,"open_issues_count":7,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-19T10:14:13.284Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/saagpatel.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-04-17T08:30:47.000Z","updated_at":"2026-06-19T08:14:39.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/saagpatel/notification-hub","commit_stats":null,"previous_names":["saagpatel/notification-hub"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/saagpatel/notification-hub","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saagpatel%2Fnotification-hub","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saagpatel%2Fnotification-hub/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saagpatel%2Fnotification-hub/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saagpatel%2Fnotification-hub/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/saagpatel","download_url":"https://codeload.github.com/saagpatel/notification-hub/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saagpatel%2Fnotification-hub/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34884278,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-28T02:00:05.809Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-28T10:03:13.089Z","updated_at":"2026-06-28T10:03:13.754Z","avatar_url":"https://github.com/saagpatel.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Notification Hub\n\n[![CI](https://github.com/saagpatel/notification-hub/actions/workflows/ci.yml/badge.svg)](https://github.com/saagpatel/notification-hub/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n\n`notification-hub` is a small local daemon that turns AI-tool events into routed notifications.\nIt accepts structured events over HTTP, watches the shared bridge file for appended activity,\nclassifies urgency with deterministic rules, and then delivers each event to the right channel.\n\n## Is this for you?\n\nThis project is personal infrastructure built for one operator's AI-tool workflow (Claude Code,\nCodex, and Claude.ai). It is published openly so others can adapt the pattern — a lightweight\nlocalhost daemon that routes structured AI-tool events to push and Slack — but it is not a\ngeneral-purpose notification library and has no stability guarantees for external users.\n\nIf you want to run it yourself, expect to substitute your own home directory, rename the LaunchAgent\nlabel to your reverse-domain prefix, wire up your own bridge file path, and adapt the policy config\nto your workflow. The Operator Commands and Policy Config sections document everything that is\nconfigurable.\n\n## What It Does\n\n- Accepts `POST /events` on `127.0.0.1:9199`\n- Watches the Claude bridge file for new activity lines\n- Classifies events as `urgent`, `normal`, or `info`\n- Persists accepted events to a local SQLite durable inbox before acknowledging producers\n- Writes processed non-burst events to a local JSONL audit log\n- Sends urgent events to push + Slack\n- Sends normal events to Slack\n- Keeps info events in the log only\n- Suppresses noise with dedup, quiet hours, and rate limits\n- Retries failed delivery and retains exhausted events in a dead-letter box\n\n## Architecture\n\n```text\nEvent sources -\u003e FastAPI intake -\u003e SQLite durable inbox -\u003e worker -\u003e classifier -\u003e suppression -\u003e delivery channels -\u003e JSONL audit log\n```\n\nCore modules:\n\n- `server.py`: FastAPI app and lifecycle\n- `watcher.py`: bridge file watcher and parsing\n- `durable_inbox.py`: SQLite accepted-event inbox, retry lifecycle, and dead-letter health\n- `pipeline.py`: routing flow across classification, suppression, and delivery\n- `classifier.py`: deterministic keyword rules\n- `suppression.py`: dedup, quiet hours, and rate limiting\n- `channels.py`: JSONL, macOS push, and Slack delivery\n- `config.py`: host, paths, and Keychain-backed webhook lookup\n\n## Local Development\n\n```bash\nuv sync --frozen --group dev\nuv run --frozen uvicorn notification_hub.server:app --host 127.0.0.1 --port 9199 --reload\n```\n\n## Operator Commands\n\n```bash\nuv run notification-hub doctor\nuv run notification-hub-doctor\nuv run notification-hub-doctor --json\nuv run notification-hub smoke\nuv run notification-hub status\nuv run notification-hub-status --json\nuv run notification-hub inbox\nuv run notification-hub-inbox --json\nuv run notification-hub coordination-snapshot\nuv run notification-hub-coordination-snapshot --json\nuv run notification-hub coordination-snapshot --save-bridge-db\nuv run notification-hub coordination-readiness\nuv run notification-hub-coordination-readiness --json\nuv run notification-hub coordination-console\nuv run notification-hub-coordination-console --json\nuv run notification-hub personal-ops-actions\nuv run notification-hub-personal-ops-actions --json\nuv run notification-hub action-proposal-dismissals\nuv run notification-hub action-proposal-undismiss DISMISSAL_KEY --reason \"signal is useful again\"\nuv run notification-hub action-proposal-group-outcome GROUP_KEY --outcome needs_follow_up --reason \"operator follow-up needed\"\nuv run notification-hub operator-daily-state\nuv run notification-hub operator-review-session\nuv run notification-hub operator-review-session --save-report\nuv run notification-hub operator-review-session-retention --keep 20\nuv run notification-hub operator-review-session-retention --keep 20 --apply\nuv run notification-hub action-export-retention --keep 20\nuv run notification-hub-action-export-retention --json\nuv run notification-hub action-export-retention --keep 20 --apply\nuv run notification-hub operator-handoff-drill\nuv run notification-hub personal-ops-actions --save-review-package\nuv run notification-hub validate-action-package path/to/actions.json\nuv run notification-hub personal-ops-import path/to/actions.json\nuv run notification-hub personal-ops-import path/to/actions.json --enqueue\nuv run notification-hub personal-ops-queue\nuv run notification-hub personal-ops-queue --queue-id QUEUE_ID --status reviewed --reason \"evidence checked\"\nuv run notification-hub personal-ops-queue --queue-id QUEUE_ID --status promoted --promotion-target-id SUGGESTION_ID --promotion-outcome accepted\nuv run notification-hub personal-ops-queue-health\nuv run notification-hub personal-ops-queue-review\nuv run notification-hub-personal-ops-queue-health --json\nuv run notification-hub personal-ops-outcome-sync-reminder\nuv run notification-hub-personal-ops-outcome-sync-reminder --json\nuv run notification-hub personal-ops-queue-burn-in\nuv run notification-hub-personal-ops-queue-burn-in --json\nuv run notification-hub personal-ops-queue-burn-in --save-report\nuv run notification-hub personal-ops-queue-scenario\nuv run notification-hub logs\nuv run notification-hub-logs --json\nuv run notification-hub burn-in --minutes 10\nuv run notification-hub-burn-in --json\nuv run notification-hub verify-runtime\nuv run notification-hub verify-runtime --verify-slack\nuv run notification-hub delivery-check --slack\nuv run notification-hub-delivery-check --json --slack\nuv run notification-hub-verify-runtime --json\nuv run notification-hub policy-check\nuv run notification-hub explain --source codex --level info --title \"Test\" --body \"Approval needed\"\nuv run notification-hub bootstrap-config\nuv run notification-hub retention --max-events 2000\n```\n\nThe doctor command checks the local API, LaunchAgent presence, bridge file path, push notifier,\nSlack Keychain setup, policy-config load status, and durable inbox status.\nThe smoke command posts a harmless `info` event and verifies the background worker lands it in the\nlive JSONL audit log.\nThe status command shows the compact day-to-day runtime view and suggests the next repair action\nwhen something is degraded, including recent Slack delivery failures found in daemon logs and\ndurable inbox dead letters or stale backlog.\nThe inbox command groups recent events by coordination intent so attention, blocked/waiting work,\nready work, completions, repeated rollups, and noisy producers are easy to scan.\nThe coordination-snapshot command combines inbox state and runtime status into bridge-ready JSON.\nBy default it only prints the snapshot; pass `--output path/to/snapshot.json` when you want a\ndurable file for a bridge-db import step.\nPass `--save-bridge-db` when you intentionally want to insert the snapshot into bridge-db as a Codex\nsystem snapshot. Use `--bridge-db-path` to target a non-default database during testing.\nThe coordination-readiness command combines runtime status, queue state, and saved queue burn-in\nreport history into one compact expansion gate. It returns `fix_noise_first`, `keep_burning_in`, or\n`ready_to_expand` without applying work.\nThe coordination-console command is the first compact expansion after that gate. It brings\nreadiness, action proposals, queue state, promoted-outcome reminders, burn-in report history, next\nreal signal state, and the next safe action into one read-only summary. It also classifies proposal\nlineage as new, queued, promoted, follow-up, resolved, or ignored so already-handled proposals stay\nvisible as history without being treated as fresh work. Its operator guide names the current stage and exact\nsafe commands for saving, validating, queueing, promoting, or outcome-syncing handoffs while keeping\napply behavior outside notification-hub. If a handoff is already queued or waiting on a promoted\noutcome, the console keeps that queue lifecycle as the next action before returning to readiness\ncleanup or new package review.\nThe console also includes a proposal-review summary that groups active proposals by source, project,\nintent, priority, and state, so the operator can tell when to review one proposal alone versus\nstaging a small batch package for inspection. The `/review` surface can save, queue, or locally\ndismiss one proposal group, and it can record a local group outcome such as `needs_follow_up`,\n`accepted`, `rejected`, `snoozed`, or `superseded`. Queueing still only creates notification-hub\nhandoff records and does not create personal-ops tasks.\nWhen a group's latest recorded outcome is terminal handled history, matching action IDs and stable\nproposal keys are treated as handled rather than fresh active proposals. `needs_follow_up` stays in\nfollow-up history, `snoozed` stays snoozed, `accepted` is resolved history, and `rejected` or\n`superseded` are closed history. A later save-only package inspection does not reopen that group,\nand repeated rollups can keep that handled state even when their newest evidence event rotates.\nQueueing, promotion, dismissal, or a different proposal key can still create a new actionable state.\nIf a handled follow-up gains rich evidence, the console keeps the proposal in handled history but\nsurfaces a `follow_up_review` mode and `review` next signal so the operator can explicitly decide\nwhether to keep it parked or record a new group outcome. Once an explicit outcome is recorded after\nthe rich evidence timestamp, that re-review prompt clears until newer rich evidence appears. The\nconsole, `/review` controls, and `action-proposal-group-outcome` default to the same 24-hour review\nwindow so an outcome records against the same evidence the console is showing.\nFor personal-ops mail approval groups, Proposal Review adds a local route recommendation that\nseparates concrete reply candidates from repeated phase or workflow chatter. The recommendation is\nadvisory only; it never promotes, suppresses, or sends by itself. The review controls can also save\nor queue just the `promote` route, or locally dismiss just the `suppress` route, so mixed mail batches\ndo not have to be handled as one all-or-nothing group.\nThe same recommendation now exposes a separate Operator Decision Required lane for real outbound\nmail approvals, so approval requests stay visible as operator work instead of being mixed into\nnoise-review follow-up. The approval lane packages every approval-titled mail item except known\nphase/workflow chatter, while the narrower promote route remains available for concrete reply\ncandidates.\nThe personal-ops-actions command turns inbox rollups into action proposals for review. It does not\nwrite to personal-ops; pass `--output path/to/actions.json` when you want a handoff file.\nIt scans a deeper candidate set than the display limit, so dismissed or policy-covered rollups do not\nhide a real operator signal that appears just below them.\nEach proposal now includes a stable dismissal key, and `action-proposal-dismiss` can hide a known\nrepeated proposal from future console/action exports without deleting the underlying event log.\n`action-proposal-dismissals` lists active or inactive dismissal records, while\n`action-proposal-undismiss` reactivates a proposal by appending a tombstone rather than rewriting\nhistory.\nThe operator-daily-state command builds a resume-ready local snapshot across runtime health, queue\nhealth, Coordination Console next signal, burn-in, dismissals, and the current rich/thin outcome\nquality summary. Pass `--save-report` when you want a timestamped JSON report under\n`~/.local/share/notification-hub/operator-state-reports/`. The HTTP review surface keeps\n`/review/operator-daily-state` read-only; use `POST /review/operator-daily-state/report` when the\nreview UI or a local script should save the same report.\nThe operator-review-session command summarizes recent local review activity, including grouped\nproposal saves, queues, dismissals, outcomes, and queue follow-through. It is read-only and mirrors\nthe review-session summary shown in `/review`; pass `--save-report` when you want a timestamped JSON\naudit report under `~/.local/share/notification-hub/operator-review-session-reports/`. The HTTP\nreview surface keeps `/review/operator-review-session` read-only; use\n`POST /review/operator-review-session/report` when the review UI or a local script should save the\nsame report. Saved review-session reports can be listed and inspected from `/review` for a compact\nsession timeline, and the review page surfaces the latest saved session as its own at-a-glance\npanel.\nThe operator-review-session-retention command prunes old saved review-session reports; it defaults to\na dry run and only deletes files when `--apply` is passed. The `/review` page also shows the same\nretention pressure as a read-only summary, so cleanup stays explicit.\nThe action-export-retention command prunes older saved action-export files; it defaults to a dry run\nand only deletes files when `--apply` is passed. The `--keep N` option controls how many newest\nfiles are preserved (default 20).\nThe operator-handoff-drill command runs the temporary queue lifecycle plus queue burn-in as a\nnon-applying rehearsal before using the same review flow for a real handoff. The `/review` drill\nbutton saves the burn-in proof by default and shows rich-evidence readiness, live-promotion\nreadiness, and the saved report status.\nThe `/review` page also includes a Real Signal Readiness lane that combines active proposals,\nhandled follow-ups, queue state, latest saved proof, the next safe command, and a rich-outcome\nguardrail so expansion stays operator-mediated until a real rich-evidence handoff resolves.\nIt also shows a structured First Rich Proof Gate with rich/thin active proposal counts, queue\nlifecycle state, and the exact next safe action for collecting the first resolved rich-evidence\nhandoff proof. The page compares the latest saved burn-in proof against the previous proof for\nreadiness and noise drift.\nWhile no rich-evidence handoff outcome is resolved, queueing is intentionally narrow: the review UI\nhides queue controls for thin or mixed groups, and the queue path rejects anything other than\nexactly one rich-evidence handoff. Thin-only groups can still be saved, dismissed, or marked\n`needs_follow_up` locally.\nThe Coordination Readiness panel includes a plain-language explanation that lists the current\nreadiness blocker when degraded, or confirms that runtime, policy, queue, and saved burn-in proof are\nclear when ready.\nPass `--save-review-package` when you want notification-hub to stage a local review package under\n`~/.local/share/notification-hub/action-exports/`; this still does not import or apply actions.\nThe validate-action-package command checks a saved review package before any future import/apply\nstep consumes it.\nBurn-in keeps repeated signatures visible for inspection, but filters active noise candidates through\nthe configured `[[noise.rules]]` so policy-covered repeats do not block coordination readiness.\nThe personal-ops-import command validates the package and stops before mutation by default. Pass\n`--enqueue` to add valid action proposals to a local personal-ops import queue under\nnotification-hub runtime state; queued items are handoff records only and are not personal-ops tasks,\napprovals, sends, or applied changes.\nThe personal-ops-queue command lists and updates queued handoffs through explicit lifecycle states:\n`queued`, `reviewed`, `rejected`, `snoozed`, `superseded`, and `promoted`. Marking an item\n`reviewed` is now treated as a reviewed-only closeout lane: evidence was checked and no downstream\npersonal-ops promotion is required. Marking an item `promoted` records that an operator-mediated\npersonal-ops task suggestion was created; it does not create that suggestion by itself. Promotion\nrecords can also store the personal-ops suggestion id and final `pending`, `accepted`, `rejected`, or\n`ignored` outcome.\nThe Coordination Console treats reviewed, follow-up, and snoozed handoffs as handled history, so\nthey do not block readiness once queue health is clean. Proposal Review also breaks handled history\ninto reviewed-only, follow-up, resolved, closed, and snoozed counts so reviewed-but-not-promoted work\nis visible. Handled mail follow-ups are summarized separately with rich/thin evidence counts, so\nrepeated handled mail echoes remain reviewable history without looking like fresh operator work.\nHandled proposals also include a lineage reason plus stable-key and evidence-rotation flags, so the\nconsole can explain when a newer event is still covered by an earlier `needs_follow_up` outcome.\nUse Coordination Console as the lineage-aware operator truth surface: raw `personal-ops-actions`\nexports are pre-lineage source evidence and can still list items that the console has already\nclassified as handled history.\nThe console also reports promoted handoff outcome quality by rich versus thin evidence and narrows\nthe monitor posture to notify only on active proposals, rich handled follow-up re-review, queued\nhandoffs, pending promoted outcomes, runtime degradation, or repeated diagnostic echoes.\nThe personal-ops-queue-health command is the normal maintenance check for this queue. It reports\nqueued item age, promoted handoffs still waiting on downstream outcome sync, stale pending\npromotions, and the next safe operator commands without applying work.\nThe personal-ops-queue-review command groups queued handoffs into review batches, highlights\noperator-decision approval counts, and shows the next local review command without approving,\nsending, or changing downstream systems.\nThe personal-ops-outcome-sync-reminder command is a narrower read-only reminder for promoted\nhandoffs that still need downstream personal-ops outcome sync. It returns `status: warn` when a\nreminder should be shown, but still leaves syncing to the operator.\nThe personal-ops-queue-burn-in command combines queue health, the temporary queue lifecycle\nscenario, and recent runtime burn-in into one non-applying readiness report. Use it before promoting\nreal handoffs or after syncing a downstream personal-ops outcome. The report now states the\noutcome-sync posture explicitly: notification-hub can show pending or stale promoted outcomes, but\nthe operator still owns creating and recording the downstream personal-ops work. Pass\n`--save-report` when you want a timestamped local report under\n`~/.local/share/notification-hub/burn-in-reports/`.\nThe personal-ops-queue-scenario command runs a temporary end-to-end queue lifecycle, including a\npromoted handoff with an accepted outcome, without touching the real operator queue.\nSee `docs/PRODUCT-BOUNDARY.md` for the current ownership split between notification-hub,\npersonal-ops, and bridge-db.\nThe logs command shows recent stored events, daemon stdout/stderr tails, and a summary of accepted\nversus rejected `/events` posts plus Slack delivery failures without changing local runtime state.\nIt also reports durable inbox status and dead-letter counts.\nThe burn-in command summarizes recent accepted/rejected event posts and repeated event signatures\nso noisy producers are easy to spot. Validation-error counts are scoped to the latest visible daemon\nstart so fixed pre-restart errors do not keep appearing as current burn-in failures. Recent Slack\ndelivery failures now degrade burn-in health so configured-but-broken delivery does not look clean.\nDaemon log files that have not changed inside the requested burn-in window are ignored for burn-in\nhealth, so older post-start failures do not block a fresh readiness check.\nDurable inbox dead letters or old queued backlog also degrade burn-in health.\nRepeated-event candidates now include review-only noise-rule suggestions so policy changes can be\ncopied deliberately instead of inferred from raw event rows.\nThe verify-runtime command combines doctor, policy-check, `/health/details`, runtime wiring checks,\ndurable inbox status, and recent burn-in health into one read-only report by default. Pass\n`--include-smoke` when you intentionally want it to post a harmless smoke event too. Pass\n`--verify-slack` or `--verify-push` when you intentionally want to send one real delivery-check\nnotification through that channel.\nThe delivery-check command runs the same explicit transport checks directly without the rest of\nthe runtime report.\nThe policy-check command inspects the current policy config for overlapping keywords, shadowed\nrouting rules, no-op rules, and drift between the live noise rules and repo sample before they cause\nconfusing behavior. It also suggests likely fixes for each warning it reports.\nThe explain command shows how a sample event would classify, route, and deliver without posting it\nto the live daemon or sending any notifications.\nThe bootstrap command copies the repo sample policy file into `~/.config/notification-hub/config.toml`\nwithout overwriting an existing config unless you pass `--force`.\nThe retention command archives older log entries into `~/.local/share/notification-hub/archive/`.\nThe daemon now also performs the same retention check automatically on a schedule, while the manual\ncommand remains available when you want to force a run immediately.\n\n## Event Contract\n\nAccepted event sources are `codex`, `cc`, `claude_ai`, `bridge_watcher`, `personal-ops`,\nand `notion-os`.\nAccepted levels are `urgent`, `normal`, and `info`; incoming `warn` and `warning` aliases are\nnormalized to `normal`.\nEvents may optionally include an `intent` value for coordination semantics. Supported intents are\n`needs_attention`, `blocked`, `waiting_on_user`, `ready_to_review`, `ready_to_merge`,\n`handoff_created`, `automation_failed`, `completed`, and `informational`. When omitted, the inbox\nuses deterministic title/body/source-level rules to infer intent.\nEvents may also include optional scalar `context` values for operator evidence, such as mail\n`thread_id`, `draft_id`, `message_id`, or `approval_id`. The hub stores and displays this context in\nrollups and review packages, but does not use it to send, approve, or mutate external systems.\nAction proposals also include an `evidence_quality` value. `rich` means the latest event has both a\nmail/thread anchor and a concrete work-item ID; `thin` means the proposal still needs more operator\ninspection before promotion.\nFor mail proposal routing, promotion-looking signals only enter the promote lane when evidence is\nrich. Thin promotion-looking signals stay in follow-up until the source emits enough context.\nProposal Review also reports promotion readiness for each active group, including which action IDs\nare ready to queue and which are blocked by thin evidence or workflow chatter.\nThe inbox report also includes `rollups` for repeated source/project/title/body patterns, so repeated\napproval drafts and completion pings can be reviewed as one grouped signal.\nPersonal-ops action exports are proposal-only: they include priority, state, suggested next action,\nevidence IDs, and optional evidence context, but they do not create tasks, send messages, approve\ndrafts, or mutate external systems.\nReview packages are local JSON files for an operator-mediated import step. They are intentionally\nseparate from any future personal-ops apply command.\nValidation checks the schema version, required action fields, duplicate action IDs, priority/state\nvalues, action counts, and optional scalar evidence context without mutating personal-ops.\nThe import stub reports `applied: false` even when validation passes, so no personal-ops task,\napproval, or send path is touched.\nThe local review surface is available at `http://127.0.0.1:9199/review` while the daemon is running.\nIt shows runtime state, inbox rollups, action proposals, and the current trust boundary without\nmutating local state.\nThe review page can also stage a review package, show recent saved review packages, inspect package\nactions/evidence, show queue lineage for already queued packages, queue import handoff items, filter\nqueued/promoted/pending/stale/resolved handoffs, mark queued items reviewed/rejected/snoozed/promoted,\nshow pending outcome-sync reminders, list and inspect saved burn-in reports, list/undismiss action\nproposal dismissals, show the Coordination Console next signal, run the temporary operator handoff\ndrill, delete saved review packages, validate the latest staged or saved package, and show the\nCoordination Console operator guide plus proposal-review grouping. It also surfaces sample-vs-live\npolicy drift and the latest saved review-session summary. The Proposal Review controls can save a\ngroup package, queue a group package for operator review, or dismiss a group locally, and each group\naction is recorded in local group-history JSONL so later console refreshes still show what happened.\nA group outcome can also be recorded locally after review. These controls still do not apply,\napprove, send, or mutate personal-ops.\nMail proposal groups include a route recommendation with promote, suppress, and follow-up counts so\nthe operator can split mixed batches before queueing or dismissing them. Route-aware group controls\nstill only stage local packages, queue local handoff records, or append local dismissals; they do not\nsend email, create personal-ops tasks, or approve work.\nThe review page also includes a Noise Candidate Review panel backed by\n`/review/noise-candidates`; it highlights repeated burn-in signatures with decision hints, while\nkeeping real mail approvals in Operator Decision Required instead of suggesting automatic policy\nsuppression.\nCoordination snapshots target bridge-db's `codex` snapshot shape: the emitted\n`bridge_snapshot` object can be passed as snapshot data after operator review, or saved directly\nwith the explicit `--save-bridge-db` flag.\nExact repeated producer bursts that match configured noise rules are accepted by the API but\nsuppressed before JSONL storage and notification delivery when they repeat inside the configured\nnoise window. Without a live policy config, the built-in default keeps suppressing repeated\n`personal-ops` reminder bursts.\n\n## Policy Config\n\nOptional runtime policy overrides live at:\n\n```text\n~/.config/notification-hub/config.toml\n```\n\nThe repo includes a starter example at:\n\n```text\nconfig/policy.example.toml\n```\n\nSupported sections today:\n\n```toml\n[classifier]\nurgent_keywords = [\"database down\", \"approval needed\"]\nnormal_keywords = [\"session complete\", \"ship it\"]\ninfo_keywords = [\"routine ping\"]\n\n[suppression]\nquiet_start_hour = 23\nquiet_end_hour = 7\ndedup_window_minutes = 30\nmax_push_per_hour = 5\nmax_slack_per_hour = 20\nmax_overflow_buffer = 500\nmax_quiet_queue = 200\n\n[[noise.rules]]\nsource = \"personal-ops\"\nproject = \"personal-ops\"\ntitle_contains = \"approval expires soon\"\nbody_contains = \"approval expires soon: review or cancel\"\nlevel = \"info\"\nwindow_minutes = 10\n\n[[noise.rules]]\nsource = \"personal-ops\"\nproject = \"personal-ops\"\ntitle_contains = \"daemon started\"\nlevel = \"info\"\nwindow_minutes = 10\n\n[[noise.rules]]\nsource = \"personal-ops\"\nproject = \"personal-ops\"\ntitle_contains = \"system needs attention\"\nbody_contains = \"run personal-ops doctor\"\nwindow_minutes = 30\n\n[[noise.rules]]\nsource = \"personal-ops\"\nproject = \"personal-ops\"\ntitle_contains = \"task suggestion pending\"\nlevel = \"info\"\nwindow_minutes = 10\n\n[[noise.rules]]\nsource = \"personal-ops\"\nproject = \"mail\"\ntitle_contains = \"draft updated\"\nlevel = \"info\"\nwindow_minutes = 10\n\n[[noise.rules]]\nsource = \"personal-ops\"\nproject = \"mail\"\ntitle_contains = \"approval requested\"\nbody_contains = \"workflow secondary approval\"\nlevel = \"urgent\"\nwindow_minutes = 30\n\n[[noise.rules]]\nsource = \"personal-ops\"\nproject = \"mail\"\ntitle_contains = \"draft ready\"\nbody_contains = \"workflow secondary approval\"\nlevel = \"info\"\nwindow_minutes = 30\n\n[[noise.rules]]\nsource = \"notion-os\"\ntitle_contains = \"external-signal-sync complete\"\nlevel = \"info\"\nwindow_minutes = 10\n\n[[noise.rules]]\nsource = \"notion-os\"\ntitle_contains = \"control-tower-sync complete\"\nlevel = \"info\"\nwindow_minutes = 10\n\n[retention]\nenabled = true\ninterval_minutes = 60\nmax_events = 2000\nkeep_archives = 10\n\n[[routing.rules]]\nproject = \"notification-hub\"\npriority = 20\nforce_level = \"normal\"\ndisable_push = true\ncontinue_matching = true\n\n[[routing.rules]]\nsource = \"bridge_watcher\"\npriority = 10\ndisable_slack = true\n\n[[routing.rules]]\nproject_prefix = \"notification-\"\ntitle_contains = \"review\"\nbody_contains = \"verification\"\ndisable_slack = true\n```\n\nIf the file is missing or invalid, notification-hub falls back to built-in defaults and reports the\nconfig status through the doctor command and `GET /health/details`.\nRouting rules are matched in order, and the first matching rule can override the classified level or\ndisable push/Slack delivery for that event.\nMatchers can now use exact source/project, `project_prefix`, and lowercase `title_contains`,\n`body_contains`, or `text_contains` checks.\nRules with a higher `priority` run first, and rules with the same priority keep their file order.\nIf a rule sets `continue_matching = true`, notification-hub keeps evaluating later rules so a policy\ncan compose multiple overrides instead of stopping at the first match.\nRetention is enabled by default with a conservative hourly check. It only rotates the log when the\nlive JSONL file grows beyond `max_events`, and it keeps up to `keep_archives` archived files.\nQuiet hours use a start-inclusive, end-exclusive window. When `quiet_start_hour \u003c quiet_end_hour`,\nthe window is same-day. When `quiet_start_hour \u003e quiet_end_hour`, the window crosses midnight.\nWhen both values are equal, quiet hours are disabled.\n\nFirst-time setup shortcut:\n\n```bash\nuv run notification-hub bootstrap-config\n```\n\nSafe policy-preview shortcut:\n\n```bash\nuv run notification-hub explain \\\n  --source codex \\\n  --level info \\\n  --title \"Review ready\" \\\n  --body \"Session complete after verification\"\n```\n\nSafe policy-audit shortcut:\n\n```bash\nuv run notification-hub policy-check\n```\n\nThe audit output is intentionally non-mutating. It reports warnings plus likely next fixes such as\nmoving a narrower rule earlier, removing a redundant matcher, or deleting a rule that does not\nchange behavior. It also flags disabled automatic retention and `continue_matching` rules that\ncannot actually continue into a later rule, redundant rules that add nothing beyond an earlier\ncontinue-matching chain, and same-priority rules where file order is still breaking the tie.\n\n## Verification\n\n```bash\nuv lock --check\nuv run --frozen pytest\nuv run --directory mcp_server --frozen pytest\nuv run --frozen ruff check\nuv run --frozen pyright\n```\n\nThe root test suite uses temporary runtime paths, so local verification does not write into the live\nmachine event log or watch the real bridge file. The MCP server smoke tests live in a separate uv\nproject under `mcp_server/`, so they are run with `uv run --directory mcp_server --frozen pytest`\nlocally and in CI.\nThe committed `uv.lock` file keeps local installs and CI in sync.\n\nRuntime diagnostics:\n\n```bash\ncurl http://127.0.0.1:9199/health\ncurl http://127.0.0.1:9199/health/details\nuv run --frozen notification-hub-doctor\nuv run --frozen notification-hub status\nuv run --frozen notification-hub inbox\nuv run --frozen notification-hub coordination-snapshot\nuv run --frozen notification-hub coordination-snapshot --save-bridge-db\nuv run --frozen notification-hub coordination-readiness\nuv run --frozen notification-hub coordination-console\nuv run --frozen notification-hub personal-ops-actions\nuv run --frozen notification-hub action-proposal-dismiss DISMISSAL_KEY --reason \"known repeated test signal\"\nuv run --frozen notification-hub action-proposal-dismissals\nuv run --frozen notification-hub action-proposal-undismiss DISMISSAL_KEY --reason \"signal is useful again\"\nuv run --frozen notification-hub personal-ops-actions --save-review-package\nuv run --frozen notification-hub validate-action-package path/to/actions.json\nuv run --frozen notification-hub personal-ops-import path/to/actions.json\nuv run --frozen notification-hub personal-ops-import path/to/actions.json --enqueue\nuv run --frozen notification-hub personal-ops-queue\nuv run --frozen notification-hub personal-ops-queue --queue-id QUEUE_ID --status rejected --reason \"duplicate\"\nuv run --frozen notification-hub personal-ops-queue-health\nuv run --frozen notification-hub-personal-ops-queue-health --json\nuv run --frozen notification-hub personal-ops-outcome-sync-reminder\nuv run --frozen notification-hub-personal-ops-outcome-sync-reminder --json\nuv run --frozen notification-hub personal-ops-queue-burn-in\nuv run --frozen notification-hub-personal-ops-queue-burn-in --json\nuv run --frozen notification-hub personal-ops-queue-burn-in --save-report\nuv run --frozen notification-hub personal-ops-queue-scenario\nuv run --frozen notification-hub operator-daily-state\nuv run --frozen notification-hub operator-review-session\nuv run --frozen notification-hub operator-review-session --save-report\nuv run --frozen notification-hub operator-review-session-retention --keep 20\nuv run --frozen notification-hub operator-review-session-retention --keep 20 --apply\nuv run --frozen notification-hub action-export-retention --keep 20\nuv run --frozen notification-hub action-export-retention --keep 20 --apply\nuv run --frozen notification-hub operator-handoff-drill\nuv run --frozen notification-hub logs\ncurl http://127.0.0.1:9199/review\ncurl http://127.0.0.1:9199/review/packages\ncurl http://127.0.0.1:9199/review/package/personal-ops-actions-YYYYMMDD-HHMMSS.json\ncurl http://127.0.0.1:9199/review/operator-review-session-retention\ncurl -X POST http://127.0.0.1:9199/review/package/personal-ops-actions-YYYYMMDD-HHMMSS.json/queue\ncurl http://127.0.0.1:9199/review/import-queue\ncurl http://127.0.0.1:9199/review/import-queue-review\ncurl http://127.0.0.1:9199/review/coordination-readiness\ncurl http://127.0.0.1:9199/review/coordination-console\ncurl http://127.0.0.1:9199/review/noise-candidates\ncurl http://127.0.0.1:9199/review/policy-check\ncurl http://127.0.0.1:9199/review/outcome-sync-reminder\ncurl http://127.0.0.1:9199/review/action-proposal-dismissals\ncurl -X POST http://127.0.0.1:9199/review/action-proposal/DISMISSAL_KEY/dismiss \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"reason\":\"known repeated test signal\"}'\ncurl -X POST http://127.0.0.1:9199/review/action-proposal/DISMISSAL_KEY/undismiss \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"reason\":\"signal is useful again\"}'\ncurl http://127.0.0.1:9199/review/operator-daily-state\ncurl -X POST http://127.0.0.1:9199/review/operator-daily-state/report\ncurl http://127.0.0.1:9199/review/operator-review-session\ncurl -X POST http://127.0.0.1:9199/review/operator-review-session/report\ncurl http://127.0.0.1:9199/review/operator-review-session-reports\ncurl http://127.0.0.1:9199/review/operator-review-session-report/operator-review-session-YYYYMMDD-HHMMSS.json\ncurl -X POST http://127.0.0.1:9199/review/operator-handoff-drill\ncurl -X PATCH http://127.0.0.1:9199/review/import-queue/QUEUE_ID \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"status\":\"reviewed\",\"reason\":\"evidence checked\"}'\ncurl -X DELETE http://127.0.0.1:9199/review/package/personal-ops-actions-YYYYMMDD-HHMMSS.json\nuv run --frozen notification-hub verify-runtime\nuv run --frozen notification-hub delivery-check --slack\nuv run --frozen notification-hub policy-check\nuv run --frozen notification-hub explain --source codex --level info --title \"Test\" --body \"Approval needed\"\nuv run --frozen notification-hub smoke\nuv run --frozen notification-hub retention --max-events 2000\n```\n\nRuntime change checklist:\n\n- Run the static gates before shipping code changes: lock check, tests, Ruff, and Pyright.\n- Run `notification-hub verify-runtime` before changing live launcher, hook, policy, or delivery\n  behavior.\n- Use `notification-hub verify-runtime --include-smoke` only when you intentionally want a real\n  POST-to-log smoke event.\n- Use `notification-hub verify-runtime --verify-slack`, `--verify-push`, or\n  `notification-hub delivery-check` only when you intentionally want a real delivery notification.\n- Confirm GitHub Actions passes after pushing to `main`.\n\n## Runtime Notes\n\n- The daemon is localhost-only.\n- The canonical local Python version is pinned in `.python-version` and matches CI's Python 3.12\n  target.\n- Accepted events are committed to `~/.local/share/notification-hub/inbox.sqlite3` before `POST\n  /events` returns 201.\n- The JSONL event log is processed-event audit history at\n  `~/.local/share/notification-hub/events.jsonl`; it is not the durability layer.\n- Slack webhook secrets are read from macOS Keychain and are never stored in repo files.\n- If the Slack webhook is not configured, the daemon stays healthy and continues local delivery\n  without spamming repeated Slack-failure warnings.\n- If a Slack webhook is added later, the daemon will retry Keychain lookup automatically within\n  about a minute, so a manual restart is usually not required.\n- LaunchAgent support lives at `~/Library/LaunchAgents/com.yourname.notification-hub.plist`.\n  The template at `ops/launchagents/com.saagar.notification-hub.plist` uses `__HOME__` tokens and\n  a `com.yourname` label placeholder — substitute your home directory and rename the label to your\n  own reverse-domain prefix before installing.\n- Repo-owned runtime templates live under `ops/`: the LaunchAgent template, Claude Code hook\n  template, and Codex hook template are the source of truth for machine-local wiring.\n- `GET /health/details` reports whether push delivery is available, whether Slack is configured,\n  whether key local files exist, whether a policy config file was loaded, how many policy warnings\n  were found, the current retention settings plus the last retention result, and current\n  suppression queue counters, and whether runtime wiring matches the checked-in templates, without\n  exposing secrets.\n\nRefresh local runtime wiring from repo templates:\n\n```bash\n# Substitute your home dir and rename the label to your reverse-domain prefix first:\nsed 's|__HOME__|'\"$HOME\"'|g; s|com\\.yourname|com.yourname|g' \\\n  ops/launchagents/com.saagar.notification-hub.plist \\\n  \u003e ~/Library/LaunchAgents/com.yourname.notification-hub.plist\ninstall -m 755 ops/hooks/claude-notify.sh ~/.claude/hooks/notify.sh\ninstall -m 755 ops/hooks/codex-notify-local.py ~/.codex/hooks/notify_local.py\nlaunchctl bootout \"gui/$(id -u)\" ~/Library/LaunchAgents/com.yourname.notification-hub.plist 2\u003e/dev/null || true\nlaunchctl bootstrap \"gui/$(id -u)\" ~/Library/LaunchAgents/com.yourname.notification-hub.plist\nlaunchctl kickstart -k \"gui/$(id -u)/com.yourname.notification-hub\"\n```\n\n## Docs\n\n- `README.md`: project overview, setup, and verification\n- `docs/CURRENT-STATE.md`: restart index plus dated repo/runtime evidence; reverify live state before treating it as current\n- `docs/PRODUCT-BOUNDARY.md`: notification-hub, personal-ops, and bridge-db ownership split\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsaagpatel%2Fnotification-hub","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsaagpatel%2Fnotification-hub","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsaagpatel%2Fnotification-hub/lists"}