roger-roger 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/package.json +45 -0
- package/skills/roger-roger/SKILL.md +289 -0
- package/skills/roger-roger/herdr-plugin.toml +38 -0
- package/skills/roger-roger/scripts/agent.mjs +132 -0
- package/skills/roger-roger/scripts/audio.mjs +392 -0
- package/skills/roger-roger/scripts/client.mjs +121 -0
- package/skills/roger-roger/scripts/daemon.mjs +604 -0
- package/skills/roger-roger/scripts/decisions.mjs +158 -0
- package/skills/roger-roger/scripts/handlers.mjs +1151 -0
- package/skills/roger-roger/scripts/herdr.mjs +140 -0
- package/skills/roger-roger/scripts/hooks-codex.mjs +154 -0
- package/skills/roger-roger/scripts/hooks-opencode.mjs +167 -0
- package/skills/roger-roger/scripts/hooks.mjs +420 -0
- package/skills/roger-roger/scripts/inbox.mjs +381 -0
- package/skills/roger-roger/scripts/install.mjs +560 -0
- package/skills/roger-roger/scripts/lib.mjs +1133 -0
- package/skills/roger-roger/scripts/names.mjs +84 -0
- package/skills/roger-roger/scripts/progress.mjs +91 -0
- package/skills/roger-roger/scripts/protocol.mjs +71 -0
- package/skills/roger-roger/scripts/roger-roger.mjs +536 -0
- package/skills/roger-roger/scripts/router.mjs +86 -0
- package/skills/roger-roger/scripts/sessions.mjs +218 -0
- package/skills/roger-roger/scripts/slack.mjs +240 -0
- package/skills/roger-roger/scripts/slackapp.mjs +205 -0
- package/skills/roger-roger/scripts/slackcli.mjs +144 -0
- package/skills/roger-roger/scripts/speaker.mjs +224 -0
- package/skills/roger-roger/scripts/speechkey.mjs +106 -0
- package/skills/roger-roger/scripts/tray.mjs +128 -0
- package/skills/roger-roger/scripts/tts.mjs +275 -0
- package/skills/roger-roger/scripts/tui.mjs +465 -0
- package/skills/roger-roger/slack/manifest.json +34 -0
- package/skills/roger-roger/sounds/alert.wav +0 -0
- package/skills/roger-roger/sounds/bubble.wav +0 -0
- package/skills/roger-roger/sounds/chime.wav +0 -0
- package/skills/roger-roger/sounds/ding.wav +0 -0
- package/skills/roger-roger/sounds/marimba.wav +0 -0
- package/skills/roger-roger/tray/main.mjs +749 -0
- package/skills/roger-roger/tray/panel.html +501 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sergej Popov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# roger-roger
|
|
2
|
+
|
|
3
|
+
An agent skill that brings you back to the conversation when you've walked away from the screen with a **Slack message**, a **ding**, a **spoken line** (Gemini 3.1 Flash TTS), or any combination. It can also **ask you to choose** between options with Slack buttons while the agent waits.
|
|
4
|
+
|
|
5
|
+
It works with any agent that supports [Agent Skills](https://skills.sh), such as Claude Code, Codex, Cursor, and others.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install -g roger-roger
|
|
11
|
+
roger-roger install
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or, without installing anything permanently:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npx roger-roger install
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`install` asks a few questions in the terminal and does the rest: it links the skill into every agent's skills folder it finds (Claude Code, Codex, OpenCode, Cursor, Gemini CLI, Copilot, and `~/.agents/skills`), registers with [Herdr](https://herdr.dev) as a plugin when it's installed, connects Slack, lets you hear the sounds and the voices before you choose, and installs the terminal-question hooks and the tray icon. Run it again whenever you want to change something.
|
|
21
|
+
|
|
22
|
+
Other ways in:
|
|
23
|
+
|
|
24
|
+
- **skills.sh**: `npx skills add code-red-app/roger-roger` copies the skill alone. The agent runs it as `node <skill-dir>/scripts/roger-roger.mjs` and, on first use, asks you to run `npx roger-roger install`.
|
|
25
|
+
- **Herdr**: `herdr plugin install code-red-app/roger-roger/skills/roger-roger`. The daemon starts with Herdr, snooze becomes an action you can bind to a key, and *roger-roger setup* opens as a popup pane.
|
|
26
|
+
|
|
27
|
+
Needs Node 18 or newer (22 for questions with buttons).
|
|
28
|
+
|
|
29
|
+
## Slack
|
|
30
|
+
|
|
31
|
+
There's nothing to configure by hand. `roger-roger install` connects Slack using Slack's official [Slack CLI](https://docs.slack.dev/tools/slack-cli/):
|
|
32
|
+
|
|
33
|
+
1. It installs the CLI if it's missing, after asking you.
|
|
34
|
+
2. It logs the CLI in: you paste a `/slackauthticket …` command into Slack, approve, and type the code Slack shows.
|
|
35
|
+
3. It creates and installs a **roger-roger** app in your workspace from [`skills/roger-roger/slack/manifest.json`](skills/roger-roger/slack/manifest.json) (Socket Mode, interactivity, DMs to the bot; `chat:write`, `im:write`, `im:history`, `files:read`, `files:write`, `reactions:write`). The CLI hands the app's tokens over, and they are kept in `~/.roger-roger/slack.json`.
|
|
36
|
+
|
|
37
|
+
Messages then arrive as DMs from the roger-roger bot. An agent can do the same steps one at a time with `roger-roger slack-setup`, if you'd rather not leave the conversation.
|
|
38
|
+
|
|
39
|
+
**Manual setup instead:** create an app from the same manifest at api.slack.com/apps, then set these environment variables. Tokens saved by the setup take priority when both exist.
|
|
40
|
+
|
|
41
|
+
| Variable | Needed for |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `SLACK_API_BOT_TOKEN` | Slack. The bot token (`xoxb-…`), with `files:read` and `files:write` for attachments in either direction. `SLACK_BOT_TOKEN` also works. |
|
|
44
|
+
| `ROGER_ROGER_SLACK_APP_TOKEN` (or `SLACK_APP_TOKEN`) | Questions with buttons (`ask`). An app-level token (`xapp-…`) with `connections:write`. |
|
|
45
|
+
|
|
46
|
+
## Speech
|
|
47
|
+
|
|
48
|
+
| Variable | Needed for |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `GEMINI_API_KEY` | Gemini TTS. A key from Google AI Studio. `GOOGLE_API_KEY` also works. |
|
|
51
|
+
|
|
52
|
+
You can also paste the key into `roger-roger install`; it's kept in `~/.roger-roger/keys.json`, readable by you only, and a variable set the ordinary way always wins over it. OpenAI and ElevenLabs voices work the same way (`OPENAI_API_KEY`, `ELEVENLABS_API_KEY`).
|
|
53
|
+
|
|
54
|
+
A missing key is not fatal. Speech falls back to the computer's built-in voice (SAPI on Windows, `say` on macOS, `spd-say`/`espeak` on Linux).
|
|
55
|
+
|
|
56
|
+
## Setup
|
|
57
|
+
|
|
58
|
+
`roger-roger install` asks, in the terminal:
|
|
59
|
+
|
|
60
|
+
- which agents should have the skill (every one it finds is ticked)
|
|
61
|
+
- whether to register with Herdr, if it's installed
|
|
62
|
+
- how to reach you: Slack, sound, speech, in any combination
|
|
63
|
+
- Slack: it connects it for you (see above), and where messages go (your DMs, or a channel)
|
|
64
|
+
- which sound to play: space plays the highlighted one
|
|
65
|
+
- who makes the voice (Gemini, OpenAI, ElevenLabs), the key if none is set, and which voice: space lets each introduce itself
|
|
66
|
+
- whether the spoken line should be a short summary or the full message
|
|
67
|
+
- when you want to be notified
|
|
68
|
+
- whether questions an agent asks in its own terminal should reach Slack too
|
|
69
|
+
- whether to put an icon in the system tray
|
|
70
|
+
|
|
71
|
+
It ends with a test notification. Your answers are saved to `~/.roger-roger/config.json`. To change them, run it again, or just tell the agent ("only ping me when you're blocked", "use the Puck voice").
|
|
72
|
+
|
|
73
|
+
## Questions from Slack
|
|
74
|
+
|
|
75
|
+
`ask` posts a question with one button per option, with the agent's recommended option in green (`--recommend`). Click one, or use **Other…** to type your own answer, and the agent gets it from `wait`.
|
|
76
|
+
|
|
77
|
+
- **Reminders**: if you haven't answered, you're reminded after 15 minutes by default, in Slack plus sound and speech (`setup --remind`).
|
|
78
|
+
- **No answer**: when a question runs out of time — 45 minutes by default — the recommended option is applied, and the message says so up front. Agents turn this off for risky decisions (`setup --on-expire nothing` turns it off everywhere).
|
|
79
|
+
- **Nothing is lost if the agent stopped waiting**: the answer is kept and handed over by the next command that agent runs, whatever it is. The message says which has happened — *saved for 🌹 rose*, then *delivered to 🌹 rose* — so you never have to guess whether it arrived.
|
|
80
|
+
|
|
81
|
+
There is no always-on service. `ask` starts a small watcher process, and one watcher serves every open question on the machine. It exits by itself a few seconds after the last question is answered, expires, or is cancelled. State lives in `~/.roger-roger/decisions/`, and the watcher's log is `~/.roger-roger/daemon.log`.
|
|
82
|
+
|
|
83
|
+
## More
|
|
84
|
+
|
|
85
|
+
- **Progress messages**: `progress --message "Phase 2/4…"` keeps one Slack message up to date during long work, and `--done` finishes it. Its **⏸ Pause**, **⏹ Stop** and **📝 Note** buttons let you steer the agent from your phone. The agent picks them up at its next check, and the message confirms once it has actually paused, stopped, or read your note.
|
|
86
|
+
- **Edit before use**: `ask --draft "…"` shows a commit message, PR text, or email with **Use as is** and **✏️ Edit**. Edit opens it pre-filled, and the agent uses your version.
|
|
87
|
+
- **Summary card**: `notify --summary --field "Tests=318 passed"` ends a task with a card: key facts, how long it took, and every decision you made along the way. `--actions "Open a PR|Run the full suite"` adds follow-up buttons.
|
|
88
|
+
- **Talk to the bot**: DM the roger-roger bot in Slack. A reply in a progress message's thread, or any DM while an agent is working, reaches that agent at its next check. A reply in a question's thread answers it. Anything else waits in an inbox for the next agent. 👀 means an agent has it, ✅ means an agent has answered it.
|
|
89
|
+
- **Attachments**: `--attach file` on `notify` and `ask` posts logs, diffs, or screenshots with the message. The bot needs `files:write`.
|
|
90
|
+
- **Send it a screenshot**: files you attach in Slack come the other way too. They're downloaded to `~/.roger-roger/files` and the agent is handed the path, so "here's what it looks like" works as an instruction. Needs `files:read`, so an app created before this needs `slack-setup create` run once more.
|
|
91
|
+
- **Multi-select**: `ask --multi` shows checkboxes and a Submit button. `--recommend "A|C"` pre-ticks options.
|
|
92
|
+
- **Session label**: a small `machine · agent · model · session` line under each Slack message tells several agents apart (`setup --session-label off` hides it).
|
|
93
|
+
- **One voice at a time**: several agents on one machine take turns at the speakers instead of talking over each other. A waiting announcement plays in full as soon as the one before it finishes.
|
|
94
|
+
- **Works with any agent**: Claude Code, Codex, OpenCode, or a plain shell script. A session is recognised by whatever id the agent publishes, or by the `--session` name it passes, or by the terminal — no per-product support needed.
|
|
95
|
+
- **Every agent has a name**: each session is given a colour nickname — 🌿 sage, 🌹 rose, 🍇 plum — shown on its Slack messages with a matching colour bar. `sessions` lists who is running; `sessions rename sage plum` changes one.
|
|
96
|
+
- **Named in Herdr too**: an agent running in a [Herdr](https://herdr.dev) pane gets its badge on the tab (`🍯 amber`) and its kind and work on the agent (`claude-build-process-improvements`), so the Herdr sidebar and the Slack messages name the same thing. Nothing to set up: the skill sees the pane it runs in and labels it on the first command, and again after a rename. As a Herdr plugin (`roger-roger install` registers it, or `herdr plugin install code-red-app/roger-roger/skills/roger-roger`), the daemon starts with Herdr, snooze is an action, and the setup opens as a popup pane.
|
|
97
|
+
- **Talk to an agent**: write "sage, rerun the tests" (or "hey sage rerun the tests", or "sage: ..."). With one agent running, a plain message goes to it. With several, but only one of them listening, it goes to that one and Slack says so, with a button to redirect it. With several, the bot asks which, with a button per agent — and remembers your answer for that thread.
|
|
98
|
+
- **You can hear who it is**: a spoken line starts with the agent's name — "Amber here.", "This is Rose.", "Jade speaking." — so three agents sharing one pair of speakers stay tellable apart (`setup --say-who off`).
|
|
99
|
+
- **Reachable while idle**: an agent that has finished can run `listen` in the background instead of going quiet. It costs nothing to wait, and when you write, it wakes up and answers. Slack shows 👀 when an agent is listening and 💤 when your message is saved for one that isn't, so you always know which.
|
|
100
|
+
- **In the tray**: `tray install` puts an icon in the system tray, started with the daemon from then on (`tray start` starts it straight away; `setup --tray off` stops it starting by itself) — every agent at a glance, a translucent panel on click, snooze on right-click. It reads the same state the daemon already keeps and runs ordinary commands, so it can be closed, killed or never installed and nothing else changes.
|
|
101
|
+
- **Questions asked in the terminal**: when the agent asks you something with its own question tool (Claude Code's question dialog, OpenCode's question tool, Codex's `request_user_input`) or wants a plan approved, it's stuck until you answer and can't tell you itself. `hooks install` hooks into each of those agents it finds: once the question has gone unanswered for a little while, it arrives in Slack with its options, and when you answer in the terminal the message turns into your answer. Claude Code gets three hooks in `~/.claude/settings.json`; OpenCode a small plugin in `~/.config/opencode/plugins/`; Codex three hooks in `~/.codex/hooks.json`, which you trust once in Codex's `/hooks`. Claude Code knows when you've stopped typing; OpenCode and Codex don't, so they wait 20 seconds first (`setup --terminal-wait`). `setup --permission-pings on` does the same for ordinary permission prompts. `hooks uninstall` takes it all out again.
|
|
102
|
+
- **Quiet hours**: `setup --quiet-hours 22:00-08:00` mutes sound and speech overnight. It's off by default.
|
|
103
|
+
|
|
104
|
+
## Sounds
|
|
105
|
+
|
|
106
|
+
`chime` · `ding` · `marimba` · `bubble` · `alert`. They are generated by `tools/make-sounds.mjs`. Run `npm run make-sounds` to regenerate them.
|
|
107
|
+
|
|
108
|
+
## How it runs
|
|
109
|
+
|
|
110
|
+
One daemon per machine does the work: it holds the Slack connection open (so a message you send is picked up immediately, whether or not an agent is waiting), owns the speakers, and is the only writer of the state under `~/.roger-roger`. Agents run the CLI, which is thin — it hands the command to the daemon over a local socket (a named pipe on Windows) and prints the answer. The daemon starts on the first command and stays; `roger-roger daemon status` shows it, `daemon stop` ends it. Nothing listens on the network, and if the daemon can't start, commands fall back to running in the calling process.
|
|
111
|
+
|
|
112
|
+
## CLI
|
|
113
|
+
|
|
114
|
+
The skill is a zero-dependency Node script. The npm package puts it on your PATH as `roger-roger`; from a skills.sh copy, run `node skills/roger-roger/scripts/roger-roger.mjs` instead.
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
roger-roger install
|
|
118
|
+
roger-roger status
|
|
119
|
+
roger-roger sessions
|
|
120
|
+
roger-roger daemon status
|
|
121
|
+
roger-roger play alert
|
|
122
|
+
roger-roger say "[positive] Hello there." --voice Puck
|
|
123
|
+
roger-roger preview-sounds
|
|
124
|
+
roger-roger preview-voices Puck Sulafat
|
|
125
|
+
roger-roger notify --kind done --message "Build finished" --say "The build is done." --attach build.log
|
|
126
|
+
roger-roger progress --message "Phase 2/4: migrating orders"
|
|
127
|
+
roger-roger control --wait 10m
|
|
128
|
+
roger-roger ask --question "Commit message OK?" --draft-file msg.txt --recommend 1
|
|
129
|
+
roger-roger ask --question "Deploy now?" --choices "Yes|Tomorrow|No" --recommend Yes --wait 10m
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
npm test
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Requires Node 18+ (22+ for `ask`, which uses the built-in WebSocket).
|
|
139
|
+
|
|
140
|
+
CI runs the tests on Linux, macOS and Windows for every push and pull request. A release is a tag:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
npm version minor # bumps package.json and herdr-plugin.toml together, commits, tags
|
|
144
|
+
git push --follow-tags # publishes to npm and creates the GitHub release
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The release workflow needs one repository secret, `NPM_TOKEN`: an npm granular access token with publish rights on the package.
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "roger-roger",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Bring yourself back to the conversation: your coding agents reach you by Slack, a sound or a spoken line when they finish, get stuck, or need a decision, and you answer from your phone.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"roger-roger": "skills/roger-roger/scripts/roger-roger.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"skills/",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test",
|
|
19
|
+
"make-sounds": "node tools/make-sounds.mjs",
|
|
20
|
+
"version": "node tools/sync-version.mjs && git add skills/roger-roger/herdr-plugin.toml"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"agent",
|
|
24
|
+
"skill",
|
|
25
|
+
"agent-skills",
|
|
26
|
+
"claude-code",
|
|
27
|
+
"codex",
|
|
28
|
+
"opencode",
|
|
29
|
+
"herdr",
|
|
30
|
+
"notification",
|
|
31
|
+
"slack",
|
|
32
|
+
"tts",
|
|
33
|
+
"text-to-speech"
|
|
34
|
+
],
|
|
35
|
+
"author": "Sergej Popov",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/code-red-app/roger-roger.git"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/code-red-app/roger-roger#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/code-red-app/roger-roger/issues"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: roger-roger
|
|
3
|
+
description: Bring the user back when they are away from the screen by posting to Slack, playing a notification sound, and/or speaking out loud with Gemini TTS (any combination), and ask them to pick between options with Slack buttons while you wait. Use when a long task finishes, when you are blocked or need the user's input or a decision, when something failed in a way that stops progress, at an important milestone in long-running work, or when the user asks to be pinged, notified, called, or told out loud.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# roger-roger
|
|
7
|
+
|
|
8
|
+
Pulls the user back to the conversation. One command can do up to three things at once, depending on the user's settings:
|
|
9
|
+
|
|
10
|
+
- **slack**: posts a message to the user's DM or a channel (`SLACK_API_BOT_TOKEN`)
|
|
11
|
+
- **sound**: plays a bundled notification sound
|
|
12
|
+
- **speech**: says a line out loud with Gemini TTS (`GEMINI_API_KEY`). If that fails, it uses the computer's built-in voice.
|
|
13
|
+
|
|
14
|
+
It can also **ask the user to choose** between options with buttons in Slack and hand you their answer (section 3).
|
|
15
|
+
|
|
16
|
+
Everything runs through one zero-dependency Node script (Node 18+; `ask` needs Node 22+). It is the `roger-roger` command when the npm package is installed; if that isn't on your PATH, run the script directly, `node <skill-dir>/scripts/roger-roger.mjs`, where `<skill-dir>` is the directory containing this SKILL.md. `roger-roger` below means whichever of the two you have.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
roger-roger <command> [flags]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Every command prints a single JSON object.
|
|
23
|
+
|
|
24
|
+
**How it runs.** Commands are thin: the work happens in one long-lived daemon that owns the Slack connection, the speakers and the state. It starts by itself on the first command and stays, which is what lets the user write to you at any moment and lets several agents share one machine without talking over each other. You never have to start or think about it; `daemon status` will show it if you're curious.
|
|
25
|
+
|
|
26
|
+
**Your name.** The daemon gives every agent session a short colour nickname — 🌿 sage, 🌹 rose, 🍇 plum — shown on all your Slack messages with a matching colour bar down their edge. That is how the user tells three agents apart and how they address you: "sage, rerun the tests". `sessions` lists who is running; `status` tells you your own name.
|
|
27
|
+
|
|
28
|
+
**In Herdr, your pane is named after you — by the skill, not by you.** When the session runs inside a [Herdr](https://herdr.dev) pane (`HERDR_ENV=1`), the first command labels the pane's tab with your badge (`🍯 amber`) and its agent with your kind and your work (`claude-build-process-improvements`: Herdr allows only lowercase letters, digits, `-` and `_` there, 32 characters at most), so the Herdr sidebar reads `code-red 🍯 amber claude-build-process-improvements` and matches your Slack messages. The work part is your `--session` name, which is one more reason to pass it, and the labels follow a `sessions rename` or a new `--session` name by themselves. Don't run `herdr tab rename` or `herdr agent rename` yourself; `status` shows what was put on under `daemon.me.herdr`, and a label Herdr refused (a name another pane already has) is in `~/.roger-roger/daemon.log`.
|
|
29
|
+
|
|
30
|
+
**Always pass `--session`**, the same 3–6 word name on every call. It is not only a label: for agents that don't publish a session id of their own it is *how you are recognised* between commands, so leaving it off can make one piece of work look like two. Nothing here is specific to any agent — if your environment publishes a session id (anything like `*_SESSION_ID`) it is used automatically; otherwise `--session` plus the working directory identifies you, and the terminal is the last resort. If you know better than all of that, pass `--session-id ID` (and `--agent-pid PID`, so a crash is noticed straight away).
|
|
31
|
+
|
|
32
|
+
## 1. First use
|
|
33
|
+
|
|
34
|
+
Run `status` first. If `configured` is `false`, don't set things up in the conversation. Ask the user to run the setup in a terminal, and wait for them:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
roger-roger install (or, with nothing installed: npx roger-roger install)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
It asks everything in one sitting, with previews: how to be reached (Slack, sound, speech), connecting Slack (it creates the roger-roger Slack app; nobody copies tokens), which sound and which voice (space plays them), how much to say out loud, when to notify, the terminal-question hooks (`hooks`, under *Other commands*), and the tray. It also links the skill into every agent's skills folder and registers with Herdr when it's there. When the user says it's done, run `status` again and carry on.
|
|
41
|
+
|
|
42
|
+
Only if they can't get to a terminal: `setup` takes every setting as a flag (below), and `slack-setup` does the Slack steps one at a time (`slack-setup` prints `next`: `install-cli`, `login` — it returns a `/slackauthticket …` command for the user to paste into Slack and you pass back the code they get with `--challenge CODE --ticket TICKET` — `create`, then `done`, with the user's member ID as `userId` for `--slack-target`).
|
|
43
|
+
|
|
44
|
+
Settings are stored globally in `~/.roger-roger/config.json`, so this happens once per machine, not once per project. Anything the user states as a preference in conversation (for example "never speak after 10pm" or "only Slack for errors") goes in `setup --notes "..."`. Read `config.notes` from `status` and follow it.
|
|
45
|
+
|
|
46
|
+
### Settings
|
|
47
|
+
|
|
48
|
+
`setup` changes any of these, keeping the rest:
|
|
49
|
+
|
|
50
|
+
| Setting | Default | Meaning |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `--methods slack,sound,speech` | `sound,speech` | Any combination. Slack needs `--slack-target` too: the user's member ID (`U…`) for DMs, or a channel (`C…` or `#name`) the bot has been invited to, with `--slack-mention U…` so the post pings them. |
|
|
53
|
+
| `--sound chime\|ding\|marimba\|bubble\|alert` | `chime` | The sound: two rising bell notes, a single bell, soft and low, two quick pops, three insistent beeps. `play NAME` previews one. |
|
|
54
|
+
| `--voice NAME` | `Kore` | Any voice from `status.options.voices`; `say "…" --voice NAME` previews one. |
|
|
55
|
+
| `--speech auto\|brief\|same` | `auto` | What is spoken: you decide (a short line, detail in Slack), always one short sentence, or exactly the Slack message. |
|
|
56
|
+
| `--when auto\|done-and-blocked\|blocked-only\|on-request` | `auto` | When to notify: your judgement (section 4), only done or blocked, only blocked, only when asked in the conversation. |
|
|
57
|
+
| `--remind 15m` | `15m` | While a question (section 3) is unanswered, remind the user this long after asking: a Slack message linking to it, plus the sound and speech. `off` disables reminders. |
|
|
58
|
+
| `--on-expire recommended\|nothing` | `recommended` | When a question with a recommended option runs out of time, settle it as that option. `nothing` lets it expire instead. |
|
|
59
|
+
| `--quiet-hours 22:00-08:00` | off | During these local hours, mute some methods for every notification, question, and reminder. `off` disables it. |
|
|
60
|
+
| `--quiet-mute speech,sound` | `speech,sound` | Which methods quiet hours mute. Slack usually stays on, so nothing is lost. |
|
|
61
|
+
| `--say-who on\|off` | `on` | Start a spoken line with the agent's name — "Amber here.", "This is Amber." — so a voice from the next room is identifiable. Off makes every agent sound the same. |
|
|
62
|
+
| `--session-label on\|off` | `on` | Show a small `name · machine · agent · model · session` line under Slack messages, to tell several agents apart. |
|
|
63
|
+
| `--speech-provider gemini\|openai\|elevenlabs` | `gemini` | Who makes the voice. Switching resets the voice and model to that provider's defaults; run `voices --refresh` afterwards to fetch its real voices and models (`voices` lists them). |
|
|
64
|
+
| `--speech-key-var NAME` | the provider's usual | The environment variable holding the provider's API key, when it isn't the usual one (`GEMINI_API_KEY`, `OPENAI_API_KEY`, `ELEVENLABS_API_KEY`). On Windows, variables saved in the system settings are found without restarting anything. `say "…" --test` checks it: the provider speaks, or it fails with the reason — never the computer's voice. |
|
|
65
|
+
| `--duck on\|off`, `--duck-level 0-100` | `on`, `40` | Turn other apps (music, videos) down to that share of their own volume while the voice speaks, and back up after. Windows only for now. |
|
|
66
|
+
| `--speech-volume 0-150` | `100` | The voice's volume, as a percentage of how the provider made it. |
|
|
67
|
+
| `--tray auto\|off` | `auto` | Once the tray is installed (`tray install`), the daemon starts it by itself. `off` leaves it to `tray start`. |
|
|
68
|
+
|
|
69
|
+
**Changing settings later**: when the user asks ("stop talking to me", "use the alert sound", "only ping me when blocked", "no noise at night"), run `setup` with only the flags that change. Existing values are kept.
|
|
70
|
+
|
|
71
|
+
## 2. Sending a notification
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
roger-roger notify --kind done --message "Refactor of the billing module is finished: 42 files changed, all 318 tests pass. Ready for review." --say "[positive] The billing refactor is done, and all tests pass." --session "billing module refactor" --agent-model "opus 5"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
| Flag | Meaning |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `--kind` | `done`, `blocked`, `input`, `milestone`, `error`, `info`. Sets the Slack emoji. |
|
|
80
|
+
| `--message` | The written message for Slack. Make it self-contained, because the user may read it on their phone without context. Say what happened and what, if anything, you need from them. Slack mrkdwn works. |
|
|
81
|
+
| `--say` | The spoken line (optional). Keep it short, one or two sentences, with no paths, code, or URLs. If you leave it out, the message is spoken. Ignored when the user's speech mode is `same`. With `brief`, always pass a one-sentence `--say`. |
|
|
82
|
+
| `--only` | Restrict this one call to some methods, e.g. `--only sound` when the user said "just ding me when done". |
|
|
83
|
+
| `--project` | Label shown in Slack. Defaults to the current directory name. |
|
|
84
|
+
| `--session` | **Always pass it.** A short name for this session, made up by you from what the session is about: 3–6 plain words, e.g. `"roger-roger skill slack integration"` or `"billing refactor tests"`. It's shown in the small `machine · agent · model · session` line under Slack messages, so the user can tell sessions apart. Decide it on your first call and reuse the exact same name for every later call in this session. Never use IDs or hashes. |
|
|
85
|
+
| `--agent-model` | **Always pass it.** Your model, short and lowercase, e.g. `"opus 5"`, `"sonnet 5"`, `"gpt-6"`. Omit it only if you really don't know it. |
|
|
86
|
+
| `--agent` | Optional. Your agent product's short name. It's detected automatically where possible (e.g. `claude` for Claude Code). |
|
|
87
|
+
| `--then-listen [25m]` | Reply and stay reachable in one command: it posts, then parks exactly as `listen` does, and returns when the user says something. **Prefer this to running `listen` afterwards** — two commands leave a few seconds where you look idle, and a message sent in that gap goes to the picker instead of to you. Run the whole thing in the background. |
|
|
88
|
+
| `--reply-to TS` | Post as a reply in a thread instead of a new message. Pass the `replyTo` that came with the message you are answering. **Always use it when you are replying to something the user said** — an answer that appears as a fresh message in the channel, away from the question, is the wrong place for it. Leave it off for a notification that starts something new, like the end of a task. |
|
|
89
|
+
| `--attach` | A file to post to Slack with the message, such as a log excerpt, a failing test's output, a diff, or a screenshot, so the user can judge from their phone. Repeat the flag for several files. Attach only what helps, and never secrets. |
|
|
90
|
+
| `--summary` | Send the message as an end-of-task card, for the final `done` (or `blocked`) of real work. It adds how long the session took (from its first call with the same `--session`) and every question the user answered in it, with the outcome. |
|
|
91
|
+
| `--field "Label=value"` | A key fact shown side by side on the card; repeatable, up to about 6 is readable. E.g. `--field "Tests=318 passed" --field "Files changed=42" --field "Commits=3"`. Use real numbers you checked, never estimates. |
|
|
92
|
+
|
|
93
|
+
| `--actions "A\|B"` | Follow-up actions offered as buttons on the message, usually on a summary card: the natural next steps, e.g. `"Open a PR\|Run the full test suite\|Deploy to staging"`. An **Other…** button lets the user type something else (`--other false` hides it). `notify` then returns an `id`: `wait` for it like any question (section 3). Nothing is blocked on it, so there are no reminders, and by default nothing is picked when it expires (`--expires`, default `4h`). `--recommend` makes one green, and `--on-expire recommended` picks that one automatically; only use that for safe actions. Needs the Slack app token. |
|
|
94
|
+
|
|
95
|
+
At the end of a substantial task, prefer one summary card over a plain message:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
roger-roger notify --kind done --summary --message "Billing refactor finished and ready for review." --field "Tests=318 passed" --field "Files changed=42" --attach review-notes.md --say "[positive] The billing refactor is done. The summary is in Slack." --session "billing module refactor" --agent-model "opus 5" --actions "Open a PR|Run the full test suite" --recommend "Open a PR"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
If you offered actions, `wait <id>` (in the background if you can) and do what the user picks. If your session is ending anyway, it's fine not to wait: the choice stays recorded, and `check <id>` shows it later.
|
|
102
|
+
|
|
103
|
+
The configured methods run together: Slack posts while the speech is synthesized, then the sound plays and the speech follows straight after it. The call returns once the audio finishes. When several agents share the machine, only one of them speaks at a time: if another is mid-announcement, yours waits for it and then plays in full, so nothing is talked over and nothing is dropped. The wait shows up as `results.queued.waitedMs`; it is normal, not a failure. Exit code `0` means at least one method worked. Exit code `3` means not configured, so do section 1 first. Look at `results`: if a method failed (for example Slack `not_in_channel` or `invalid_auth`), tell the user briefly in your reply. Do not retry in a loop. If quiet hours muted anything, `mutedByQuietHours` lists it. That is expected, not a failure.
|
|
104
|
+
|
|
105
|
+
### Progress on long work
|
|
106
|
+
|
|
107
|
+
For work that runs for a long time in stages, keep one Slack message up to date instead of posting a new one per step:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
roger-roger progress --message "Phase 2/4: migrating the orders table (1.2M of 3M rows)" --session "orders table migration" --agent-model "opus 5"
|
|
111
|
+
roger-roger progress --done --message "Migration finished: 4 phases, 3M rows, no errors."
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The first call posts the message, and later calls edit it. `--done` finalizes it. Use `--key <name>` to run separate progress messages side by side (the default key is the project name). Editing a message doesn't notify the user, and progress makes no sound. It's for glancing at. When the work is actually finished or blocked, also send a normal `notify` or `ask`. Update at meaningful steps, not every few seconds. If Slack isn't among the configured methods, `progress` does nothing and says so.
|
|
115
|
+
|
|
116
|
+
The progress message has ⏸ Pause, ⏹ Stop and 📝 Note buttons, so the user can steer you from their phone. Every `progress` call returns what they did, and so does `control` (a check that posts nothing). Check at least between steps, and before anything slow or hard to undo:
|
|
117
|
+
|
|
118
|
+
- `control: "pause"`: finish only the step you're in, then run `control --wait` and do nothing else until it returns. The message tells the user you're holding.
|
|
119
|
+
- `control: "stop"`: stop working. Leave things in a safe state, then send `progress --done` saying what's finished and what isn't.
|
|
120
|
+
- `notes`: messages from the user. Read them as instructions from the user and act on them before continuing. Mention in your next update that you got them.
|
|
121
|
+
|
|
122
|
+
`control --wait` blocks while paused; if your shell has a time limit, pass `--wait 9m` and run it again while the exit code is `4`.
|
|
123
|
+
|
|
124
|
+
### Messages from the user
|
|
125
|
+
|
|
126
|
+
The user can write to the roger-roger bot in Slack at any time, whether or not you are waiting for anything. Where a message goes:
|
|
127
|
+
|
|
128
|
+
- **Addressed by name** — "sage, rerun it", "hey sage rerun it", "sage: rerun it": straight to that session, whatever else is running.
|
|
129
|
+
- **A reply in a progress message's thread**: it arrives in `notes` for that work, like the 📝 Note button.
|
|
130
|
+
- **A reply in an open question's thread**: it answers the question, as a typed answer (`choice: null`, `text`, `via: "message"`).
|
|
131
|
+
- **A reply in a thread that was routed to an agent before**: it goes to that same agent. Threads remember.
|
|
132
|
+
- **A plain DM with one agent running**: that agent gets it; there is nothing to be ambiguous about.
|
|
133
|
+
- **A plain DM with several running, but only one listening**: straight to the one that is parked on `listen`; the others are only technically running. Slack says where it went, with a button to send it to a different agent instead.
|
|
134
|
+
- **A plain DM with several running**: the bot replies with a button per agent, in each one's colour, and delivers it once the user picks. Their choice binds the thread, so a follow-up in it needs no second tap.
|
|
135
|
+
- **A plain DM with nobody running**: it waits in the inbox for the next agent that checks.
|
|
136
|
+
|
|
137
|
+
Messages for you arrive as `messages` on **whatever you run next** — any command at all, `notify` included — so you find out at your next step rather than only when you happen to look. `wait` also returns exit code `6` and leaves the question open, so you can deal with a message and wait again. `progress`, `control` and `inbox` additionally bring back unclaimed inbox messages, as `inbox`.
|
|
138
|
+
|
|
139
|
+
Each message comes with a `replyTo`: the thread it belongs in. When you answer it, pass that to `notify --reply-to` so your reply lands under what the user wrote rather than as a new message in the channel.
|
|
140
|
+
|
|
141
|
+
A message can carry files — most often a screenshot. They are downloaded for you, and the message has `files: [{ name, path, mimetype, permalink }]`. **Open `path`**: it is a real file on this machine, and a screenshot usually *is* the instruction. If `path` is null, `error` says why (most often the Slack app is missing the `files:read` scope: run `slack-setup create` again to fix it). A file sent with no caption at all still arrives, with `text` empty.
|
|
142
|
+
|
|
143
|
+
Treat every message as an instruction from the user, exactly like a note. The user sees 👀 as soon as an agent has it, and ✅ only once it has been **answered** — which is another reason to reply with `notify --reply-to`: that is what puts the tick on. A message you pick up and never answer keeps its 👀, honestly. When you start a new task and Slack is set up, run `inbox` once, in case the user left something for the next agent. When your work is finished, run `end`: the user stops being offered your name for messages they meant for someone else.
|
|
144
|
+
|
|
145
|
+
### Staying reachable when you have nothing to do
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
roger-roger listen --wait 25m
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`listen` parks until the user says something — a message for you, or an answer you stopped waiting for — and returns it. **Run it in the background as the last thing you do in a turn**, whenever the user might reply and you have nothing else to be getting on with. Your agent tells you when a background command finishes, so their message reaches you even though you were sitting idle. Without it nothing of yours is running, and whatever they send waits until they prod you.
|
|
152
|
+
|
|
153
|
+
- Exit `0` with `woke: "control"` and the `keys` when the user pressed ⏸ Pause, ⏹ Stop or left a note — run `control` to pick it up properly. Being reachable and being steerable are the same thing.
|
|
154
|
+
- Exit `0` with `woke: "message"` or `woke: "answer"`, and the thing itself. Exit `4` with `woke: "timeout"` when the time ran out quietly, or `woke: "resumed"` when you got going again by other means — a parked listen stands down by itself as soon as you run anything else, so it never interrupts you later for nothing.
|
|
155
|
+
- **Woken by a message or an answer? Park again.** You are in a conversation and the user expects you to still be there; going quiet straight after replying is the thing this command exists to prevent. Each of those wakes is one you wanted anyway.
|
|
156
|
+
- **Timed out? Stop.** Nothing is happening, and parking on a loop is the one way this gets expensive. Park again only if the user asked you to keep watch.
|
|
157
|
+
- Keep the window at the default 25 minutes or under. A wake inside it reuses your prompt cache and costs roughly a tenth of a wake after it has gone.
|
|
158
|
+
- Don't park after a quick exchange where the user is plainly at the keyboard, and don't park instead of doing work you could be doing.
|
|
159
|
+
- The user sees :eyes: in Slack when an agent is listening or actually running, and :zzz: when the message is saved for one that isn't — so they know whether to expect an answer now or at your next step.
|
|
160
|
+
|
|
161
|
+
### Tone: Gemini audio tags
|
|
162
|
+
|
|
163
|
+
Gemini 3.1 Flash TTS reads inline tags in square brackets and changes its delivery. Put a tag where the change should happen. Choose tags that fit the moment, and use them sparingly (one or two per line):
|
|
164
|
+
|
|
165
|
+
- Finished / good news: `[positive]`, `[enthusiasm]`, `[excited]`
|
|
166
|
+
- Blocked / needs input: `[serious]`, `[neutral]`, `[determination]`
|
|
167
|
+
- Failure: `[serious]`, `[tension]`, `[frustration]` (mildly)
|
|
168
|
+
- Pacing: `[short pause]`, `[slow]`, `[fast]`
|
|
169
|
+
- Flavour, rarely: `[laughs]`, `[sighs]`, `[whispers]`
|
|
170
|
+
|
|
171
|
+
Examples:
|
|
172
|
+
|
|
173
|
+
- `--say "[positive] Deployment finished. [short pause] Everything is green."`
|
|
174
|
+
- `--say "[serious] I'm blocked. I need the production database password to continue."`
|
|
175
|
+
|
|
176
|
+
Tags must be separated by words or punctuation, never placed back to back. The script strips tags from Slack text and from the local-voice fallback, so they are safe to include. Very short or odd lines (a single word) are sometimes refused by Gemini, so write a natural sentence.
|
|
177
|
+
|
|
178
|
+
## 3. Asking the user to choose
|
|
179
|
+
|
|
180
|
+
When you need a decision and the user may be away, post the question to Slack with the options. It also plays the sound and speaks, like `notify`.
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
roger-roger ask --question "The migration found 12 orphaned rows. How should I handle them?" --choices "Delete them|Archive to a table|Stop and leave them" --recommend "Archive to a table" --details "All 12 reference customers deleted in 2023." --say "[serious] I need a decision on the migration. The options are in Slack." --session "orders table migration" --agent-model "opus 5"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
| Flag | Meaning |
|
|
187
|
+
|---|---|
|
|
188
|
+
| `--question` | The question. Make it self-contained. |
|
|
189
|
+
| `--choices` | 2–10 short options separated by a pipe character, e.g. `"Yes\|No"`. |
|
|
190
|
+
| `--recommend` | Optional. The option you recommend, as its label or its number (1-based). Its button is shown in green. Use it when you have a real recommendation; leave it out when the options are genuinely equal. With `--multi`, give several separated by `\|`; they start out ticked. |
|
|
191
|
+
| `--multi` | Let the user pick several options: checkboxes plus a **Submit** button, instead of one button per option. Use it for "which of these should I do?" questions (e.g. which fixes to apply). Up to 10 options. |
|
|
192
|
+
| `--details` | Optional extra context under the question. |
|
|
193
|
+
| `--say`, `--only`, `--project`, `--kind`, `--attach` | As for `notify`. `--kind` defaults to `input`. `--only` affects the sound and speech only, since Slack is always used. |
|
|
194
|
+
| `--expires` | How long the question stays open, e.g. `15m` or `2h` (default `45m`). Choose it for the situation: how long can you usefully wait? |
|
|
195
|
+
| `--on-expire recommended\|nothing` | What happens when time runs out. Defaults to the user's setting, which is `recommended` unless they changed it. With `recommended`, the question settles as the recommended option, and the message tells the user this in advance. Pass `nothing` when proceeding without the user would be risky (deleting data, spending money, anything hard to undo), so the question just expires. |
|
|
196
|
+
| `--remind 15m\|off` | Reminder times for this question. Defaults to the user's setting. |
|
|
197
|
+
| `--other true\|false` | Show an **Other…** button that opens a text box for a typed answer (default `true`). Pass `false` when only the listed options make sense. |
|
|
198
|
+
| `--draft TEXT` / `--draft-file PATH` | Ask the user to approve or edit text before you use it: a commit message, PR description, release note, email, or docs paragraph. The draft is shown in the message with a **✏️ Edit** button that opens it pre-filled in a text box. `--choices` becomes optional and defaults to one button, "Use as is" (use `--recommend 1` to make it green). You can add others, e.g. `"Use as is\|Skip it"`. Up to 3000 characters; for longer text, `--attach` it and ask a normal question. Not combinable with `--multi`. |
|
|
199
|
+
| `--reply-to TS` | Ask as a reply in a thread, for a question that follows from something the user said. Pass the `replyTo` that came with their message; it also ticks that message. |
|
|
200
|
+
| `--wait [duration]` | Block until it is settled, rather than returning straight away. The same as running `wait` afterwards. |
|
|
201
|
+
|
|
202
|
+
`ask` returns `{ id, status: "pending", settlesAt }` straight away. The question settles itself at `settlesAt` — 45 minutes by default — whether or not the user answers, so waiting for it can never hang. Then:
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
roger-roger wait <id>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
If you are replying to the user at the same time, use `notify --then-listen` instead: it posts and parks in one command, with no gap between the two where a message could miss you.
|
|
209
|
+
|
|
210
|
+
**Run that in the background and carry on working.** Waiting costs nothing: it is one held connection to the daemon, not polling, and it always comes back — with the user's answer the moment they click, or with the recommended option applied when the question settles itself. Your agent tells you when a background command finishes, and that is how the answer reaches you without sitting still. Only if you cannot run a command in the background, pass `--timeout` below your shell's limit and call `wait` again while the exit code is `4`.
|
|
211
|
+
|
|
212
|
+
Behind it, the daemon sends the reminders and applies `--on-expire`. The user answers by clicking a button, or by using **Other…** and typing. The Slack message is then rewritten to show the outcome, and the buttons go away.
|
|
213
|
+
|
|
214
|
+
- The exit code is `0` when answered (including when the recommended option was applied on expiry), `5` when expired or cancelled, and `4` when still pending once `--timeout` is up.
|
|
215
|
+
- Exit code `6` means the user sent a message while you waited, so they're talking to you now; the JSON has `messages`. Act on them: reply with `notify`, and if a message makes the question moot (e.g. "don't commit anything"), `cancel` the question. Then `wait` again if it's still open.
|
|
216
|
+
- The JSON `answer` holds:
|
|
217
|
+
- `choice`: the button's label, or `null` for a typed answer.
|
|
218
|
+
- `index`: 0-based, or `null` for a typed answer.
|
|
219
|
+
- `choices` and `indices`: arrays instead of `choice` and `index`, for a `--multi` question submitted with the checkboxes. They may be empty, meaning the user chose none of the options.
|
|
220
|
+
- `text`: only for a typed answer. Read it carefully: it may be a choice in other words, a new instruction, or a question back to you.
|
|
221
|
+
- `files`: what the user attached when they answered in the question's thread, each with a local `path` to open.
|
|
222
|
+
- `by`: the Slack user ID.
|
|
223
|
+
- `auto: true`: only when nobody answered and the recommended option was applied. Say so in your reply, e.g. "no answer, so I went with Archive".
|
|
224
|
+
- `edited: true` with `text`: for a `--draft` question, the user's edited version. Use that text exactly as given, including its line breaks. When they clicked "Use as is" instead, `choice` is set and you use your draft unchanged.
|
|
225
|
+
- **An answer is never lost.** If you stopped waiting — the command timed out, your turn ended, the process was killed — the answer is kept for you, and **the next command you run, whatever it is, hands it over** as `answers: [{ id, question, answer, waited }]`. Treat those exactly as if you had waited for them, and act on them before carrying on. Until you pick one up, its Slack message says the answer is saved for you rather than delivered, so the user knows you haven't seen it yet.
|
|
226
|
+
- While waiting, also tell the user in the conversation what you asked, so they can answer in either place. **If they answer in the conversation instead, run `cancel <id> --reason "answered in the terminal"`**, so the Slack message stops offering buttons.
|
|
227
|
+
- `check <id>` shows the current state without waiting. `decisions` lists pending ones (`--all` includes recent closed ones).
|
|
228
|
+
|
|
229
|
+
Offer a sensible set of options even though **Other…** exists, because clicking is quicker than typing on a phone. If `ask` fails because Slack or the app token isn't set up, ask in the conversation and mention the error once.
|
|
230
|
+
|
|
231
|
+
## 4. When to notify
|
|
232
|
+
|
|
233
|
+
Follow `config.when` and `config.notes`. With `auto`, use judgement. When you are blocked on a choice between clear options, prefer `ask` over `notify`:
|
|
234
|
+
|
|
235
|
+
**Notify** when the user is likely not watching and something needs them or would interest them:
|
|
236
|
+
- A long task is finished: you worked for several minutes, or many steps, without the user writing.
|
|
237
|
+
- You are **blocked** or need a decision or input to continue. Notify *before* you stop and wait.
|
|
238
|
+
- A failure stops progress and you cannot fix it on your own.
|
|
239
|
+
- An important milestone in long work (for example "phase 1 of 3 done, starting migration"). Keep these rare: at most one every half hour or so of work.
|
|
240
|
+
|
|
241
|
+
**Don't notify**:
|
|
242
|
+
- For quick back-and-forth turns where the user is clearly at the keyboard.
|
|
243
|
+
- More than once for the same event, or for every small step.
|
|
244
|
+
- When `config.when` is `on-request` and the user hasn't asked in this conversation.
|
|
245
|
+
|
|
246
|
+
When unsure at the end of a long task, one notification is better than none. Send the notification as the last thing you do, right before your final reply.
|
|
247
|
+
|
|
248
|
+
### Ask before doing something that can't be taken back
|
|
249
|
+
|
|
250
|
+
`ask` is how you get *permission*, not only an opinion. Before anything hard or impossible to undo, ask first and do it only if the answer says to:
|
|
251
|
+
|
|
252
|
+
- deleting or overwriting data — dropping a table, `rm -rf`, `git push --force`, rewriting history
|
|
253
|
+
- deploying, releasing, or changing anything already live
|
|
254
|
+
- spending money, or starting anything that bills
|
|
255
|
+
- sending something to another person: an email, a message, a PR comment, an invite
|
|
256
|
+
|
|
257
|
+
Show exactly what you are about to do — `--details` for the plan, `--draft` for text you would send, `--attach` for a diff or a plan file — and always pass `--on-expire nothing`, so that no answer means it does not happen. An expired gate is a *no*: say so, and get on with whatever you can do without it.
|
|
258
|
+
|
|
259
|
+
Judge it by what happens if you are wrong, not by whether it feels routine. A force-push to a branch nobody uses needs no permission; the same command against `main` does.
|
|
260
|
+
|
|
261
|
+
## Other commands
|
|
262
|
+
|
|
263
|
+
- `install`: the interactive setup, for the user to run in a terminal (section 1). `install --yes` asks nothing: it links the skill for every agent on the machine and registers with Herdr, and prints what it did.
|
|
264
|
+
- `status`: current config, whether the tokens are present, the daemon, running sessions, and every valid option value.
|
|
265
|
+
- `slack-setup [status|install-cli|login|create]`: the Slack steps `install` runs, one at a time, for repairing a connection by hand. `create` again is safe: it updates the same app, which is what to do after the manifest changed or when a scope is missing.
|
|
266
|
+
- `sessions [--all]`: which agents are running, what each is called, and what each is doing. `sessions rename sage plum` gives one a different name (the user's request, not yours to change on a whim).
|
|
267
|
+
- `end [name]`: this session is finished. Do this when your work is done, so messages meant for someone else don't come to you.
|
|
268
|
+
- `tray status|start|stop|install`: an icon in the system tray showing every agent, with a translucent panel behind it and snooze on the right-click menu. The native part is a separate package installed on request into `~/.roger-roger` (about 4 MB, nothing compiled); everything here works without it. Once installed, the daemon starts it by itself whenever it starts (`setup --tray off` to stop that).
|
|
269
|
+
- `snooze [1h|off]`: mute sound and speech for a while; Slack keeps working. For "not now" moments that quiet hours don't cover.
|
|
270
|
+
- `hooks status|install|uninstall [claude|opencode|codex|all]`: the terminal-question hooks. When you use your own question tool (Claude Code's question dialog, OpenCode's question tool, Codex's `request_user_input`) or present a plan to approve, you are frozen until it's answered and can't send anything yourself; with the hooks, the agent itself runs roger-roger, and once the question has gone unanswered for a little while the user gets it in Slack with its options. `roger-roger install` offers them; without a name, `hooks install` covers the agents on this machine. What each gets: Claude Code, three entries in `~/.claude/settings.json`; OpenCode, a small plugin at `~/.config/opencode/plugins/roger-roger.js` (restart OpenCode to load it); Codex, three entries in `~/.codex/hooks.json`, to trust in `/hooks`. They follow the user's `when`, quiet hours and snooze like any notification, and log each run to `~/.roger-roger/hooks.log`. Claude Code pings once nobody has typed for a few seconds; OpenCode and Codex can't tell, so they wait `setup --terminal-wait` (default 20s) and ping only if it's still unanswered. Ordinary permission prompts (leave to run a command) don't ping unless the user asks: `setup --permission-pings on` (Claude Code and OpenCode; Codex gives no signal when one is answered). Run `hooks install` again after the skill moves, so they point at the new copy; `hooks status` says if they don't.
|
|
271
|
+
- `daemon status|start|stop`: the always-on process behind everything. It starts on demand, so this is only for checking on it or restarting it after a change to the skill's code.
|
|
272
|
+
- `play [sound]`: preview a sound (defaults to the configured one).
|
|
273
|
+
- `say "text" [--voice Name] [--local]`: preview a voice. `--local` skips Gemini and uses the built-in voice.
|
|
274
|
+
- `preview-sounds [sound...] [--voice Name] [--local]`: says each sound's name, then plays the sound. Covers all sounds by default. The JSON `order` lists what played, in order.
|
|
275
|
+
- `preview-voices [voice...] [--all] [--text "..."]`: each voice introduces itself by name. Covers a shortlist of six by default. Needs Gemini, and the audio is generated before playback starts, so it takes a few seconds before the first voice speaks.
|
|
276
|
+
|
|
277
|
+
## Requirements
|
|
278
|
+
|
|
279
|
+
- Node.js 18 or newer.
|
|
280
|
+
- Any agent: nothing in the skill depends on a particular one. A session is recognised by an id the agent publishes if there is one (`CLAUDE_CODE_SESSION_ID`, `CODEX_THREAD_ID`, `OPENCODE_SESSION_ID`, or anything matching `*_SESSION_ID`), else by the `--session` name with the working directory, else by the terminal. `--session-id` and `ROGER_ROGER_SESSION_ID` override all of it. `status` reports which of these answered, as `daemon.me.source`.
|
|
281
|
+
- The daemon: one process per machine, started on demand, reached over a local socket (a named pipe on Windows, `daemon.sock` in `~/.roger-roger` elsewhere). Nothing listens on the network. It holds the Slack connection open so messages arrive live, keeps the audio player warm, and is the only writer of the state files. If it can't be started, commands still run in the calling process and say so in `daemon`; everything works except Slack buttons, which need a live connection to arrive.
|
|
282
|
+
- One announcement at a time: the daemon plays them in turn, and a lock file (`speaker.lock`) keeps any process that had to fall back from talking over it. A crashed holder is stepped over after about 15 seconds. `ROGER_ROGER_NO_SPEAKER_LOCK=1` turns the queue off; `ROGER_ROGER_NO_DAEMON=1` keeps everything in the calling process.
|
|
283
|
+
- Audio players: Windows uses PowerShell (built in). macOS uses `afplay` and `say` (built in). Linux uses one of `paplay`, `pw-play`, `aplay`, or `ffplay` for sound, and `spd-say` or `espeak(-ng)` for the local voice.
|
|
284
|
+
- Slack, the normal way: `roger-roger install` (or `slack-setup`, step by step) creates the roger-roger app with the Slack CLI and keeps its tokens in `~/.roger-roger/slack.json`. Tokens saved that way take priority over environment variables.
|
|
285
|
+
- Slack, manually: create a Slack app yourself (the bundled `slack/manifest.json` can be pasted at api.slack.com/apps → *Create New App* → *From a manifest*), install it, and set:
|
|
286
|
+
- `SLACK_API_BOT_TOKEN` (or `SLACK_BOT_TOKEN`): the bot token (`xoxb-…`), with `chat:write`, `im:write`, `im:history` (to take in DMs sent while nothing was connected), `reactions:write` (the 👀 and ✅ on the user's messages), `files:write` for `--attach`, and `files:read` to receive the screenshots the user sends. Invite the bot to any channel it should post in.
|
|
287
|
+
- `ROGER_ROGER_SLACK_APP_TOKEN` (or `SLACK_APP_TOKEN` if that variable isn't used for something else): an app-level token (`xapp-…`) with `connections:write`, needed for `ask`. Button clicks reach the daemon over **Socket Mode**, so turn on Socket Mode and **Interactivity** for the app. No public URL is needed.
|
|
288
|
+
- `status.env` shows whether usable tokens are present and where they come from (`slackTokenSource`).
|
|
289
|
+
- Gemini: an API key from Google AI Studio, in `GEMINI_API_KEY` or pasted into `roger-roger install` (kept in `~/.roger-roger/keys.json`, readable by the user only). The default model is `gemini-3.1-flash-tts-preview`; change it with `setup --model`.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# roger-roger as a Herdr plugin. The same folder is the agent skill; this file only tells Herdr what
|
|
2
|
+
# in it is worth a keybinding. `roger-roger install` links it (`herdr plugin link <this folder>`), or
|
|
3
|
+
# install it from GitHub with `herdr plugin install code-red-app/roger-roger/skills/roger-roger`.
|
|
4
|
+
#
|
|
5
|
+
# Commands run with this folder as the working directory, so the script path is relative.
|
|
6
|
+
|
|
7
|
+
id = "roger-roger"
|
|
8
|
+
name = "roger-roger"
|
|
9
|
+
version = "0.1.0"
|
|
10
|
+
min_herdr_version = "0.9.0"
|
|
11
|
+
description = "Slack, a sound or a spoken line from the agents in your panes, and questions you answer from your phone"
|
|
12
|
+
platforms = ["linux", "macos", "windows"]
|
|
13
|
+
|
|
14
|
+
# The daemon holds the Slack connection open, so a message sent before any agent has run still
|
|
15
|
+
# arrives. Starting it with Herdr means it is there from the first pane.
|
|
16
|
+
[[startup]]
|
|
17
|
+
command = ["node", "scripts/roger-roger.mjs", "daemon", "start"]
|
|
18
|
+
|
|
19
|
+
[[actions]]
|
|
20
|
+
id = "snooze"
|
|
21
|
+
title = "roger-roger: snooze sound and speech for an hour"
|
|
22
|
+
contexts = ["workspace"]
|
|
23
|
+
command = ["node", "scripts/roger-roger.mjs", "snooze", "1h"]
|
|
24
|
+
|
|
25
|
+
[[actions]]
|
|
26
|
+
id = "unsnooze"
|
|
27
|
+
title = "roger-roger: stop snoozing"
|
|
28
|
+
contexts = ["workspace"]
|
|
29
|
+
command = ["node", "scripts/roger-roger.mjs", "snooze", "off"]
|
|
30
|
+
|
|
31
|
+
# The interactive setup, in a popup: change the sound, the voice, or when to be told.
|
|
32
|
+
[[panes]]
|
|
33
|
+
id = "setup"
|
|
34
|
+
title = "roger-roger setup"
|
|
35
|
+
placement = "popup"
|
|
36
|
+
width = 100
|
|
37
|
+
height = 40
|
|
38
|
+
command = ["node", "scripts/roger-roger.mjs", "install"]
|