Know when a worker finishes or asks

Desktop toasts with a sound per event, Claude Code's own Notification hook, Telegram from your phone, and the ask escalation that reaches a human.

Goal#

A worker seals, fails, gets promoted, or stops with a question — know it without staring at the terminal. Four layers, each optional and stacking on the previous one: a toast on this machine, the CLI's own "I stopped" event, Telegram when you walked away, and a digest when you come back.

1. On this machine#

notify.desktop raises an OS toast on every event (sealed, failed, promoted, needs_approval, needs_answer, notification) — on by default wherever a display exists:

notify:
  desktop: true # default; false turns the toast off
  sound: on # on|off|questions (default on)

notify.sound: questions keeps the machine quiet until something is actually waiting on you — a question or an approval — and silent on a plain sealed. Each event kind gets its own sound so the ear tells a finished task from a question without reading the screen.

The toast title is orch · <workspace> · <seat> — the workspace name and the agent/session that raised it, so several terminals with different Orchemax seats stay distinguishable at a glance. The body carries the event and the task's own words.

Prove the channel works, or force it on for a one-off check:

orch notify test --desktop --event failed

Every emission is also appended to .orch/logs/notify.log, whether or not a toast could actually be shown on this OS.

2. The CLI's own stop#

Some events happen inside the agent CLI itself — it needs a permission answer, or it has been idle waiting for input — and nothing else in Orchemax can see that moment. Claude Code exposes it as a Notification hook; wiring it is one command:

orch guard wire --claude

This adds a Notification entry (project scope, same Orchemax-marked block as the rest of guard wire) that runs orch hook notification on every permission prompt or idle wait. The hook raises the desktop toast (and Telegram, when configured) with the CLI's own message, deduped within 30 seconds, and never blocks the CLI — it always exits 0.

The same command also wires SessionStart to run orch notify-hook — the idle-return digest from step 5 below fires the moment you reopen a seat, without waiting on the first tool call.

Other CLIs — cursor-agent, Copilot, opencode, agy — have no equivalent event; they rely entirely on the desktop channel from step 1, which fires on sealing regardless of which CLI ran the session.

3. Away from the machine#

Pair a Telegram bot once, and every event the desktop channel carries also reaches your phone:

  1. Talk to @BotFather on Telegram, create a bot, copy its token.

  2. Put the token in an environment variable and name that variable in orch.yaml:

    notify:
      telegram:
        bot_token_env: ORCH_TELEGRAM_TOKEN
    
  3. Pair your chat — no manual chat-id hunting:

    orch notify telegram pair
    

    This prints the bot's t.me link, waits for you to send /start, adds your chat id to notify.telegram.chat_allowlist in orch.yaml, and sends one test event so you know it worked.

orch daemon starts the long-poll worker automatically once notify.telegram.bot_token_env is set — nobody has to keep a terminal open for it. From an allowlisted chat:

  • /status — how many sessions are running.
  • /approve <session> / /reject <session> — merge or drop a sealed session.
  • /answer <id> <text> — answer a worker's pending question (see below).

A message from any chat not in the allowlist gets orch: unauthorized chat and nothing else runs.

4. Questions#

A worker's ask blocks that worker until someone answers. If nobody does within ask.escalate_after_sec (default 60 seconds — well under the ask's own 600-second timeout), Orchemax escalates it as needs_answer on every configured channel — desktop, Telegram, webhooks — carrying the question and its ask id:

ask:
  escalate_after_sec: 60

Answer it from Telegram (/answer <id> <text>), or from the CLI on this machine:

orch ask list
orch ask answer <id> "keep the shared helper, do not duplicate it"

Either path calls the same AnswerAsk — the blocked worker resumes immediately, no restart.

5. Coming back#

When you open a seat again after being away longer than notify.idle_digest_min (default 10 minutes), the next hook drain — or SessionStart, which checks regardless of the gap — injects one summary line ahead of the regular messages:

while you were away (23m): 2 sealed, 1 failed, 1 question pending (#41)

Nothing to report — no seal, no failure, no pending question — means no line at all: the digest never announces "nothing new", and it never forces another turn just to say so.

notify:
  idle_digest_min: 10

What never leaves the machine#

Every channel in this page carries status and summary only — event name, workspace, session, verdict, the question's own text for needs_answer — never a diff or file content.