smart-ntfy

$npx mdskill add megalithic/dotfiles/smart-ntfy

Send context-aware notifications via ntfy for task completion, questions, errors, or milestones.

  • Helps agents notify users intelligently during various workflow stages.
  • Integrates with Bash tools and the ntfy command-line notification system.
  • Uses urgency levels and channel selection to determine notification priority.
  • Delivers results through command-line commands with options for titles, messages, and tracking.

SKILL.md

.github/skills/smart-ntfyView on GitHub ↗
---
name: smart-ntfy
description: Send intelligent notifications via ~/bin/ntfy with context-aware channel selection. Use when completing tasks, asking questions, encountering errors, or reaching milestones.
tools: Bash
---

# Smart Notification System (ntfy)

## Overview

You have access to a sophisticated multi-channel notification system via `~/bin/ntfy`. This skill helps you make smart decisions about when and how to notify the user.

## Quick Reference

```bash
# Basic notification
ntfy send -t "Title" -m "Message"

# With urgency levels: normal|high|critical
ntfy send -t "Title" -m "Message" -u high

# Send to phone via Pushover (for remote notifications)
ntfy send -t "Title" -m "Message" -P

# Question that may need retry (tracks until answered)
ntfy send -t "Question" -m "Should I continue?" -q

# Mark a question as answered
ntfy answer -t "Question" -m "Should I continue?"

# List pending unanswered questions
ntfy pending
```

## Command Structure

```bash
ntfy <command> [options]

Commands:
  send      Send a notification
  answer    Mark a question as answered
  pending   List pending questions
  help      Show help message
```

## Options

| Short | Long         | Description                                 |
|-------|--------------|---------------------------------------------|
| `-t`  | `--title`    | Notification title (required)               |
| `-m`  | `--message`  | Notification message (required)             |
| `-u`  | `--urgency`  | normal\|high\|critical (default: normal)    |
| `-s`  | `--source`   | Source app name (auto-detected if omitted)  |
| `-S`  | `--no-source`| Disable source prefix in title              |
| `-p`  | `--phone`    | Send to phone via iMessage                  |
| `-P`  | `--pushover` | Send via Pushover                           |
| `-q`  | `--question` | Track for retry if unanswered               |

## Source Detection

By default, ntfy auto-detects the calling program by walking up the process tree
and prefixes the title (e.g., `[claude] Task Done`). This helps identify which
tool sent the notification.

```bash
# Auto-detection (default) - title becomes "[claude] Done" if called from Claude
ntfy send -t "Done" -m "Tests passed"

# Disable source prefix entirely
ntfy send -t "Done" -m "Tests passed" -S

# Override with custom source name
ntfy send -t "Done" -m "Tests passed" -s "myapp"  # → "[myapp] Done"
```

## Notification Channels

The ntfy script automatically routes based on user attention:

1. **Canvas Notification** - On-screen overlay (HAL 9000 icon)
   - Normal: Bottom-left, 5 seconds
   - High/Critical: Center screen with dimmed background

2. **macOS Notification Center** - Always sent for logging
   - Captured by Hammerspoon watcher
   - Logged to SQLite: `~/.local/share/hammerspoon/hammerspoon.db`

3. **Pushover** - Remote phone notification
   - Auto-sent on `critical` urgency
   - Or explicitly with `-P`

4. **iMessage** - Direct to user's phone
   - Auto-sent on `critical` urgency
   - Or explicitly with `-p`

## Decision Trees

### "Should I send a notification?"

```
Should I notify?
│
├─▶ Task completed after 30+ seconds?
│   └─▶ YES → Send normal urgency
│
├─▶ Error/failure occurred?
│   └─▶ Recoverable error → high urgency
│   └─▶ Critical error → critical urgency (sends to phone)
│
├─▶ Need user input/decision?
│   └─▶ YES → Send with -q flag (question tracking)
│   └─▶ Set urgency to high for prominence
│
├─▶ Security issue found?
│   └─▶ ALWAYS send critical (phone notification)
│
├─▶ Progress update on long task?
│   └─▶ Only at milestones, normal urgency
│
└─▶ Minor step completed?
    └─▶ DON'T send - too noisy
```

### "What urgency should I use?"

```
Urgency selection?
│
├─▶ User MUST see this NOW (security, critical failure)?
│   └─▶ critical (auto-sends to phone)
│
├─▶ User should see soon but not life-threatening?
│   └─▶ high (centered overlay, longer duration)
│
├─▶ FYI / completed task / progress?
│   └─▶ normal (corner overlay, 5 seconds)
│
└─▶ User is actively watching this terminal?
    └─▶ Consider not sending at all
```

### "Should I send to phone?"

```
Phone notification?
│
├─▶ Urgency is critical?
│   └─▶ Auto-sent to phone (you don't need to add -p or -P)
│
├─▶ User explicitly requested remote notification?
│   └─▶ Use -P (Pushover) or -p (iMessage)
│
├─▶ User is away from desk (display asleep)?
│   └─▶ Auto-routed to phone for critical
│   └─▶ Normal/high → phone only if explicitly requested
│
└─▶ User is at desk?
    └─▶ Canvas overlay is sufficient
```

## Urgency Guidelines

| Situation | Urgency | Why |
|-----------|---------|-----|
| Task completed successfully | `normal` | User will see canvas |
| Task completed with warnings | `high` | Draw more attention |
| Task failed/error | `critical` | Sends to phone too |
| Question needing answer | `high` | Centered, prominent |
| Security vulnerability found | `critical` | Always notify phone |
| Long task progress update | `normal` | Non-intrusive |

## When to Send Notifications

**DO send for:**
- Task completion (especially long-running)
- Errors requiring user attention
- Questions needing user input
- Significant milestones
- Security findings

**DON'T send for:**
- Minor steps completed
- Info user is actively watching
- Debugging output
- Redundant status updates

## Message Best Practices

1. **Titles:** Keep under 50 characters, be specific
2. **Messages:** Keep under 200 characters, include key details
3. **Include metrics:** "42 tests passed in 3.2s" not just "Tests passed"
4. **Be actionable:** "Check logs at /tmp/build.log" not just "Error occurred"

## Question Tracking

The `-q` flag marks a notification as a question that needs acknowledgment:

```bash
# Send a question
ntfy send -t "Confirm" -m "Deploy to production?" -u high -q

# Later, mark it as answered
ntfy answer -t "Confirm" -m "Deploy to production?"

# Or answer by ID (returned from send)
ntfy answer -i <question_id>

# Check pending questions
ntfy pending
```

## Attention Detection

The ntfy script automatically detects if you're paying attention:
- Checks if terminal app is frontmost
- Checks current tmux session/window
- Checks display state (asleep/locked)

If user IS paying attention → subtle NC notification only
If user NOT paying attention → canvas overlay + NC + optional remote

## Examples

```bash
# Task completed
ntfy send -t "Build Complete" -m "42 tests passed, 0 failures in 3.2s"

# Error with high urgency
ntfy send -t "Build Failed" -m "3 type errors in src/auth.ts:45,78,123" -u high

# Critical security finding (auto-sends to phone)
ntfy send -t "Security Alert" -m "Found hardcoded API key in config.js" -u critical

# Question for user
ntfy send -t "Clarification Needed" -m "Should I refactor the auth module or just fix the bug?" -u high -q

# Send to phone when away
ntfy send -t "Task Done" -m "Deployment completed successfully" -p
```

## Internal Architecture

The ntfy script delegates all logic to Hammerspoon's notification system:

```
ntfy send → N.send(opts) → routeNotification() → sendCanvas/sendMacOS/sendPhone
                                                      ↓
                                              sendCanvasNotification()
```

### Key Function Signatures

**N.send()** - Main entry point (lib/notifications/send.lua)
```lua
N.send({
  title = "string",      -- Required
  message = "string",    -- Required
  urgency = "normal",    -- "normal"|"high"|"critical"
  phone = false,         -- Send via iMessage
  pushover = false,      -- Send via Pushover
  question = false,      -- Track for retry
  context = "session:window:pane",  -- tmux context for attention detection
})
-- Returns: { sent = bool, channels = {"macos","phone"}, reason = string, questionId = string|nil }
```

**sendCanvasNotification()** - Visual overlay (lib/notifications/notifier.lua)
```lua
sendCanvasNotification(title, message, opts)
-- opts: { subtitle?, duration?, anchor?, position?, dimBackground?, appImageID?, appBundleID?, includeProgram?, ... }
-- Uses U.defaults() for merging - subtitle defaults to "", duration to config.defaultDuration or 5
```

**M.process()** - Rule-based routing (lib/notifications/processor.lua)
```lua
M.process(rule, opts)
-- opts: { title, subtitle?, message, axStackingID, bundleID, notificationID?, notificationType?, subrole?, matchedCriteria?, urgency? }
-- Uses U.defaults() for merging - title/subtitle/message default to "", urgency to "normal"
```

### Attention Detection Flow

1. Check display state (awake/asleep/locked)
2. Check if terminal is frontmost app
3. Query tmux for active session:window:pane
4. Compare against calling context
5. Route: `paying_attention` → subtle | `not_paying_attention` → full | `display_asleep` → remote_only

## Related Files

- `~/bin/ntfy` - CLI wrapper (bash)
- `~/.dotfiles/config/hammerspoon/lib/notifications/send.lua` - N.send() API
- `~/.dotfiles/config/hammerspoon/lib/notifications/notifier.lua` - Canvas rendering
- `~/.dotfiles/config/hammerspoon/lib/notifications/processor.lua` - Rule processing
- `~/.dotfiles/config/hammerspoon/watchers/notification.lua` - NC capture
- `~/.local/share/hammerspoon/hammerspoon.db` - Notification history

## Return Values and Error Handling

### ntfy send Output

The send command returns a space-separated status line:

```
<status> <reason> <channels> [questionId]
```

| Field | Values | Description |
|-------|--------|-------------|
| `status` | `sent` / `suppressed` | Whether notification was sent |
| `reason` | `paying_attention` / `not_paying_attention` / `display_asleep` | Why routing decision was made |
| `channels` | `macos,canvas,phone` | Comma-separated list of channels used |
| `questionId` | UUID or empty | ID for tracking questions |

**Example outputs:**
```bash
sent not_paying_attention canvas,macos      # User away, canvas shown
sent paying_attention macos                  # User watching, subtle NC only
sent display_asleep phone,macos              # Screen locked, sent to phone
suppressed paying_attention                  # User watching, suppressed
```

### Exit Codes

| Code | Meaning |
|------|---------|
| 0 | Success (notification sent or suppressed as intended) |
| 1 | Invalid arguments (missing title/message) |
| 1 | Unknown command |
| Non-zero | Hammerspoon error (hs command failed) |

### Error Handling Patterns

```bash
# Check if notification was sent
result=$(ntfy send -t "Done" -m "Task complete")
if [[ "$result" == sent* ]]; then
  echo "Notification delivered"
fi

# Check which channels were used
result=$(ntfy send -t "Done" -m "Task complete" -u critical)
if [[ "$result" == *"phone"* ]]; then
  echo "Sent to phone"
fi

# Get question ID for later answering
result=$(ntfy send -t "Question" -m "Continue?" -q)
question_id=$(echo "$result" | awk '{print $4}')
if [[ -n "$question_id" ]]; then
  # Store for later: ntfy answer -i "$question_id"
  echo "Question ID: $question_id"
fi
```

## Troubleshooting

### Notifications Not Appearing

```bash
# 1. Check Hammerspoon is running
pgrep Hammerspoon || echo "Hammerspoon not running!"

# 2. Check hs CLI works
hs -c "print('hello')"
# Should print: hello

# 3. Check N module loads
hs -c "local N = require('lib.notifications'); print('loaded')"
# Should print: loaded

# 4. Verify macOS permissions
# System Settings → Notifications → Hammerspoon → Allow

# 5. Check for Lua errors
hs -c "local N = require('lib.notifications'); N.send({title='test', message='test'})"
# Should return status line, not error
```

### Canvas Not Showing

```bash
# Check canvas availability
hs -c "print(hs.canvas)"
# Should print: table: 0x...

# Check screen count
hs -c "print(#hs.screen.allScreens())"
# Should be > 0

# Force canvas test
hs -c "
local c = hs.canvas.new({x=100,y=100,w=200,h=100})
c:appendElements({type='rectangle', fillColor={red=1}})
c:show()
hs.timer.doAfter(2, function() c:delete() end)
"
# Red rectangle should appear for 2 seconds
```

### Phone Notifications Not Working

```bash
# Check Pushover credentials
hs -c "print(N.config.pushover.userKey and 'configured' or 'missing')"

# Check iMessage can send
hs -c "print(N.config.phoneNumber or 'no phone number')"

# Test iMessage directly (be careful, actually sends!)
# hs -c "hs.messages.iMessage('phone_number', 'test')"
```

### Question Tracking Issues

```bash
# List pending questions
ntfy pending

# Check question database
hs -c "
local N = require('lib.notifications')
local pending = N.getPendingQuestions()
print(#pending .. ' pending questions')
"
```

## Self-Discovery Patterns

### Exploring the API

```bash
# Show help
ntfy help

# Show N module functions
hs -c "for k,v in pairs(require('lib.notifications')) do print(k, type(v)) end"

# Show config values
hs -c "local N = require('lib.notifications'); for k,v in pairs(N.config or {}) do print(k,v) end"
```

### Testing Different Urgencies

```bash
# Test normal (corner, 5s)
ntfy send -t "Test" -m "Normal urgency" -u normal

# Test high (centered, longer)
ntfy send -t "Test" -m "High urgency" -u high

# Test critical (centered + phone)
# Warning: Actually sends to phone!
# ntfy send -t "Test" -m "Critical urgency" -u critical
```

### Checking Attention State

```bash
# Check if terminal is focused
hs -c "print(hs.window.focusedWindow():application():name())"

# Check display state
hs -c "print(hs.caffeinate.get('displayIdle') and 'active' or 'idle')"

# Check current tmux context
tmux display-message -p '#S:#I:#P' 2>/dev/null || echo "not in tmux"
```

## Known Limitations

1. **iMessage requires permissions** - System must have access to Messages
2. **Pushover requires API key** - Must be configured in Hammerspoon
3. **Canvas requires Accessibility** - Must grant Hammerspoon accessibility access
4. **Source detection heuristic** - May not always identify the correct calling app
5. **Question tracking in memory** - Questions lost on Hammerspoon restart

More from megalithic/dotfiles

SkillDescription
brave-searchWeb search and content extraction via Brave Search API. Use for searching documentation, facts, or any web content. Lightweight, no browser required.
cli-toolsModern CLI tool usage (fd, rg) for fast file and content searching. Critical for Nix store searches and large codebases. Use when searching files or content, especially in /nix/store.
hsComprehensive guide for Hammerspoon development in this dotfiles repo. Covers config patterns, debugging decision trees, API reference, performance monitoring, and troubleshooting.
image-handlingImage handling for Claude API constraints (5MB max, 8000px max dimension). Use when working with images, screenshots, or MCP browser tools.
jjJujutsu (jj) version control workflow, commands, and best practices. Use when working with version control in jj-enabled repos. Covers commits, bookmarks, workspaces, and safe push patterns.
nixExpert help with Nix, nix-darwin, home-manager, flakes, and nixpkgs. Use for dotfiles configuration, package management, module development, hash fetching, debugging evaluation errors, and understanding Nix idioms and patterns.
notesExpert help with the meganote system - cross-tool note capture, daily notes, and obsidian.nvim integration. Covers Hammerspoon, Shade, nvim, and the full capture → daily note linking pipeline.
nvimComprehensive guide for Neovim configuration in this dotfiles repo. Covers plugin management, LSP debugging, treesitter, keymaps, performance, and troubleshooting decision trees.
previewDisplay code, diffs, images, and other content in a tmux pane or popup. Auto-detects nvim/megaterm for floating popups.
shadeExpert help with Shade - the native Swift note capture app. Use for debugging Shade issues, understanding IPC protocols, implementing Hammerspoon integration, nvim RPC, context gathering, and meganote workflows.