Skip to content

Add bidirectional Slack status watcher#246

Merged
kjgbot merged 5 commits into
mainfrom
factory-sdk/sb-bidirectional-slack
Jun 12, 2026
Merged

Add bidirectional Slack status watcher#246
kjgbot merged 5 commits into
mainfrom
factory-sdk/sb-bidirectional-slack

Conversation

@kjgbot

@kjgbot kjgbot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add an orchestrator-owned Slack reply watcher for in-flight factory issue threads
  • respond to human replies with live Linear state, active dispatched agents, and PR URL
  • guard against self responses, non-factory/non-thread replies, pre-existing replies, and duplicate inbound delivery

Verification

  • npx vitest run packages/factory-sdk
  • npx tsc --noEmit -p tsconfig.node.json

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2c6f0efc-4024-466c-bd3d-593024bf42ac

📥 Commits

Reviewing files that changed from the base of the PR and between 8d92fb2 and fd52281.

📒 Files selected for processing (9)
  • packages/factory-sdk/src/config/schema.test.ts
  • packages/factory-sdk/src/config/schema.ts
  • packages/factory-sdk/src/mount/relayfile-cloud-mount-client.ts
  • packages/factory-sdk/src/orchestrator/factory.test.ts
  • packages/factory-sdk/src/orchestrator/factory.ts
  • packages/factory-sdk/src/ports/mount.ts
  • packages/factory-sdk/src/testing/fakes.ts
  • packages/factory-sdk/src/writeback/slack.ts
  • packages/factory-sdk/src/writeback/writeback.test.ts

📝 Walkthrough

Walkthrough

Factory now watches Slack dispatch threads: when dispatch completes, it creates a thread, seeds existing replies, subscribes to new messages (with polling fallback), dedupes parsed replies, and responds in-thread when humans request status. Configuration adds slack.botUserId, mount clients track writebackTransport type, and Slack writeback extracts thread IDs from confirmed writes.

Changes

Slack Dispatch Thread Watching

Layer / File(s) Summary
Configuration schema and mount client contract
packages/factory-sdk/src/config/schema.ts, packages/factory-sdk/src/config/schema.test.ts, packages/factory-sdk/src/ports/mount.ts
Configuration schema adds slack.botUserId field with default 'U0B2596R7EZ'. Mount client interface adds optional writebackTransport property constrained to 'relayfile-cloud' | 'test' to identify transport type.
Mount client implementations
packages/factory-sdk/src/mount/relayfile-cloud-mount-client.ts, packages/factory-sdk/src/testing/fakes.ts
RelayfileCloudMountClient and FakeMountClient now expose writebackTransport properties identifying their respective transport types ('relayfile-cloud' and 'test').
Slack writeback thread ID extraction and transport guards
packages/factory-sdk/src/writeback/slack.ts, packages/factory-sdk/src/writeback/writeback.test.ts
Updated Slack writeback to parse thread timestamps from confirmed write content, store thread ID references in internal map, and assert transport type before posting/replying. confirmPath now returns readback content; slackTsFromContent extracts timestamps from wrapped payload; assertCloudWriteback guards non-test transports. Tests verify thread ack replay and rejection of local-mirror transports.
Factory watcher state and lifecycle hooks
packages/factory-sdk/src/orchestrator/factory.ts (setup section)
Factory adds Slack state tracking (per-issue thread IDs, active watchers, startup promises), tuning constants for reply event polling, constructor assignment of Slack client, cleanup on shutdown via stop(), and integration point after successful dispatch via ensureSlackDispatchThread. Issue completion stops associated watcher.
Slack watcher core: thread creation, reply watching, and response
packages/factory-sdk/src/orchestrator/factory.ts (watcher implementation & helpers)
Implements thread lifecycle: creates thread via postThread, seeds existing replies, subscribes to new message events with polling fallback, dedupes parsed replies using stable hash of payload, parses/validates thread-specific replies, filters bot replies, responds in-thread with current issue state and agent/PR info via respondToSlackStatus, cleans up watcher state. Includes helpers for Slack timestamp/path parsing, event identity extraction, reply parsing, bot detection, issue labels, and PR URL formatting.
Test infrastructure, helpers, and watcher behavior coverage
packages/factory-sdk/src/orchestrator/factory.test.ts
slackConfig helper produces test configuration; multiple Slack writeback/mount fakes (recording, confirming, failure-injection variants); OrderedSlackMountClient tracks root vs reply write ordering; Slack fixture path builders and emit helpers for message/reply events; comprehensive watcher tests covering status replies in watched thread, top-level messages, self-ignore, thread/channel guards, positive control, non-response outside thread, watcher connection semantics, and duplicate deduplication.

Sequence Diagram

sequenceDiagram
  participant DispatchFlow as Dispatch Flow
  participant Factory
  participant SlackWriteback
  participant SlackMount as SlackMount (Cloud)
  participant SlackWatcher as Slack Watcher
  participant HumanUser as Human in Thread

  DispatchFlow->>Factory: dispatch() produces result
  Factory->>Factory: ensureSlackDispatchThread(issue)
  Factory->>SlackWriteback: postThread(payload)
  SlackWriteback->>SlackMount: confirmPath(fileWrite)
  SlackMount-->>SlackWriteback: readback with ts/thread_ts
  SlackWriteback->>SlackWriteback: parseSlackTsFromContent()
  SlackWriteback-->>Factory: threadId
  
  Factory->>SlackWatcher: watch(issue, threadId)
  SlackWatcher->>SlackMount: subscribe/poll for replies
  
  HumanUser->>SlackMount: post status request in thread
  SlackMount-->>SlackWatcher: new reply event
  SlackWatcher->>SlackWatcher: parseSlackReply, dedup by hash
  SlackWatcher->>SlackWatcher: isOwnSlackBotReply? no
  SlackWatcher->>Factory: respondToSlackStatus(issue)
  Factory->>SlackWriteback: reply(issueState, prUrl)
  SlackWriteback->>SlackMount: confirmPath(fileWrite)
  SlackMount-->>HumanUser: status response in thread
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • AgentWorkforce/pear#235: Main PR extends FactoryLoop with Slack dispatch-thread watching logic directly related to PR #235's introduction of the core FactoryLoop orchestration in the same module.
  • AgentWorkforce/pear#229: Both PRs modify packages/factory-sdk/src/config/schema.ts and test file, adding/expecting slack.botUserId configuration field.
  • AgentWorkforce/pear#243: Both PRs modify packages/factory-sdk/src/writeback/slack.ts thread writeback flow, with main PR extending postThread/reply to extract thread IDs and guard on transport type.

Poem

🐰 Slack threads now bloom when issues arise,
With watchers that listen and humanize—
Bot posts sweet status when asked with care,
Thread IDs tracked through writeback's air!

✨ 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 factory-sdk/sb-bidirectional-slack

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed due to a network error.


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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a mechanism to watch in-flight factory Slack threads and reply to human status requests with live state, roster, and PR information. The reviewer feedback focuses on improving robustness and error handling: wrapping the event handler in a try-catch block to prevent unhandled rejections from breaking the polling loop, wrapping the Slack dispatch thread setup in a try-catch block to prevent auxiliary Slack failures from crashing the core dispatch operation, and updating the event identity resolver to support numeric IDs.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +643 to +679
const handle = async (event: ChangeEvent): Promise<void> => {
if (stopped || !event.resource.path.startsWith(replyPrefix)) {
return
}

const eventKey = eventIdentity(event)
if (!eventKey) {
if (!missingIdentityLogged) {
missingIdentityLogged = true
this.#logger.warn?.('[factory] Slack reply event missing stable identity; falling back to path/content dedupe')
}
} else if (preExistingEvents.has(eventKey)) {
return
}

if (preExistingPaths.has(event.resource.path)) {
return
}

const reply = await this.#readSlackReply(event.resource.path)
if (!reply || reply.threadTs !== threadId || reply.channelDir !== channelDir) {
return
}

const replyKey = `${eventKey ?? event.resource.path}:${stableHash(JSON.stringify(reply.raw))}`
if (seenReplies.has(replyKey)) {
this.#logger.debug?.('[factory] suppressed duplicate Slack reply payload', { issue: record.issue.key, path: event.resource.path })
return
}
seenReplies.add(replyKey)

if (reply.isBot) {
return
}

await this.#respondToSlackStatus(record, threadId)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

If an error occurs during the processing of a Slack reply (e.g., due to transient network issues when calling the Fleet API or Slack API), the unhandled rejection will propagate and abort the for...of loop in the polling mechanism. Since the cursor is advanced before processing the events, any remaining events in the current page will be silently skipped and never processed. Wrapping the body of the handle function in a try...catch block ensures that an error in one event does not disrupt the processing of other events and prevents unhandled promise rejections in the subscription callback.

    const handle = async (event: ChangeEvent): Promise<void> => {
      try {
        if (stopped || !event.resource.path.startsWith(replyPrefix)) {
          return
        }

        const eventKey = eventIdentity(event)
        if (!eventKey) {
          if (!missingIdentityLogged) {
            missingIdentityLogged = true
            this.#logger.warn?.('[factory] Slack reply event missing stable identity; falling back to path/content dedupe')
          }
        } else if (preExistingEvents.has(eventKey)) {
          return
        }

        if (preExistingPaths.has(event.resource.path)) {
          return
        }

        const reply = await this.#readSlackReply(event.resource.path)
        if (!reply || reply.threadTs !== threadId || reply.channelDir !== channelDir) {
          return
        }

        const replyKey = (eventKey ?? event.resource.path) + ":" + stableHash(JSON.stringify(reply.raw))
        if (seenReplies.has(replyKey)) {
          this.#logger.debug?.('[factory] suppressed duplicate Slack reply payload', { issue: record.issue.key, path: event.resource.path })
          return
        }
        seenReplies.add(replyKey)

        if (reply.isBot) {
          return
        }

        await this.#respondToSlackStatus(record, threadId)
      } catch (error) {
        this.#logger.error?.('[factory] failed to handle Slack reply event', error)
      }
    }

Comment on lines +569 to +587
async #ensureSlackDispatchThread(record: InFlightIssue, result: DispatchResult): Promise<void> {
if (!this.#slack || !this.#config.slack || result.dryRun) {
return
}

const key = issueKey(record.issue)
if (this.#slackThreadIds.has(key) || this.#slackWatcherStarts.has(key)) {
await this.#slackWatcherStarts.get(key)
return
}

const start = this.#postAndWatchSlackDispatchThread(record, result)
this.#slackWatcherStarts.set(key, start)
try {
await start
} finally {
this.#slackWatcherStarts.delete(key)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

If posting to Slack or watching the Slack thread fails (e.g., due to rate limits or Slack API downtime), the error will propagate up and cause the entire dispatch operation to fail. Since the agents have already been spawned and the Linear state has been updated, failing the dispatch at this stage is undesirable. Slack status watching is an auxiliary feature and should be treated as best-effort. Wrapping the await start call in a try...catch block inside #ensureSlackDispatchThread prevents Slack-related failures from crashing the core dispatch loop.

  async #ensureSlackDispatchThread(record: InFlightIssue, result: DispatchResult): Promise<void> {
    if (!this.#slack || !this.#config.slack || result.dryRun) {
      return
    }

    const key = issueKey(record.issue)
    if (this.#slackThreadIds.has(key) || this.#slackWatcherStarts.has(key)) {
      try {
        await this.#slackWatcherStarts.get(key)
      } catch {
        // Ignored, logged by the initiator
      }
      return
    }

    const start = this.#postAndWatchSlackDispatchThread(record, result)
    this.#slackWatcherStarts.set(key, start)
    try {
      await start
    } catch (error) {
      this.#logger.warn?.("[factory] failed to establish Slack dispatch thread for " + record.issue.key, error)
    } finally {
      this.#slackWatcherStarts.delete(key)
    }
  }

Comment on lines +957 to +961
const eventIdentity = (event: ChangeEvent): string | undefined => {
const record = event as unknown as Record<string, unknown>
const id = stringValue(record.id) ?? stringValue(record.event_id) ?? stringValue(record.seq)
return id ? `event:${id}` : undefined
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The stringValue helper strictly checks typeof value === 'string'. If the event identity (such as seq or id) is a number, eventIdentity will return undefined. This causes the system to fall back to path/content deduplication and log unnecessary warnings. Updating eventIdentity to handle both string and number IDs ensures reliable event deduplication.

Suggested change
const eventIdentity = (event: ChangeEvent): string | undefined => {
const record = event as unknown as Record<string, unknown>
const id = stringValue(record.id) ?? stringValue(record.event_id) ?? stringValue(record.seq)
return id ? `event:${id}` : undefined
}
const eventIdentity = (event: ChangeEvent): string | undefined => {
const record = event as unknown as Record<string, unknown>
const rawId = record.id ?? record.event_id ?? record.seq
const id = typeof rawId === 'string' || typeof rawId === 'number' ? String(rawId) : undefined
return id ? "event:" + id : undefined
}

agent-relay-code Bot added a commit that referenced this pull request Jun 12, 2026
@agent-relay-code

Copy link
Copy Markdown
Contributor

Implemented fixes in the PR scope.

Changes:

  • Made Slack dispatch thread setup best-effort so Slack failures do not fail core dispatch after agents/Linear work: factory.ts
  • Wrapped Slack reply event handling so one failed response does not break subscription/poll processing: factory.ts
  • Accepted numeric event IDs for stable dedupe identity: factory.ts
  • Avoided dropping fresh replies by pre-existing event identity alone; pre-existing suppression is now path-scoped for the watched thread: factory.ts
  • Added regression coverage for those cases: factory.test.ts

Addressed comments

  • CodeRabbit bot: review skipped because the PR is draft; no code issue raised, no change needed.
  • Gemini Code Assist bot: Slack reply handler errors could abort polling or create unhandled rejections; fixed in factory.ts, covered in factory.test.ts.
  • Gemini Code Assist bot: Slack dispatch thread setup failure could fail dispatch after core work completed; fixed in factory.ts, covered in factory.test.ts.
  • Gemini Code Assist bot: numeric id/seq values were treated as missing event identity; fixed in factory.ts, covered in factory.test.ts.
  • Gemini Code Assist bot review summary: summarized the same three actionable inline findings above; all were fixed. The deprecation notice did not require code changes.

Advisory Notes

None.

Validation run locally:

  • npm ci
  • npm run verify:mcp-resources-drift
  • npm run lint passed with existing warnings only
  • npm run typecheck:web
  • npm run typecheck:node
  • npm test
  • npx vitest run
  • npm run build
  • npm run build:web
  • npx playwright install --with-deps chromium
  • npx playwright test --config playwright.fidelity.config.ts
  • npx playwright test --config playwright.redraw.config.ts

I did not print READY because the PR is still marked draft in GitHub metadata.

@agent-relay-code

Copy link
Copy Markdown
Contributor

pr-reviewer could not complete review for #246 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@agent-relay-code

Copy link
Copy Markdown
Contributor

Reviewed and fixed the current checkout defined by .workforce/context.json (AgentWorkforce/watchdog-agents#10), which differs from the prompt’s AgentWorkforce/pear#246.

Changes made:

Addressed comments

  • CodeRabbit: review was rate-limited and raised no actionable code issue; no code change needed.
  • Gemini Code Assist: claimed claude-sonnet-4-6 is invalid; invalid against current Anthropic docs, so no change.
  • Gemini Code Assist: allowed_write_paths only supported trailing *; fixed in intent-to-spec.mjs.
  • Gemini Code Assist: JSDoc should change Anthropic default model; invalid for the same current-docs reason, so no change.
  • Gemini Code Assist: unreadable --repo silently dropped grounding; fixed in intent-to-spec.mjs.
  • Gemini Code Assist: Anthropic JSON extraction was brittle; fixed in intent-to-spec.mjs.
  • Gemini Code Assist: OpenAI JSON extraction needed the same robust path; fixed in intent-to-spec.mjs.
  • Gemini Code Assist review summary: covered by the specific bullets above; sunset notice was informational only.
  • agent-relay-code bot: reported prior push failure and advisory fixes not applied; current checkout now includes the script fixes, lockfile refresh, and typecheck fixes at the paths above.

Advisory Notes

  • npm run evals still fails 13/23 in existing customer-success/router harness cases, including missing fixture/path errors under customer-success. This is outside the BuildSpec PR diff and I left it unchanged.
  • No .github/workflows files exist in this checkout; README identifies npm run typecheck and npm run compile as the repo validation commands.

Verification

Passed locally:

  • npm ci
  • npm run typecheck
  • npm run compile
  • node generative-ui/buildspec/intent-to-spec.mjs --validate generative-ui/buildspec/examples/csv-export.buildspec.json
  • Direct glob validation check for src/*.ts and lib/**/*.ts
  • Unreadable --repo failure-path check exited nonzero with the expected error

Not printing READY because the eval suite is still failing and I cannot confirm remote PR mergeability/check status from the local checkout alone.

kjgbot pushed a commit that referenced this pull request Jun 12, 2026
@kjgbot kjgbot force-pushed the factory-sdk/sb-bidirectional-slack branch from f111140 to dc5564f Compare June 12, 2026 10:35
@agent-relay-code

Copy link
Copy Markdown
Contributor

Fixed one validated PR issue: Slack reply event identity now recognizes Relayfile’s camelCase eventId, so real events don’t fall back to path/content dedupe unnecessarily. Added a regression covering that event shape.

Changed:

Addressed comments

  • No bot or reviewer comment artifacts were present locally under .workforce beyond pr.diff, changed-files.txt, and context.json; there were no external review threads to validate or modify.

Advisory Notes

None.

Local validation completed:

  • npm run verify:mcp-resources-drift
  • npm run lint
  • npm run typecheck:web
  • npm run typecheck:node
  • npm test
  • npx vitest run
  • npm run build
  • npm run build:web
  • npx playwright test --config playwright.fidelity.config.ts
  • npx playwright test --config playwright.redraw.config.ts

I did not print READY because GitHub mergeability/check status was not queried in this no-gh workflow.

@agent-relay-code

Copy link
Copy Markdown
Contributor

Reviewed PR #246 against .workforce/pr.diff, changed files, and related factory SDK Slack/writeback paths. I did not find a current-checkout breakage that required code changes, so I left the PR code unchanged.

Addressed comments

  • No bot or reviewer comments were present in the provided .workforce metadata, so there were no external review threads to fix or mark stale.

Validation run:

  • npm ci
  • npx vitest run packages/factory-sdk/src/orchestrator/factory.test.ts
  • npm run typecheck
  • npm run verify:mcp-resources-drift
  • npm run lint passed with existing warnings only
  • npm test
  • npx vitest run
  • npm run build
  • npm run build:web
  • npx playwright install chromium
  • npx playwright test --config playwright.redraw.config.ts
  • npx playwright test --config playwright.fidelity.config.ts

Note: the first Playwright attempt failed because Chromium was not installed locally; after installing Chromium, redraw passed and fidelity passed when rerun in CI’s sequential style. I did not run the macOS dist:mac packaging smoke on this Linux checkout, and I cannot verify GitHub mergeability or remote CI status from here.

@kjgbot kjgbot force-pushed the factory-sdk/sb-bidirectional-slack branch from b1c5bed to fd52281 Compare June 12, 2026 11:54
@kjgbot kjgbot marked this pull request as ready for review June 12, 2026 12:00
@kjgbot kjgbot merged commit 53ce3a6 into main Jun 12, 2026
4 checks passed
@kjgbot kjgbot deleted the factory-sdk/sb-bidirectional-slack branch June 12, 2026 12:00
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