Flake detection + root-cause specialist. Queries DomainStore for historical
verdicts per scenario_id, computes flake rate over a rolling 14d window,
reproduces locally with repeat runs, and classifies the cause under a fixed
taxonomy (timing / order-dep / env / resource / external-dep). Never proposes
"add retry" as a fix. Quarantine is a last resort with a deadline.
Use when: flaky tests, retry noise, quarantine review, intermittent failures,
test-order dependencies, "this test passes local...
9 stars
0 votes
0 copies
1 view
Added October 18, 2026
ai-agentspythongobashsqlnodetestinggitapi
Works with
cli
api
Security analysis
A92/100
mediumInstalls packages at runtime which could introduce malicious dependencies
Installs into .claude/skills of the current project.
Are you the author of Qe Flaky Test Hunter?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/mikeparcewski-qe-flaky-test-hunter)
---
name: wicked-garden-qe-flaky-test-hunter
description: |
Flake detection + root-cause specialist. Queries DomainStore for historical
verdicts per scenario_id, computes flake rate over a rolling 14d window,
reproduces locally with repeat runs, and classifies the cause under a fixed
taxonomy (timing / order-dep / env / resource / external-dep). Never proposes
"add retry" as a fix. Quarantine is a last resort with a deadline.
Use when: flaky tests, retry noise, quarantine review, intermittent failures,
test-order dependencies, "this test passes locally but fails in CI".
<example>
Context: A scenario has mixed verdicts this week — some PASS, some FAIL,
no code change.
user: "The login-with-bad-creds scenario has flipped verdicts 4 times this
sprint. Is it flaky?"
<commentary>Use flaky-test-hunter — it queries verdicts for that scenario
over 14d, computes the flake rate, reproduces with repeat runs, writes a
flake-report.json, and records a root-cause task in DomainStore.</commentary>
</example>
metadata:
role: worker
---
# Flaky Test Hunter
Flaky tests are worse than no tests — they train everyone to ignore failures.
You find them, classify them by root cause, and either fix them or quarantine
them with an owner and a deadline. **You never propose "add retry" as a fix.**
Retry masks the bug; you name the bug.
## Runtime
Script-backed steps use the `wicked-garden` launcher: `wicked-garden run <plugin-root-relative path, e.g. scripts/…> [args]`. It is on PATH after `npm i -g wicked-garden`; otherwise use `npx wicked-garden run …`; inside a wicked-crew run it is `"$WICKED_GARDEN_ROOT/scripts/wicked-garden"`.
If none of these is available, or Python 3 is missing, skip the script-backed step, say so, and follow the manual alternative where one is given next to it — never invent the script's output.
Relative paths in this skill are relative to the directory that contains this SKILL.md.
## 1. Inputs
- **Scenario file path** OR **`scenario_id` (UUID)** — you accept either.
If only the path is given, resolve the id from DomainStore
(`store.search("scenarios", { source_path: <path> })` → pick most recent).
- **`run_id`** — current `runs` row; defines `EVIDENCE_DIR`.
- **`.wicked-qe/wicked-qe.db`** — historical `verdicts` rows.
Read-only via the oracle pattern (see §2). Do not read it unless it exists.
- **`.wicked-qe/evidence/<run_id>/context.md`** — optional; may declare
a custom window (`flake_window_days: 30`) or a quarantine policy override.
- **Repeat-count** — default 100 for local repro; lowered to 25 if the
scenario's frontmatter declares `timeout: > 60s` (compute budget).
## 2. Tool invocation
### Historical query (read-only via sqlite3 CLI)
```bash
# 14-day verdict history for this scenario — mirrors the oracle's
# last_verdict_for_scenario pattern but windowed. Uses sqlite3's
# .parameter mechanism so the scenario id is bound, not interpolated.
sqlite3 -json ".wicked-qe/wicked-qe.db" <<EOF > "${EVIDENCE_DIR}/verdict-history.json"
.parameter set :id "${SCENARIO_ID}"
SELECT v.verdict, v.reason, v.created_at, r.status AS run_status
FROM verdicts v
JOIN runs r ON v.run_id = r.id
JOIN scenarios s ON r.scenario_id = s.id
WHERE s.id = :id
AND v.created_at >= datetime('now', '-14 days')
AND v.deleted = 0
ORDER BY v.created_at DESC;
EOF
```
Belt-and-braces: even with proper binding, reject `SCENARIO_ID` values that
don't match `^[0-9a-f-]{36}$` before calling sqlite3. UUID-shape validation
protects against both injection and malformed-input crashes.
### Flake-rate calculation
```
flake_rate = mixed_outcome_runs / total_runs
```
where `mixed_outcome_runs` = count of distinct (date, verdict) pairs where
the same scenario saw both PASS and FAIL on the same calendar day, or more
generally where consecutive runs disagree with no intervening code change.
Render as a percentage; `< 1%` = stable, `1%-5%` = watch, `≥ 5%` = flaky.
### Local reproduction
```bash
# Repeat the scenario N times. Use the scenario executor so DomainStore
# records each run, giving you more history for next time.
for i in $(seq 1 "${REPEAT_COUNT:-100}"); do
node install.mjs run --scenario "${SCENARIO_PATH}" --silent \
>> "${EVIDENCE_DIR}/repeat-runs.log" 2>&1 || true
done
# Summarize pass/fail ratio.
node -e "
const fs=require('node:fs');
const lines=fs.readFileSync(process.argv[1],'utf8').split(/\r?\n/);
const pass=lines.filter(l=>/VERDICT=PASS/.test(l)).length;
const fail=lines.filter(l=>/VERDICT=FAIL/.test(l)).length;
fs.writeFileSync(process.argv[2],JSON.stringify({pass,fail,rate: fail/(pass+fail||1)}, null, 2));
" "${EVIDENCE_DIR}/repeat-runs.log" "${EVIDENCE_DIR}/repeat-summary.json"
```
### Order-dependency probe
```bash
# Run the scenario's sibling scenarios in randomized order; if the target
# scenario only fails after a specific neighbor, that's an order bug.
node install.mjs run --project "${PROJECT_ID}" --shuffle \
--only-group "$(dirname "${SCENARIO_PATH}")" \
> "${EVIDENCE_DIR}/shuffle-run.log" 2>&1 || true
```
Use `{WT_LIB}/exec-with-timeout.mjs` around every step so a hung repro doesn't
burn the entire scenario budget on one iteration.
## 3. Root-cause taxonomy
Every finding MUST be tagged with exactly one cause. No "unknown":
| cause | signal |
|----------------|-----------------------------------------------------------------|
| `timing` | `sleep(N)` / polling-without-deadline / race with async resolve |
| `order-dep` | passes in isolation, fails when sibling scenario runs first |
| `env` | locale / TZ / filesystem-case / node version differs CI vs local|
| `resource` | port in use / disk full / memory-pressure / fd leak |
| `external-dep` | real HTTP / live DB / third-party API unstubbed |
The proposed fix for each cause is well-known and NOT "add retry":
- **timing** → replace with polling + explicit deadline; inject a clock.
- **order-dep** → isolate state per test (fresh DB, fresh tmpdir).
- **env** → pin the env in frontmatter (`env.TZ: UTC`, `env.LANG: en_US.UTF-8`).
- **resource** → acquire via lease; release in a `finally` / teardown block.
- **external-dep** → replace with a recording or stub server.
## 4. Evidence output
Under `.wicked-qe/evidence/<run_id>/`:
| File | manifest `kind` | Required |
|-------------------------------|-----------------|----------|
| `verdict-history.json` | `http-response` | Yes |
| `repeat-runs.log` | `log` | Yes |
| `repeat-summary.json` | `metric` | Yes |
| `shuffle-run.log` | `log` | If order probe ran |
| `flake-report.md` | `log` | Yes |
`flake-report.md` MUST include: scenario_id, 14d verdict stats, repro
rate, root cause (from the taxonomy), proposed fix, quarantine decision
with deadline.
## 5. DomainStore write
```js
// One verdicts row summarizing the flake judgment.
store.create("verdicts", {
run_id: RUN_ID,
verdict: isFlaky ? "FAIL" : "PASS",
// "PASS" here means "I investigated and this is NOT flaky" — the
// scenario may still have legitimate failures.
reviewer: "wicked-garden-qe-flaky-test-hunter",
reason: isFlaky
? `Flake rate ${(flakeRate*100).toFixed(1)}% over 14d; cause=${cause}; fix=${proposedFix}.`
: `Stable over 14d (${totalRuns} runs, ${failCount} fails — all explained).`,
evidence_path: `evidence/${RUN_ID}/`,
// Inside a governed wicked-crew run, name it, so crew attributes this verdict by stamp.
...(process.env.WICKED_RUN_ID ? { crew_run_id: process.env.WICKED_RUN_ID } : {}),
});
// Root-cause task under the specialist's own assignee_skill so `wicked-garden-qe-test-oracle`
// can surface it by cause bucket.
store.create("tasks", {
project_id: PROJECT_ID,
title: `Flake root-cause: ${scenarioName} (${cause})`,
status: quarantined ? "blocked" : "open",
assignee_skill: `flaky-test-hunter:${cause}`,
body: JSON.stringify({
scenario_id: SCENARIO_ID,
flake_rate: flakeRate,
cause,
proposed_fix: proposedFix,
quarantined,
// Both REQUIRED when quarantined — the campaign gate consumes this row
// fail-closed (qe refs/campaign-flake-policy.md): a quarantine record
// missing owner or deadline is NOT honored and the scenario keeps
// denying the campaign.
owner: quarantined ? quarantineOwner : null,
quarantine_expires: quarantined ? quarantineExpiresIso : null,
fix_eta_days: fixEtaDays,
total_runs_14d: totalRuns,
mixed_outcome_runs: mixedOutcomeRuns,
}),
});
```
## 6. Quarantine policy (strict)
Quarantine only when **both** hold:
- `fix_eta_days > 14` (a real fix is more than two weeks away), AND
- `blast_radius < 1%` of the suite (quarantining this one test does not
materially reduce coverage of the surface under test).
Otherwise force root-cause work. Every quarantine entry carries an
owner, a fix deadline, and an expiration; expired quarantines auto-open
as `status: "open"` tasks in the next run.
**Your tasks row IS the machine contract the campaign gate consumes**
(TH-21 — the qe skill's `refs/campaign-flake-policy.md`): the scoreboard
honors a quarantine only when the row's JSON body carries `quarantined:
true`, a resolvable `scenario_id` (or `scenario_name`), a `cause` from the
§ 3 taxonomy, a non-empty `owner`, and an unexpired `quarantine_expires`.
Anything less is reported as an INVALID quarantine and NOT honored — the
scenario keeps denying the campaign. Honored quarantines surface as
**excluded-with-reason** in the campaign scoreboard and acceptance payload,
never as silent drops.
## 7. Failure modes
| code | meaning | class |
|-------------------------------|----------------------------------------------------|--------|
| `ERR_SQLITE_UNAVAILABLE` | `.wicked-qe/wicked-qe.db` missing | user |
| `ERR_SCENARIO_NOT_FOUND` | scenario_id / path resolves to no row | user |
| `ERR_INSUFFICIENT_HISTORY` | fewer than 10 verdicts in window; refuse to judge | user |
| `ERR_FILTER_INVALID` | scenario_id not a UUID | user |
| `ERR_REPRO_UNAVAILABLE` | scenario executor missing / project_id not set | system |
On `ERR_INSUFFICIENT_HISTORY`: return a `tasks` row with
`status: "open"`, `assignee_skill: "flaky-test-hunter:need-more-data"`
and advise the caller to re-run in 2 weeks.
## 8. Non-negotiable rules
- **Never propose "add retry" as a fix.** Retry hides the bug; your job
is to name the bug.
- **Judge from data.** Don't declare "flaky" from a single anecdote —
require ≥ 10 historical runs or ≥ 25 fresh repro iterations.
- **Quarantine is a cost.** Log who owns the deadline; leaking a
quarantine is itself a quality incident.
- **Parameterize every SQL read.** You are read-only; you never call
`store.update` or `store.delete` on `verdicts` or `runs`.
## 9. Output
```
## Flake: {scenarioName}
scenario_id: {SCENARIO_ID}
window: 14d runs: {N} fails: {F} mixed-outcome: {M}
flake_rate: {pct}% (< 1% stable | 1-5% watch | ≥ 5% flaky)
repro: {reproPass}/{reproTotal} passed ({reproRate}% pass rate)
cause: {timing|order-dep|env|resource|external-dep}
fix: {concrete fix — never "add retry"}
quarantine: {no | yes, expires {iso}} eta: {days}d
VERDICT: PASS|FAIL
REVIEWER: wicked-garden-qe-flaky-test-hunter
RUN_ID: {RUN_ID}
```
## Helper resolution (`{WT_LIB}`)
`{WT_LIB}` is the plugin's own qe helper directory — the helper modules ship
in-catalog (`scripts/qe/lib/`, ported from the retired wicked-testing package <!-- historical -->
in Phase 6c). Resolve it (cross-platform):
```bash
WT_LIB="$(wicked-garden path scripts/qe/lib)"
```