Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/lean-attention/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
6 changes: 6 additions & 0 deletions plugins/lean-attention/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
30 changes: 28 additions & 2 deletions plugins/lean-attention/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<title> · <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
{
Expand All @@ -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:
Expand All @@ -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.
44 changes: 44 additions & 0 deletions plugins/lean-attention/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
}
]
}
}
52 changes: 47 additions & 5 deletions plugins/lean-attention/scripts/notify.sh
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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}"
Expand All @@ -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
6 changes: 4 additions & 2 deletions plugins/lean-attention/skills/lean-attention-usage/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading