diff --git a/plugins/lean-attention/.claude-plugin/plugin.json b/plugins/lean-attention/.claude-plugin/plugin.json
index 6f119cc..f71e054 100644
--- a/plugins/lean-attention/.claude-plugin/plugin.json
+++ b/plugins/lean-attention/.claude-plugin/plugin.json
@@ -3,7 +3,7 @@
"name": "lean-attention",
"displayName": "Lean Attention",
"description": "Desktop notification and a spoken line whenever your agent waits for you: a permission prompt, a question, or an idle session. The script is agent-agnostic. Linux and macOS.",
- "version": "0.1.0",
+ "version": "0.2.0",
"author": {
"name": "LeanCode"
},
diff --git a/plugins/lean-attention/CHANGELOG.md b/plugins/lean-attention/CHANGELOG.md
index 3c3fe08..c9019d0 100644
--- a/plugins/lean-attention/CHANGELOG.md
+++ b/plugins/lean-attention/CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog
+## 0.2.0
+
+- `LEAN_ATTENTION_ESCALATE_COMMAND`: your own command, run when nobody reacts within `LEAN_ATTENTION_ESCALATE_AFTER` seconds (default 120, `0` runs it right away). Gets the title and message as `$1` and `$2`.
+- `hooks/hooks.json`: `UserPromptSubmit`, `PostToolUse`, `Stop` and `SessionEnd` hooks cancel a pending escalation through `notify.sh --cancel`.
+- No new built-in channels; README shows a KDE Connect phone ping as an example.
+
## 0.1.0
- Initial `lean-attention` plugin.
diff --git a/plugins/lean-attention/README.md b/plugins/lean-attention/README.md
index 187fea1..0858a70 100644
--- a/plugins/lean-attention/README.md
+++ b/plugins/lean-attention/README.md
@@ -31,6 +31,8 @@ Set these in the agent's environment (the `env` block of its settings file):
| `LEAN_ATTENTION_PHRASE` | `Your agent is waiting for you` | The spoken line |
| `LEAN_ATTENTION_TITLE` | `Agent` | Notification title, shown as `
· ` |
| `LEAN_ATTENTION_LANG` | speech-dispatcher default | Linux voice language, e.g. `pl` for a Polish phrase |
+| `LEAN_ATTENTION_ESCALATE_COMMAND` | unset | Your own command, run when nobody reacts in time; see [Escalation](#escalation) |
+| `LEAN_ATTENTION_ESCALATE_AFTER` | `120` | Seconds to wait before the escalation command; `0` runs it right away |
```json
{
@@ -41,6 +43,27 @@ Set these in the agent's environment (the `env` block of its settings file):
}
```
+## Escalation
+
+The notification and voice are the default. For anything beyond that, plug in your own command:
+it runs only if you don't react within `LEAN_ATTENTION_ESCALATE_AFTER` seconds, and gets the title
+and the message as `$1` and `$2`. Reacting means sending a prompt, letting a tool run, or the turn
+ending; the plugin's other hooks cancel the pending command then.
+
+A ping on an Android phone through KDE Connect two minutes after the desktop notification:
+
+```json
+{
+ "env": {
+ "LEAN_ATTENTION_ESCALATE_COMMAND": "kdeconnect-cli -d --ping-msg \"$1: $2\"",
+ "LEAN_ATTENTION_ESCALATE_AFTER": "120"
+ }
+}
+```
+
+`kdeconnect-cli -a --id-only` prints the device id. Anything else you want (a phone push, a
+Slack message, a smart lamp) goes in the same variable.
+
## Other agents
`scripts/notify.sh` is agent-agnostic and takes its input in three shapes:
@@ -56,10 +79,13 @@ notify = ["bash", "/path/to/lean-attention/scripts/notify.sh"]
```
For any other agent, point its "run a command when the agent stops or needs input" hook at the
-script and send one of the shapes above. Only the bundled `Notification` hook is tested so far.
+script and send one of the shapes above. A delayed escalation needs a signal that you came back:
+call `notify.sh --cancel` (same input shapes) from the agent's "user replied" hook. An agent without
+one, such as Codex, should set `LEAN_ATTENTION_ESCALATE_AFTER=0` so the command runs right away
+instead of after a delay nobody can cancel. Only the bundled hooks are tested so far.
## Assets
-- `hooks/hooks.json`: the `Notification` hook that runs the script.
+- `hooks/hooks.json`: the `Notification` hook that runs the script, plus `UserPromptSubmit`, `PostToolUse`, `Stop` and `SessionEnd` hooks that cancel a pending escalation (a no-op when none is pending).
- `scripts/notify.sh`: picks the notifier and voice for the platform; always exits 0, so it never blocks the agent.
- `/lean-attention-usage`: what the plugin does, how to fix a silent setup, and how to wire it to another agent.
diff --git a/plugins/lean-attention/hooks/hooks.json b/plugins/lean-attention/hooks/hooks.json
index 5fcf27f..ea500dc 100644
--- a/plugins/lean-attention/hooks/hooks.json
+++ b/plugins/lean-attention/hooks/hooks.json
@@ -10,6 +10,50 @@
}
]
}
+ ],
+ "UserPromptSubmit": [
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/notify.sh\" --cancel",
+ "timeout": 5
+ }
+ ]
+ }
+ ],
+ "PostToolUse": [
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/notify.sh\" --cancel",
+ "timeout": 5
+ }
+ ]
+ }
+ ],
+ "Stop": [
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/notify.sh\" --cancel",
+ "timeout": 5
+ }
+ ]
+ }
+ ],
+ "SessionEnd": [
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/notify.sh\" --cancel",
+ "timeout": 5
+ }
+ ]
+ }
]
}
}
diff --git a/plugins/lean-attention/scripts/notify.sh b/plugins/lean-attention/scripts/notify.sh
index 637ec3a..f7d3bec 100755
--- a/plugins/lean-attention/scripts/notify.sh
+++ b/plugins/lean-attention/scripts/notify.sh
@@ -1,9 +1,21 @@
#!/usr/bin/env bash
-# Agent-agnostic "waiting for you" alert: a desktop notification plus a spoken line.
+# Agent-agnostic "waiting for you" alert: a desktop notification plus a spoken line, and an
+# optional escalation command that runs only if nobody reacts within a delay.
# Input: a JSON payload on stdin (hook-style agents) or as the last argument (Codex `notify`),
-# or plain text arguments. Always exits 0 so it never blocks the agent.
+# or plain text arguments. `notify.sh --cancel` (same inputs) disarms a pending escalation.
+# Always exits 0 so it never blocks the agent.
set -u
+state_dir="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/lean-attention"
+
+mode=notify
+if [ "${1:-}" = "--cancel" ]; then
+ mode=cancel
+ shift
+ # Runs on every tool call: return before reading anything when nothing is armed.
+ [ -d "$state_dir" ] && [ -n "$(ls -A "$state_dir" 2>/dev/null)" ] || exit 0
+fi
+
default_message="Waiting for you"
payload=""
if [ $# -gt 0 ]; then
@@ -17,14 +29,23 @@ elif [ ! -t 0 ]; then
fi
message="$default_message"
-project=""
+key=""
+cwd=""
if [ -n "$payload" ] && command -v jq >/dev/null 2>&1; then
message=$(jq -r --arg d "$default_message" '.message // $d' <<<"$payload" 2>/dev/null || echo "$default_message")
+ key=$(jq -r '.session_id // empty' <<<"$payload" 2>/dev/null || true)
cwd=$(jq -r '.cwd // empty' <<<"$payload" 2>/dev/null || true)
- [ -n "$cwd" ] && project=$(basename "$cwd")
fi
-[ -z "$project" ] && project=$(basename "$PWD")
+[ -z "$cwd" ] && cwd="$PWD"
+[ -z "$key" ] && key="$cwd"
+state_file="$state_dir/$(printf '%s' "$key" | tr -c 'A-Za-z0-9_-' '_')"
+if [ "$mode" = cancel ]; then
+ rm -f "$state_file"
+ exit 0
+fi
+
+project=$(basename "$cwd")
title="${LEAN_ATTENTION_TITLE:-Agent} · $project"
phrase="${LEAN_ATTENTION_PHRASE:-Your agent is waiting for you}"
speak="${LEAN_ATTENTION_SPEAK:-1}"
@@ -43,4 +64,25 @@ case "$(uname -s)" in
fi
;;
esac
+
+escalate="${LEAN_ATTENTION_ESCALATE_COMMAND:-}"
+if [ -n "$escalate" ]; then
+ after="${LEAN_ATTENTION_ESCALATE_AFTER:-120}"
+ case "$after" in '' | *[!0-9]*) after=120 ;; esac
+ # The command gets the title and message as $1 and $2.
+ if [ "$after" -eq 0 ]; then
+ sh -c "$escalate" sh "$title" "$message" >/dev/null 2>&1 || true
+ else
+ mkdir -p "$state_dir"
+ token="$$-$(date +%s%N)"
+ printf '%s' "$token" >"$state_file"
+ # Detached so the hook returns now; fires only if no cancel replaced or removed the token.
+ nohup sh -c '
+ sleep "$1"
+ [ "$(cat "$2" 2>/dev/null)" = "$3" ] || exit 0
+ rm -f "$2"
+ sh -c "$4" sh "$5" "$6"
+ ' sh "$after" "$state_file" "$token" "$escalate" "$title" "$message" >/dev/null 2>&1 &
+ fi
+fi
exit 0
diff --git a/plugins/lean-attention/skills/lean-attention-usage/SKILL.md b/plugins/lean-attention/skills/lean-attention-usage/SKILL.md
index aa361fc..63d6278 100644
--- a/plugins/lean-attention/skills/lean-attention-usage/SKILL.md
+++ b/plugins/lean-attention/skills/lean-attention-usage/SKILL.md
@@ -17,10 +17,12 @@ plugin `README.md`; read it rather than restating it.
1. To explain the plugin, summarize the paragraph above and point at the plugin `README.md`.
2. To change or mute the voice, show the `env` example from the README with the variable the user needs.
-3. To use it from another agent, point at the "Other agents" section of the README; do not invent hook config for an agent the README does not cover.
-4. To troubleshoot silence, check in this order and stop at the first failure:
+3. To get pinged somewhere else (phone, chat) when they don't react, point at the README "Escalation" section; the user's own `LEAN_ATTENTION_ESCALATE_COMMAND` is the extension point, there are no built-in channels beyond desktop and voice.
+4. To use it from another agent, point at the "Other agents" section of the README; do not invent hook config for an agent the README does not cover.
+5. To troubleshoot silence, check in this order and stop at the first failure:
- The hook is loaded: ask the user to open `/hooks` and look for a `Notification` entry from `lean-attention`. If it is missing, restart the agent.
- The script runs: `echo '{"message":"test","cwd":"'"$PWD"'"}' | "/scripts/notify.sh"`.
- The tools exist: Linux `command -v notify-send spd-say jq`, macOS `command -v osascript say jq`.
- On Linux with no voice: `spd-say -w "test"`; if that is silent, speech-dispatcher has no output module (install `speech-dispatcher-espeak-ng`).
- On macOS with no banner: System Settings → Notifications → Script Editor.
+ - Escalation never fires: run the command from `LEAN_ATTENTION_ESCALATE_COMMAND` by hand with two arguments; then check that a pending escalation file appears under `${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/lean-attention/` after a notification.