| name | sweep-channels |
| description | Scan all bot-accessible Slack channels for untracked backlog items, cross-post approved candidates to |
| disable-model-invocation | true |
Sweep Channels
Scan all Slack channels the FeedForward bot has access to for messages that
could be backlog items but aren't yet tracked. Review findings with the user,
then cross-post approved items to #ideas and add external links to existing
cards where Slack threads add evidence.
This is the upstream feeder for /sync-ideas. Sync-ideas processes #ideas only;
this play finds signal buried in other channels and surfaces it to #ideas.
Also Read
Before starting, read these references:
reference/tooling-logistics.md: tested Slack and Shortcut API recipes.
Don't re-derive pagination patterns, reaction endpoints, or external_links
payload shapes from scratch.
box/shortcut-ops.md: Shortcut constants, search operators, and update
recipes — needed for dedup (Step 5) and external link updates (Step 8).
Arguments
$ARGUMENTS is optional:
--days N: Override the watermark-based scan window. Applies to all
channels including new ones.
- No arguments: Uses watermark for known channels,
--days 90 for new
channels.
Constraints
- Mutation gate: A PreToolUse hook blocks all Slack and Shortcut mutations
through Bash. Route through
agenterminal.execute_approved.
- Human-in-the-loop: Present the full candidate list for review. User
selects which items get cross-posted. Don't post without explicit approval.
- Subagent verification: Subagent classifications and Shortcut dedup results
are filters, not findings. Spot-check YES matches (items being dropped) and
NO matches (items driving cross-posts) by reading the actual Shortcut cards
before presenting the review surface.
- One story per cross-post: Each cross-post should represent a single story
that would have its own fix PR, not a theme bucket. Decompose multi-issue
threads into separate cross-posts. Exception: when a cluster of related issues
needs holistic evaluation before decomposition, post one item and note it.
- Rate limiting: 0.5s delay between sequential API calls. Respect
Retry-After.
- Before Shortcut or Slack API calls: check
reference/tooling-logistics.md
for tested recipes. Don't re-derive payload shapes or endpoint paths.
- Shortcut wrapper scripts: Use
shortcut-mutate.py for all Shortcut
mutations (add-link, set-field, move, etc.) instead of raw API calls.
external_links and other array fields use replace-all semantics on PUT —
raw calls silently delete existing entries. The wrapper scripts handle
read-merge-write with built-in verification. Proved 2026-05-01.
Constants
| Constant | Value |
|---|
| #ideas channel ID | C0ADJ4ATJE4 |
| Watermark key | slack-backlog-sweep |
Known channels (as of 2026-04-16)
| Channel | ID | Signal expectation |
|---|
| #product-feedback | C04DURMU9B2 | High — direct user feedback |
| #cs-squad-questions | CK4TLM9TR | High — customer-facing issues |
| #cs-squad | CDC3U0SUW | Medium — CS team ops, weekend Intercom summaries |
| #engineering | CJ4S4GE76 | Medium — tech debt, bugs |
| #feature-launch | C0E586F33 | Low — mostly ship announcements |
| #post-release-measurement | C0AGD4ZEC6M | Medium — measurement gaps |
| #proj-duck-keywords | C08CGT97BEZ | Medium — project-specific |
| #proj-personalized-onboarding | C0AJV066CSX | Medium — project-specific |
| #proj-smartpin | C072YAKP4RX | Medium — project-specific |
| #proj-tailwind-turbo | C09762G179D | Medium — project-specific |
| #bug-reporting | C04CYGR7NKY | High — bugs with jam.dev recordings |
| #daily-digest | C0ASG05F1SB | Derivative — see note below |
| #general | C026G3XB4 | Low — high noise |
| #share-the-love | C035A1VQY | Low — celebrations |
| #ideas | C0ADJ4ATJE4 | Special — scan for untracked items already here |
Update this table when new channels are discovered in Step 1.
#daily-digest is derivative, not primary. This channel contains
FeedForward Bot's automated Intercom daily summaries. Every signal it
surfaces (bug reports, feature requests, pain points) originates from
Intercom conversations that are tracked through the Intercom sync pipeline
and surface independently in CS channels (#cs-squad, #cs-squad-questions).
Classify all #daily-digest messages as not_actionable. Do not cross-post
from this channel — it would create duplicates of items that arrive through
primary channels. Scan it only to confirm no human-posted messages are
being missed.
Steps
1. Discover channels
Get the full list of channels the bot is a member of. Always paginate —
conversations.list paginates even with limit=999.
import json, urllib.request
token = ...
members = []
cursor = ''
while True:
url = f'https://slack.com/api/conversations.list?types=public_channel&limit=999&exclude_archived=true'
if cursor:
url += f'&cursor={cursor}'
req = urllib.request.Request(url, headers={'Authorization': f'Bearer {token}'})
d = json.loads(urllib.request.urlopen(req).read())
for c in d.get('channels', []):
if c.get('is_member'):
members.append((c['id'], c['name']))
cursor = d.get('response_metadata', {}).get('next_cursor', '')
if not cursor:
break
Compare against the Known channels table above. New channels (not in the table)
have no watermark — they get a full-history scan.
First run (no prior state): If no watermark exists and the Known channels
table has never been updated, treat all discovered channels as new — apply
--days 90 to all.
Report the channel list before proceeding: how many channels, any new ones
discovered, any previously-known channels where the bot is no longer a member.
2. Determine scan window
python3 box/watermark.py get slack-backlog-sweep --days
If it prints a number, use that as --days for channels in the Known channels
table. If none, fall back to --days 90. New channels (not in the Known
channels table) always get --days 90 regardless of watermark.
If $ARGUMENTS includes --days N, use that for all channels.
3. Scan channels (delegated)
Delegate to subagents via agenterminal.delegate in batches of 3 channels.
Don't set timeout_ms — the default is the maximum (30 min).
Each subagent runs two commands per channel:
python3 box/slack-scanner.py --channel {ID} --days N --untracked-only --summary \
> box/research/slack-backlog-sweep-raw/{channel-name}.txt 2>&1
python3 box/slack-scanner.py --channel {ID} --days N --untracked-only
Each subagent:
- Saves summary output to
box/research/slack-backlog-sweep-raw/{channel-name}.txt
- Classifies from the full JSON output (which includes thread context)
- Returns structured classification per message
Subagent instructions: Save raw files first, then classify from the full
JSON. This decouples API work from classification — if the subagent times out
during classification, the raw files survive for a re-dispatch that reads from
disk instead of calling the API again.
Use output_instructions to constrain return shape:
Return a JSON object:
{
"channels": [
{
"name": "#channel-name",
"total_untracked": number,
"candidates": [
{
"permalink": "full slack permalink URL",
"author": "display name",
"date": "YYYY-MM-DD",
"summary": "one sentence",
"backlog_signal": "feature_idea | bug_report | pain_point |
workflow_friction | measurement_gap | not_actionable",
"reasoning": "why this classification"
}
]
}
]
}
Include ALL untracked messages. Classify each one — don't filter.
Timeout escalation: If a batch times out at 10 minutes, check whether raw
files were saved. If yes, re-dispatch pointing at the saved files (no API calls,
classification only). If no raw files, split the batch into individual channels
and re-dispatch each separately. Do not absorb the work into primary context.
Dispatch and collection: Dispatch all batches in parallel. Collect results
as they complete (parallel agenterminal.collect calls). After collecting each
batch, report to the user: which channels finished, how many untracked messages
found, whether any channel returned suspiciously high or zero volume.
4. Read strong candidates directly
For messages classified as potential backlog items, read the full thread context
in main context using the scanner's --threads mode:
python3 box/slack-scanner.py --channel {ID} --threads permalink1,permalink2
Don't present subagent summaries as findings — verify the classification against
the actual thread content.
When a thread includes customer identifiers or references an Intercom
conversation, follow them. CS threads often contain Jarvis links, org IDs,
emails, or phrases like "I have someone writing in." These point to the
actual Intercom conversation — the Slack thread is the relay, not the source.
Search the Intercom index by those identifiers and read the conversation as
part of this step. The primary source may contain discriminating details
(slot transfers, account-specific failures, prior history) that the relay
omits or de-emphasizes. Proved 2026-04-27: relay said "can't add pins to
Turbo"; Intercom conversation revealed slot-transfer trigger that changed
classification from SC-1541 external link to new cross-post.
After reading all threads, produce a reclassification table before proceeding
to Step 5. For each channel where the subagent reported >0 signal items, show:
| Channel | Subagent classification | Your classification | Changed? | Rationale |
Count matching (raw file message counts match subagent totals) is NOT
classification matching — they are separate verification claims. When presenting
spot-check results, state which claim the check verified. Proved twice
(2026-04-20, 2026-04-21): ✅ on count match presented as full confirmation,
masking silent reclassification of 5 signal items to 0.
5. Dedup against Shortcut (main context)
This step runs in main context, not delegated. The dedup involves judgment
calls (is this card the same scope?) that require reading actual cards. The
first run had a proxy trust failure when dedup was delegated and the results
were presented without verification.
Search existing Shortcut cards across ALL states including Released (items
within the scan window could have been carded, fixed, and shipped).
Search order:
- Load all active cards:
python3 box/shortcut-cards.py --active --summary
- Keyword search against Released cards for the scan window period
- For each candidate classified as cross-post (NO match), run at least 2
additional searches with different terms (feature area + symptom keyword)
python3 box/shortcut-cards.py --state "STATE" --summary
python3 box/shortcut-cards.py --product-area AREA --active --summary
Prior sweep runs add :shortcut: reactions to external-linked threads (Step 8),
so --untracked-only filters them out before they reach this step. For
cross-posted items, the card /sync-ideas creates from the #ideas message is
the dedup target — search by content similarity, not by Slack permalink.
Verification gate: Before presenting dedup results, directly read (via
shortcut-cards.py --id) every card cited as a YES match and verify every NO
match with the additional searches above. If all searches return no match,
classify as cross-post.
Scope verification for proposed matches: When proposing a card as a dedup
match, state the candidate's product area and the proposed card's product area.
If they don't align, it's not a dedup — it's a different feature that happens
to share a keyword. Proved 2026-04-23: linked a Turbo extension language filter
request to SC-245 (Keywords: Enable language filters in Keyword Search) based
on 'language filter' keyword match. User caught the scope mismatch.
Consult .claude/rules/product-knowledge.md for known product area boundaries
and entity scope distinctions.
Codebase verification for feature requests: For candidates classified as
feature_idea, pain_point, or workflow_friction (not bugs), verify whether the
requested capability already exists in the codebase before classifying as
cross-post. Run in main context (same rationale as dedup — it gates a
mutation). The check is targeted: grep the relevant feature area for UI
components, API endpoints, or feature flags matching the request. Shortcut
tracks planned and in-progress work — shipped features that predate the card
system, or whose card titles don't match user language, are invisible to card
search. Proved 2026-04-15: Turbo report/block/hide fully shipped with 7
report reasons including "Inauthentic/misleading AI content," but no card
title matched "AI image report."
Classify each candidate:
- Drop: exact match to existing card (verified by reading the card)
- Cross-post: no matching card exists (verified by multiple searches)
AND, for non-bug items, no matching capability in the codebase
- External link: Slack thread adds evidence to an existing non-released card
Check for PRs before cross-posting bug reports. When a thread has
someone saying "I'll fix it" or "I'll get a fix up," search GitHub PRs
(gh pr list --repo tailwind/aero --search) before classifying as cross-post. The fix may
already be merged, making a card redundant. Proved 2026-05-01: Bill's
PR #3542 (filter malformed pin images) was merged same day as the report,
caught by PR search before cross-posting.
External link verification gate: For every candidate classified as
external link, read the target card (shortcut-cards.py --id) and verify:
- The thread's signal matches the card's scope (not just keyword overlap)
- The thread adds evidence the card doesn't already have
- The mechanism connection is verified, not assumed from symptom similarity
Proved 2026-04-29: assumed a SmartPin scrape failure matched SC-1737
(Lambda timeout) based on "scrape failure + intermittent" without verifying
the failure mechanism. User caught the ungrounded assumption. Separately,
SC-884 was grouped with SC-882 in presentation, approved for SC-882 only,
and SC-884 was silently dropped from scope.
Each external link is a separate approval item. When a thread maps to
multiple cards (e.g. SC-882 and SC-884), present each card link separately.
Grouping cards in presentation creates silent drops when only one is approved.
#ideas items: Items found in #ideas that are untracked should NOT be
classified as cross-post candidates (they're already in #ideas). Flag them as
"needs /sync-ideas tracking" in the review surface. /sync-ideas handles
Shortcut card creation for #ideas messages.
6. Compile and present review surface
Build box/research/slack-backlog-sweep-candidates.md with:
- Strong candidates (cross-post to #ideas)
- External link additions (Slack threads → existing cards)
- Items flagged for /sync-ideas (untracked #ideas messages)
- Excluded items with one-line reason per drop (user can challenge)
Present via approve_content with content_type: "sweep-candidates",
filename: "sweep-YYYYMMDD". The saved file is compaction insurance — if
context compacts before Step 7, read the saved file to recover the approved
list.
Present items one at a time (not as a batch). This gives the user room
to explain reasoning per item and steer decomposition decisions.
The user decides:
- Which candidates get cross-posted
- Whether multi-issue threads should be decomposed or posted as evaluation items
- Which external links to add
7. Cross-post approved items
Execute only the items the user confirmed in Step 6. Step 6 is the per-item
approval; this step executes the approved subset.
Format: <permalink|Distilled idea title>
Slack auto-unfurls the linked message. /sync-ideas picks these up on future
passes and creates cards + adds tracking markers on the #ideas message.
Do NOT add :shortcut: reactions or tracking replies to source threads in
the originating channels. No card exists yet for these items. /sync-ideas
handles tracking when it processes the #ideas cross-post and creates a card.
Use slack-mutate.py post for each cross-post via execute_approved:
python3 box/slack-mutate.py post C0ADJ4ATJE4 '<permalink|title>'
The script posts and verifies the message is present in the channel.
Check the script's verification output (exit code 0 = verified) before
proceeding to the next post. One execute_approved call per post.
8. Add external links to existing cards
For each approved external link addition, use the safe wrapper:
python3 box/shortcut-mutate.py add-link STORY_ID PERMALINK
This handles read-merge-write (reads existing links, appends, PUTs full
array) and verifies the link is present afterward. Check the script's
verification output (exit code 0 = verified, 1 = verification failed)
before proceeding. Do NOT use raw GET/PUT — external_links is a
replace-all array field.
After updating the card, add tracking markers to the source thread:
python3 box/slack-mutate.py react CHANNEL_ID THREAD_TS shortcut
python3 box/slack-mutate.py reply CHANNEL_ID THREAD_TS 'Linked to <shortcut_url|SC-NNN: Story title>'
Check each script's verification output (exit code 0 = verified) before
proceeding to the next mutation. One execute_approved call per mutation.
The reply text says "Linked to" (not "Tracked") to distinguish from
/sync-ideas card creation.
9. Close
- Update the Known channels table in this skill file if new channels were
discovered in Step 1.
- Update the completion record in
box/research/slack-backlog-sweep-brief.md:
- Date
- Scan window (per-channel if different windows were used)
- Full channel list with IDs
- Counts: cross-posts made, external links added
- Note for new-channel handling
- Set watermark via
agenterminal.execute_approved (the production mutation
gate blocks this command): python3 box/watermark.py set slack-backlog-sweep
- Commit artifacts
- Update investigation log if there were operational learnings
Do not delete or overwrite box/research/slack-backlog-sweep-brief.md —
append new completion records. The file contains the channel list audit trail
needed for new-channel detection on future runs.
Skill file updates: The Write tool is blocked on .claude/skills/. Use
the Bash heredoc workaround to update the Known channels table in this file.
See feedback_write_tool_skills_dir.md.
Idempotency
Before each mutation:
- Cross-posts to #ideas: Check #ideas for an existing message containing
the source permalink. If it exists, skip.
- External links on cards: Check the card's current
external_links array
for the permalink. If present, skip.
- :shortcut: reactions:
already_reacted from Slack = success, not error.
- Thread replies: Check thread for existing bot reply containing the card
URL before posting.
If the watermark doesn't advance (e.g. Step 9 is interrupted), the next run
rescans the same window. The dedup step (Step 5) and these idempotency checks
prevent duplicate cross-posts and links.
Correct End State
After a complete run:
- Watermark set to current timestamp
box/research/slack-backlog-sweep-brief.md has a new completion record
box/research/slack-backlog-sweep-raw/ has per-channel scan files
box/research/slack-backlog-sweep-candidates.md overwritten with this run's
reviewed surface (one file per run, not appended)
- Cross-posted messages appear in #ideas (picked up by next /sync-ideas run)
- External-linked cards have updated
external_links arrays
- Source threads for external-linked items have :shortcut: + "Linked to" reply
- Source threads for cross-posted items have NO tracking markers
- All artifacts committed
Known Gotchas
conversations.list paginates even with limit=999. Always follow
next_cursor.
--untracked-only filters on :shortcut: reaction, bot reply, and human
Shortcut link. Cards may exist without the Slack thread being linked.
Shortcut dedup (Step 5) catches these.
> in link text breaks Slack mrkdwn. Check cross-post titles before posting.
- Shell quoting: titles with double quotes break JSON payloads. Use Python
json.dumps() for all Slack API calls.
- #feature-launch is mostly ship announcements (FeedForward Bot posts).
Expect high volume, near-zero signal.
- #share-the-love is celebrations. Expect zero signal.
- Scanner doesn't surface file attachments from thread replies. If a message
references a screenshot in a reply, it won't be visible.
- Subagent timeout: 5 minutes is too short for channels with many threads
when the subagent must call the API. Use 10 minutes, and follow the
escalation path in Step 3.
- Cross-posted source threads will re-appear as untracked in future scans
(no Slack-side marker). The dedup step catches them by matching against
the card that /sync-ideas creates from the #ideas cross-post.