Platform Friction Detector
Scan downstream repos, PRs, commits, and Slack for signals that users are quietly working around your platform libraries instead of reporting problems. These silent workarounds represent untracked friction — users who hit a wall and routed around it without filing a bug.
Configuration
See _shared/report-config.md for the standard config resolution pattern.
Config schema (~/.claude/skills/platform-friction-detector/config.json):
platform_packages: list of package name strings your team owns
platform_imports: list of import pattern strings (auto-derived from packages if not set)
search_scope: Sourcegraph repo pattern string (default: org-specific)
exclude_repos: list of your own platform repo strings to skip
slack_channels: list of Slack channel ID strings where users discuss your platform
lookback_days: number (default: 30)
Required scope: at least platform_packages must be set.
Arguments
- platform_packages: package names your team owns that downstream users depend on
- platform_imports: import patterns to search for in diffs (auto-derived from packages if not set)
- search_scope: Sourcegraph repo pattern to search (default: org-specific, configure in config.json)
- exclude_repos: your own platform repos to skip (signals in these are internal, not friction)
- slack_channels: channel IDs where users discuss your platform
- lookback_days: how far back to scan (default: 30)
Search Robustness
Sourcegraph diff_search on corp/* times out for common patterns. Handle this:
- One package per query. Never batch multiple packages into one diff_search. Search
"from metaflow" separately from "from dagobah".
- On timeout, narrow scope. If the wildcard scope times out, retry with narrower prefixes (e.g.,
repos=["github.com/myorg/team-a-*", "github.com/myorg/team-b-*", ...]) — break the wildcard into smaller prefixes.
- On second timeout, add count limit. Retry with
count:10 to get partial results rather than nothing.
- Never let one failed query skip the rest. A timeout on dagobah doesn't excuse skipping metaflow. Each package is searched independently.
- Record what timed out. The report notes which packages had incomplete coverage and why.
Workflow
Steps 1-5: Fire all signal scans in parallel. These are independent data-gathering operations — don't wait for one to finish before starting the next. Each package is also independent, so scan all packages concurrently.
Scan for removed platform imports. For EACH platform package independently, use Sourcegraph diff_search with removed=true:
diff_search(pattern="from {package}", repos=[search_scope], removed=true, after="{lookback}")
diff_search(pattern="import {package}", repos=[search_scope], removed=true, after="{lookback}")
- On timeout: narrow repos scope per Search Robustness rules above.
- Each hit is a potential workaround. Record: repo, file, author, date, what replaced it.
Scan for dependency file changes. Use Sourcegraph diff_search filtered to dependency files:
diff_search(pattern="{package}", repos=[downstream_repos], removed=true, after="{lookback}") with file:requirements or file:pyproject.toml or file:setup.cfg
- A removed line in requirements.txt/pyproject.toml containing your package name is a strong signal.
Scan for workaround language in commits. Use Sourcegraph commit_search for commits mentioning your packages alongside friction words:
commit_search(repos=[downstream_repos], messageTerms=["workaround {package}", "replace {package}", "remove {package}", "migrate from {package}", "instead of {package}", "drop {package}", "too heavy", "dependency conflict"]) filtered by date
- Also search: "bypass", "hack", "temporary", "lightweight alternative"
Scan for replacement patterns in added code. Use Sourcegraph diff_search with added=true to find what replaced your library:
- If step 1 found removed metaflow S3 imports, search the same repos for added
import boto3 or from boto3 in the same time window
- Common replacements: raw boto3 for S3 ops, raw requests for API calls, custom implementations of platform features
Search Slack for friction signals. If slack_channels configured, follow _shared/slack-search.md for the standard Slack search workflow. Use query terms per package: "{package} broken", "{package} issue", "{package} conflict", "alternative to {package}", "can't use {package}", "{package} too heavy", "{package} dependency".
Fetch PR context for each signal. For every diff/commit signal found, resolve the PR:
- Use
gh api repos/{org}/{repo}/commits/{sha}/pulls to find the associated PR
- Get PR title, body, author — the PR description often explains the "why" behind the workaround
Classify and group signals. For each signal, classify:
- Signal type:
dependency_removal | import_replacement | workaround_commit | slack_friction | dependency_downgrade
- Severity:
high (package fully removed) | medium (partial replacement or workaround) | low (discussion only, no code change)
- Root cause (infer from PR body/commit message): dependency conflict, performance, complexity, missing feature, breaking change
- Group by root cause — multiple users hitting the same issue = one problem, not N separate signals
Generate report. Output markdown:
## Platform Friction Report — {date}
Packages monitored: {list} | Period: last {lookback_days} days | Signals found: {N}
### Summary
- Dependency removals: {count} | Import replacements: {count} | Workaround commits: {count} | Slack friction: {count}
### Grouped by Root Cause
#### 1. {Root Cause} — {N} signals, severity: {high|medium|low}
What: {1-2 sentence description of the friction}
Evidence:
- {PR link}: {title} by {author} — {what they did}
- {PR link}: {title} by {author} — {what they did}
- {Slack thread permalink}: {summary}
Impact: {N} repos affected, {N} users impacted
Suggested action: {what the platform team should do}
#### 2. {Root Cause} — {N} signals
...
### Ungrouped Signals
(Signals that don't cluster into a clear root cause — may be one-off or need investigation)
### All Clear
(Packages with zero friction signals — positive confirmation)
Deliver as HTML. Follow the shared HTML delivery pattern in _shared/html-delivery.md. Report name: friction-report. TLDR includes package count, total signals found, and top root cause.
Terminate. Report is complete when all packages are scanned across all signal types and HTML uploaded (or fallback noted).
Design Principles
- Group by root cause, not by signal type. Three PRs removing metaflow S3 because of dependency conflicts is ONE problem, not three. The reader needs to understand friction themes, not raw signal counts.
- Deterministic classification first. Signal type and severity are computed from code changes (removed import = high, Slack mention = low). The LLM infers root cause from PR descriptions but never invents signals.
- Show what replaced your library. "User removed metaflow" is not actionable. "User replaced metaflow S3 with boto3 because of dependency weight" tells you what to fix.
Golden Rules
- Removed imports are the strongest signal. A user removing
from metaflow import S3 is definitive evidence of a workaround. Always scan for this first.
- Fetch the PR for every code signal. The PR body explains the "why." A diff without context is just a change — the PR makes it a friction signal.
- Group by root cause. Multiple users hitting the same issue = one problem to fix, with higher urgency.
- Include the replacement. What did they use instead? This tells you what the user actually needed that your library didn't provide well enough.
- Scan ALL configured packages. Don't stop after finding signals for the first package.
- "All clear" is valuable. A package with zero friction signals is positive evidence that it's working well. Report it.
Anti-Rationalization Counter-Table
| Excuse |
Reality |
| "I found the removed imports, that's enough." |
The PR body has the root cause. Fetch it. "They removed it" is not actionable without knowing why. |
| "Only one user removed the package, it's probably fine." |
One visible workaround often means 5 users who struggled and stayed silent. Investigate. |
| "The Slack search didn't find anything, so users are happy." |
Slack is noisy and semantic search has limited recall. Absence of Slack signal doesn't mean absence of friction. The code signals (removed imports) are definitive. |
| "I'll just search for the package name in diffs." |
Undirected search returns both additions and removals. Filter removed=true for friction signals. |
| "The PR is too old to matter." |
A workaround from 3 months ago that's still in production means the friction is ongoing. Age doesn't reduce severity. |
| "The search timed out so I'll skip that package." |
Narrow the scope and retry. One package per query, smaller repo prefixes on timeout. A timeout is not a pass — it's incomplete coverage that must be noted. |
Termination Labels
| Label |
Meaning |
report_complete |
All packages scanned, all signal types checked, PRs fetched, grouped by root cause |
report_partial |
Some packages or signal types checked but errors prevented full coverage |
no_signals_found |
All packages scanned, zero friction signals — report "all clear" with scan details |
api_error |
Sourcegraph or GitHub API unreachable |
Self-Review Checklist
1---2name: platform-friction-detector3description: Use when scanning for silent user workarounds, platform friction signals, dependency removals, or users routing around your libraries. Trigger phrases: platform friction, user workarounds, who's dropping our libs, silent churn, friction scan, are users working around us.4---56# Platform Friction Detector78Scan downstream repos, PRs, commits, and Slack for signals that users are quietly working around your platform libraries instead of reporting problems. These silent workarounds represent untracked friction — users who hit a wall and routed around it without filing a bug.910## Configuration1112See [`_shared/report-config.md`](../_shared/report-config.md) for the standard config resolution pattern.1314**Config schema** (`~/.claude/skills/platform-friction-detector/config.json`):15- `platform_packages`: list of package name strings your team owns16- `platform_imports`: list of import pattern strings (auto-derived from packages if not set)17- `search_scope`: Sourcegraph repo pattern string (default: org-specific)18- `exclude_repos`: list of your own platform repo strings to skip19- `slack_channels`: list of Slack channel ID strings where users discuss your platform20- `lookback_days`: number (default: 30)2122**Required scope:** at least `platform_packages` must be set.2324## Arguments2526- **platform_packages**: package names your team owns that downstream users depend on27- **platform_imports**: import patterns to search for in diffs (auto-derived from packages if not set)28- **search_scope**: Sourcegraph repo pattern to search (default: org-specific, configure in config.json)29- **exclude_repos**: your own platform repos to skip (signals in these are internal, not friction)30- **slack_channels**: channel IDs where users discuss your platform31- **lookback_days**: how far back to scan (default: 30)3233## Search Robustness3435Sourcegraph `diff_search` on `corp/*` times out for common patterns. Handle this:36371. **One package per query.** Never batch multiple packages into one diff_search. Search `"from metaflow"` separately from `"from dagobah"`.382. **On timeout, narrow scope.** If the wildcard scope times out, retry with narrower prefixes (e.g., `repos=["github.com/myorg/team-a-*", "github.com/myorg/team-b-*", ...]`) — break the wildcard into smaller prefixes.393. **On second timeout, add count limit.** Retry with `count:10` to get partial results rather than nothing.404. **Never let one failed query skip the rest.** A timeout on dagobah doesn't excuse skipping metaflow. Each package is searched independently.415. **Record what timed out.** The report notes which packages had incomplete coverage and why.4243## Workflow4445**Steps 1-5: Fire all signal scans in parallel.** These are independent data-gathering operations — don't wait for one to finish before starting the next. Each package is also independent, so scan all packages concurrently.46471. **Scan for removed platform imports.** For EACH platform package independently, use Sourcegraph `diff_search` with `removed=true`:48 - `diff_search(pattern="from {package}", repos=[search_scope], removed=true, after="{lookback}")`49 - `diff_search(pattern="import {package}", repos=[search_scope], removed=true, after="{lookback}")`50 - On timeout: narrow repos scope per Search Robustness rules above.51 - Each hit is a potential workaround. Record: repo, file, author, date, what replaced it.52532. **Scan for dependency file changes.** Use Sourcegraph `diff_search` filtered to dependency files:54 - `diff_search(pattern="{package}", repos=[downstream_repos], removed=true, after="{lookback}")` with `file:requirements` or `file:pyproject.toml` or `file:setup.cfg`55 - A removed line in requirements.txt/pyproject.toml containing your package name is a strong signal.56573. **Scan for workaround language in commits.** Use Sourcegraph `commit_search` for commits mentioning your packages alongside friction words:58 - `commit_search(repos=[downstream_repos], messageTerms=["workaround {package}", "replace {package}", "remove {package}", "migrate from {package}", "instead of {package}", "drop {package}", "too heavy", "dependency conflict"])` filtered by date59 - Also search: "bypass", "hack", "temporary", "lightweight alternative"60614. **Scan for replacement patterns in added code.** Use Sourcegraph `diff_search` with `added=true` to find what replaced your library:62 - If step 1 found removed metaflow S3 imports, search the same repos for added `import boto3` or `from boto3` in the same time window63 - Common replacements: raw boto3 for S3 ops, raw requests for API calls, custom implementations of platform features64655. **Search Slack for friction signals.** If slack_channels configured, follow [`_shared/slack-search.md`](../_shared/slack-search.md) for the standard Slack search workflow. Use query terms per package: `"{package} broken"`, `"{package} issue"`, `"{package} conflict"`, `"alternative to {package}"`, `"can't use {package}"`, `"{package} too heavy"`, `"{package} dependency"`.66676. **Fetch PR context for each signal.** For every diff/commit signal found, resolve the PR:68 - Use `gh api repos/{org}/{repo}/commits/{sha}/pulls` to find the associated PR69 - Get PR title, body, author — the PR description often explains the "why" behind the workaround70717. **Classify and group signals.** For each signal, classify:72 - **Signal type**: `dependency_removal` | `import_replacement` | `workaround_commit` | `slack_friction` | `dependency_downgrade`73 - **Severity**: `high` (package fully removed) | `medium` (partial replacement or workaround) | `low` (discussion only, no code change)74 - **Root cause** (infer from PR body/commit message): dependency conflict, performance, complexity, missing feature, breaking change75 - Group by root cause — multiple users hitting the same issue = one problem, not N separate signals76778. **Generate report.** Output markdown:7879```80## Platform Friction Report — {date}81Packages monitored: {list} | Period: last {lookback_days} days | Signals found: {N}8283### Summary84- Dependency removals: {count} | Import replacements: {count} | Workaround commits: {count} | Slack friction: {count}8586### Grouped by Root Cause8788#### 1. {Root Cause} — {N} signals, severity: {high|medium|low}89What: {1-2 sentence description of the friction}90Evidence:91- {PR link}: {title} by {author} — {what they did}92- {PR link}: {title} by {author} — {what they did}93- {Slack thread permalink}: {summary}94Impact: {N} repos affected, {N} users impacted95Suggested action: {what the platform team should do}9697#### 2. {Root Cause} — {N} signals98...99100### Ungrouped Signals101(Signals that don't cluster into a clear root cause — may be one-off or need investigation)102103### All Clear104(Packages with zero friction signals — positive confirmation)105```1061079. **Deliver as HTML.** Follow the shared HTML delivery pattern in [`_shared/html-delivery.md`](../_shared/html-delivery.md). Report name: `friction-report`. TLDR includes package count, total signals found, and top root cause.10810910. **Terminate.** Report is complete when all packages are scanned across all signal types and HTML uploaded (or fallback noted).110111## Design Principles1121131. **Group by root cause, not by signal type.** Three PRs removing metaflow S3 because of dependency conflicts is ONE problem, not three. The reader needs to understand friction themes, not raw signal counts.1142. **Deterministic classification first.** Signal type and severity are computed from code changes (removed import = high, Slack mention = low). The LLM infers root cause from PR descriptions but never invents signals.1153. **Show what replaced your library.** "User removed metaflow" is not actionable. "User replaced metaflow S3 with boto3 because of dependency weight" tells you what to fix.116117## Golden Rules1181191. **Removed imports are the strongest signal.** A user removing `from metaflow import S3` is definitive evidence of a workaround. Always scan for this first.1202. **Fetch the PR for every code signal.** The PR body explains the "why." A diff without context is just a change — the PR makes it a friction signal.1213. **Group by root cause.** Multiple users hitting the same issue = one problem to fix, with higher urgency.1224. **Include the replacement.** What did they use instead? This tells you what the user actually needed that your library didn't provide well enough.1235. **Scan ALL configured packages.** Don't stop after finding signals for the first package.1246. **"All clear" is valuable.** A package with zero friction signals is positive evidence that it's working well. Report it.125126## Anti-Rationalization Counter-Table127128| Excuse | Reality |129|---|---|130| "I found the removed imports, that's enough." | The PR body has the root cause. Fetch it. "They removed it" is not actionable without knowing why. |131| "Only one user removed the package, it's probably fine." | One visible workaround often means 5 users who struggled and stayed silent. Investigate. |132| "The Slack search didn't find anything, so users are happy." | Slack is noisy and semantic search has limited recall. Absence of Slack signal doesn't mean absence of friction. The code signals (removed imports) are definitive. |133| "I'll just search for the package name in diffs." | Undirected search returns both additions and removals. Filter `removed=true` for friction signals. |134| "The PR is too old to matter." | A workaround from 3 months ago that's still in production means the friction is ongoing. Age doesn't reduce severity. |135| "The search timed out so I'll skip that package." | Narrow the scope and retry. One package per query, smaller repo prefixes on timeout. A timeout is not a pass — it's incomplete coverage that must be noted. |136137## Termination Labels138139| Label | Meaning |140|---|---|141| `report_complete` | All packages scanned, all signal types checked, PRs fetched, grouped by root cause |142| `report_partial` | Some packages or signal types checked but errors prevented full coverage |143| `no_signals_found` | All packages scanned, zero friction signals — report "all clear" with scan details |144| `api_error` | Sourcegraph or GitHub API unreachable |145146## Self-Review Checklist147148- [ ] Every configured package was scanned for removed imports149- [ ] Dependency files (requirements.txt, pyproject.toml) were searched for removals150- [ ] PR context fetched for every code-based signal (not just the diff)151- [ ] Signals grouped by inferred root cause, not listed individually152- [ ] Each signal group includes what replaced the platform library153- [ ] Severity classification based on signal type (code change > Slack mention)154- [ ] Report date and lookback period noted in header155- [ ] "All clear" packages listed explicitly156- [ ] HTML version uploaded to S3 with commuter link (unless `--no-html` or upload failed with noted fallback)157- [ ] Slack/chat delivery uses TLDR + link, not the full report