fix(slack): answer in a thread, and make the manifest readable

Merged
KH
2mo ago
+368-40

Both problems observed on the first live @mention after cloud#3231 deployed.

1. Replies went to the channel, not a thread

postReply only threaded when the incoming message was already in one:

const result = msg.threadTs
  ? await slack.reply(chanId, msg.threadTs, text)
  : await slack.post(chanId, text);   // ← top-level mention dumps into the channel

That is worst exactly where agents are useful. Every agent shares one Slack identity, so a single @Agent Relay … wakes every agent watching that channel — the production dispatch for one message matched 8 deployments — and they all replied into the main channel. Now the reply threads under the incoming message, and stays in the existing thread when there is one.

2. capabilities --json was an unreadable wall

~4KB of minified JSON posted as plain text. Now pretty-printed inside a ```json fence and led by one line naming the agent and pointing at plain-language questions — nobody should have to parse JSON to learn they can just ask.

Blast radius, deliberately

The threading change is in shared/slack.ts, so it applies to every agent using that helper (hn-monitor, joke-bot, inbox-buddy, review, …). That is the intent: an agent answering a channel mention should thread. Calling it out because it is broader than the askable-gtm title suggests.

The existing test asserting a channel-level post encoded the old behaviour; it now asserts the thread, with the reasoning in a comment so it is not "fixed" back later.

Validation

  • npm run typecheck: pass
  • tests/askable-gtm.test.mjs: 46 pass, 2 new — fenced/parseable manifest, and an existing thread answered in that thread rather than a new one
  • full suite: 322 pass / 0 fail

Not fixed here

One @Agent Relay mention still wakes every agent watching the channel, because they share a Slack identity — cloud#3234 gates on the bot being mentioned, which does not disambiguate which agent is being addressed. Threading makes that liveable, not solved. Worth a follow-up.

🤖 Generated with Claude Code


Summary by cubic

Fixes four problems from the first live @mention: replies thread under the message instead of landing in the channel, capabilities --json stays parseable for machines while Slack gets a readable fenced view, cited evidence is an excerpt rather than a whole post, and answers lead with results instead of a preamble.

  • Replies thread under the incoming message, and replies to a message already in a thread stay in that thread.
  • Threading is opt-in via startThread in postReply/conversationKeyForSlack; only askable-gtm enables it because it keeps no cross-turn Slack context.
  • capabilities --json stays standalone parseable JSON over relay; the Slack actor fences and pretty-prints it via a new presentJson hook.
  • Evidence lines are excerpted to 180 characters on a word boundary, and LinkedIn rows are attributed by author handle.
  • Answers drop the question restatement and coverage preamble, keeping only a compact source-failure note and the disclosure for metered managed access.
  • Unsolicited watch deliveries get a compact header naming the query, cadence, and watch id so the reader knows which one fired and can unwatch it.
  • Tests drive both transport halves through the real handlers, so the relay/Slack split can't silently regress.
  • One @Agent Relay mention still wakes every agent watching the channel since they share a Slack identity; threading makes that liveable, not solved.