---
name: red-doctor
working-mode: interactive
description: Adoption/process doctor — reports how fully a repo has adopted the RedSkills engineering stack. Read-only by default; `--fix` applies the canonical fix for every finding, gated per hard-to-reverse change. The recurring counterpart to the one-time `/red-setup`. Use when asked "red doctor", "check adoption", before a large `/afk` drain, or to verify a repo against canonical conventions.
argument-hint: "[--repo <path|owner/name>] [--fix] [--session-mcp <servers|none>]"
---

# Doctor (adoption / process)

**Run the adoption/process check read-only; apply every canonical fix with `--fix` — never hand-edit what a single-writer tool owns, because each gap's fix-home already exists.**

The recurring counterpart to the one-time `/red-setup` — the same split the
`memory` plugin has between `context-status` (read-only) and its setup. It reports
how fully a repo adopted the RedSkills stack and **names the fix-home for each gap**.
**Default is read-only.** With `--fix` it becomes the reconciler that actually heals
every finding, applying each gap's canonical fix and gating every hard-to-reverse
change behind explicit per-item confirmation.

<what-to-do>

**Always run Pass 1 (Diagnose, read-only) and print the scorecard. Then, only when invoked with `--fix`, run Pass 2 (Fix) — apply the canonical fix for every finding, batching the safe ones and confirming each hard-to-reverse one individually.**

### Hard rules

- ✅ **Default (no `--fix`) is read-only**: no `gh label create/edit/delete`, no file writes, no `gh issue edit`, no MCP/hook/statusline install. Pass 1 is a pure diagnostic.
- ✅ **`--fix` is the only mutating path**, and even then: the safe fixes apply in a batch (with a one-line receipt each); every **hard-to-reverse** fix — label rename/retire, config-key migration, `blocked:*` rotation, MCP rewrite — is **confirmed individually before it runs**.
- ❌ Never hand-edit what a single-writer tool owns: a version mismatch is fixed by **running** the version script (ADR 0040), never by editing a manifest. `--fix` invokes the tool; it does not patch around it.
- ✅ Compose existing surfaces — `memory context-status`, `/wiki lint`, the development-workflow injector, `gh` — instead of re-implementing them.
- ✅ Resolve the canonical vocabulary from the **target repo's** `.red/agents/triage-labels.md` (each repo may map roles to its own strings — respect that mapping, don't impose red-skills' defaults).

### Pass 1 — Diagnose (always; read-only)

Run the checks against the target repo (cwd by default; `--repo <path|owner/name>` for another, or sweep a list). Read only.

1. **Local context stack** — invoke `memory context-status` (or its CLI) and fold its result in. Do not duplicate its checks (CLAUDE/AGENTS, `.red/CONTEXT(-MAP)`, `.red/adr/*`, memory, wiki).
2. **Label conformance** — `gh label list` vs the canonical families in `triage-labels.md`. Classify every label per the *Label classes* table in `<supporting-info>`. Report `❌ synonym` / `⚠️ legacy` / `⚠️ naming` with the suggested rename; in this pass never apply it.
3. **`blocked:*` hygiene** — list open issues carrying `ready-for-agent` **or** `running` **together with** any `blocked:*` label (stale reason not rotated on re-queue). Count them.
4. **AGENTS ≡ CLAUDE Agent-skills parity** — both files exist **and** both carry the `## Agent skills` block. Report `C/A` (e.g. `1/0` = block in CLAUDE only). Missing files, missing blocks, or unequal treatment are findings tagged `→ /red-setup`.
5. **AGENTS ≡ CLAUDE Development-workflow parity** — both files exist **and** both carry the `## Development workflow` block with the same treatment. Report `C/A` for presence (e.g. `1/0` = block in CLAUDE only), and report any missing file, missing block, or out-of-parity block as a finding tagged `→ /red-setup`. During this read-only pass: do not run `inject-development-workflow`, do not create files, do not edit either agent rules file — that is the `--fix` lane's job.
6. **Config namespacing + primary-branch guard** — read `.red/config.yaml`, two findings:
    - **Guard flag** — report whether the primary-branch guard resolves to `true`. The canonical key is the namespaced `plugins.dev.lock.primary-branch`; the legacy top-level `dev.lock.primary-branch` and flat `lock-primary-branch` still resolve through the ADR 0042 / PR #697 fold (the whole `plugins.dev.*` block folds onto the `dev.*` accessors, the namespaced form winning). Treat an absent config file, absent block, absent key, or any value other than `true` as "unset" → recommend `→ /red-setup`; report `true` as adopted.
    - **Namespacing conformance** (the strict structural check) — dev-plugin settings belong under `plugins.dev.*`. Flag any **legacy top-level placement** as a migration finding: a top-level `afk:` block (canonical `plugins.dev.afk.*`), a top-level `dev:` block carrying plugin settings such as `dev.lock.*` (canonical `plugins.dev.lock.*`), or the flat `lock-primary-branch`. The fold still reads these, so this is **hygiene, not breakage** — but the canonical written form is namespaced and `/red-setup` now writes it that way, so a top-level form is drift to migrate. Tag `→ /red-setup`.

    During this read-only pass: never write `.red/config.yaml`.
7. **Statusline drift** — the installed `.claude/settings.json` `statusLine` command resolves the **daemon's stable bundle** (`~/.red/redskilled/bundles/redskilled-*.bundle.min.mjs`), which is the copy nothing on the machine prunes. Two forms are findings, both a renderer ADR 0147 deleted: the cached dev bundle (`~/.cache/red-skills/bundles/dev-*.bundle.min.mjs`) and the older launcher (`…/plugins/cache/red-skills/dev/*/…/afk.mjs`). A dev-bundle form is not merely stale — `dev-3.21.0` is the last dev bundle that will ever exist, so it renders a 3.21.0-era line against v4 state lanes it cannot read and reports success while saying nothing about Workers. Tag `→ /red-statusline`, and note that any leftover `dev-*` bundle is now inert and safe to delete.
8. **MCP wiring** — does the repo wire the expected MCPs? The `dev` plugin should expose **`rs_dev`** and nothing else (renamed from `redskilled` by ADR 0147 §2) (ADR 0147 §4 switched `navigator` and `rsp` off at the declaration; their code stays, so a surviving entry is drift, not a feature) — the canonical complete project interface backed by the `redskilled` daemon and the `@reddb-io/worker` body, which `/afk` and `/go` drive as clients (ADR 0120, naming amended by ADR 0142); a missing `redskilled` entry, or a surviving `castle` entry, in `plugins/dev/.mcp.json` is a finding. Treat the stdio MCP as a client surface: the **`redskilled` daemon's Project control state** owns engine state, GitHub adapters, registration renewal, and background belts, and the same daemon is host process/budget truth (ADR 0143's second per-project process was retired into it by ADR 0147). Development, cache, npm, and Release layouts must carry the matching `rs_dev` MCP + `redskilled` daemon bundle pair; one missing half is distribution drift, never permission for a local engine fallback. Project status may report the daemon version, protocol, PID, uptime, client count, handover state, and bounded numeric resources, but socket paths, process argv, and secrets are findings. A report must preserve that split — never describe the stdio host as the thing that holds Project control state. The `memory` plugin should expose **`red-memory` (the local data MCP, a local server that execs the in-repo `bootstrap.mjs`, built from `apps/plugin-memory`, RedDB-backed via `@reddb-io/sdk`)** — this local shape is the **intended** wiring, **not** drift (ADR 0041 Amendment 1 reversed the release-fetched migration). **`red-ui` (the visualizer consumer) is opt-in**: it belongs in the declaration only when `.red/config.yaml` sets `plugins.red-ui.enabled: true`, so a default `red-ui` stanza in a shipped `.mcp.json` is a finding and its absence never is. What *is* a finding: a server still named **`memory`** that was never renamed to `red-memory`, or a `.mcp.json`/routing-guide that tries to **fetch `red-memory` from a GitHub release** (there is no such release). Also check whether the repo wires `red-memory` for its **own** dev (root `.mcp.json` or agent-doc reference).
9. **Version coherence** — every plugin manifest pair is on the same version: for each `plugins/*/`, `.claude-plugin/plugin.json` `version` **==** `.codex-plugin/plugin.json` `version`. A mismatch is what `validate-install-metadata.sh` and the repo-wide version-train invariant reject. Fix-home = the next Release-standard Version-PR; `release.version_surfaces` is the single-writer contract (ADR 0139).
10. **Workflow naming convention** — list `.github/workflows/*.yml` and classify each by **role**, decidable from its content: has `workflow_call:` → must be `reusable-*`; `uses:` a `reusable-*` → must be `rs-*` (a caller / instantiation); otherwise → must be `red-*` (a standalone workflow). Applies both in red-skills itself and in an adopter repo. Two findings, both tagged `→ /red-setup`:
    - **Naming drift** — a file whose filename prefix doesn't match its role: a reusable caller named `red-*` (should be `rs-*`), a standalone named `rs-*`/`reusable-*` (should be `red-*`), a `workflow_call` workflow not named `reusable-*`, or a legacy `red-skills-*` (the retired caller prefix → now `rs-*`). Report the rename to the role-correct prefix. (A `reusable-*` referenced via `uses:` from another repo is exempt — it is *called*, never copied.)
    - **AFK lane auth gap** — if `rs-afk-attempt.yml` (or a drifted copy under another prefix) is installed, best-effort check that an OpenCode auth secret **name** is present via `gh secret list --repo <repo>` (names only — never read or print a value). If none of `MINIMAX_API_KEY`/`OPENAI_API_KEY`/`OPENROUTER_API_KEY` is listed, flag the lane as installed-but-unauthed (the public-repo org-secret gotcha — on a public repo an org secret resolves empty unless its Repository access includes the repo). Skip silently if `gh secret list` 403s (no admin scope); report `unknown`, not a failure.
11. **`.red/.gitignore` self-ignore** — only when `.red/` exists: check that `.red/.gitignore` is present **and** ignores `tmp/`, `state/`, and `researches/`, so ephemeral runtime state, durable machine state, and generated research reports can't be committed even if the repo-root `.gitignore` lacks the patterns (ADR 0067 / ADR 0098; `/red-setup` writes this on creation). Report `❌` when `.red/` exists but `.red/.gitignore` is absent or missing any required pattern (one-line evidence: which patterns are missing); `✅` when all three are ignored. Tag `→ /red-setup`. During this read-only pass: never write `.red/.gitignore`. Skip the check silently when `.red/` does not exist.
12. **AFK hook / backpressure static validation** — read `.red/config.yaml` (the `plugins.dev.afk.backpressure` list + the `plugins.dev.afk.hooks.<point>` entries, legacy bare `afk.*` fallback) and the `.red/hooks/<point>/` tree, and classify every backpressure command and hook script **statically — never execute one** (the trust model is "scripts in your own repo are trusted"; this check resolves, it does not run). Per command: `❌` when it references a renamed/missing `package.json` script (`pnpm run <gone>`) or a non-existent file path, or a `red-*` library/shadow target that does not resolve; `⚠️` (conservative) when it cannot be statically resolved (a bare PATH binary like `curl`/`make` — maybe valid, never a hard fail); `✅` when it resolves. **Unknown hook names** (today a hard boot error) are pre-caught here read-only and reported `❌` before the next drain parks every issue. Tag `→ /red-setup`. The classifier is `apps/plugin-dev/src/core/hook-doctor.ts` (`validateHookConfig`), driven by the canonical hook registry (`hook-registry.ts`, #834). During this read-only pass: never write config and **never execute a command**.
    - **AFK Worktree setup declaration** — in the same read-only config pass, compare `plugins.dev.afk.setup` with root lockfiles, `package.json.packageManager`, and detected `lefthook`/`husky` dependencies or `prepare`. Report `❌` when setup is undeclared, invokes a different package manager, or runs a detected hook manager without `LEFTHOOK=0`, `HUSKY=0`, or explicit `--ignore-scripts`; report `✅` for the confirmed match. Tag `→ /red-setup`, which owns inspection and human confirmation. The classifier is `apps/plugin-dev/src/core/worktree-setup-doctor.ts` (`auditWorktreeSetup`). Never run the declared command to test it.
13. **Per-plugin runtime distribution** — audit, per plugin (`dev`, `memory`, `brain`), the **ADR 0084 control-plane contract**: a plugin the config marks on **must** have a present, readable, checksum-valid, current cached bundle. This turns the former **silent-no-op class** (`enabled: true` whose runtime never arrived) into visible, named findings. Read the nearest `.red/config.yaml` `plugins.<name>.enabled` flag, the installed version from `.claude-plugin/plugin.json`, and the cached bundle (`~/.cache/red-skills/bundles/<plugin>-<version>.bundle.min.mjs`, ADR 0034/0038). Three failure axes:
    - **Enabled vs runtime present** — `plugins.<name>.enabled: true` but **no cached bundle** → `❌ runtime-missing`; only the **inert marker a failed fetch left behind** → `❌ inert-marker`. The plugin is "on" but does nothing.
    - **Version drift** — the cached bundle is **behind the latest compatible Release**, stale beyond the self-update expectation (#1033) → `⚠️ version-drift`. It still runs, just not the current runtime. **Suppressed when the latest release can't be resolved** (offline / rate-limited) — never a false positive when we could not ask.
    - **Cache integrity** — the cached bundle is **unreadable or its bytes fail the published sha256** → `❌ cache-corrupt` (outranks drift: a bundle whose bytes are wrong has no version to trust).

    A **disabled** plugin is **inert by design** (the ADR 0067 gate) → never a finding, whatever its cache. A **healthy three-plugin setup produces zero findings**. Every finding's remediation is the **launcher fetch** (`red-fetch.mjs <plugin> <version>`), never a hand edit of a fetched asset. Tag `→ launcher fetch`. The classifier is `apps/plugin-dev/src/core/runtime-doctor.ts` (`auditRuntimes`), pure + IO-free like `hook-doctor.ts` (every cache/version fact is injected). During this read-only pass: never fetch a bundle and **never touch the network** — the audit reads observed facts, it does not resolve them.
14. **`req:<Spec>` dependency-edge audit** — dependency edges must point at executable slices, never at a Spec (see `triage-labels.md` *Dependency Edges*; #907/#928 incident). List every open issue carrying a `req:N` label whose target #N carries `type:spec`, and emit **one warn line per offending edge** (`⚠️ #<dependent> req:<N> → #<N> is type:spec — re-point at its spec:<N> slices`). Resolve it read-only: for each `req:*` label in use (`gh label list --search req:` or scan `gh issue list --label req:<n>`), check the target's labels with `gh issue view <N> --json labels`; a target with `type:spec` is a finding. Report `✅` when no `req:<Spec>` edge exists, `⚠️` with the count and per-edge lines otherwise. Tag `→ /triage` (re-point each offending edge). During this read-only pass: never edit a label.
15. **Native blocked-by vs `req:N` divergence audit** — ADR 0094 deliberately keeps two dependency surfaces: native GitHub blocked-by edges for humans and `req:N` labels for the AFK runtime. Compare them across open Tickets (exclude parent Specs carrying `type:spec`) and emit one warn line per divergent edge in either direction: native blocked-by edge without the matching `req:N` label, or `req:N` label without the matching native blocked-by edge.

    Concrete read path (read-only GETs only, matching `/red-setup` issue-tracker-github *Dependency & hierarchy operations*):

    ```bash
    gh issue list --state open --json number,labels \
      --jq '.[] | select(([.labels[].name] | index("type:spec")) | not) | {number, labels: [.labels[].name]}'

    gh api "repos/{owner}/{repo}/issues/<ticket-number>/dependencies/blocked_by" \
      --jq '.[] | {number, id, state, title}'
    ```

    Run the second command once for each open non-Spec Ticket from the first command. Build the injected audit input as one object per Ticket:

    ```ts
    { number: <ticket-number>, labels: ["req:<blocker-number>", ...], nativeBlockedBy: [<blocker-number>, ...] }
    ```

    `labels` comes from the `gh issue list` row. `nativeBlockedBy` comes from the `number` fields returned by the Ticket's `dependencies/blocked_by` endpoint. Do not use `issue_dependencies_summary.blocked_by` for this check; it is only a count and cannot populate the blocker list. Feed the resulting array to `auditDependencyEdges()` and report `✅` when every open Ticket's native blocked-by set equals its `req:N` label set, or `⚠️` with the count and per-Ticket lines otherwise. Tag `→ /triage` (refresh dependency metadata so both surfaces match). The pure classifier is `apps/plugin-dev/src/core/dependency-edge-doctor.ts` (`auditDependencyEdges`); every GitHub fact is injected, and the check is read-only. During this read-only pass: never add/remove labels and never create/delete native edges.
16. **Native sub-issue vs `spec:N` divergence audit** — ADR 0094 also keeps two hierarchy surfaces: native GitHub sub-issue edges for humans and `spec:N` labels for the AFK runtime. Walk open plus recently-closed Specs carrying `type:spec`, compare every label-child carrying `spec:<Spec>` against the Spec's native sub-issues, and emit one warn line per divergent edge in either direction: `spec:N` child without the matching native sub-issue edge, or native sub-issue child without the matching `spec:N` label. Also flag a Spec that still carries `needs-slicing` once it has at least one `spec:N` child. Concrete read path (read-only GETs only):

    ```bash
    gh issue list --label type:spec --state all --json number,state,closedAt,labels
    gh issue list --label spec:<spec-number> --state all --json number,labels
    gh api "repos/{owner}/{repo}/issues/<spec-number>/sub_issues" \
      --jq '.[] | {number, id, state, title}'
    ```

    Build the injected audit input as one object per Spec:

    ```ts
    { number: <spec-number>, labels: ["type:spec", ...], labelChildren: [<ticket-number>, ...], nativeSubIssues: [<ticket-number>, ...] }
    ```

    Feed the resulting array to `auditSpecSubIssueEdges()` and report `✅` when every Spec's `spec:N` child set equals its native sub-issue set, or `⚠️` with the count and per-Spec lines otherwise. Tag `→ Spec sub-issue reconciler`. The pure classifier and fixer live in `apps/plugin-dev/src/core/spec-subissue-reconciler.ts` (`auditSpecSubIssueEdges`, `executeSpecSubIssueReconcile`); every GitHub fact is injected. During this read-only pass: never add/remove labels and never create/delete native edges.
17. **ask-red router coverage sync** — compare the registered dev skill names from the plugin manifest with the slash-command names covered by `plugins/dev/skills/engineering/ask-red/SKILL.md`. Emit one warn line for each registered skill missing from the router and each stale router entry that no longer names a registered skill. Report `✅` when both sets match, `⚠️` with the finding count otherwise. Tag `→ ask-red maintenance rule` (update the router's Coverage Inventory and route text). The pure classifier is `apps/plugin-dev/src/core/ask-red-router-doctor.ts` (`auditAskRedRouterCoverage`); every manifest and router fact is injected, and the check is read-only. During this read-only pass: never edit manifests and never rewrite `ask-red`.
18. **Host toolchain** — report `gh` presence and version against the repo-declared `GH_MIN_VERSION` (`gh >= 2.47.0`). When `gh` is present, detect whether it is managed by asdf `github-cli` (shim or `.tool-versions` pin), apt, brew, or a direct binary, and print that manager's exact upgrade recipe. For ADR 0097, `tq` is mandatory, and its version is a **floor rather than an equality**: the danger runs one way only — a `tq` OLDER than the library cannot read what the library writes, while a newer one reads everything, because that is how the format ships. Read the catalog-derived `@reddb-io/toon` version and, where the repo has no pnpm catalog to derive from, `.red/config.yaml` `host_binaries.tq.version` (floor `0.26.2`); the floor is whichever of the two is higher. Report `❌` when `gh` is absent or older than the minimum, when `tq` is absent, or when the observed `tq` is BELOW that floor, which is the `toolchain-drift` finding. A host ahead of the floor is `✅` — reddening it reported a late toolchain watcher as the operator's fault and told them to downgrade (#3466). Absence or version drift is a red finding, not a warning, because `gh` compatibility is required by fleet landing and there is no jq fallback for RedSkills-owned TOON/TOONL logs. Tag `→ /red-doctor --fix` and print the canonical package-manager fix: `cargo install reddb-io-tq --version 0.26.2 --locked --force`. The pure classifiers are `apps/plugin-dev/src/core/host-toolchain-doctor.ts` and `apps/plugin-dev/src/core/host-binary-doctor.ts`; every version and manager fact is injected. During this read-only pass: never run package-manager installs during Pass 1, never execute an upgrade or installer during Pass 1, never mutate `.red/config.yaml`, and never offer a jq fallback.
19. **.red lifecycle taxonomy + tmp janitor** — audit the target repo's `.red/` tree against ADR 0098's lifecycle tiers and named lane registry. Emit one taxonomy finding for each violation: a loose file directly under `.red/tmp/` (target `.red/tmp/scratch/`), a directory directly under `.red/tmp/` that is not in the lane registry (target a registered `.red/tmp` lane or extend ADR 0098), a known durable-state filename still living under `.red/tmp/` (target the matching `.red/state/*` lane or file), and a top-level `.red/` directory not documented by ADR 0098 (target a documented tier or an ADR extension). Also report reclaimable tmp state from the ADR 0098 janitor: stale worker dirs whose represented issues are closed and whose `worker.pid` is not live, expired managed lanes from `planTmpJanitor` (`logs`, `scratch`, `diagnostics`, `worktrees/feedback`), and unknown tmp-root entries from `auditTmpRoot`. Report `✅` when no taxonomy or reclaimable-tmp findings exist, `⚠️`/`❌` with the finding count otherwise. Tag `→ ADR 0098 lane owner`. The pure taxonomy classifier is `apps/plugin-dev/src/core/red-taxonomy-doctor.ts` (`auditRedTaxonomy`); the janitor planner is `apps/plugin-dev/src/core/tmp-janitor.ts` and runtime adapter is `apps/plugin-dev/src/runtime/tmp-janitor.ts`. During the read-only pass: never move, delete, or create files; tmp janitor output is counts plus paths only.
20. **Worker state lane** — audit the durable AFK Worker state lane after the boot migration. When `.red/state/castle/` is present, validate that present `history.toonl` and `validation.toonl` files are TOON-decodable against the castle state contracts, and that `workers/<id>/state.toon` / `supervisors/<id>/state.toon` snapshot directories decode as `red.castle.state.v1` with matching `kind` and `id`. Flag a populated legacy `.red/state/afk/` directory only when a live castle state lane also exists — that is post-migration residue, not a repo with no AFK history. Repos with no castle state lane and no legacy AFK lane pass clean. Tag `→ red-path-migration`. The classifier is `apps/plugin-dev/src/core/castle-state-doctor.ts` (`auditCastleStateLane`). During this read-only pass: never rewrite history, validation, or snapshots; never delete legacy residue.
21. **Unlanded `.red/` docs** — detect glossary/ADR docs that exist in the primary checkout but are not landed on `origin/{base}`. Use the shared Docs Sweep detector (`apps/plugin-dev/src/core/docs-sweep.ts` via `apps/plugin-dev/src/core/unlanded-docs-doctor.ts`); never implement a second `.red/` docs comparison. The read path fetches/verifies `origin/{base}` first, compares the same doc paths as the ADR 0092 finalizer (`.red/CONTEXT.md`, `.red/CONTEXT-MAP.md`, `.red/contexts/**`, `.red/adr/**`), includes untracked files, and reports a scorecard finding with the rendered file list (`state:path`). Report `✅` when the shared detector is clean, `⚠️` when docs can be landed, and `❌` when the detector halts because origin is unreachable or a zero-precedent ignored doc cannot be safely landed. Tag `→ ADR 0092 doc-landing lane`. The doctor helper is pure and IO-free; every git/GitHub fact and the lander are injected. During this read-only pass: never create a branch, push, open a PR, merge, or mutate the primary checkout.
22. **Operational probe registry** — run the shared operational probe registry (`apps/plugin-dev/src/core/operational-probes.ts`), the same surface fleet boot consumes after precheck. Report one row per probe and one finding per red probe, always naming the probe, evidence, and canonical fix. The first proof probe is the local HTTPS-remote refusal: local AFK boot requires SSH remotes unless the Actions lane explicitly allows HTTPS. Tag `→ operational probe registry`. During this read-only pass: never rewrite remotes and never execute a fix; `--fix` owns gated remediation. AI-facing output is TOON.
23. **Executable ticket acceptance-criteria lint** — list open executable candidates carrying `ready-for-agent` and lint each issue body for machine-checkable acceptance criteria. The body must contain an `## Acceptance criteria` section with checklist items that name a verifiable artifact: a test, command, fixture, or pinned observable behavior. Feed the injected issue bodies to `apps/plugin-dev/src/core/executable-acceptance.ts` (`lintExecutableAcceptanceCriteria`) and report `✅` when every executable candidate passes, or `⚠️` with one line per failing issue naming the missing piece. Tag `→ /triage` because triage owns the authoring validation and the idempotent recipe comment. During this read-only pass: never edit labels and never post comments; doctor reports executable ticket acceptance-criteria lint (check 23) only.
24. **Execution daemon provisioning** — report whether this host has a reachable `redskilled` daemon (ADR 0130). Four checks, in the order they must be cured: `home` (the host-scoped `~/.red/redskilled/`, owner-only `0700`), `daemon-entry` (a published bundle to run, naming every probed path when none resolves), `reach` (a daemon answered a ping on the session socket), and `supervisor-unit` (the optional user unit). Report `✅` when the verdict is `ok`, `❌` when any check is `missing`, and `⚠️` when the home exists but drifted wider than owner-only. **The unit is reported and never flagged** — an absent unit is `ok` with a stated absence, because a host with no `systemd --user` session is provisioned directly and a doctor that reddens over a host arrangement it cannot have teaches operators to ignore a red row. Tag `→ /red-setup` (Section E3), which provisions by running `npx -y -p @reddb-io/red-skills@<version> red-skills-redskilled provision`. The pure classifier is `apps/redskilled/src/provision.ts` (`auditRedskilledProvisioning`); every host fact is injected by `readRedskilledProvisionFacts`. During this read-only pass: **never spawn the daemon** — reach is probed with a ping, because a report that started the daemon it was asked about would answer its own question — and never create the home, which belongs to `redskilled` and not to this doctor.
25. **HUMAN-ONLY type declaration** — the label and its declaration are ONE protection with two halves (#2966, #3013), so check the pair, not either half. List the tracker's labels, read `.red/config.yaml` as written, and report `⚠️` for every installed HUMAN-ONLY type label (`wayfinder:grilling`, `wayfinder:prototype`) that `afk.labels.hitl_types` does not name — a repo carrying the label without the declaration LOOKS protected while every unblocked Ticket of that type enters the autonomous queue. Report `✅` when each installed one is declared, when none is installed, or when there is no issue tracker to ask; report `❌` when the label list could not be read (an unreadable tracker is not a clean repo) or when `.red/config.yaml` does not parse. The pure audit is `apps/plugin-dev/src/core/hitl-type-declaration-doctor.ts` (`auditHitlTypeDeclaration`). Tag `→ /red-doctor --fix`, which merges the missing entry after a diff preview. A repo with no `.red/config.yaml` at all is still the finding, but tagged `→ /red-setup`: only `/red-setup` creates a repository's `.red/` (ADR 0067). During this read-only pass: never create the label, never write config.

26. **Marketplace registration source** — **red-dev owns the RedSkills registration, so this check reports it and never rewrites it.** red-dev acquires RedSkills and wires each host CLI from a tree it manages, which registers as a **Directory** source; the retired standalone installer registered the **GitHub** source instead, and healing a directory registration back to that repository is exactly how a re-run tore out red-dev's wiring (#3978). Read each installed host CLI's registrations (`claude plugin marketplace list`, `codex plugin marketplace list`) and classify the `red-skills` entry's source. Report `⚠️ standalone-source` for a **GitHub** or **git** source (the retired installer's leftover — it still resolves, so it is a warning, and the cure is the bootstrap `mise use --global red-dev@1 && red-dev install`), `⚠️ source-unknown` when the CLI could not answer (an unreadable registration is not a clean one), and `✅` for a **Directory** source, an unregistered marketplace, or a host CLI that is not installed. Gemini installs from a local path through the documented flow, so it is not probed. **There is no `--fix` here**: the registration belongs to red-dev, and a doctor that writes its own is the second owner this check exists to keep off the machine. The pure classifier is `apps/plugin-dev/src/core/marketplace-source-doctor.ts` (`auditMarketplaceSources`); every host transcript is injected. During this read-only pass: never add, remove, or update a registration.

27. **Declared-but-unloaded MCP servers** — a host CLI registers MCP servers **at plugin load**, so a plugin installed or updated **mid-session** has its declaration written and its server processes never started: `.mcp.json`, the manifests and the launchers all valid on disk, and zero tools in the session. The symptom wears the shape of an outage and reads as "the marketplace does not load our MCPs right", when the cure is one line. For each plugin the config marks `enabled: true`, read the server names its `.mcp.json` declares (in-repo `plugins/<name>/.mcp.json`, the `CLAUDE_PLUGIN_ROOT`/`CODEX_PLUGIN_ROOT` sibling, or the host's marketplace checkout) and compare them against the MCP servers this session actually sees. Report `❌ declared-unloaded` when the session sees **none** of them, `⚠️ partially-loaded` when some loaded and some did not (one server failed, not the whole load), `⚠️ session-unobserved` when nobody stated the session, and `✅` when every declared server is present, when the plugin declares none, or when the plugin is disabled — a disabled plugin is inert by design, so its absent servers are not a finding. Every finding names the cure verbatim: **restart the session, or run `/reload-plugins`**.

    **The seam, stated honestly.** This doctor is a CLI process; it cannot introspect its host's loaded MCP servers, so the session half is **injected by the caller**: pass `--session-mcp "<servers this session sees>"`, either bare server names (`rs_dev`) or the host-prefixed tool names an agent reads off its own tool list (`mcp__plugin_dev_rs_dev__project_status`) — the audit resolves the slug either way. `--session-mcp none` (or an empty value) is the explicit statement "I see none", which is the observation the whole check turns on. **Omitting the flag is never read as a clean session**: it produces the `session-unobserved` warn that names the flag, because a doctor that reported ✅ on a dimension it never asked about is the silence this check exists to end.

    Tag `→ host session reload`. The pure classifier is `apps/plugin-dev/src/core/mcp-load-doctor.ts` (`auditMcpLoad`); both halves are injected. During this read-only pass: never restart a host, never reload plugins, never start an MCP server, and never edit `.mcp.json` — the cure belongs to the operator's session, not to a doctor.

28. **Uncommitted `/red-setup` output** — `/red-setup` writes `.red/config.yaml`, `.red/.gitignore` and `.red/hooks/**` and is forbidden to `git add` any of them, so a freshly set-up repository is **dirty by contract** and its operator is never told (#3106). Read the target repo's `git status --porcelain` and report `⚠️ uncommitted-setup-files` naming every dirty path setup owns, `✅` when none is pending. The operator's own WIP is not this check's business and never appears in the evidence. Report the state, never cure it: the whole reason setup leaves these files uncommitted is that landing `.red/` in git is the operator's decision. Tag `→ /red-setup closing report`. The pure classifier is `apps/plugin-dev/src/core/setup-owned-dirt.ts` (`classifyDirtyTree` + `auditSetupOwnedDirt`). The setup-owned path list exists only for this doctor finding; the trunk-freshness guard uses the shared porcelain parser but judges every dirty path by collision with incoming commits, never by ownership. During this read-only pass: never `git add`, never commit, never edit `.gitignore`.

29. **Project registration liveness** — read the current project's daemon registration state and the executable `ready-for-agent` queue already collected for check 23. Report `❌ lapsed-with-work` when the daemon holds no current registration, its bounded lapse tail records when and why the project expired, and executable work remains queued. A missing daemon answer is `unknown`, never a clean result. The check is read-only: it never spawns the daemon or registers the project. Tag `→ AFK runtime`; the independent registration belt restores a recently lapsed active drain when its next queue observation confirms work, while an older runtime must be upgraded or restarted.

30. **Feedback command authority** — read `plugins.dev.afk.feedback.commands` and the Trunk's branch-protection required status-check contexts. An absent declaration is `skip` because the discovered local `test`/`typecheck`/`lint`/`build` harness remains intact. A declaration replaces that harness, including `commands: []`; report `✅` only when branch protection requires a check named `test`, and `⚠️ narrowed-feedback-without-required-test` when it does not. An unreadable protection surface is `⚠️ required-checks-unavailable`, never green. Tag `→ branch protection / AFK config`: require the merge queue's `test` check or restore discovered local feedback. The pure classifier is `apps/plugin-dev/src/core/feedback-authority-doctor.ts` (`auditFeedbackAuthority`); config and GitHub facts are injected. During this read-only pass, never edit branch protection or `.red/config.yaml`.

31. **Validation declaration vs engine** — compare every configured moment key under `plugins.dev.afk.validation` with both the parser vocabulary and the lifecycle engine's independent `ENGINE_VALIDATION_MOMENTS` registry. Report `❌ unsupported-declaration` when a project declares a moment the engine cannot run, `❌ declaration-not-wired` when config accepts a moment the engine omitted, and `❌ engine-not-declarable` when the engine runs a moment no project can declare. A matching declaration is `✅`, including an intentionally undeclared moment because ADR 0135 says it skips. Tag `→ AFK config / engine`: remove or rename an unsupported project key, or wire the two code registries together in the owning implementation. The pure classifier is `apps/plugin-dev/src/core/validation-moment-doctor.ts` (`auditValidationMomentDrift`); all facts are injected. During this read-only pass, never run a Validation command and never edit `.red/config.yaml`.

**Scorecard** (always printed): one row per check (✅/⚠️/❌ + one-line evidence) + a readiness score (count of green checks, like `context-status`) + a prioritized recommendation list, **every recommendation carrying a fix-home tag** from the *Fix-home* table. End with the single highest-impact next step.

### Pass 2 — Fix (only with `--fix`; gated apply)

For **every** non-green finding from Pass 1, apply its canonical fix. Running with `--fix` → read [`APPLY.md`](APPLY.md) for the per-finding action and gate. There is no finding the doctor reports but cannot heal: each row maps to a concrete action or a delegation to the single-writer tool that owns it.

The loop:

1. **Group the findings** into **safe** (idempotent, low-blast-radius) and **hard-to-reverse** (per the gate column in [`APPLY.md`](APPLY.md)).
2. **Apply the safe batch** — run each safe fix, print a one-line receipt per action (`✅ created label needs-triage`, `✅ injected ## Development workflow into AGENTS.md`, …). Re-running is a no-op.
3. **Confirm each hard-to-reverse fix individually** — show the exact mutation (the `gh label rename old new` that re-tags N issues, the config-key migration, the `blocked:*` removal on issue #N, the `.mcp.json` edit) and apply only on an explicit yes. A no leaves that finding open and recorded.
4. **Delegate** what a single-writer tool owns — a version mismatch runs the version/release tool (never a manual manifest edit); context-stack gaps run the `memory`/context skills. `--fix` triggers the tool, then re-checks.
5. **Re-diagnose** the touched checks and print a fix receipt: what was applied, what was confirmed-then-applied, what was skipped (declined), and the new readiness score.

</what-to-do>

<supporting-info>

### Label classes (check 2)

| Class | Meaning | Action |
|---|---|---|
| ❌ non-canonical synonym | duplicates a canonical role under a different name (`needs-human-decision` ↔ `ready-for-human`) | rename to the canonical role |
| ⚠️ legacy / superseded | older form replaced by a newer one (bare `blocked` vs typed `blocked:<reason>`) | migrate + retire |
| ⚠️ naming violation | not kebab-case nor `prefix:value` (uppercase / CamelCase / snake_case / spaces) | normalize |
| ✅ accepted aux | outside the triage families but legitimate (language labels, repo-custom like `drill`, `release-blocker`) | none |
| ✅ GitHub default | `bug`, `enhancement`, `documentation`, `duplicate`, `good first issue`, `help wanted`, `invalid`, `question`, `wontfix` | none |

Canonical families live in the target repo's `.red/agents/triage-labels.md`: state (`needs-triage`, `needs-info`, `ready-for-agent`, `running`, `ready-for-human`, `wontfix`), dependency (`blocked:dependency`, `req:N`), typed blocked-reasons (`blocked:quota|runner-transient|merge-conflict|spec|validation|crashed|policy|stalled|infra`), type (`type:spec`, `type:bug`, `needs-slicing`), priority (`priority:high|low|urgent`), relationship (`spec:N`), verification bar (`verify:live|tests|gate-only` — the minimum Countersign class a Ticket's land requires, ADR 0156 §2), operational (`runner-error`).

### Operational probe families

Check 22 is the shared operational probe registry (`apps/plugin-dev/src/core/operational-probes.ts`). The registry is the source of truth for what it checks, the evidence it shows, and the canonical fix text. `/red-doctor` renders the probes read-only by default; destructive fixes are individually gated under `--fix` by each probe's `fix.gate: "confirm"` contract. If a probe has no wired automated fixer, `--fix` reports a `noop` receipt instead of inventing a mutation.

| Probe id | Probe name | What it checks | Evidence it shows | Fix authority |
|---|---|---|---|---|
| `git.remote.https-forbidden` | SSH-only git remotes | Local AFK boot is not using HTTPS git remotes unless the Actions lane explicitly sets `allowHttpsRemote`. | Count of forbidden HTTPS remotes; named remotes also carry the SSH rewrite target in probe data. | Confirm before rewriting named remotes to SSH; otherwise use SSH manually or run in the Actions lane. |
| `afk.queue-visibility` | AFK queue visibility | The AFK engine and REST queue readers can both list the same open queue issue set; a mismatch is re-sampled after a short delay before it becomes red. | Transport class (`sso-or-scope`, `rate-limit`, GraphQL/REST/generic transport), an info log when transient skew clears, or persistent engine-vs-REST counts plus the symmetric-difference issue numbers. | No mutation; repair GitHub auth, SSO, rate limits, or network reachability, then restart `/afk`. |
| `afk.focal-branch-resolution` | AFK focal branch resolution | The resolved focal branch from branch-lock, pin, or trunk is coherent and any branch-lock target still exists or is live-held. | Resolved branch/source, configured trunk, raw lock value, target existence, and live-holder state. | Confirm before clearing a stale branch-lock; live intentional locks stay in place. |
| `afk.base-freshness` | AFK local trunk freshness | The local trunk is not behind `origin/<trunk>` before autonomous work starts. | Local/remote SHAs when known, ahead/behind counts, the shared finalizer guard verdict, and local-to-origin SHA pairs for superseded commits. | Fleet boot auto-applies the reconciliation when the guard says on-trunk, clean tree, and either local ancestor or every local-only commit patch-equivalent on origin; `red-doctor --fix` still confirms before applying the same guarded repair manually. |
| `afk.fleet-truth` | AFK fleet truth | The recorded fleet supervisor pid, heartbeat/state freshness, and bundle version describe a live, current fleet. | Pid liveness, heartbeat/state ages, threshold, bundle/latest version, red finding kind (`zombie`, `version-skew`), and inconclusive note kind (`version-unknown` — reported and logged, never red). | Confirm before SIGTERM for a zombie supervisor; optional relaunch is separately confirmed. |
| `afk.bundle-coherence` | AFK bundle coherence | The stable pointer, newest cached lane bundle, npm newest same-major version, and last self-update check are coherent. | Installed, pointer, lane, and npm versions plus stale failed-check evidence. | No mutation; run any dev shim to reconcile pointer-vs-lane, let self-update retry or warm the package cache, then restart stale fleets. |
| `afk.claim-hygiene` | AFK claim hygiene | Open queue issues do not carry dangling claim markers from this machine's dead workers, foreign namespaces, or unknown own workers. | Issue number, marker comment id, namespace, worker, and pid state, plus live/unknown counts. | Boot heals provably dead own-machine claims under the per-issue 24h ledger; the third heal and judgment-requiring foreign/unknown ownership quarantine only that issue. |
| `afk.label-body-coherence` | AFK label/body coherence | `ready-for-agent` issues do not still carry an active `Current blocker` section in their body. | Issue number, labels, blocker kind/ref/summary/next. | **Boot quarantines automatically** (removes `ready-for-agent`, adds `quarantine`, appends the diagnosis to the issue body), then continues boot. The Issue curator auto-releases resolved issues or parks them `ready-for-human` after three failed re-checks. `red-doctor --fix` still confirms per issue before archiving the blocker into resolved history and clearing the current blocker. |
| `config.coherence` | Config coherence | The real config loader successfully parsed `.red/config.yaml`, did not discard it for defaults, and root-level folded accessor spellings do not shadow the canonical `plugins.dev.*` block. | Loaded file, malformed line/construct when parse fallback happened, off-contract root keys with canonical relocation, or healthy resolved trunk/gate/lock values. | Confirm after diff preview before relocating root-level blocks to `plugins.dev.*`; malformed syntax is reported with the offending line/construct and must be repaired before boot. |
| `runtime.process-census` | Runtime process census | The host has no stamped orphans, aged unstamped suspects, or crash dump files hidden outside the daemon's Worker accounting. | Counts for active Worker units, daemon-held Workers, stamped orphans, unstamped suspects, and dump files. | **Detection only** in `/red-doctor`: inspect the same census with `npx -y -p @reddb-io/red-skills@<version> red-skills-redskilled reap --report`. Report mode performs no adoption, signalling, or deletion; the periodic daemon reaper remains the owner of stamped-orphan cleanup. |
| `runtime.lane-census` | Runtime lane census | The registered project and host TOONL lanes stay within retention policy, and no unregistered TOONL lanes or dead-pid replacement temps remain. | Every registered lane's bytes and lines against each declared ceiling, plus redacted project/host paths for unknown lanes and dead temps. | **Detection only** in `/red-doctor`: repair the owning writer so it enforces its ceiling or declares the lane; the owning boot sweep removes dead-pid temps. The probe never trims or deletes state. |

**Fleet boot refusal.** The fleet boot path runs the same operational probe registry after precheck. A queue-visibility mismatch is re-sampled after a short delay; transient skew that clears is logged as info and boot continues, while persistent divergence remains red with the differing issue numbers. If `afk.base-freshness` is red and its own shared finalizer guard already passed, boot auto-applies the guarded reconciliation, logs the before/after SHAs plus any superseded local-to-origin SHA pairs, and continues. `afk.claim-hygiene` heals provably dead own-machine claims within the ledger budget and quarantines the issue on the third heal or when ownership needs judgment. `afk.label-body-coherence` likewise quarantines each incoherent issue (`ready-for-agent` → `quarantine`, diagnosis appended to the body) and continues — a single dirty issue never halts the whole execution plane. Per-issue tracker write failures stay locally excluded for that drain and never halt healthy siblings; boot halts only when a quarantine mutation itself fails. For other red probes, including repo-level corruption and a refused base-freshness guard, boot refuses to spawn workers and raises `BootHaltError("operational-probe")`, prints the probe name, evidence, and canonical fix. Doctor can show the same findings without mutation, and `red-doctor --fix` can apply only probe-owned, confirmed repairs.

### Fix-home (every Pass-1 recommendation carries one)

| Fix-home | Findings it owns |
|---|---|
| `→ /red-setup` | execution daemon provisioning (check 24) — run `npx -y -p @reddb-io/red-skills@<version> red-skills-redskilled provision`, which creates the host-scoped home and starts the daemon; a HUMAN-ONLY type label in a repo with no `.red/config.yaml` (check 25) — only `/red-setup` creates a repository's `.red/`; AGENTS≡CLAUDE `## Agent skills` parity, AGENTS≡CLAUDE `## Development workflow` parity, `dev.lock.primary-branch` adoption, statusline drift, MCP wiring, label provisioning, `.red/.gitignore` self-ignore, workflow naming-convention drift (`reusable-*`/`rs-*`/`red-*` by role) + AFK-lane auth gap, AFK Worktree setup declaration drift, and AFK hook/backpressure static-validation findings (stale `package.json` script, missing file, unknown hook name). |
| `→ /red-doctor --fix` | HUMAN-ONLY type label without its `afk.labels.hitl_types` declaration (check 25) — the merge is a confirmed config write, previewed as a diff first; host toolchain findings — an approved asdf-managed `gh` upgrade and the canonical pinned `tq` installer; sudo-backed and other `gh` managers remain report-only instructions. |
| `→ AFK runtime` | `blocked:*` accumulation (labels must be rotated/cleared on re-queue, plus the re-claim cap); project registration liveness (check 29) — the daemon's independent sustain/recovery belt owns repair, and the doctor only exposes a lapse that still has queued work. |
| `→ launcher fetch` | per-plugin runtime distribution findings (`runtime-missing`, `inert-marker`, `version-drift`, `cache-corrupt`) — the cache is owned by the launcher (`red-fetch`/`afk.mjs`, ADR 0034/0038), never hand-edited. |
| `→ /triage` | `req:<Spec>` dependency edges (check 14) — re-point each offending edge at the target Spec's executable slices; native blocked-by vs `req:N` divergence (check 15) — refresh dependency metadata so both surfaces match; executable ticket acceptance-criteria lint (check 23) — refresh the issue body so `## Acceptance criteria` contains machine-checkable checklist items, then let `/triage` re-run the readiness transition. `/triage` owns the authoring validation. |
| `→ Spec sub-issue reconciler` | native sub-issue vs `spec:N` divergence (check 16) — run the shared reconciler to attach missing native sub-issue edges and remove stale `needs-slicing` from Specs that already have slices. |
| `→ ask-red maintenance rule` | ask-red router coverage sync (check 17) — update `plugins/dev/skills/engineering/ask-red/SKILL.md` when skills or flows change. |
| `→ ADR 0098 lane owner` | `.red` lifecycle taxonomy (check 19) — move content through the writer that owns the source/target lane, or amend ADR 0098 before introducing a new top-level directory or tmp lane. |
| `→ red-path-migration` | the Worker's state lane findings (check 20) — run the dev durable path migration entrypoint so `.red/state/castle/` is the single live AFK state lane; never hand-delete ambiguous residue. |
| `→ ADR 0092 doc-landing lane` | unlanded `.red/` docs (check 21) — land exactly one `docs:` PR from an isolated docs worktree, then merge it so `origin/{base}` carries the docs AFK workers need. |
| `→ operational probe registry` | red operational probes (check 22) — use the probe's own canonical fix. Adding a probe requires only its module plus one registry entry; doctor and fleet boot consume the registry generically. |
| `→ /red-setup closing report` | uncommitted `/red-setup` output (check 28) — the cure is the operator's own `git commit` of the files setup wrote, or a deliberate decision to keep `.red/` out of git. `--fix` never stages them: a doctor that committed a repo's config on the operator's behalf would decide the thing setup deliberately left open. |
| `→ host session reload` | declared-but-unloaded MCP servers (check 27) — the cure is a host-session action, not a repo edit: restart the session, or run `/reload-plugins`. `--fix` never performs it, because a doctor that restarted the session it was invoked from would kill its own caller. |
| `→ AFK config / engine` | Validation declaration/engine drift (check 31) — remove or rename an unsupported configured moment, or update the parser and lifecycle registries together; the read-only doctor never runs the declared commands. |
| `→ manual / maintainer` | label renames (`gh label edit`), retiring legacy labels — the operator decides. |
| `→ release` | cross-manifest version mismatch — owned by the single-writer version script + `validate-install-metadata.sh` gate (ADR 0040); never hand-edit one manifest. |

### Scope & boundaries

- **Single repo** by default; multi-repo sweep is opt-in. With `--fix`, a sweep applies per-repo with the same gating.
- **Public-repo safe**: reads only conventions already public in the repo; **emits no secret values** (it may list secret *names* via `gh secret list` to detect the AFK-lane auth gap — names are not sensitive — and never reads or prints a value). It does **not** impose RedSkills' own `red-*` CI (release/bench/drift-guard/upstream-watch) onto an adopter repo — that stays out of scope. What it *does* audit is the **adoption coherence of `rs-*` workflows already installed** (correct installed-name + the AFK lane's auth secret), never installing a lane the repo didn't opt into.
- Pairs with `/adr-editor` (decision-record coherence) and `memory:doctor` (graph health) — three doctors over different axes; this one owns **process/adoption** and is the one that can both diagnose and, with `--fix`, heal.

</supporting-info>
