From 5a7174ed66f3eaea0876cfba406494cacb457c85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Garstecki?= Date: Tue, 6 Oct 2026 14:20:26 +0200 Subject: [PATCH] lean-attention: run a custom command when nobody reacts in time --- .../lean-attention/.claude-plugin/plugin.json | 2 +- plugins/lean-attention/CHANGELOG.md | 6 +++ plugins/lean-attention/README.md | 30 ++++++++++- plugins/lean-attention/hooks/hooks.json | 44 ++++++++++++++++ plugins/lean-attention/scripts/notify.sh | 52 +++++++++++++++++-- .../skills/lean-attention-usage/SKILL.md | 6 ++- 6 files changed, 130 insertions(+), 10 deletions(-) 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 ` · <project>` | | `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 <device-id> --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"'"}' | "<plugin root>/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.