/find-async-lifecycle-drift
Run this before or after product-workflow work that starts background
jobs, polls progress, exports files, downloads pages/images, or resumes
work. The output is advisory and should be triaged by confidence.
Default scope is not hard-coded to /sites. With no positional paths,
scripts/run.py passes None into detect(), and
product_health.expand_paths() scans workflow_targets(project_root)
from .engineering/docs/product-workflows.md (## Targets). If that
descriptor or section is absent, the default target set is empty. To
scan a concrete surface, pass explicit positional paths after
scripts/run.py.
How success is judged
- The runner exits 0 and prints the scan directory it wrote.
reports/find-async-lifecycle-drift/scan-<UTC>/detections.jsonl,
report.md, findings.json, and latest exist under the selected
--project-root.
- The target scope is explicit in the command or comes from
workflow_targets(project_root); do not describe a no-descriptor run
as a /sites scan.
- Findings are grouped by async lifecycle detector bands and are treated
as advisory triage input, not proof that the workflow is broken.
Pipeline
.venv/bin/python .claude/skills/find-async-lifecycle-drift/scripts/run.py <paths...>
<paths...> are positional path or glob arguments. Omit them only when
the host repo has declared ## Targets in
.engineering/docs/product-workflows.md.
The runner writes detections.jsonl, report.md, findings.json, and
latest under reports/find-async-lifecycle-drift/scan-<UTC>/, and logs
an effectiveness row.
Detector Bands
unguarded_polling_timer: polling timer exists without a cleanup path.
missing_terminal_poll_stop: progress/status polling observes terminal
states without stopping the timer.
missing_stale_response_guard: async fetch mutates UI without a
request-generation, abort, or latest-response guard.
missing_recovery_control: job progress UI lacks nearby retry, cancel,
abort, or resume controls.
duplicate_job_path: backend start/run/queue path dispatches work
without an active/existing/running guard.
When things go sideways
| Symptom |
Action |
| Descriptor absent |
Treat the default workflow target set as empty; pass explicit positional paths or add ## Targets to .engineering/docs/product-workflows.md. |
| Zero findings |
Check whether detections.jsonl is empty because the workflow target set expanded to no .py or .js files; rerun with explicit paths before calling the lifecycle surface clean. |
| Script failure |
Re-run the exact .venv/bin/python .claude/skills/find-async-lifecycle-drift/scripts/run.py ... command, capture stderr, and fix the path/import/argparse failure before interpreting results. |
1---2name: find-async-lifecycle-drift3description: Advisory SUSPECT scan for polling, job, cancel/resume, and export lifecycle drift on configured product-workflow targets or explicit paths: unguarded polling, missing terminal handling, stale response hazards, duplicate job starts, and missing retry/cancel controls.4---56# /find-async-lifecycle-drift78Run this before or after product-workflow work that starts background9jobs, polls progress, exports files, downloads pages/images, or resumes10work. The output is advisory and should be triaged by confidence.1112Default scope is not hard-coded to `/sites`. With no positional paths,13`scripts/run.py` passes `None` into `detect()`, and14`product_health.expand_paths()` scans `workflow_targets(project_root)`15from `.engineering/docs/product-workflows.md` (`## Targets`). If that16descriptor or section is absent, the default target set is empty. To17scan a concrete surface, pass explicit positional paths after18`scripts/run.py`.1920## How success is judged2122- The runner exits 0 and prints the scan directory it wrote.23- `reports/find-async-lifecycle-drift/scan-<UTC>/detections.jsonl`,24 `report.md`, `findings.json`, and `latest` exist under the selected25 `--project-root`.26- The target scope is explicit in the command or comes from27 `workflow_targets(project_root)`; do not describe a no-descriptor run28 as a `/sites` scan.29- Findings are grouped by async lifecycle detector bands and are treated30 as advisory triage input, not proof that the workflow is broken.3132## Pipeline3334```35.venv/bin/python .claude/skills/find-async-lifecycle-drift/scripts/run.py <paths...>36```3738`<paths...>` are positional path or glob arguments. Omit them only when39the host repo has declared `## Targets` in40`.engineering/docs/product-workflows.md`.4142The runner writes `detections.jsonl`, `report.md`, `findings.json`, and43`latest` under `reports/find-async-lifecycle-drift/scan-<UTC>/`, and logs44an effectiveness row.4546## Detector Bands4748- `unguarded_polling_timer`: polling timer exists without a cleanup path.49- `missing_terminal_poll_stop`: progress/status polling observes terminal50 states without stopping the timer.51- `missing_stale_response_guard`: async fetch mutates UI without a52 request-generation, abort, or latest-response guard.53- `missing_recovery_control`: job progress UI lacks nearby retry, cancel,54 abort, or resume controls.55- `duplicate_job_path`: backend start/run/queue path dispatches work56 without an active/existing/running guard.5758## When things go sideways5960| Symptom | Action |61|---|---|62| Descriptor absent | Treat the default workflow target set as empty; pass explicit positional paths or add `## Targets` to `.engineering/docs/product-workflows.md`. |63| Zero findings | Check whether `detections.jsonl` is empty because the workflow target set expanded to no `.py` or `.js` files; rerun with explicit paths before calling the lifecycle surface clean. |64| Script failure | Re-run the exact `.venv/bin/python .claude/skills/find-async-lifecycle-drift/scripts/run.py ...` command, capture stderr, and fix the path/import/argparse failure before interpreting results. |