agentsclimarketplace

Doctor

Skill Scoutflo/ai-readiness/skills/doctor

Preflight connection check; a bundled read-only script reads ~/.scoutflo/toolkit.yaml, makes one cheap call per configured integration, and emits a JSON-per-check matrix with fix hints and distinct exit codes. Use when the user mentions doctor, preflight, connection check, cannot reach Grafana, Sentry, or Prometheus, 401 or timeout errors before an audit, or blocked audit checks. Do not use to create or rotate credentials (use connect) or for scored assessment (use the audit-* skills).From its SKILL.md

Install
npx -y skills add Scoutflo/ai-readiness --skill doctor

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

23.8 KB, as published. Nobody here has run it

Doctor: Connection Preflight

Doctor answers one question before any audit or setup skill spends your time: can this machine actually reach everything ~/.scoutflo/toolkit.yaml says it can, with the credentials currently in the environment?

Run it after /scoutflo:connect, before your first /scoutflo:audit-all, and whenever an audit reports a blocked check. Every audit-* and setup-* skill re-runs the subset of these checks it needs as its own doctor gate; this skill is the full matrix in one place.

All checking lives in one bundled script, scripts/doctor.sh. It is read-only, non-interactive, and never prints a secret: tokens are checked for presence and sent only inside request headers. The single exception to read-only is the optional Slack test post, which sends a visible message and runs only when you pass --slack-test after the user explicitly confirms. Doctor produces no findings.json and no score; it is a preflight, not an audit, and a failed check here is a stop-and-fix, never a finding.

Prerequisites

  • curl required; jq checked because every other skill needs it.
  • yq optional. Without it, the script falls back to a POSIX parser that handles the flat two-level layout of templates/toolkit.yaml.example. Anything more nested needs yq.
  • A vendor CLI only when its provider is configured, and doctor emits a binary-<cli> row (pass/fail) for each so a missing one is a clear, named failure rather than a downstream crash: kubectl (a kubernetes: block), aws (aws:), gcloud (gcp:), doctl (digitalocean:). Every other provider is HTTPS+token and needs no CLI.

Step 0: Report the storage location (no choice required)

You do not have to decide where reports go. By default they land in ./scoutflo-audits/ next to wherever you run — zero configuration, and it just works. This step simply prints the resolved location so you know where to look; it never asks you to pick a directory. Setting a custom location is optional and only matters if you want one durable history that follows you across folders.

The resolved location is:

  • SCOUTFLO_AUDIT_DIR if you have exported it (opt-in, for a fixed location), else
  • ./scoutflo-audits — the default, next to where you launched Claude Code.

Why an env var and not a config value: every command runs in a fresh shell, which inherits your environment but cannot read a per-run config file, so SCOUTFLO_AUDIT_DIR is the only thing that reliably carries the location across all of them. reports_dir in ~/.scoutflo/toolkit.yaml is just a convenience — this step reads it to print the one export line that pins the location, and flags it if set but not yet exported. None of this is required to start; the default is fine for a first run.

set -eu
CFG="${SCOUTFLO_CONFIG:-}"
[ -n "$CFG" ] || for _c in "./.scoutflo/toolkit.yaml" "$(cat "$HOME/.scoutflo/active-config" 2>/dev/null || true)" "$HOME/.scoutflo/toolkit.yaml"; do [ -f "$_c" ] && { CFG="$_c"; break; }; done
[ -n "$CFG" ] || CFG="$HOME/.scoutflo/toolkit.yaml"

# What the runs will ACTUALLY write to (only the env var threads through fresh shells).
if [ -n "${SCOUTFLO_AUDIT_DIR:-}" ]; then EFFECTIVE="$SCOUTFLO_AUDIT_DIR"; EFFSRC="SCOUTFLO_AUDIT_DIR env"
else EFFECTIVE="./scoutflo-audits"; EFFSRC="default (launch directory)"; fi

# What the config ASKS for (used only to generate the export line / detect a gap).
RD=""
[ -f "$CFG" ] && RD="$(sed -n 's/^reports_dir:[[:space:]]*//p' "$CFG" | head -1 | sed 's/[[:space:]]*#.*$//; s/^"//; s/"$//; s/^'\''//; s/'\''$//')"

abspath() { case "$1" in "~"|"~/"*) set -- "$HOME${1#\~}";; esac; case "$1" in /*) printf '%s' "$1";; ./*) printf '%s/%s' "$(pwd)" "${1#./}";; *) printf '%s/%s' "$(pwd)" "$1";; esac; }
EFF_ABS="$(abspath "$EFFECTIVE")"
echo "Reports will be written to: ${EFF_ABS}"
echo "  (source: ${EFFSRC})"

if [ -n "$RD" ]; then
  RD_ABS="$(abspath "$RD")"
  if [ "$RD_ABS" != "$EFF_ABS" ]; then
    echo "  ACTION NEEDED: toolkit.yaml sets reports_dir to ${RD_ABS}, but runs write to"
    echo "  ${EFF_ABS} until SCOUTFLO_AUDIT_DIR is exported. Add this line to your shell"
    echo "  profile (~/.zshrc or ~/.bashrc) AND to ~/.scoutflo/env for scheduled runs,"
    echo "  then relaunch Claude Code:"
    echo "      export SCOUTFLO_AUDIT_DIR=\"${RD_ABS}\""
  fi
elif [ "$EFFSRC" = "default (launch directory)" ]; then
  echo "  This is the default and is fine to start. Optional: if you want one durable"
  echo "  history that follows you across folders, export SCOUTFLO_AUDIT_DIR to a fixed"
  echo "  absolute path once (add it to your shell profile, e.g. ~/.zshrc). Otherwise, just run from the same"
  echo "  folder each time and reports accumulate here with full run-to-run history."
fi

# A stray default tree while an explicit location is in force means unmerged history.
if [ "$EFFSRC" = "SCOUTFLO_AUDIT_DIR env" ] && [ -d "./scoutflo-audits" ] && [ "$EFF_ABS" != "$(pwd)/scoutflo-audits" ]; then
  echo "  WARNING: a separate ./scoutflo-audits exists in this folder from an earlier run;"
  echo "  its history will NOT merge with ${EFF_ABS}. Move it in or ignore it deliberately."
fi

Step 1: Run the script

Fixed step: run as written, do not modify flags, do not re-implement the checks by hand.

# ${CLAUDE_PLUGIN_ROOT} is set by the plugin runtime. Running from a repo
# checkout instead: export CLAUDE_PLUGIN_ROOT as the repo root first.
OUT_DIR="${SCOUTFLO_AUDIT_DIR:-./scoutflo-audits}/doctor/$(date -u +%Y-%m-%d)"   # run directory for this doctor run
sh "${CLAUDE_PLUGIN_ROOT}/skills/doctor/scripts/doctor.sh" --out "${OUT_DIR}"
echo "doctor_exit=$?"
# Expect: doctor_exit=0 when every configured integration passes.
# 1 = toolkit.yaml missing, 2 = a required env var is unset, 3 = a live check failed.

The script emits one JSON line per check to stdout, human-readable progress to stderr, and appends every row to ${OUT_DIR}/matrix.tsv. Reruns append below the earlier rows; for a clean table, delete matrix.tsv first or pass a fresh --out.

Flags:

FlagMeaning
--out DIRRun directory for matrix.tsv. Default: ${SCOUTFLO_AUDIT_DIR:-./scoutflo-audits}/doctor/<UTC date> — the SCOUTFLO_AUDIT_DIR env var if exported, else ./scoutflo-audits (same as Step 0; reports_dir is not read here)
--config FILEConfig to read. Default: ~/.scoutflo/toolkit.yaml
--slack-testSend the Slack webhook test post. Only after explicit user confirmation; see below

Step 2: Interpret the results

Judgment step: read the JSON lines and decide what to fix first. Each line has seven fields:

{"integration":"grafana","check":"health","configured":"yes","env_var":"GRAFANA_TOKEN","result":"pass","http_code":"200","hint":"-"}
FieldMeaning
integrationTemplate-order row: toolkit, grafana, sentry, pagerduty, datadog, elk, jsm, zenduty, groundcover, prometheus, alertmanager, loki, tempo, mimir, victoriametrics, vmalert, digitalocean, gcp, aws, kubernetes, slack
checkWhat was checked: env (variable presence), a live endpoint (e.g. health, identity, org, abilities, analytics, validate, monitors-read, cloudwatch-read, cost-permissions, alerting-health, alerts-read, teams-read, spaces, query, status, config, ready, rbac, account, vmalert-health, monitoring-api, webhook-post), binary-*, or configured for unconfigured rows
configuredyes when the block exists with a non-empty url, host, token_env, project, account_id, or context. Unconfigured rows are informational, never failures
env_varThe *_env variable name, none when the block names no token, - when not applicable. Names only; values never
resultpass, fail, env-missing, or skipped
http_codeCaptured status code; "000" means the transport failed before any HTTP response; null for non-HTTP checks
hintThe concrete fix, quoting the observed failure shape (curl exit code or HTTP status), never a guess

Result vocabulary:

  • pass: the check succeeded.
  • fail: the check ran and failed. The hint says why and what to fix.
  • env-missing: the block names a *_env variable that is not set in this shell. The integration's live checks are recorded as skipped, never attempted, so no empty Authorization header is ever sent.
  • skipped: not attempted for a stated reason (unconfigured, blocked by env-missing, or the Slack post awaiting confirmation). Skipped rows never fail the run.

Exit code precedence: 2 (env var missing) wins over 3 (live check failed), because exporting the variable may fix the live checks too. Fix in that order.

Step 3: Render the connection matrix

OUT_DIR="${SCOUTFLO_AUDIT_DIR:-./scoutflo-audits}/doctor/$(date -u +%Y-%m-%d)"   # same directory used in Step 1
column -t -s "$(printf '\t')" "${OUT_DIR}/matrix.tsv"

Typical output:

integration      check         configured  env_var        result   http_code  hint
toolkit          binary-curl   yes         -              pass     -          -
toolkit          binary-jq     yes         -              pass     -          -
grafana          env           yes         GRAFANA_TOKEN  pass     -          -
grafana          health        yes         GRAFANA_TOKEN  pass     200        -
grafana          identity      yes         GRAFANA_TOKEN  pass     200        -
sentry           env           yes         SENTRY_TOKEN   env-missing  -      export SENTRY_TOKEN in this shell, then rerun doctor
sentry           org           yes         SENTRY_TOKEN   skipped  -          blocked: SENTRY_TOKEN is not set
prometheus       query         yes         none           fail     000        curl exit 7: connection refused; wrong port, service not exposed, or the port-forward is not running
loki             configured    no          -              skipped  -          add a loki block via /scoutflo:connect if you run Loki
kubernetes       rbac          yes         -              pass     -          -
slack            webhook-post  yes         SCOUTFLO_SLACK_WEBHOOK  skipped  -  test post sends a visible channel message; rerun with --slack-test after the user confirms

Close with a verdict:

  • Exit 0: "Ready." Then, only if a kubernetes block is configured: "Run /scoutflo:map-topology (it needs kubectl + a kubernetes.context), then /scoutflo:audit-all." Otherwise send them straight to audit: "Run /scoutflo:audit-all, or a single audit like /scoutflo:audit-grafana." Never point a non-Kubernetes user at map-topology — it is Kubernetes-only and will exit on a missing kubectl/context.
  • Exit 1: no ~/.scoutflo/toolkit.yaml yet — run /scoutflo:connect first, then rerun doctor.
  • Exit 2 or 3: name the affected skills, fix, then rerun doctor. Exit 2 wording names the env var ("audit-sentry will not run until SENTRY_TOKEN is set"); exit 3 wording quotes the live evidence ("grafana failed its live check: http_code 000, curl exit 7 - host unreachable"), never the env var. Never advise starting an audit over a failed row, and never downgrade a doctor failure into a finding.

Verify the verdict mechanically before declaring it:

OUT_DIR="${SCOUTFLO_AUDIT_DIR:-./scoutflo-audits}/doctor/$(date -u +%Y-%m-%d)"   # same directory used in Step 1
awk -F '\t' 'NR>1 && $3=="yes" && ($5=="fail" || $5=="env-missing")' "${OUT_DIR}/matrix.tsv"
# Expect: no output. Every printed row is exactly a check still to fix.

What the script checks

One cheap read-only call per configured integration. Unconfigured integrations are skipped cleanly; they are never failures.

IntegrationLive checkHealthyCommon failure meaning
grafanaGET /api/health, then GET /api/org200, 200401: bad or expired token. 403 on /api/org with health 200: token authenticated but lacks even the Viewer basic role. Transport failure: wrong grafana.url. GET /api/user is deliberately never used: confirmed live on Grafana 10.4.1 that a real, correctly-scoped service-account token hard-403s there regardless of role, since that endpoint identifies an interactively logged-in user, not a service account
sentryGET /api/0/organizations/<org>/200404: wrong region host or org slug; run the region probe in connect references/providers.md
pagerdutyGET /abilities, then POST /analytics/metrics/incidents/all (read-only filter body)200, 200401 on abilities: key invalid/revoked or wrong region host (pagerduty.region us vs eu). The analytics probe never fails the run: 402/403 there records skipped with a reason, because read-only keys are documented GET-only and Analytics may be plan-gated; audit-pagerduty reads that row to include or honestly exclude its actionability section
elkGET /api/alerting/_health (Kibana, Authorization: ApiKey)200404: elk.kibana_url points at Elasticsearch not Kibana, or a space/base-path prefix is wrong. 401/403: key invalid or the role lacks Kibana Read on Stack Rules. The response fields (is_sufficiently_secure, has_permanent_encryption_key) are themselves audit signals
datadogGET /api/v1/validate (API key), then GET /api/v1/monitor?page_size=1 (app key + monitors_read)200, 200Datadog needs a key PAIR; both headers are sent together. 403 on validate: API key wrong or wrong datadog.site (a valid key on the wrong site 403s). 403 on monitor: app key invalid, unscoped for monitors_read, or its creating user was disabled (app keys are user-bound). A cost-permissions row records whether usage_read/billing_read are present; a miss there is skipped, never a failure — audit-datadog reads it to run or exclude the non-scored cost section
jsmresolve cloud_id (from jsm.cloud_id, else the unauthenticated tenant_info route), then GET /jsm/ops/api/<cloud_id>/v1/alerts?size=1 (Atlassian API token over Basic auth)non-empty cloud_id, then 200No cloud_id: jsm.site wrong or its tenant_info route blocked; set jsm.cloud_id explicitly. 401: bad token or email (the token is the Basic password, not a classic Opsgenie GenieKey). 403: the token's user has no JSM Operations access. 404: wrong cloud_id or the site has no Operations
zendutyGET /api/account/teams/ (Authorization: Token <key>)200401/403: key invalid or not prefixed correctly — the header must be Authorization: Token <key> (the literal word Token, not Bearer). 429: rate-limited — Zenduty's per-endpoint-class limits are tight; wait ~1 minute. Zenduty has no read-only key scope, so a Bot Token (Beta) is the least-privilege path
groundcoverPOST /api/monitors/list (Authorization: Bearer, {"sources":[]})200There is no whoami endpoint, so listing monitors is the auth probe (a read-by-query POST, not a mutation). 401: key invalid. 403: key lacks Viewer access, or this is a multi-backend account and groundcover.backend_id (the X-Backend-Id header) is missing or wrong
prometheusGET /api/v1/query?query=vector(1)200vector(1) succeeds even with zero targets, so it tests the API, not your fleet. /-/ready is deliberately not used: it varies by deployment
alertmanagerGET /api/v2/status200404 with a working root usually means the URL points at something other than Alertmanager
loki, tempo, mimirGET /ready200Behind a multi-tenant gateway or path prefix, /ready may 404 while queries work; verify the health path against your deployment before concluding the store is down
victoriametricsGET /health200Cluster editions may serve health per component; verify the path against your deployment
vmalertGET /health200If /health is not exposed in your setup, GET /api/v1/rules returning JSON is an equivalent read-only proof
digitaloceandoctl installed (binary check), then GET /v2/accountdoctl present, then 200binary-doctl fail: doctl not installed but a digitalocean: block is configured (audit-digitalocean is doctl-based) — install doctl. 401: token missing, invalid, or expired. 403: token valid but scoped too low for account read
gcpgcloud auth print-access-token (or application-default when credentials_env is set), then GET the Monitoring API notificationChannelsnon-empty token, then 200No token at all: not logged in, or the key file credentials_env names is missing or invalid; run gcloud auth login or fix the key file. 403 on the API call: identity lacks monitoring.viewer. 404: gcp.project is wrong
awsaws sts get-caller-identity, then aws cloudwatch describe-alarms --max-records 1 (via the AWS CLI, honoring aws.profile/aws.region); a cost-permissions row records Compute Optimizer / Cost Explorer / Support read accessidentity resolves, then reachableIdentity fails: no credentials in the chain (env, profile, or assumed role) — run aws configure or export credentials. A resolved identity whose account differs from aws.account_id is an account-mismatch stop, not a warning. cloudwatch-read fail: the principal lacks cloudwatch:DescribeAlarms (grant the read-only policy in connect references/providers.md). The cost-permissions row is skipped, never a failure, when CO/CE/Support are not readable — audit-aws reads it to run or honestly exclude its non-scored Cost & Resource Optimization section
kuberneteskubectl --context <ctx> auth can-i get podsyesno: the context reaches the cluster but lacks read RBAC; bind the view ClusterRole. A context error: the config value does not exist in your kubeconfig; run kubectl config get-contexts
slackPOST test message (only with --slack-test)200 and the message appears404 or no_service: the webhook was revoked; create a new one via /scoutflo:connect

Authorization headers are sent only when the block names a token_env and that variable is non-empty. PagerDuty uses its own Authorization: Token token=<key> scheme rather than a Bearer header, Datadog uses a DD-API-KEY + DD-APPLICATION-KEY header pair, Kibana uses Authorization: ApiKey <encoded>, JSM Operations uses HTTP Basic auth (email:token) with an Atlassian API token, Zenduty uses Authorization: Token <key> (the literal word Token, not Bearer), and Groundcover uses Authorization: Bearer <key> plus an X-Backend-Id header on multi-backend accounts; the same never-send-empty rule applies to all of them. A named-but-unset variable yields env-missing, not a call with an empty header. GCP has no static token_env; its identity comes from gcloud, and the same empty-credential rule applies: no access token, no call.

The Slack test post

This is the one check that writes: it posts a visible message to your channel. Ask the user for explicit confirmation in the conversation before passing --slack-test; if declined or unanswered, the default skipped row is a valid healthy-enough state. The webhook URL is itself the credential; the script never prints it, and neither should you.

# Only after the user has explicitly confirmed the visible test post.
OUT_DIR="${SCOUTFLO_AUDIT_DIR:-./scoutflo-audits}/doctor/$(date -u +%Y-%m-%d)"   # run directory for this doctor run
sh "${CLAUDE_PLUGIN_ROOT}/skills/doctor/scripts/doctor.sh" --out "${OUT_DIR}" --slack-test
echo "doctor_exit=$?"
# Expect: doctor_exit=0 and a slack webhook-post row with result pass, http_code 200.

Fix hints per failure class

The script's hints quote the observed failure shape. This table is the deeper read once you have a row in hand:

Failure classLikely causeFix
exit 1, config not foundNo ~/.scoutflo/toolkit.yamlRun /scoutflo:connect (or export SCOUTFLO_CONFIG if yours lives elsewhere); it seeds the file from templates/toolkit.yaml.example
env-missingThe *_env variable is not exported in this shellExport it here (env vars are per-shell), or load it from your profile; created per connect references/providers.md
http_code 000, curl exit 6DNS lookup failedTypo in the URL, or the host resolves only on VPN or internal DNS
http_code 000, curl exit 7Connection refusedWrong port, service not exposed, or the port-forward is not running
http_code 000, curl exit 28TimeoutFirewall or network path; confirm you can reach the host at all before raising CURL_MAX_TIME
http_code 000, curl exit 35 or 60TLS failureInternal CA not trusted by your system; install the CA properly, never disable verification
http_code 000, curl exit 52, 55, or 56Connection dropped mid-transfer (no HTTP response)A proxy or corporate firewall between you and the host, not the token; retry once with proxy vars cleared (env -u HTTPS_PROXY -u https_proxy ...) and confirm the host is reachable from this network. It is never an auth (401/403) error
HTTP 401Token missing, invalid, or expiredRecreate per the provider section in connect; confirm the variable is exported in this shell
HTTP 403Token valid, scope or role too lowRaise to the tier scopes in connect references/providers.md
HTTP 404Wrong path, wrong region host, or a path prefixSentry: run the region probe. Gateways: verify the health path against your deployment
kubernetes rbac fail with "no"Context reaches the cluster, read RBAC missingBind the view ClusterRole per connect references/providers.md
kubernetes rbac fail with a context errorkubernetes.context not in your kubeconfigkubectl config get-contexts, then fix the config value
A key you set reads as unconfiguredBlock name or indentation mismatch, or nesting beyond the fallback parserCompare against templates/toolkit.yaml.example; install yq for anything nested

Common Failure Modes

FailurePrevention
Doctor passes in one terminal, the audit fails in anotherEnv vars are per-shell; export them in your profile or run doctor and the audit in the same shell
Empty Authorization header sent to an unauthenticated endpoint, causing a proxy 401Run the bundled script; it attaches the header only when token_env is named and set. Never hand-roll a check that adds the header unconditionally
Checks re-implemented inline instead of running the scriptThe script is the check contract: exit codes, JSON shape, and header guards live there. Inline curl loops drift and leak
/ready 404 behind a path-prefixed gateway read as "service down"Health paths vary by deployment; verify the path, and treat a successful real query as authoritative
Sentry 404 read as "org does not exist"Wrong region host; probe both region hosts before concluding anything about the org
Grafana health ok but every later audit call 403The script checks identity via /api/org during doctor, not mid-audit; fix the 403 row before any audit
Slack test post fired without askingThe post is visible to the channel; pass --slack-test only on explicit confirmation, and skipped is a valid state
Secret value printed while debugging a failing checkDebug with the recorded http_code and curl exit codes; the script never prints values, and neither should any manual follow-up
A failed doctor row carried into an audit as a "blocked finding"Doctor failures stop the run; fix and rerun doctor before starting any audit
Stale rows read after a rerun into the same --out directorymatrix.tsv appends; delete it first or use a fresh --out, and treat the JSON stdout of the latest run as the truth

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.