fix(agent-team): handle real slack_bolt event envelope + map thread replies; bind start invoker #26

Merged
amoussa1229 merged 2 commits from fix/agent-team-slack-inbound-envelope into main 2026-06-22 19:30:48 +00:00

2 commits

Author SHA1 Message Date
ae5aa020f3 fix(agent-team): bind subscription invoker in the start CLI
`run-team.py start` runs the clarifier graph to the first human gate IN the CLI
process, and the clarifier calls Claude (assess_confidence). The invoker is a
process-local binding that only `serve` set, so `start` failed with
"claude_invoke has no invoker bound". Bind the real subscription invoker here,
mirroring Coordinator.serve(). Found during the live R720 P1 bring-up.
2026-06-22 15:04:57 -04:00
0f3c0fa57a fix(agent-team): handle real slack_bolt event envelope + map thread replies to open questions
The Socket Mode inbound listener was unit-tested against a SYNTHETIC payload
shape that does not match what slack_bolt actually delivers, so the suite was
green while a real Slack thread reply was silently dropped (the clarifier
question stayed `open`). Real slack_bolt delivers an Events API message /
app_mention as `{"type":"event_callback","event":{"type":"message",...}}` and
a free-text thread reply carries NO callback_id/question_id/metadata.

Three breaks fixed (all on the free-text reply path):

1. Type gate — handle_event gated on the OUTER `type`, which is
   "event_callback" for a real message/app_mention, so the event fell outside
   _ANSWER_BEARING_TYPES and was dropped. Now collapsed to the discriminating
   INNER `event.type` via _discriminating_type / _inner_event.

2. question_id recovery — a real reply has no callback_id/question_id/metadata
   (the bot's metadata is on the QUESTION message, not the reply). When explicit
   id recovery fails, the listener now resolves the question by the inner
   event's `thread_ts` against the OPEN ledger row whose `channel_ref` equals it
   (new schema helper find_open_question_by_channel_ref, constrained to
   status='open' as anti-replay). Explicit id recovery still takes precedence.

3. answer extraction — a real message event carries its text at `event.text`,
   not a top-level `answer`/`text`. The thread-reply path now takes the inner
   `event.text` (stripped) as the answer value.

AUTHZ-01 is unchanged and still runs FIRST: authorization gates on the sender's
Slack user id (`event.user` for the Events API shape) and fails closed on an
empty/unknown allowlist or unrecoverable sender. The new mapping only resolves
WHICH question is answered, never WHO may answer. Answers stay opaque DATA
(parameterized SQL + json.dumps; never eval/exec/interpolate).

Tests: replaced the synthetic events-API fixtures with REAL Bolt envelopes and
added regression coverage — real thread reply maps via channel_ref and is
accepted, text is stripped, non-owner reply rejected (row stays open), thread_ts
matching no open row is a no-op, reply to an already-answered row is a no-op
(anti-replay), and app_mention is normalized identically. block_actions /
slash_command paths retained.
2026-06-22 15:02:55 -04:00