Skip to content
Back to skills

Doctor

ASecurity

Read-only scripted health check of a scaffolded QA project — last-run freshness and zero-test guards, substrate drift, vocab mirrors, stale context and manifests, orphan sentinels, unreviewed or uncompiled specs. Use when something looks off, before trusting a green run, after a plugin upgrade, or when another command points here. Writes nothing; `--verify-invariants <spec>` is the only mode that runs tests.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 19, 2026
testingrustgobashnoderailsgitapidocumentation

Works with

  • claude code
  • cli
  • api
  • mcp

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 19, 2026

npx -y skills add its-magdy/qa-warden --skill doctor --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Doctor?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Doctor
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/its-magdy-doctor/badge)](https://www.skillsdirectory.com/skills/its-magdy-doctor)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
description: Read-only scripted health check of a scaffolded QA project — last-run freshness and zero-test guards, substrate drift, vocab mirrors, stale context and manifests, orphan sentinels, unreviewed or uncompiled specs. Use when something looks off, before trusting a green run, after a plugin upgrade, or when another command points here. Writes nothing; `--verify-invariants <spec>` is the only mode that runs tests.
argument-hint: "[--verify-invariants specs/<area>/<feature>.md]"
context: fork
background: false
allowed-tools: Bash(bash "${CLAUDE_SKILL_DIR}/scripts/doctor.sh" *)
---

`/qa-warden:doctor` runs the deterministic self-checks (checks 0–21) the rest of the toolkit describes
in **prose** ("confirm the run resolved tests", "verify freshness", "the 16-key vocab must
match byte-for-byte", "every `@smoke` test must carry the tag"). The plugin's `PreToolUse`
hooks cover only what is decidable from a single write (CLAUDE.md §Oracle defense); for
everything else, correctness rides on an agent remembering those sentences. `doctor` collapses them into one scripted pass so a
human (or CI) can *run* the guard instead of *remembering* it. It is **read-only**:
it reruns no tests by default, heals nothing, hits no network, writes nothing.

Run from the QA project root (the dir with `playwright.config.ts` + `CLAUDE.md`).

## Run the scripted pass

The entire deterministic check suite (checks 0–21 + rollup) lives in this skill's own
`scripts/doctor.sh`, inside the plugin. It is not stamped into the project, so it is always the same
build as the templates it compares the project against. Run it — do not re-derive the checks inline.

Run this ONE command, from the project root, as its own Bash call (an `if …; fi` wrapper matches
no allow rule and is denied headless). The `allowed-tools` rule above pre-approves exactly this form:

```bash
bash "${CLAUDE_SKILL_DIR}/scripts/doctor.sh"
```

The script prints ✅ / ⚠️ / ❌ per check plus a rollup line, and exits 1 on any ❌
(0 on healthy or warnings-only). **Never silently pass.** A lone
`❌ not a QA Warden project` line means the wrong directory: cd to the project root, or run
`/qa-warden:init` if there is none. If `playwright.config.ts` and `package.json` are both absent
(the run prints `❌ substrate missing` for each), the project was *never scaffolded* →
`/qa-warden:init`; a substrate that exists but is missing some files is stale →
`/qa-warden:init --resync`.

## Interpreting the output (what to run per failure)

Print a compact report to chat: one line per check, blocking ❌ first. `doctor`
does not fix anything — each finding names its repair:

- **Check 0/1 (jq / last-run.json):** the freshness + zero-test gate is single-sourced
  in `scripts/check-last-run.sh` (shared with `/qa-warden:report` — they used to mirror it in
  prose and drifted). `total` there = expected+unexpected+skipped+flaky — every
  *resolved* test, so an **all-red run is fresh, not zero-test** (`expected` alone
  counts only PASSED tests). Corrupt/zero-test → fix the run and refresh via
  `/qa-warden:run mode=smoke`; stale/missing → confirm which run you mean or re-run.
- **Check 0b (ripgrep missing, WARN):** `rg` is an undeclared plugin-wide dependency (the healer,
  reviewer and planner greps use it). `/qa-warden:retire` checks for it and falls back to
  `grep -rlE` for its consumer checks; install it anyway (`brew install ripgrep`).
- **Check ids 9b / 9bb / 9bc / 9bd / 9be / 9d / 9e / 9g / 9h / 9i / 9j / 20, and 9f arm 1, belong to
  `bin/qa-selfcheck`, not doctor.** They read only the plugin's own source (agents, skills, hooks,
  the shipped templates, the plugin's `manifests/`), so nothing in a project could fix one; they run
  from the toolkit repo as `bin/qa-selfcheck` (same ids). If a user reports one of those ids, the
  fault is in the plugin build, not their project — the remedy is updating the plugin, not editing
  anything here.
- **Check 9c (retired permission rule still in settings.json):** `.claude/settings.json` is
  excluded from Check 9's byte-compare and from `--resync` (shared ownership — users add their own
  rules), and the scaffold's jq merge is an additive UNION that can only ADD. So a rule the toolkit
  **retracts** is stranded in every already-scaffolded project forever with nothing to notice it.
  Delete the named rule by hand; `/qa-warden:init --resync` will not do it for you.
  A second arm raises ONE warning for any path-scoped `Write(<path>)` rule: Claude Code checks
  file permissions against `Edit(...)`/`Read(...)` rules only, so such a rule is never consulted
  (and is warned about at startup). The template shipped 30 of them, each with an `Edit` twin,
  until 0.4.0. Delete them; give any rule without an `Edit` twin one.
- **Check 9f (healer MCP grants reaching this project):** arm 1 — the plugin's own
  `agents/healer.md` `tools:` list against `templates/settings.json` — is a `bin/qa-selfcheck`
  check, because both sides are plugin source. What doctor runs is the **WARN** arm that compares the shipped MCP grants against this project's
  `.claude/settings.json`: this is the complement of 9c — 9c catches a rule the toolkit
  RETRACTED and stranded downstream, this catches one it ADDED that never reached a project
  scaffolded earlier. Repair is a plain `/qa-warden:init` (the permission merge is additive and runs
  every time), not a hand edit. Scoped to the `mcp__` entries on purpose: those are capability
  grants with one right answer, whereas a user may legitimately delete a policy rule, and a
  whole-allow-list check would nag them forever.
  **Arm 3 (WARN)** covers what the repair cannot: the merge needs `jq`, and without it the
  scaffold parks the shipped rules in `.claude/settings.qa-suggested.json`. That file's presence
  is proof the merge never ran — unlike a subset check it cannot confuse *undelivered* with
  *declined* — so doctor names it, counts the rules still missing, and says what they cost. A
  successful merge deletes the file, so the signal cannot go stale; a hand-merged leftover is
  reported separately as stale. The presence test sits outside the `jq` gate, because the state
  it detects is `jq` being absent.
- **Check 9, CLAUDE.md vocabulary containment:** the delivery half of the upgrade path.
  `CLAUDE.md` is shared-ownership, so it is excluded from `--resync` *and* from Check 9's
  byte-compare — yet it is where every new authoring feature is documented, and the in-project
  agents read it rather than the plugin. After a clean, green, exit-0 `/qa-warden:init --resync` the
  stamped `CLAUDE.md` can therefore lack every new authoring feature (`lock:`, `verifier`, …)
  while doctor says nothing else. The spec format itself (`fault:`, `clock:`, the oracle keys)
  is no longer in that set: it moved into the plugin's `test-case-contract` skill, which ships
  with the plugin. The symbols come from the plugin's
  `manifests/claude-md-vocab.txt` (never stamped into a project) and are gated on the shipped template, the same derive-don't-
  hand-enumerate shape as the `.env.example` stock-var containment beside it. The check is
  symbol-level, not section-level: new rules land inside pre-existing sections, so a heading
  diff reports green on exactly the release it was built for. **WARNs, never FAILs:** alone among
  Check 9's findings the repair is a hand-merge against the shipped template, not a command, and
  a repair the toolkit cannot perform must not be louder than the ones it can.
- **Check 9, old spec-format section in CLAUDE.md (WARN):** a project stamped before the spec
  format moved into the `test-case-contract` skill still has a `## Test case format contract`
  section in its `CLAUDE.md`. Run `/qa-warden:init --resync` first, move any project-specific
  rules out of the section, then replace it (down to the next `## ` heading) with the shipped
  template's `## Spec format` pointer. The agents preload the skill, so the old copy only drifts
  away from it. The containment check above asks for that pointer (`test-case-contract`) too.
- **Check 9k (unmanifested-walk ownership):** "which compiled tests carry no route manifest" is
  one rule with two consumers that must never disagree — `/qa-warden:impact` prints it as
  `### BLIND SPOTS` (a zero-match there is only honest if the caller is told which specs were
  invisible) and `/qa-warden:coverage` dim 0 needs it to avoid the label it used to print, where an
  unmanifested test's routes landed under "touched by NO compiled test (planned, untested)" —
  false for a spec that was authored and compiled, and it sent the reader to write a second
  spec instead of clearing the blocker the verifier withheld the manifest for. The walk under
  it is `spec-links.sh`'s collected-test inventory (`collected_tests()`), which doctor,
  coverage and `post-run-checks.sh` read too. **Arms 1 and 2 key on SYMBOLS** — the
  `unmanifested)` mode name, the `collected_tests()` walk, its `(*.metamorphic)` twin case and
  the arm's `= twin` skip, and the walk's two `_` exclusions. Arm 2 is the one that matters: a
  twin never carries a manifest BY DESIGN (verifier V4 emits one per spec), so a copy that drops
  the skip makes both consumers cry wolf on every twin; a walk that drops a `_` exclusion
  reports scratch specs Playwright's `testIgnore` never runs. A stamp with no `collected_tests()`
  predates the inventory: that is an uncounted ℹ️, not a ❌, because doctor, coverage and impact
  then run the plugin's copy of the script; the drift ⚠️ carries the `/qa-warden:init --resync`.
  **Arm 3 keys on the CONSTRUCT** — an existence
  test on a manifest path built from a variable — and scans **code, not comments**, because an
  ownership check has to name the construct it bans and so matches its own documentation
  otherwise. Fix by calling `scripts/spec-links.sh unmanifested`; never re-derive the walk.
- **Check 9l (`test lock:` pairing + shard voidance):** `lock?: string | string[]` on
  `TestDetails` (Playwright 1.63+) is the only cross-file mutual-exclusion primitive the runner
  ships, and reviewer Check 13 offers it for the cross-FEATURE mutator/consumer case. It is a
  hand-mirrored cross-file **string with no runtime signal**, so this is its deterministic half.
  **Arm 1** — a lock name at exactly **one** site under `tests/**` is silently inert: nothing to
  exclude, the pair races exactly as before, every oracle still green. The consumer side carries
  the name for no reason of its own, so it is the side a typo or a later edit drops. Fix by
  naming the same literal on the other side of the contention, or by dropping the lock and
  folding into one file's serial `describe`. Counted per **site**, not per file — two same-lock
  tests in one file genuinely do exclude each other. **Arm 2** — locks plus a sharding workflow
  is a void remedy: the shard filter splits by test count and never reads `group.locks`, so the
  pair can land in two shard processes that share no lock table. Fix by dropping the sharding or
  by taking the shard-safe fix (fold into one file, or give the mutator a throwaway entity).
  Both arms scan **code, not comments** (the 9k lesson).
- **Check 2 (vocab drift):** this project's `scripts/oracle-keys.txt` names a key the plugin's
  `test-case-contract` skill does not. Run `/qa-warden:init --resync`, or update the plugin if the
  project is newer. (The plugin-side half, 9b, is a `bin/qa-selfcheck` check.)
- **Check 3 (vacuous smoke):** tag at least the P1 happy path `@smoke`, then
  `/qa-warden:run mode=smoke`.
- **Check 4 (orphaned sentinels):** close the loop per the sentinel→action table in
  `${CLAUDE_PLUGIN_ROOT}/reference/sentinel-actions.md` — re-seed / re-explore / migrate as the sentinel names.
- **Check 5/5b (stale or draft context):** `/qa-warden:explore mode=area site=<id> area=<name>` to refresh
  (both keys — exploration STOPs on either one missing rather than guessing; Check 5b prints the
  exact command for each stale file),
  then clear `draft:` / bump `last_verified:`.
- **Check 6 (undeclared env):** declare the variable in `.env.example` (commented is
  fine for optional-with-fallback vars).
- **Check 7/7d (pins / idiom):** fix `package.json` / regenerate the offending
  `fixtures/schemas/` file in the pinned major's idiom.
- **Check 8b (Playwright FOUR-SITE paired-bump lockstep):** `package.json`'s `overrides_comment`
  mandates four sites move together, and the check enforces all of them. **Inside
  `package.json`:** `@playwright/test` must equal `overrides.playwright` must equal
  `overrides.playwright-core`, and `@playwright/test` must be **exact** (no `^`/`~`). Skew the
  runner above the forced core and `overrides` drag core back down (T-01); pin `playwright`
  without `playwright-core` and a nested **alpha** core survives, needing a browser revision
  `playwright install` never fetches (F-002). A caret on the runner makes the equality arm a false
  green, because a lockfile-less install floats it. **Across files:** `.mcp.explore.json`'s
  `npx -y playwright@X.Y.Z mcp` version must equal `overrides.playwright` — since 1.62 the MCP
  server ships bundled with `playwright`, so these are one version mirrored in two files. A
  **bare, unpinned** `playwright` npx arg is itself a FAIL: with no `node_modules` (fresh clone,
  lockfile-less CI) npx silently fetches the LATEST Playwright, recreating the same skew from the
  explore config, where `overrides` cannot reach it. Fix by bumping **all four** sites in one
  change, never by unpinning. Note Check 7 does **not** cover this: 7a screens only for
  `alpha`/`beta`/`rc` strings and 7c screens only the lockfile's resolved core for alpha, so a
  clean, stable, *skewed* set passes both. A missing site is reported, not failed — `package.json`
  is in `resync-set.txt`, so Check 9 owns the deleted-block case.
- **Check 8/9 (canonical scripts / substrate drift):** `/qa-warden:init --resync` (backs up
  drifted files to `*.qa-bak`). A missing stock var in `.env.example` → restore it from
  the shipped template, or park it commented (`# VAR=`) — the stock list is derived from
  the template, and the file itself is shared-ownership and never byte-compared. The
  drift baseline is the plugin this doctor ships in, so it never compares against a different
  build. An ℹ️ naming
  `scripts/resync-set.txt`, `scripts/claude-md-vocab.txt` or `scripts/prod-guard-rails.txt` as a
  leftover is not a finding: those manifests now live only in the plugin, nothing in the project
  reads the old copies, and the user can delete them. Likewise an ℹ️ naming
  `scripts/review-marker.sh`: `/qa-warden:review` runs the plugin's copy, so the user can delete
  the stamped one together with its two `settings.json` rules. An ℹ️ naming
  `Bash(bash scripts/doctor.sh*)` is the same: nothing calls the stamped path now, so the user can
  delete that rule, but keeps `scripts/doctor.sh` itself. Its sibling
  `Bash(CLAUDE_PLUGIN_ROOT=* bash scripts/doctor.sh*)` is a Check 9c ⚠️ instead, because its first
  `*` pre-approves other assignments and whole programs. A ⚠️ drift on `scripts/doctor.sh`
  after a plugin update is expected: that stamped file is now only a launcher for running doctor
  by hand, and `--resync` replaces the old full copy with it.
- **Check 10 (site↔project routing):** never edit `playwright.config.ts` (toolkit-owned; it
  derives a project per set `BASE_URL_<SITE>`). ❌ — make the id lowercase `[a-z0-9-]` and its
  `base_url_env` exactly `BASE_URL_<ID>` (`-`→`_`). ⚠️ — set that var in `.env`.
- **Check 11 (bug Status):** add the `## Status:` marker to the named `bugs/*.md`.
- **Check 11b (durable evidence, WARN):** an open `bugs/*.md` that cites its trace/screenshot
  under `artifacts/test-results/…` (volatile — wiped the next time the test runs green) or a
  path that is already gone. Copy the evidence into a durable `bugs/<slug>/` folder and cite
  that (healer HEAL03), or resolve the bug if its test is now green (HEAL04).
- **Check 11c (parked marker on a resolved bug, WARN):** a live `test.fail`/`test.fixme` marker
  whose linked `bugs/*.md` is already `Status: fixed`/`reverted` — a stale marker laundering a
  resolved defect into the green roll-up. Remove the marker, or reopen the bug if it regressed
  (mirrors reviewer Check 3).
- **Check 12 (stale manifest):** `/qa-warden:gen <spec>` to regenerate the route manifest.
- **Check 13 (declared-mutation isolation, WARN):** the basis declares
  `mutates_server_state: true` but a resolved test shows no isolation marker
  (parallelIndex / serial / seeded fixture) — un-isolated mutation is green at 1 worker,
  flaky at scale. Resolution mirrors `/qa-warden:coverage` dim 1: the direct
  `tests/<feature>.spec.ts` PLUS every fanned spec linking back via `basis:` (twins
  included); a mutating feature that resolves to ZERO tests is its own WARN, never a
  silent skip. Wire the isolation (see the basis `test_data:` block) or regenerate via
  `/qa-warden:gen`. The reviewer's code-level Check 13 remains the enforcing gate.
- **Check 14 (unmanaged test, WARN):** a `tests/**/*.spec.ts` with no paired
  `specs/<area>/<feature>.md` sits outside the assertion contract — write the spec
  (`/qa-warden:new-spec`) or consciously exempt it (move to `tests-legacy/`).
- **Check 15/15b (review attestation, WARN):** the spec/test pair changed since its
  `reports/review/<area>/<feature>.reviewed` marker (or was never reviewed), the marker is
  current but records `result: FAIL` ("reviewed, FAILED" — fix the test, then review again), or a
  `// verified: must_fail_when "…"` stamp no longer matches the spec's invariant text —
  re-run `/qa-warden:review` (and `--verify-invariants` for a stale stamp). Markers are
  agent-writable convenience — this catches *forgotten* reviews, not malicious ones.
- **Check 16 (smoke-tagged spec with no test, WARN):** a spec declares a `tags: [… smoke …]`
  scenario (spec-level, per-scenario inside `scenarios:`, or a block list; read by
  `scripts/spec-links.sh`'s one tag parser, so prose never counts) but was never compiled — `/qa-warden:run mode=smoke`'s `--grep @smoke` is blind to that P1 flow, so a
  fully-broken governed path (admin RBAC, checkout charge==total) passes the smoke gate because no
  test exists to fail. Complements Check 3 (which only proves *some* test carries the tag). Compile
  it (`/qa-warden:gen`) or drop the smoke tag from the spec.
- **Check 17 (spec with no compiled test — ANY tag, WARN):** the complement to Check 16 — a
  `@regression`-only spec authored but never generated sits outside the assertion contract and no
  run exercises it (Check 15 `continue`s past it, Check 16 is smoke-only). `/qa-warden:gen` it, or
  `/qa-warden:retire` / `# waived:` it if its scenarios are intentionally folded into a sibling.
- **Check 18 (abandoned authoring chain, WARN):** a `.cases.md` (the approval artifact) with no
  downstream spec — the intake→ideate→approve chain ran but never produced a spec, so the intended
  coverage silently never shipped. `/qa-warden:new-spec <area/feature>` or `/qa-warden:retire` the chain.
- **Check 19 (unattested interview, WARN):** a `.basis.md` with `[human-answered]` oracle rules but
  `interview: none`/absent — the human grounding is unattested (a fabricated basis looks identical).
  `/qa-warden:intake` should stamp the real question count in the basis `interview:` field.
- **Check 21 (fresh-project readiness, INFO):** not a warning — on a brand-new project (no
  `app.context.md`/specs) it prints a "setup looks healthy — next run `/qa-warden:explore`" verdict so the
  expected empty-project WARNs read as "expected at this stage", not "something's broken".
  **21a (unconfigured target, WARN):** runs `scripts/prod-guard.sh` in its offline default mode and
  WARNs on each empty/placeholder refusal (`BASE_URL_APP` empty, or any `BASE_URL_*`/`API_URL` still
  `CHANGEME`, `<…>`, or an `example.com`/`.example` host) — while one stands, the readiness verdict
  says "NOT ready yet — fill in .env" instead of "setup looks healthy". Remedy: edit `.env` to a real
  staging/QA host. Empty `QA_USER_*` is not flagged (a site without a login has none).

## Optional: `--verify-invariants specs/<area>/<feature>.md` (executable `must_fail_when`)

Only when `$ARGUMENTS` contains `--verify-invariants`: read
`${CLAUDE_SKILL_DIR}/reference/verify-invariants.md` and follow it. That file is THE single
source of the injection mechanism (the verifier's Process step 8b runs the same one); do not
re-derive it here. In the default read-only pass, skip this section entirely — it is the only
mode that runs tests.

This skill runs as a forked subagent, so the caller sees only what you return: report the
per-check ✅/⚠️/❌ lines and the rollup, then the interpretation above for each ❌/⚠️. Never
return a bare "healthy" while any ❌ stands.

This skill reruns nothing in default mode, invokes no healer, and writes nothing (the opt-in mode's TEMP probe is reverted, so it too leaves the tree unchanged).

Files in this skill

  • SKILL.md20.7 KB
  • reference/verify-invariants.md4.8 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.