Skip to content

feat(slack): umbrella Slack UX upgrade — buttons, status, reactions, slash commands#1757

Merged
Wirasm merged 4 commits into
devfrom
slack-ux-umbrella
May 26, 2026
Merged

feat(slack): umbrella Slack UX upgrade — buttons, status, reactions, slash commands#1757
Wirasm merged 4 commits into
devfrom
slack-ux-umbrella

Conversation

@Wirasm
Copy link
Copy Markdown
Collaborator

@Wirasm Wirasm commented May 25, 2026

Summary

  • Problem: Slack adapter feels frozen during long workflows, has no in-thread interactivity, and surfaces no cost data. Approval gates force users to leave Slack for the web UI.
  • Why it matters: First step toward a shared team Slack instance — these are the cheapest, highest-impact UX gaps without touching the schema or breaking the single-user model.
  • What changed: Block Kit Approve/Reject/Cancel buttons, lifecycle reactions (🔄/✅/❌) on the triggering message, an in-thread status message edited in place as DAG nodes progress, /archon + /archon-workflow slash commands over Socket Mode, _part i/n_ annotations on split messages, and an italic cost/token footer after assistant replies.
  • What did not change (scope boundary): No user_id plumbing (deferred — needs schema migration), no other adapters (Telegram/Discord/GitHub untouched), no workflow engine / event emitter changes, no DB schema changes.

UX Journey

Before

User mentions @archon in #channel
    │
    ▼
Bot creates thread, posts replies
    │
    ▼
Workflow runs silently — thread appears frozen
    │
    ▼
Approval gate hits  ──▶  user opens Web UI to click Approve
    │
    ▼
Workflow ends, final reply posted (no cost info, no status indicator)

After

User mentions @archon in #channel
    │
    ▼
[🔄] reaction added to user's message
    │
    ▼
Bot posts [status message] in thread:
   :arrows_counterclockwise: Workflow running
   Workflow: assist · Run: a1b2c3d4 · Elapsed: 12s
   :hourglass_flowing_sand: `plan`
   :white_circle: `apply`
   [Cancel button]
    │  (chat.update as nodes start/complete, debounced 500ms)
    ▼
Approval gate?  ──▶  [Block Kit message with Approve / Reject buttons in-thread]
                     click → operation runs → message edits to "Approved by @alice — workflow resumed"
    │
    ▼
Workflow ends:
  [🔄] removed → [✅] (or [❌])
  Status message → "Workflow completed · total cost: $0.0234"
  Direct-chat replies get appended: `_cost: $0.0234 · 12.4k tokens · stop: end_turn_`

Plus: `/archon-workflow list` / `status` / `approve <id>` etc. work natively from any channel

Architecture Diagram

Before

                 ┌──────────────────────┐
                 │ WorkflowEventEmitter │
                 └──────────┬───────────┘
                            │
              ┌─────────────┴───────────┐
              │                         │
              ▼                         ▼
   ┌────────────────────┐    (no other subscribers)
   │ WebEventBridge     │
   │ (SSE → web UI)     │
   └────────────────────┘

   Slack flow:
   SlackAdapter ──▶ messageHandler ──▶ Orchestrator ──▶ Provider
        ▲                                                  │
        └────────────── sendMessage ◀──────────────────────┘

After

                 ┌──────────────────────┐
                 │ WorkflowEventEmitter │
                 └──────────┬───────────┘
                            │
              ┌─────────────┴───────────┐
              │                         │
              ▼                         ▼
   ┌────────────────────┐    ┌─────────────────────────┐
   │ WebEventBridge     │    │ SlackWorkflowBridge [+] │
   │ (SSE → web UI)     │    │ (reactions / status /   │
   └────────────────────┘    │  buttons via Bolt app)  │
                             └────────────┬────────────┘
                                          │ getApp().client.*
                                          ▼
   Slack flow (modified):     ┌────────────────────┐
   SlackAdapter [~] ─────────▶│ SlackAdapter app   │
     │   ↑                    │ - chat.postMessage │
     │   │                    │ - chat.update [+]  │
     │   │ sendResultFooter[+]│ - reactions.add[+] │
     │   │                    │ - app.action(...)[+]
     │   │                    │ - app.command(...)[+]
     │   │                    └────────────────────┘
     ▼   │
   messageHandler ──▶ Orchestrator [~] ──▶ Provider
                       (captures `result`
                        chunk, calls
                        sendResultFooter)

Connection inventory:

From To Status Notes
WorkflowEventEmitter SlackWorkflowBridge new Bridge subscribes globally, filters Slack convs via adapter.getTriggeringMessage
SlackWorkflowBridge SlackAdapter.getApp().client.chat.* new postMessage, update for status + approval blocks
SlackWorkflowBridge SlackAdapter.getApp().client.reactions.* new reactions.add/remove on triggering message
SlackWorkflowBridge workflowOperations.{approveWorkflow,rejectWorkflow,abandonWorkflow} new Button handlers call existing operations
SlackWorkflowBridge workflowDb.getWorkflowRun new Reads metadata.total_cost_usd on terminal events
SlackAdapter.start() app.command('/archon'|'/archon-workflow') new Slash command handlers
Orchestrator IPlatformAdapter.sendResultFooter new (optional) Called at end of direct-chat turns; Slack implements, others no-op
server/index.ts SlackWorkflowBridge new Attaches bridge before slack.start()

Label Snapshot

  • Risk: risk: low
  • Size: size: L
  • Scope: adapters
  • Module: adapters:slack

Change Metadata

  • Change type: feature
  • Primary scope: adapters

Linked Issue

  • Closes #
  • Related #
  • Depends on # (if stacked) — none
  • Supersedes # (if replacing older PR) — none

Validation Evidence (required)

bun run validate
# All 6 checks pass:
#   check:bundled            ✓ (36 commands, 20 workflows)
#   check:bundled-skill      ✓ (21 files)
#   type-check               ✓ all 10 packages
#   lint --max-warnings 0    ✓
#   format:check             ✓
#   test                     ✓ all packages
#
# Two new test files added (25 tests total, all passing):
#   packages/adapters/src/chat/slack/blocks.test.ts          (16 tests)
#   packages/adapters/src/chat/slack/workflow-bridge.test.ts (9 tests)
  • Evidence provided: full bun run validate exit-code 0 on the branch tip.
  • Intentionally skipped: end-to-end Slack workspace verification — requires the new scopes (commands, reactions:write) installed on a real Slack app. Listed under Human Verification.

Security Impact (required)

  • New permissions/capabilities? Yes — two new Slack bot scopes: commands (slash commands) and reactions:write (lifecycle emoji). Documented in slack.md.
  • New external network calls? No — same Bolt Socket Mode connection; new API verbs only (chat.update, reactions.add, reactions.remove).
  • Secrets/tokens handling changed? No — same SLACK_BOT_TOKEN / SLACK_APP_TOKEN.
  • File system access scope changed? No.
  • Risk/mitigation: Block Kit button clicks are gated by the existing SLACK_ALLOWED_USER_IDS whitelist inside every action handler. Unauthorized clicks are silently dropped and logged with masked user IDs. Verified by unauthorized click is silently dropped test in workflow-bridge.test.ts.

Compatibility / Migration

  • Backward compatible? YessendResultFooter is an optional method on IPlatformAdapter; only Slack implements it. Telegram/Discord/GitHub/Web flow unchanged.
  • Config/env changes? No new env vars. Operators must add commands + reactions:write scopes and register the two slash commands in the Slack app config and reinstall to workspace (one-time manual step, documented).
  • Database migration needed? No.
  • Upgrade steps:
    1. Pull dev, deploy
    2. In Slack app settings: add commands and reactions:write to Bot Token Scopes
    3. Toggle on Interactivity & Shortcuts (leave Request URL blank — Socket Mode handles it)
    4. Register /archon and /archon-workflow slash commands (blank Request URL)
    5. Reinstall app to workspace to issue a new token with the scopes

Human Verification (required)

  • Verified scenarios: Full bun run validate (10 packages, all tests pass). The two new unit test files cover: cost footer formatting (cost-only, tokens-only, combined, NaN/Infinity edge cases), approval/resolution/status block builders, reaction name constants, workflow_started → status post + reaction add, approval_pending → buttons in-thread, approve button → approveWorkflow called + message edited, reject under retry threshold → "will retry" note, cancel → abandonWorkflow, unauthorized click silently dropped, workflow_completed → reaction swap + cost in context block, workflow_failed → x reaction + reason.
  • Edge cases checked: long messages now correctly footer'd with _part i/n_; bridge gracefully drops events for non-Slack conversations (verified by does nothing when there is no Slack trigger test); reactions API failures are swallowed-and-logged (won't fail the workflow); chat.update on a missing approval message is non-fatal (logged at warn).
  • What was not verified: Live Slack workspace round-trip. Needs the new scopes installed on a real app, which has to happen after this lands. Failure modes I expect to see in real-world testing: rate-limit pushback on chat.update if a DAG has very fast nodes (debounced 500ms but theoretical) and not_in_channel errors if the bot was added to a channel without channels:join already there.

Side Effects / Blast Radius (required)

  • Affected subsystems/workflows: Slack adapter only. Other platform adapters unaffected. Orchestrator's direct-chat code paths emit an extra sendResultFooter call on adapters that implement it (no-op for Telegram/Discord/GitHub/Web).
  • Potential unintended effects:
    • chat.update requires the bot to own the message — guaranteed since the bridge tracks result.ts from its own chat.postMessage. If the message is deleted manually, edits fail silently (logged).
    • triggeringMessages map is bounded at 1000 entries with FIFO eviction; chat-only conversations that never run a workflow won't grow it.
    • Stream-mode adapters now receive a sendResultFooter call after the final assistant chunk — currently only Slack implements it, but any adapter that adopts it should be prepared for it to run before the orchestrator decides to dispatch a workflow (current code skips it correctly on dispatch via early-return).
  • Guardrails / monitoring: All Slack-side failures (reactions, edits, postMessage) go through getLog().warn / .debug with named events (slack.bridge_reaction_add_failed, slack.bridge_status_update_failed, etc.) — easy to grep in production logs.

Rollback Plan (required)

  • Fast rollback: git revert <merge-commit> on dev. The PR is self-contained — no schema changes, no migrations, no other-adapter touch points. Reverting restores the pre-PR Slack adapter behavior exactly.
  • Feature flags / toggles: None. The bridge is unconditional once the Slack adapter is enabled. To temporarily disable: set SLACK_BOT_TOKEN empty (skips the whole Slack adapter init).
  • Observable failure symptoms:
    • Slack action handlers failing → logs like slack.bridge_approval_action_failed with the run/node ID
    • Bolt app.start() failing post-attach → slack.bot_started log never fires; existing slack_adapter_skipped path triggers on missing tokens
    • Cost footer disappearing → check orchestrator.result_footer_failed warn logs

Risks and Mitigations

  • Risk: A user with SLACK_ALLOWED_USER_IDS access could click Cancel on another teammate's workflow.
    • Mitigation: Acceptable for v1 — same trust model as the existing thread-shared-state assumption. Per-user attribution is the documented follow-up (deferred from this PR's scope).
  • Risk: Slack chat.update rate limit hit on DAGs with rapid node transitions.
    • Mitigation: 500ms trailing-edge debounce in the bridge; failures are warn-logged not thrown.
  • Risk: Block Kit action handler must ack() within 3 seconds, and approveWorkflow / rejectWorkflow make DB writes.
    • Mitigation: All action handlers call await ack() before invoking operations. Operations are pure DB writes (no AI calls, no network) — fast in practice.
  • Risk: Slash command flow posts a "seed" message (e.g. <@U123> ran /archon-workflow list) to create a thread root, which is more visible than a pure ephemeral response.
    • Mitigation: Documented in slack.md; chosen deliberately so the conversation has a real ts to thread under. An ephemeral acknowledgement is also sent to the invoker so they see "see thread for output."

Summary by CodeRabbit

  • New Features

    • Slash commands (/archon, /archon-workflow) to start workflows and seed threads.
    • In-thread interactive controls: Approve / Reject / Cancel with in-place resolution edits.
    • Workflow status messages with realtime node progress, reaction lifecycle, and tracked thread roots for later edits/reactions.
    • Long-message handling now appends per-chunk “part i/n” labels and reserves space for them.
    • Automatic posting of an italic cost/token footer (when available), best-effort and non-blocking.
  • Documentation

    • Updated Slack setup, scopes, and In-Thread UX.
  • Tests

    • Added unit tests for Block Kit builders, adapter behaviors, and workflow-to-Slack interactions.

Review Change Stack

…slash commands

Single Slack adapter PR pulling together the in-thread interactivity primitives
the team will need on a shared instance:

- Interactive Block Kit Approve/Reject buttons on approval gates
- Cancel button on a per-run status message edited in place as DAG nodes progress
- Lifecycle reactions on the triggering message (🔄 → ✅ / ❌)
- Native `/archon` and `/archon-workflow` slash commands (Socket Mode, no URL needed)
- `_part i/n_` annotations on long replies split across multiple messages
- Italic cost/token footer after direct-chat replies and on terminal workflow status

Approve/Reject/Cancel buttons call existing platform-agnostic operations
(approveWorkflow / rejectWorkflow / abandonWorkflow); no schema or workflow
engine changes. Authorization re-uses the existing SLACK_ALLOWED_USER_IDS
whitelist for button clicks and slash commands.

Per-user attribution in thread context is intentionally deferred to a separate
PR — it needs a user_id column on conversations/messages/workflow_runs and
orchestrator plumbing.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 709c4841-3935-4363-8100-af312ecb449a

📥 Commits

Reviewing files that changed from the base of the PR and between 7f88112 and d3798ef.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • CLAUDE.md
  • packages/adapters/package.json
  • packages/adapters/src/chat/slack/adapter.test.ts
  • packages/adapters/src/chat/slack/adapter.ts
  • packages/adapters/src/chat/slack/blocks.test.ts
  • packages/adapters/src/chat/slack/blocks.ts
  • packages/adapters/src/chat/slack/workflow-bridge.test.ts
  • packages/adapters/src/chat/slack/workflow-bridge.ts
  • packages/core/src/types/index.ts
  • packages/docs-web/src/content/docs/guides/approval-nodes.md
  • packages/docs-web/src/content/docs/reference/architecture.md
  • packages/server/src/index.ts
💤 Files with no reviewable changes (2)
  • packages/adapters/src/chat/slack/blocks.test.ts
  • packages/adapters/src/chat/slack/adapter.ts
✅ Files skipped from review due to trivial changes (2)
  • CLAUDE.md
  • packages/docs-web/src/content/docs/guides/approval-nodes.md

📝 Walkthrough

Walkthrough

This PR adds interactive in-thread workflow visualization to Slack by introducing Block Kit builder helpers, extending the Slack adapter to track triggering messages and emit cost footers, implementing a workflow-event bridge that mirrors state changes as reactions and editable status messages, hooking the orchestrator to emit result footers, and updating type contracts and documentation to complete the feature.

Changes

Slack Workflow Integration with In-Thread UX

Layer / File(s) Summary
Block Kit builder and helpers
packages/adapters/src/chat/slack/blocks.ts, packages/adapters/src/chat/slack/blocks.test.ts
New module exports workflow state types (NodeState, RunTerminalState, NodeSnapshot, RunSnapshot), formatting helpers for elapsed time and cost/token/stopReason footers, and builders for approval prompts (buildApprovalBlocks), approval outcomes (buildApprovalResolutionBlocks), and editable status messages (buildStatusBlocks) with per-node state glyphs and optional cancel controls. Comprehensive test coverage validates formatting edge cases, block structure, and reaction constants.
Slack adapter enhancements
packages/adapters/src/chat/slack/adapter.ts, packages/adapters/src/chat/slack/adapter.test.ts
Extended adapter tracks triggering Slack message references (channel/ts) in a capped per-conversation map (SlackMessageRef, triggeringMessages), adds sendResultFooter method to post optional cost/token context blocks (fail-safe), annotates long-message chunks with _part i/n_ footers, implements /archon and /archon-workflow slash command handlers that seed threads and reuse message-handler paths, and exposes getTriggeringMessage, clearTriggeringMessage, getAllowedUserIds. Includes tests for chunking, trigger eviction, and slash-command error/authorization paths.
Slack workflow bridge
packages/adapters/src/chat/slack/workflow-bridge.ts, packages/adapters/src/chat/slack/workflow-bridge.test.ts
New SlackWorkflowBridge listens to WorkflowEventEmitter, manages per-run state (status messages, node snapshots, approval refs), posts initial in-thread status and running reaction, debounces status message edits on node progress, wires Bolt action handlers for approve/reject/cancel with authorization checks, swaps reactions on terminal states, fetches optional final cost from workflowDb, and posts terminal cost/reason overlays. Comprehensive test suite validates no-op behavior when no trigger exists, status/reaction lifecycle, approval UI structure and interactions, and authorization gating.
Orchestrator result footer integration
packages/core/src/orchestrator/orchestrator-agent.ts
Captures latest result metadata (cost, tokens, stopReason) from AI responses in both streaming and batch modes and calls maybeSendResultFooter after assistant turn completes, skipping footer emission when metadata is absent, adapter doesn't implement the hook, or errors occur.
Type contract and exports
packages/core/src/types/index.ts, packages/adapters/src/chat/slack/index.ts, packages/adapters/src/index.ts, packages/server/src/index.ts, packages/adapters/package.json
IPlatformAdapter gains optional sendResultFooter method accepting conversationId and cost/tokens/stopReason info; SlackWorkflowBridge exported from slack module and re-exported at adapters barrel; server initialization creates bridge and calls attach() before slack.start() to register handlers early and detaches on shutdown; adapters package adds @archon/providers dependency and test script updates.
Documentation & guides
packages/docs-web/src/content/docs/adapters/slack.md, packages/docs-web/src/content/docs/guides/approval-nodes.md, packages/docs-web/src/content/docs/reference/architecture.md, CLAUDE.md
Clarified chat:write scope covers message updates; added reactions:write and commands scopes; documented interactivity setup and /archon//archon-workflow slash command registration; added "In-Thread UX" section describing reaction lifecycle, editable status messages, approval gates, cancel controls, cost footers, multipart response annotations, and authorization whitelist enforcement; updated test isolation guidance.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • coleam00/Archon#1581: Modifies orchestration stream/batch handling in the same assistant-turn code paths and may intersect with result-footer emission changes.

Poem

🐰 A workflow blooms in Slack's green thread,
Buttons tap, reactions spin ahead,
Status edits hum and cost notes peep,
Approval gates guard dreams we keep—
The rabbit tends the threaded sweep.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: a comprehensive Slack UX upgrade adding buttons, status messaging, reactions, and slash commands.
Description check ✅ Passed The PR description is thorough and well-structured, addressing all major template sections: summary with problem/rationale/changes/scope, detailed UX journeys (before/after), architecture diagrams with connection inventory, validation evidence, security analysis, compatibility/migration steps, human verification details, side effects/blast radius, and rollback plan with identified risks and mitigations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch slack-ux-umbrella

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/docs-web/src/content/docs/adapters/slack.md (1)

57-67: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix im:read scope description (it’s basic DM info, not DM message history).

Update the im:read bullet in packages/docs-web/src/content/docs/adapters/slack.md (around line 63), since im:read is for “basic information about direct messages that your Slack app has been added to”; DM content/message reading corresponds to im:history.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/docs-web/src/content/docs/adapters/slack.md` around lines 57 - 67,
Update the `im:read` bullet in
packages/docs-web/src/content/docs/adapters/slack.md to say it grants basic
information about direct messages the app has been added to (not DM message
history); keep `im:history` described as the permission for reading DM message
history. Locate the `im:read` and `im:history` bullets in the list and change
only the `im:read` description accordingly so it no longer claims it reads DM
messages.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/adapters/src/chat/slack/adapter.ts`:
- Around line 422-431: The current unauthorized branch in the Slack adapter
calls respond with an ephemeral denial; change it to silently drop the request
per adapter policy: inside the check using isSlackUserAuthorized(actorId,
this.allowedUserIds) (and referencing actorId, this.allowedUserIds, kind,
getLog(), respond), keep the masked logging via getLog().info({ maskedUserId:
`${actorId.slice(0,4)}***`, kind }, 'slack.slash_unauthorized') but remove the
await respond(...) call and any user-facing text, and simply return to stop
processing the slash command; ensure no additional responses are sent for
unauthorized users.

In `@packages/adapters/src/chat/slack/blocks.ts`:
- Around line 223-225: The footer currently only shows failureReason when
snapshot.terminal === 'failed'; update the condition in the block that builds
footerParts (referencing snapshot.terminal, snapshot.failureReason, and
footerParts) to also include cancelled runs—i.e., check for both 'failed' and
'cancelled' terminal states (or that terminal is not 'completed' and
failureReason exists) and push the truncated failureReason into footerParts so
cancellation reasons are preserved in the terminal footer.

In `@packages/adapters/src/chat/slack/workflow-bridge.ts`:
- Around line 498-507: Replace the user-facing Slack message that includes
err.message with a generic, non-sensitive message while keeping the detailed
error in logs: in the code block that calls
this.adapter.getApp().client.chat.postMessage (using state.channel,
state.threadTs, runId), change the text payload to something like "Could not
cancel run `<runId>`. Please try again or contact support." and ensure the
original err is logged server-side (do not remove existing logging). Do not send
err.message to Slack or other end-users.

---

Outside diff comments:
In `@packages/docs-web/src/content/docs/adapters/slack.md`:
- Around line 57-67: Update the `im:read` bullet in
packages/docs-web/src/content/docs/adapters/slack.md to say it grants basic
information about direct messages the app has been added to (not DM message
history); keep `im:history` described as the permission for reading DM message
history. Locate the `im:read` and `im:history` bullets in the list and change
only the `im:read` description accordingly so it no longer claims it reads DM
messages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b453f60c-0b09-41d6-8492-95e244248a17

📥 Commits

Reviewing files that changed from the base of the PR and between c903c79 and c38ef09.

📒 Files selected for processing (11)
  • packages/adapters/src/chat/slack/adapter.ts
  • packages/adapters/src/chat/slack/blocks.test.ts
  • packages/adapters/src/chat/slack/blocks.ts
  • packages/adapters/src/chat/slack/index.ts
  • packages/adapters/src/chat/slack/workflow-bridge.test.ts
  • packages/adapters/src/chat/slack/workflow-bridge.ts
  • packages/adapters/src/index.ts
  • packages/core/src/orchestrator/orchestrator-agent.ts
  • packages/core/src/types/index.ts
  • packages/docs-web/src/content/docs/adapters/slack.md
  • packages/server/src/index.ts

Comment thread packages/adapters/src/chat/slack/adapter.ts
Comment thread packages/adapters/src/chat/slack/blocks.ts Outdated
Comment thread packages/adapters/src/chat/slack/workflow-bridge.ts Outdated
Wirasm added 2 commits May 25, 2026 21:29
CI's stricter package-resolution caught that @archon/adapters imports
@archon/providers/types (TokenUsage) without declaring the workspace
dependency. Locally bun resolved it transitively via @archon/core; CI's
clean install does not.
- Drop ephemeral denial from slash command auth path so unauthorized
  users are silently rejected, matching the existing app_mention /
  message.im pattern. Posting a denial leaks that a bot is listening.
- Surface failureReason on cancelled runs too, not just failed. The
  type already documents this for both terminal states.
- Stop forwarding raw error messages to Slack when a cancel click
  fails. Backend / DB errors stay in server logs; user sees a generic
  "check the server logs or try again" line.

Adds a test for the cancelled-with-reason rendering.
@Wirasm
Copy link
Copy Markdown
Collaborator Author

Wirasm commented May 25, 2026

PR Review Summary — 6-Agent Pass

code-reviewer · docs-impact · pr-test-analyzer · silent-failure-hunter · type-design-analyzer · comment-analyzer

Critical Issues

ID Agent Issue Location
C1 code-reviewer @archon/workflows imported but not declared in packages/adapters/package.json — violates the architecture boundary (CLAUDE.md says @archon/adapters depends on @archon/core only). Resolves today via workspace hoisting; breaks under stricter installs. packages/adapters/src/chat/slack/workflow-bridge.ts:17 + packages/adapters/package.json
C2 code-reviewer workflow-bridge.test.ts calls mock.module('@archon/core', …) and mock.module('@archon/workflows/event-emitter', …) (both irreversible per bun#7823) but is not split into its own bun test invocation — the file header comment claims it is, but the adapter test script runs the entire src/chat/ tree in one process. No active pollution today, but the invariant is broken; next test that imports those modules in the same batch silently gets mocks. packages/adapters/package.json test script
C3 comment-analyzer Debounce edge label is factually wrong in two places — comments say "trailing-edge" but the implementation is leading-edge (first event sets the timer, subsequent events in the burst are dropped; the timer reads current state when it fires). Will mislead anyone reasoning about burst batching. workflow-bridge.ts:60, workflow-bridge.ts:335

Important Issues

ID Agent Issue Location
I1 code-reviewer slackBridge.detach() never called in graceful shutdown — leaks the WorkflowEventEmitter subscription and lets a pending 500ms debounce fire chat.update() against a closed Bolt connection (swallowed but log-noisy). packages/server/src/index.ts shutdown path (~L659-L699)
I2 code-reviewer + silent-failure let comment: string | undefined is declared in handleApprovalDecision, passed through applyResolutionEditbuildApprovalResolutionBlocks, but never assigned. The &gt; ${comment} quote in the resolution block is permanently unreachable. Also rejectWorkflow(runId) is called without the optional reason argument the operation accepts. workflow-bridge.ts:445-474
I3 silent-failure await this.applyResolutionEdit(...) in handleApprovalDecision (and parallel in handleCancelClick) sits outside the try/catch. If buildApprovalResolutionBlocks throws, the action handler rejects unhandled — Bolt has no app.error() registered, so the user sees nothing on a button click. workflow-bridge.ts:467-476
I4 silent-failure handleCancelClick exits silently when state is undefined (run already terminated) — no thread notification, no ephemeral, no log. User clicks Cancel and gets zero feedback. workflow-bridge.ts cancel path
I5 type-design body as ActionBody / action as ActionElement casts bypass Bolt's BlockAction/ButtonAction types and depart from the SDK-types convention in CLAUDE.md ("Import SDK types directly"). Future Bolt SDK field changes won't be caught at compile time. workflow-bridge.ts:418-426
I6 pr-test-analyzer Slash-command authorization is entirely untested (criticality 9/10). If parseAllowedUserIds ever regresses (e.g. empty-string user always authorized), no test catches it. adapter.ts:237-247
I7 pr-test-analyzer triggeringMessages 1000-entry FIFO eviction is untested (criticality 8/10). Also: Map.set() on an existing key does NOT move it to the end — re-tracked conversations stay at their old FIFO position. Logic is correct (verified) but the eviction trigger and ordering deserve a test. adapter.ts:209-216
I8 pr-test-analyzer Slash-command seed-post failure path untested (criticality 8/10) — if chat.postMessage throws, the ephemeral error path and the "do-not-call-messageHandler-with-undefined-ts" guard are unverified. adapter.ts:283-293
I9 pr-test-analyzer Multi-chunk _part i/n_ annotation boundary untested (criticality 6/10) — single-chunk path (total === 1 skips annotation) is easy to regress. adapter.ts:88-94
I10 pr-test-analyzer rejectWorkflow → { cancelled: true, maxAttemptsReached: true } branch untested (criticality 7/10). Three distinct outcome strings, only the "will retry" branch is asserted. workflow-bridge.ts:455-458
D1 docs-impact packages/docs-web/src/content/docs/reference/architecture.md:102-107 IPlatformAdapter listing does not include the new sendResultFooter? method. architecture.md
D2 docs-impact packages/docs-web/src/content/docs/guides/approval-nodes.md:57-60 says "Natural-language messages, the CLI, and the Web UI approve button all auto-resume" — now also true for Slack in-thread Approve/Reject. approval-nodes.md
D3 docs-impact CLAUDE.md:133 test-isolation section says @archon/adapters (3) but the package has 5 batches (pre-existing). CLAUDE.md
Cmt1 comment-analyzer removeReactionSafe silent catch has no rationale comment — the parallel addReactionSafe documents already_reacted/rate-limit are non-fatal; the remove counterpart should explain no_reaction/already_removed similarly (CLAUDE.md mandates documenting intentional fallbacks). workflow-bridge.ts:393-406
Cmt2 comment-analyzer STATUS_UPDATE_DEBOUNCE_MS = 500 has no rationale — Slack chat.update rate limit (~1/s/channel) is the non-obvious driver; future tuners have no anchor. workflow-bridge.ts:66

Suggestions

ID Agent Suggestion Location
S1 type-design Use TokenUsage from @archon/providers/types in IPlatformAdapter.sendResultFooter instead of the inline { input; output; total?; cost? } — already imported via MessageChunk. packages/core/src/types/index.ts:175
S2 comment-analyzer Drop the "for v1" tag in // Loop / tool / artifact events are surface-noisy — skipped for v1. Either keep the rationale alone or convert to a tracked TODO. workflow-bridge.ts:156
S3 comment-analyzer Remove what-comments: // Slash commands: /archon (general) and /archon-workflow (workflow control) (adapter.ts:394); the second sentence of the blocks.ts file-level doc; blocks.test.ts file header. various

Strengths

  • handleEvent top-level try/catch with slack.bridge_event_handler_failed event name + structured context — exactly the pattern CLAUDE.md prescribes.
  • addReactionSafe correctly documents why swallowing is intentional.
  • maybeSendResultFooter in orchestrator-agent.ts:1431-1444 is properly wrapped, logs orchestrator.result_footer_failed at .warn with structured context, and guards on method presence.
  • handleApprovalDecision DB-error path falls through to a user-visible outcomeNote = 'error: …' in the resolution message instead of silently failing.
  • unauthorized click is silently dropped test asserts the mock operation is not called rather than peeking at log output — correct behavioral verification.
  • formatCostFooter comprehensively tested for NaN/Infinity/zero-token/million-scale boundaries.
  • File-header lifecycle ordering comment (attach() BEFORE SlackAdapter.start()) captures a non-obvious temporal invariant.
  • _part i/n_ reserve-headroom comment (adapter.ts:85-87) is a textbook WHY-comment with a worked example.
  • KnownBlock from @slack/bolt is used directly per the SDK-types convention.
  • NodeState / RunTerminalState discriminated unions + Record<NodeState, string> glyph maps give compile-time exhaustiveness.

Verdict: NEEDS FIXES

C1–C3 are blocking. I1–I5 are real correctness/UX gaps that should land before merge; the test gaps (I6–I10) and docs (D1–D3) can land alongside or in a fast follow.

Recommended Order

  1. Fix C1 by adding "@archon/workflows": "workspace:*" to packages/adapters/package.json.
  2. Fix C2 by splitting workflow-bridge.test.ts into its own bun test invocation in the adapter test script.
  3. Fix C3 by either changing the comments to "leading-edge" or adding the rate-limit rationale (Cmt2 in the same edit).
  4. Address I1 (wire slackBridge.detach() into shutdown()), I2 (delete dead comment plumbing or wire it up), I3 (widen the try/catch), I4 (log + ephemeral on cancel-with-no-state).
  5. Add tests for I6–I10.
  6. Doc fixes D1–D3.
  7. Polish: I5 (Bolt types), S1 (TokenUsage), Cmt1, Cmt2, S2, S3.

Critical:
- Declare @archon/workflows as an explicit workspace dep on @archon/adapters
  (same class of fix as the providers one). Resolves today via hoisting but
  breaks under stricter installs.
- Split workflow-bridge.test.ts into its own bun test invocation so its
  irreversible mock.module() calls on @archon/core and
  @archon/workflows/event-emitter cannot leak into the slack/telegram
  batch.
- Fix "trailing-edge" debounce comments — the implementation is
  leading-edge. Document the Slack chat.update rate limit as the 500ms
  rationale.

Important:
- Wire slackBridge.detach() into the server graceful shutdown path so the
  event subscription doesn't leak and a pending chat.update can't fire
  against a closed Bolt socket.
- Drop dead `comment` plumbing through handleApprovalDecision /
  applyResolutionEdit / buildApprovalResolutionBlocks — Block Kit buttons
  have no UI to capture it.
- Widen the action-handler try/catch to also cover applyResolutionEdit so
  block-builder or chat.update failures don't bubble as unhandled
  rejections.
- Cancel-click with missing run state now logs and posts an ephemeral
  acknowledgement (using the button message's channel/ts) so the user
  isn't left wondering whether the click registered.
- Use Bolt's BlockButtonAction / ButtonAction types directly on the
  app.action() registrations instead of the ad-hoc ActionBody /
  ActionElement aliases.

Test coverage:
- Slash command silent-rejection of unauthorized users.
- triggeringMessages 1000-entry FIFO eviction at the cap boundary.
- Slash command seed-post failure → ephemeral error + handler not called.
- Single-chunk message path skips the _part i/n_ footer.
- rejectWorkflow → { cancelled: true, maxAttemptsReached: true } branch.

Docs:
- architecture.md IPlatformAdapter listing includes sendResultFooter.
- approval-nodes.md mentions the Slack in-thread Approve button.
- CLAUDE.md test-isolation batch count for @archon/adapters updated to 6
  (was 3 — pre-existing drift, now also accounts for workflow-bridge).

Polish:
- removeReactionSafe gets the same intentional-fallback comment as
  addReactionSafe (no_reaction is a normal terminal-state interleave).
- IPlatformAdapter.sendResultFooter signature uses TokenUsage directly.
- Drop "for v1" tag on the unhandled-event comment.
- Remove what-comments from blocks.ts / blocks.test.ts / adapter.ts.
@Wirasm Wirasm merged commit 8491f00 into dev May 26, 2026
4 checks passed
@Wirasm Wirasm deleted the slack-ux-umbrella branch May 26, 2026 08:54
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