Slack module: manage TaskYou from Slack (two-way) - #33
Conversation
From TaskYou task #3723. Proposes modules/slack/, modeled on modules/linear/linear-poll.mjs: watch a Slack channel/DM/@mention, classify intent, drive ty, post replies, and tail notifications.jsonl to push task.blocked/completed back to the channel. Enabled via SLACK_ENABLED. Separates the chat-control bridge (recommended) from the harder hosted/remote-MCP path (deferred to the credential-proxy work). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
notifications.jsonl survives as the push substrate, but the channels refactor changes three things: copy taskyou-channel.ts (bun/TS) instead of linear-poll.mjs; reuse assigned_gm (#32 + workflow#561) for per-GM routing (answers the shared-bot vs per-user question); adopt the IS_LOCAL/SSH runRemote pattern + OS-aware hooks dir (#31). Adds a dependencies section and merge order (#28 -> #31/#32 -> Slack). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#28 (channels) and #31 (local/macOS) are merged to main, so the design is concrete: a Slack module is taskyou-channel.ts with its two ends re-pointed — reuse pollNotifications() + runRemote() verbatim; net-new is just a Slack adapter + LLM classifier. Ship outbound (hook -> Slack webhook) first, inbound control second. Removes the assigned_gm / per-GM scoping dependency (out of scope: single bot, single operator). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Builds the Slack integration end to end (outbound + inbound), per the
design in docs/plans/2026-06-08-slack-module-design.md.
modules/slack/slack-bridge.mjs — zero-dependency Node daemon (raw fetch +
global WebSocket, like the Linear poller). Runs on the agents server next
to ty + notifications.jsonl:
• Outbound: tails notifications.jsonl → chat.postMessage (blocked/
completed/failed/started). Slack-originated tasks answer in-thread;
everything else goes to SLACK_NOTIFY_CHANNEL.
• Inbound: Slack Socket Mode (no public URL) → allowlist check →
intent classification (Anthropic API, with a keyword-heuristic
fallback when no key) → ty create/execute/input/list. Replies in-thread.
Also: unit tests for the pure logic (node --test, 11 cases), a
README, the ty-slack systemd service template, and setup.sh wiring
(server + exe.dev install the service; local mode renders files + .env)
plus SLACK_* in config.example.env and the README modules list. The
module is gated behind SLACK_ENABLED=true.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
194487f to
1584275
Compare
QA evidenceAll run against the pushed commit Summary
1) node --check + unit tests (
|
Classifier no longer requires a separate ANTHROPIC_API_KEY: it prefers the
already-authenticated on-box `claude` CLI (claude -p — the same primitive the
GM/executors use), falls back to the Anthropic API if a key is set, then a
keyword heuristic. Override via SLACK_CLASSIFIER.
Loop / token-burn hardening (per review):
- claude -p sandboxed per call: --strict-mcp-config --mcp-config {} +
--disallowedTools … keep it to a single turn (no agentic spiral);
--max-budget-usd (default $0.05) hard-caps cost; timeout + maxBuffer; no retry.
- Self-loop guards: ignore own/bot/edited messages (bot_id/subtype/BOT_USER_ID).
- Concurrency cap (SLACK_MAX_CONCURRENT, default 3) bounds in-flight
classifications; excess messages declined, not queued.
- Socket generation guard: only the newest connection's handlers stay live, so
a reconnect race can't double-deliver events.
- Outbound poller: per-tick cap (SLACK_MAX_NOTIFS_PER_POLL, default 25) with
byte-accurate gradual drain; only complete lines consumed (partial hook line
held); lost/corrupt state skips backlog instead of replaying.
Tests: +2 (buildClassifierContext, readNewChunk partial-line/rotation) → 13/13.
Docs: README "Runaway / cost protection", config knobs, design-doc note.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Runaway-loop / token-burn review (commit
|
| Risk | Mitigation |
|---|---|
| Bot replies to itself → infinite reply→classify→reply loop | Ignore own/bot/edited/system messages (bot_id / subtype / BOT_USER_ID). Our replies carry bot_id, so the loop can't start even if auth.test failed at boot. |
claude -p spirals into a multi-turn agentic run / burns tokens |
Per call: --strict-mcp-config --mcp-config '{"mcpServers":{}}' (no MCP servers) + --disallowedTools Bash,Read,Write,… → single turn, no tools; --max-budget-usd (default $0.05) hard cost cap; wall-clock timeout + capped buffer; one attempt, no retry (failure → fall back). |
| Burst of messages → unbounded concurrent subprocesses/spend | SLACK_MAX_CONCURRENT (default 3) in-flight cap; excess messages declined, not queued. |
| Slack event retries re-trigger classification | Envelope acked in <3s (stops Slack resending) + event_id dedup. |
| Socket reconnect race → two sockets double-deliver events | Monotonic generation guard — only the newest connection's handlers stay live. |
| Notification flood / lost state → Slack post storm or history replay | Per-tick cap SLACK_MAX_NOTIFS_PER_POLL (default 25) with byte-accurate gradual drain; only complete lines consumed (partial hook line held); null/corrupt cursor skips the backlog instead of replaying. |
Trust boundary unchanged: only SLACK_ALLOWED_USERS can drive ty at all.
Proof
Unit tests — 13/13 (added buildClassifierContext + readNewChunk partial-line/rotation):
ok 12 - buildClassifierContext includes thread + open tasks + message
ok 13 - readNewChunk: only complete lines; partial last line is held back
# tests 13
# pass 13
# fail 0
Live hardened claude -p classification (exact flags the bridge uses), message "go with option 2":
is_error: false num_turns: 1 total_cost_usd: 0.0157717 (cap: 0.05)
result: ```json { "action": "help", … } ``` ← single turn, under budget, JSON parses
num_turns: 1 confirms no agentic spiral; cost is well under the per-call ceiling.
QA harness (qa/run-qa.sh) against this commit → 17/17 (no setup regression).
Still credential-gated, so not exercised here: the live Slack round-trip and the API-key classifier path. The
claude -ppath itself is proven above.
Summary
From TaskYou task #3723 ("Explore taskyou MCP sidecar extension for Slack"). Implements a two-way Slack module so people can manage TaskYou from Slack — and includes the design rationale in
docs/plans/2026-06-08-slack-module-design.md.What's here
modules/slack/slack-bridge.mjs— a zero-dependency Node daemon (rawfetchto the Slack + Anthropic HTTP APIs and the globalWebSocket, exactly like the Linear poller — nonpm install). Runs on the agents server next toty+notifications.jsonl:notifications.jsonl(the same file the hooks + Claude Code channel use) →chat.postMessage. Tasks created/run from Slack answer back in their thread; everything else goes toSLACK_NOTIFY_CHANNEL.ty create/execute/input/list. Replies in-thread.modules/slack/slack-bridge.test.mjs—node --testunit tests for the pure logic (11 cases, all green).templates/ty-slack.service.tmpl— systemd user service (long-running; Socket Mode holds a WebSocket).setup.shwiring —server+exe.devinstall thety-slackservice; local mode renders the files +.env. Gated behindSLACK_ENABLED=true.config.example.envSlack block + README modules entry +modules/slack/README.md.Design refinement vs the original plan
The plan said "copy
taskyou-channel.ts+runRemote()." During build it became clear the bridge runs on the server next totyandnotifications.jsonl, so it reads the file and runstydirectly — no SSH/runRemote. And it's Node.mjs(the server has Node; bun is a GM-machine prereq), matchingmodules/linear. Scope: single bot / single operator (perty-email); per-GM routing and the hosted-remote-MCP path remain out of scope.Testing
node --testinmodules/slack/→ 11/11 pass.node --checkon the bridge → clean.qa/run-qa.sh) against this branch → 17/17 pass (mysetup.shchanges don't regress the channel/hook/notification flow).setup.sh server(local mode) withSLACK_ENABLED=true→ bridge +.envinstall correctly; caught and fixed aSLACK_PROJECT_MAPdouble-brace bug in the heredoc default.modules/slack/README.md.Files
modules/slack/{slack-bridge.mjs,slack-bridge.test.mjs,README.md},templates/ty-slack.service.tmpl,setup.sh,config.example.env,README.md,docs/plans/2026-06-08-slack-module-design.md— +1238.🤖 Generated with Claude Code