Ensure async-path backend code that could block the asyncio event loop is protected by a teeth-verified runtime anchor in tests/blocking_io/. Use when changing backend Python under app/, packages/harness/deerflow/, or scripts/, when running a blocking-IO triage round over the whole repo, or when a reviewer/CI asks for blocking-IO coverage. Runs a deterministic scan (changed-lines or full-repo), routes each candidate, drafts/extends an anchor, and proves it fails when the blocking IO regresses.
79
100%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Help a contributor ship backend async changes together with the runtime anchor that lets DeerFlow's blocking-IO CI gate actually see the new code. The dynamic detector only catches blocking IO on paths a test executes — this skill closes that gap, either for your own diff or for a repo-wide triage round.
Read references/good-anchor-rules.md before writing any anchor.
Only read references/sop-skeleton.md when generalizing this SOP to another
detector domain — it is not needed to execute the steps below.
backend/app/,
backend/packages/harness/deerflow/, or backend/scripts/ and may run on
the async event loop (Mode A). If unsure, run Step 0 — it answers
deterministically.Mode A — your own diff (default, pre-PR). From repo root:
uv run --project backend python scripts/scan_changed_blocking_io.py --base origin/mainLists blocking-IO candidates your change introduces: findings on lines the
diff added, plus findings that are new versus the merge base — the latter
catches a new async caller exposing an old sync helper whose blocking line is
not in the diff. The diff is <base>...HEAD, so commit your work first —
uncommitted lines are not selected.
If the list is empty, this change introduces no blocking-IO surface that the
static detector can see in the changed files. One residual blind spot
remains: reachability is same-file only, so a new async caller of a sync
helper defined in another file is invisible to both selections. If your
diff adds an async call into a helper that lives elsewhere, check that helper
manually (codegraph or git grep) before stopping.
Mode B — full-repo triage round. From repo root:
make detect-blocking-ioPrints a summary and writes the complete structured finding list to
.deer-flow/blocking-io-findings.json. Work HIGH priority first; do not start
MEDIUM until every HIGH is dispositioned (fixed, guarded, or recorded
NO-ACTION).
Batching policy (PR sizing). One fix unit per PR while any HIGH remains: a fix unit is one root cause — usually a single HIGH, but two HIGHs resolved by the same one-place fix belong together. Once no HIGH remains, MEDIUM/LOW may be batched (about five per round, grouped by module or by disposition) so each PR stays reviewable. A new Blockbuster rule is never batched with anything — it always ships alone (see Step 5).
Both modes emit the same JSON shape per finding: priority, location
(path/line/function), blocking_call (category/operation/symbol),
event_loop_exposure, reason, code. Priority is a deterministic review
ordering, not proof of a bug — Step 1 makes the actual call.
Read the code around each candidate and route it:
asyncio.to_thread, run_in_executor, async client) →
GUARD: add/extend an anchor that locks the offload so a future edit cannot
move it back onto the loop.ASYNC_REACHABLE_SAME_FILE). If the candidate is a sync helper, check for
async callers in other files (codegraph or git grep) before deciding
NO-ACTION.Offload the blocking call in production code, then re-run the Step 0 scan and
confirm the candidate no longer appears. If the offloaded call sits in a
finally / cleanup path, keep it best-effort and bounded (swallow-and-log,
asyncio.wait_for) so a failing or hung cleanup cannot mask the primary
exception. Match by the stable key
(path, function, symbol) — line numbers shift after edits, so never
compare by line.
This is pattern-level feedback in seconds; it complements but never replaces Step 5 — only the runtime gate proves the event loop is actually protected.
Look in backend/tests/blocking_io/ for a test that drives the production async
entry point reaching this candidate's branch.
templates/anchor.template.py.Follow references/good-anchor-rules.md. Drive the specific branch (e.g. force
the create failure that hits the cleanup shutil.rmtree). Never bypass the
blocking surface with a test-only asyncio.to_thread wrapper.
cd backend && make test-blocking-io (or target the one test). It must
go RED.A real block that stays GREEN means Blockbuster has no rule for that
primitive — that is the RULE route; see references/good-anchor-rules.md
for the admission criteria before adding one.
Commit the anchor(s) with your change; make test-blocking-io green. In the PR,
note: candidates found, each disposition, the re-scan result (Step 2), and
the teeth evidence (red→green). Include the reason for any NO-ACTION. A new
Blockbuster rule, if any, goes in its own commit with the evidence from Step 5.
5f0108f
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.