Skip to content

Add Slack task watcher - #19

Merged
mgreau merged 3 commits into
mainfrom
mgreau/slack-task-watcher
Aug 30, 2026
Merged

Add Slack task watcher#19
mgreau merged 3 commits into
mainfrom
mgreau/slack-task-watcher

Conversation

@mgreau

@mgreau mgreau commented Aug 28, 2026

Copy link
Copy Markdown
Owner

What

A new opt-in feature for zen watch daemon: poll for a self-added emoji reaction (default :claudecode:), and turn each new one into a feature worktree with a live agent session, seeded with the Slack thread as the initial prompt. When that session later goes idle, send a Slack DM back — not just the existing desktop notification — with a resume command and a link to the thread. Stop sending those DMs once the task is actually done: either a merged PR from the worktree's branch, or a configured done-emoji (default :done_check:) reacted onto the original message.

Also includes a standalone fix (separate commit): IsProcessRunning matched the session ID against process argv, which only works for --resume invocations — a freshly launched session never carries its own ID in argv, so it read "stopped" from the very first scan. Fixed to match on the process's cwd instead (falling back to the old argv match), which also fixes the existing desktop "session waiting" notification for any freshly-launched zen work new/zen review session, not just this feature.

Why

The existing daemon watches GitHub for PR review requests; this gives it a second, symmetric watch loop for a different kind of "an agent should look at this" signal — reacting on something in Slack, which happens far less often than PR review requests, so unlike the PR-review flow (which only preps worktrees silently), this launches the agent immediately.

Kept the Slack I/O to a dedicated client (internal/slack, stdlib net/http only) authenticated by its own token via ZEN_SLACK_TOKEN, rather than trying to piggyback on the claude.ai account connector — confirmed empirically that connector isn't available to headless/daemon Claude Code invocations at all (auth-method-gated), so a background daemon needs its own credentials regardless.

Notes

  • Disabled by default (slack.enabled: false); requires slack.default_repo and a token with reactions:read, channels:history, groups:history, im:history, mpim:history, chat:write scopes (see docs/configuration.md#slack-task-watcher). Degrades to a logged warning (not a crash) if enabled without a valid token.
  • Extracted the worktree-from-origin/main git sequence into internal/worktree.CreateFromMain, shared between zen work new and the new reconciler, instead of a third copy of the same "Could not write new index file" two-step dance.
  • Verified end-to-end against real Slack threads over several real daemon poll cycles: reaction detected, worktree created, agent launched, completion DM delivered (confirmed by reading it back via conversations.history).
  • The ack-reaction and done-emoji paths need reactions:write/reactions:read respectively on whatever Slack app provides the token — not exercised by CI, only by hand against a real workspace.

mgreau added 3 commits August 28, 2026 11:39
IsProcessRunning matched the session ID against process argv via pgrep -f.
That only works for --resume invocations, which carry the ID explicitly —
a freshly launched session (claude/codex started with just a prompt, no
--resume) never has its own session ID in argv, since the agent generates
that ID internally after starting. Every fresh session therefore read as
"stopped" from the very first scan, even while actively running.

Match on the process's cwd against the worktree path instead (via lsof),
falling back to the old argv match for resumed sessions. Confirmed live:
before this fix, a freshly launched worktree's session read "stopped" in
zen agent status despite the process being alive (verified via lsof); after
the fix it correctly reads "running" then "waiting" once idle.
Poll for a self-added emoji reaction (default :claudecode:) and turn each
new one into a feature worktree with a live Claude Code/Codex session,
seeded with the Slack thread as the initial prompt. Unlike zen review/zen
work new's PR-review flow, which only prepares worktrees silently for later
manual resume, this launches the agent immediately: a Slack reaction is a
deliberate, comparatively rare signal, not the high-volume PR-review
firehose, so there's no need to hold back.

On pickup: ack the message in-thread (default 👀), fetch the thread,
create the worktree from origin/main, launch the agent, and record the
Slack origin (channel/ts/permalink) alongside the worktree. When that
worktree's session later goes idle, the existing session-scan loop also
sends a Slack DM back — with a resume command and a link to the original
thread — instead of just the desktop notification.

To stop the DMs once a task is actually done, each origin is checked (on
the existing cleanup ticker, and once more right before any notify) for
either a merged PR from its branch or a configured done-emoji (default
:done_check:) on the original message; either one clears the origin so no
further transition re-fires it.

Uses its own Slack Web API client (internal/slack, stdlib net/http only,
no new dependency) authenticated by a dedicated personal token via
ZEN_SLACK_TOKEN — never through the claude.ai account connector, which
isn't available to headless/daemon processes, and never stored in
config.yaml. Extracted the worktree-from-origin/main git sequence
(internal/worktree.CreateFromMain) out of zen work new so the new
reconciler doesn't duplicate it a third time.

Disabled by default (slack.enabled: false); degrades to a logged warning
and continues running the rest of the daemon if enabled without a valid
token, rather than failing to start.

Verified end-to-end against real Slack threads: reaction detected, worktree
created, agent launched, and the completion DM delivered to a real Slack
DM (confirmed via conversations.history), across several real poll cycles.
…cher

* origin/main:
  Feat/macos terminal (#16)

# Conflicts:
#	docs/architecture.md
#	docs/configuration.md
@mgreau
mgreau merged commit 84dbb3a into main Aug 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant