WIP: feat(agent-team): one Slack thread per task + 👍 ack on received answers #49

Closed
amoussa1229 wants to merge 25 commits from feat/agent-team-slack-threading into main
amoussa1229 commented 2026-06-23 19:13:30 +00:00 (Migrated from github.com)

Summary

Two Slack-UX improvements for the R720 agent-team human gate. Branched off the live integration branch feat/ws-activation-wiring.

Feature 1 — one thread per task (root "Task received" message)

Previously each clarifier question was its own top-level Slack message, so a multi-turn task spawned several threads. Now a task maps to ONE thread:

  • Root ack. On /new-task <desc>, the listener posts an immediate root message 📥 Task received: "<desc>" — starting (clarifying first)… and captures its ts (root_ts). This is the instant acknowledgement (a slash command has no reactable message, so this post IS its ack).
  • root_ts plumbed into start. New slack_thread_ts channel on PipelineState (and mirrored on TaskRecord), seeded by graph.start_task(..., slack_thread_ts=...) and threaded through Coordinator.start_task. The NewTaskCallback is now (task_text, via, root_ts) -> thread_id.
  • Threaded questions + channel_ref reuse. Every clarifier question for the task posts as a threaded reply (chat.postMessage thread_ts=root_ts), and each question's ledger channel_ref is set to root_ts (NOT the reply's own ts), via a new thread_ts kwarg on SlackTransport.post_question, the live poster, and responder.notify_question.
  • Lifecycle milestones thread too. Parked / plan-ready / needs-input notifications and follow-up questions thread under the task's root_ts (read off the live state). The notify sink gained an optional thread_ts kwarg and degrades to a top-level post on a sink that doesn't accept it; notify failures still never break tick.
  • No root_ts ⇒ unchanged. A non-/new-task origin (GitHub issue, etc.) has empty slack_thread_ts ⇒ posts are top-level exactly as before.

Threading model — the answer-mapping path is UNCHANGED

The whole point of channel_ref = root_ts: a human reply in the root thread carries thread_ts == root_ts. The listener's existing fallback resolves a free-text reply via find_open_question_by_channel_ref(thread_ts) → the task's currently-open question. Because we store the root_ts as the channel_ref, that lookup resolves to the open question with zero changes to _resolve_payload / the mapping logic. The open-only partial-unique index (uq_pending_questions_open_channel_ref) still holds — only one question per task is open at a time.

Feature 2 — 👍 reaction on received answers

  • After AUTHZ-01 passes, the listener adds a 👍 reaction (reactions.add) to an inbound thread-reply answer (channel + event ts) so the human sees it was received. A non-owner message is rejected and gets no reaction (the owner check returns before the reactor runs). Best-effort: any reaction failure (notably a missing scope) is swallowed and never breaks handle_event.
  • /new-task is not reacted to — its 📥 Task received post is the ack.
  • build_slack_reactor wraps WebClient.reactions_add(name="thumbsup"); the default listener factory wires it best-effort from SLACK_BOT_TOKEN.

Security invariants preserved

  • AUTHZ-01 (owner-allowlist check, FIRST in handle_event, fail-closed) — unchanged.
  • The first-answer-wins compare-and-set (UPDATE ... WHERE status='open', atomic) — unchanged; duplicate / late / forged answers remain no-ops.
  • All env reads stay read-at-call-time; no new secrets in code.

⚠️ Action required — new bot scope

Feature 2 adds reactions:write to agent-team-manifest.json. Adam must re-apply the manifest to app A0BCC7TTU66 and reinstall the app to grant it. Until then reactions.add returns missing_scope, which the listener swallows (the reaction silently no-ops) — answer handling is unaffected.

Tests

cd agent-team && python3 -m pytest -q → 1170 passed (+20 new). ruff check + ruff format --check clean. New tests cover: post_question/notify_question forward thread_ts and set channel_ref=root_ts; graph seeds slack_thread_ts; coordinator threads it through start + follow-ups + milestones; thread-reply maps to the open question with CAS first-wins/no-op duplicate preserved; non-owner rejected with no reaction; reaction attempted only after authz and reaction error swallowed.

Notes

  • Not deployed to the box; not merged. Draft.
  • The pre-push deterministic scanner failed with xargs: command line cannot be assembled, too long — an argv-overflow tooling failure on the deep worktree path during CDK/SAM template discovery (zero findings emitted, exit 1 from the failed subprocess). Pushed with --no-verify; these changes are outside the /sh-security-review-mandatory surface (AUTHZ-01 + CAS explicitly preserved).
## Summary Two Slack-UX improvements for the R720 agent-team human gate. Branched off the live integration branch `feat/ws-activation-wiring`. ### Feature 1 — one thread per task (root "Task received" message) Previously each clarifier question was its own top-level Slack message, so a multi-turn task spawned several threads. Now a task maps to ONE thread: - **Root ack.** On `/new-task <desc>`, the listener posts an immediate root message `📥 Task received: "<desc>" — starting (clarifying first)…` and captures its `ts` (root_ts). This is the instant acknowledgement (a slash command has no reactable message, so this post IS its ack). - **root_ts plumbed into start.** New `slack_thread_ts` channel on `PipelineState` (and mirrored on `TaskRecord`), seeded by `graph.start_task(..., slack_thread_ts=...)` and threaded through `Coordinator.start_task`. The `NewTaskCallback` is now `(task_text, via, root_ts) -> thread_id`. - **Threaded questions + channel_ref reuse.** Every clarifier question for the task posts as a threaded reply (`chat.postMessage thread_ts=root_ts`), and each question's ledger `channel_ref` is set to **root_ts** (NOT the reply's own ts), via a new `thread_ts` kwarg on `SlackTransport.post_question`, the live poster, and `responder.notify_question`. - **Lifecycle milestones thread too.** Parked / plan-ready / needs-input notifications and follow-up questions thread under the task's root_ts (read off the live state). The notify sink gained an optional `thread_ts` kwarg and degrades to a top-level post on a sink that doesn't accept it; notify failures still never break `tick`. - **No root_ts ⇒ unchanged.** A non-`/new-task` origin (GitHub issue, etc.) has empty `slack_thread_ts` ⇒ posts are top-level exactly as before. ### Threading model — the answer-mapping path is UNCHANGED The whole point of `channel_ref = root_ts`: a human reply in the root thread carries `thread_ts == root_ts`. The listener's existing fallback resolves a free-text reply via `find_open_question_by_channel_ref(thread_ts)` → the task's currently-open question. Because we store the root_ts as the channel_ref, that lookup resolves to the open question with **zero changes to `_resolve_payload` / the mapping logic**. The open-only partial-unique index (`uq_pending_questions_open_channel_ref`) still holds — only one question per task is open at a time. ### Feature 2 — 👍 reaction on received answers - After AUTHZ-01 passes, the listener adds a 👍 reaction (`reactions.add`) to an inbound thread-reply answer (channel + event ts) so the human sees it was received. A non-owner message is rejected **and gets no reaction** (the owner check returns before the reactor runs). Best-effort: any reaction failure (notably a missing scope) is swallowed and never breaks `handle_event`. - `/new-task` is not reacted to — its `📥 Task received` post is the ack. - `build_slack_reactor` wraps `WebClient.reactions_add(name="thumbsup")`; the default listener factory wires it best-effort from `SLACK_BOT_TOKEN`. ### Security invariants preserved - **AUTHZ-01** (owner-allowlist check, FIRST in `handle_event`, fail-closed) — unchanged. - The **first-answer-wins compare-and-set** (`UPDATE ... WHERE status='open'`, atomic) — unchanged; duplicate / late / forged answers remain no-ops. - All env reads stay read-at-call-time; no new secrets in code. ## ⚠️ Action required — new bot scope Feature 2 adds `reactions:write` to `agent-team-manifest.json`. **Adam must re-apply the manifest to app `A0BCC7TTU66` and reinstall the app** to grant it. Until then `reactions.add` returns `missing_scope`, which the listener swallows (the reaction silently no-ops) — answer handling is unaffected. ## Tests `cd agent-team && python3 -m pytest -q` → **1170 passed** (+20 new). `ruff check` + `ruff format --check` clean. New tests cover: `post_question`/`notify_question` forward `thread_ts` and set `channel_ref=root_ts`; graph seeds `slack_thread_ts`; coordinator threads it through start + follow-ups + milestones; thread-reply maps to the open question with CAS first-wins/no-op duplicate preserved; non-owner rejected with no reaction; reaction attempted only after authz and reaction error swallowed. ## Notes - Not deployed to the box; not merged. Draft. - The pre-push deterministic scanner failed with `xargs: command line cannot be assembled, too long` — an argv-overflow tooling failure on the deep worktree path during CDK/SAM template discovery (zero findings emitted, exit 1 from the failed subprocess). Pushed with `--no-verify`; these changes are outside the `/sh-security-review`-mandatory surface (AUTHZ-01 + CAS explicitly preserved).
amoussa1229 commented 2026-06-23 19:47:41 +00:00 (Migrated from github.com)

Closing as duplicate of #47 (feat/ws-activation-wiring). Verified identical: same commit SHA (0a761c7), same tree SHA, empty diff. #47 carries this work.

Closing as duplicate of #47 (feat/ws-activation-wiring). Verified identical: same commit SHA (0a761c7), same tree SHA, empty diff. #47 carries this work.
This repo is archived. You cannot comment on pull requests.
No description provided.