ccc-doctor · diff
git:20260729.eb9f438 to git:20261013.fbe187d
216 added, 398 removed. Audit B to A.
---
name: ccc-doctor
- description: "Diagnostic tool for CC Commander. Paste-ready report covering plugin version, update freshness, Node, marketplace clone state, MCP servers, settings.json, sessions, and full-stack drift checks."
- model: sonnet
- effort: medium
+ description: "Paste-ready Commander health report: install mode, installed vs latest version, clone and cache state, settings, MCP servers, plugin validate and details, mods, Codex doctor, optional source drift checks. Use when Commander misbehaves, after an update, or before filing a support issue."
allowed-tools:
- Bash
- Read
- AskUserQuestion
argument-hint: "[quick | full]"
+ model: sonnet
+ effort: medium
---
- # $ccc-doctor — CC Commander Diagnostic Report
+ # $ccc-doctor — Diagnose the Commander install
- Generates a paste-ready diagnostic snapshot. Paste the output into a GitHub issue or Discord support thread.
+ **CC Commander** · $ccc-doctor · one probe → paste-ready diagnostic report → the exact next command
- ## What to do
+ ## When to use / when not to
- Run the Bash block below in a single call. Handle every missing file gracefully — use `2>/dev/null || echo "n/a"`. Never crash if a file doesn't exist.
+ - Use when Commander skills or hooks misbehave, after an update, or to produce a report for a GitHub issue or support thread.
+ - Doctor is read-only. Its single write is the consent-gated `autoUpdate` toggle in Step 4, always backed up and verified.
+ - To update the plugin, use `$ccc-update`. To clean junk or add settings keys, use `$ccc-tuneup`. For this repository's vendor submodules, use `$ccc-upgrade`.
+ - `$ARGUMENTS`: `quick` (default) runs Steps 1–5; `full` adds the source-tree drift checks in Step 6.
+ - In Codex, ask each AskUserQuestion picker as a plain-text question with the same options; the probe runs unchanged and reports Codex rows.
+ ## What happens
+
+ ### 1. Probe the install and platform (one Bash call)
+
+ Run the block below in a single Bash call. It runs under `bash` regardless of the login shell, never fails on a missing file (every field falls back to `n/a`), and makes no network call except the version check.
+
```bash
- # ── Marketplace clone root ───────────────────────────────────────────
- # The marketplace clone dir is named after the MARKETPLACE key
- # (commander-hub), not the plugin — .git lives directly here. The repo's
- # OWN top-level layout then nests the plugin source one level further at
- # commander/cowork-plugin/. Both segments are real; do not collapse them.
- CLONE="$HOME/.claude/plugins/marketplaces/commander-hub"
- [ -d "$CLONE" ] || CLONE="n/a"
+ bash -s <<'PROBE'
+ J() { node -e 'try{const j=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));const v=process.argv[2].split(".").reduce((o,k)=>o==null?o:o[k],j);process.stdout.write(v==null?"n/a":String(v))}catch(e){process.stdout.write("n/a")}' "$1" "$2" 2>/dev/null || echo n/a; }
+ has() { command -v "$1" >/dev/null 2>&1; }
+ P="$HOME/.claude/plugins"
+ # Clone root (has .git) and plugin source (has .claude-plugin/plugin.json) are
+ # different directories: the repo nests the plugin at commander/cowork-plugin/.
+ CLONE="$P/marketplaces/commander-hub"; [ -d "$CLONE" ] || CLONE="n/a"
PLUGIN_SRC="n/a"
- if [ "$CLONE" != "n/a" ] && [ -d "$CLONE/commander/cowork-plugin" ]; then
- PLUGIN_SRC="$CLONE/commander/cowork-plugin"
- fi
+ [ "$CLONE" != "n/a" ] && [ -d "$CLONE/commander/cowork-plugin" ] && PLUGIN_SRC="$CLONE/commander/cowork-plugin"
- # Cache-only fallback (no marketplace clone — removed by the user, or an
- # install flow that never cloned): the INSTALLED runtime recorded in
- # installed_plugins.json is authoritative. SHAPE DIFFERENCE: the clone nests
- # the source at commander/cowork-plugin/; the cache installPath IS the plugin
- # source directly — no wrapper segment.
- if [ "$PLUGIN_SRC" = "n/a" ] && [ -f "$HOME/.claude/plugins/installed_plugins.json" ]; then
- PLUGIN_SRC=$(node -e "
- try {
- const j=JSON.parse(require('fs').readFileSync(process.env.HOME+'/.claude/plugins/installed_plugins.json','utf8'));
- const rows=(j.plugins&&(j.plugins['commander@commander-hub']||j.plugins['commander']))||[];
- const r=Array.isArray(rows)?rows[0]:rows;
- if(r&&r.installPath) process.stdout.write(r.installPath);
- } catch(e) {}
- " 2>/dev/null || echo "")
- [ -z "$PLUGIN_SRC" ] && PLUGIN_SRC="n/a"
- [ "$PLUGIN_SRC" != "n/a" ] && [ ! -d "$PLUGIN_SRC" ] && PLUGIN_SRC="n/a"
- fi
+ # installed_plugins.json: the executing copy. Its installPath IS the plugin
+ # source (cache/<mp>/<plugin>/<version>/), with no commander/cowork-plugin wrapper.
+ INSTALLED_JSON="$P/installed_plugins.json"
+ INST=$(node -e 'try{const d=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));const k=Object.keys(d.plugins||{}).find(k=>k.startsWith("commander@"));const e=k&&[].concat(d.plugins[k])[0];process.stdout.write(e?[k,e.version||"n/a",e.installPath||"n/a"].join("\t"):"not listed\tn/a\tn/a")}catch(e){process.stdout.write("not found or unparseable\tn/a\tn/a")}' "$INSTALLED_JSON" 2>/dev/null)
+ INSTALLED_KEY=$(printf '%s' "$INST" | cut -f1); INSTALLED_VERSION=$(printf '%s' "$INST" | cut -f2); INSTALLED_PATH=$(printf '%s' "$INST" | cut -f3)
+ [ "$PLUGIN_SRC" = "n/a" ] && [ -d "$INSTALLED_PATH" ] && PLUGIN_SRC="$INSTALLED_PATH"
- # Desktop-managed (account-synced) fallback: Cowork/Claude Desktop provisions the
- # plugin per session under Application Support — no clone AND no
- # installed_plugins.json entry is a HEALTHY state in that mode, not a broken
- # install. Report the mode; do not advise CLI marketplace commands (there is no
- # clone for them to act on). Update path in this mode: Desktop → Settings →
- # Plugin Marketplace (stuck versions: remove at claude.ai web → Cmd+Q → reinstall).
+ # Desktop-managed (account-synced): no clone and no installed_plugins.json
+ # entry is HEALTHY in this mode. Update path: Desktop settings, not the CLI.
INSTALL_MODE="cli-marketplace"
if [ "$PLUGIN_SRC" = "n/a" ]; then
- DESKTOP_MANIFEST=$(/bin/ls -t "$HOME/Library/Application Support/Claude/"local-agent-mode-sessions/*/*/rpm/plugin_*/.claude-plugin/plugin.json 2>/dev/null | head -50 | while read -r f; do
- if /usr/bin/grep -q '"name"[[:space:]]*:[[:space:]]*"commander"' "$f" 2>/dev/null; then echo "$f"; break; fi
- done)
- if [ -n "$DESKTOP_MANIFEST" ]; then
- INSTALL_MODE="desktop-managed"
- PLUGIN_SRC=$(dirname "$(dirname "$DESKTOP_MANIFEST")")
- fi
+ INSTALL_MODE="not-found"
+ DM=$(/bin/ls -t "$HOME/Library/Application Support/Claude/"local-agent-mode-sessions/*/*/rpm/plugin_*/.claude-plugin/plugin.json 2>/dev/null | head -50 | while read -r f; do
+ /usr/bin/grep -q '"name"[[:space:]]*:[[:space:]]*"commander"' "$f" 2>/dev/null && { echo "$f"; break; }; done)
+ [ -n "$DM" ] && { INSTALL_MODE="desktop-managed"; PLUGIN_SRC=$(dirname "$(dirname "$DM")"); }
fi
- echo "INSTALL_MODE=$INSTALL_MODE"
- # ── Plugin version (from the marketplace clone's own plugin.json) ────
- PLUGIN_JSON="$PLUGIN_SRC/.claude-plugin/plugin.json"
- PLUGIN_VERSION="n/a"
- [ "$PLUGIN_SRC" != "n/a" ] && PLUGIN_VERSION=$(node -e "
- try { process.stdout.write(JSON.parse(require('fs').readFileSync('$PLUGIN_JSON','utf8')).version || 'n/a'); }
- catch(e) { process.stdout.write('n/a'); }
- " 2>/dev/null || echo "n/a")
-
- # ── Node + system ────────────────────────────────────────────────────
- NODE_VER=$(node --version 2>/dev/null || echo "n/a")
- OS_INFO=$(uname -srm 2>/dev/null || echo "n/a")
-
- # ── git HEAD SHA of the marketplace clone (git root is $CLONE, NOT
- # $CLONE/commander — there is no .git one level deeper) ───────────
- GIT_SHA="n/a"
- [ "$CLONE" != "n/a" ] && GIT_SHA=$(git -C "$CLONE" rev-parse --short HEAD 2>/dev/null || echo "n/a")
-
- # ── Marketplace clone presence ──────────────────────────────────────
- if [ "$CLONE" != "n/a" ]; then CLONE_PRESENT="yes — $CLONE"; else CLONE_PRESENT="NO — directory missing"; fi
-
- # ── Temp dirs (mid-clone drift indicator) ───────────────────────────
- TEMP_DIRS=$(ls "$HOME/.claude/plugins/marketplaces/" 2>/dev/null | grep '^temp_' | tr '\n' ' ')
- if [ -z "$TEMP_DIRS" ]; then TEMP_DIRS="none"; fi
-
- # ── installed_plugins.json ──────────────────────────────────────────
- # Shape: { version, plugins: { "<plugin>@<marketplace>": [{ installPath,
- # version, installedAt, lastUpdated, gitCommitSha, scope }] } }. installPath
- # points into plugins/cache/<mp>/<plugin>/<version>/ — the ACTUAL executing
- # copy, distinct from the marketplace source clone above.
- INSTALLED_JSON="$HOME/.claude/plugins/installed_plugins.json"
- INSTALLED_VERSION="n/a"
- INSTALLED_PATH="n/a"
- if [ -f "$INSTALLED_JSON" ]; then
- INSTALLED_COMMANDER=$(node -e "
- try {
- const d = JSON.parse(require('fs').readFileSync('$INSTALLED_JSON','utf8'));
- const k = Object.keys(d.plugins||{}).find(k => k.startsWith('commander@'));
- if (k && d.plugins[k][0]) {
- const e = d.plugins[k][0];
- process.stdout.write('found — ' + k + ' v' + e.version + ' @ ' + e.installPath);
- } else {
- process.stdout.write('not found in installed list');
- }
- } catch(e) { process.stdout.write('parse error: ' + e.message); }
- " 2>/dev/null || echo "parse error")
- INSTALLED_VERSION=$(node -e "
- try {
- const d = JSON.parse(require('fs').readFileSync('$INSTALLED_JSON','utf8'));
- const k = Object.keys(d.plugins||{}).find(k => k.startsWith('commander@'));
- process.stdout.write(k && d.plugins[k][0] ? (d.plugins[k][0].version||'n/a') : 'n/a');
- } catch(e) { process.stdout.write('n/a'); }
- " 2>/dev/null || echo "n/a")
- INSTALLED_PATH=$(node -e "
- try {
- const d = JSON.parse(require('fs').readFileSync('$INSTALLED_JSON','utf8'));
- const k = Object.keys(d.plugins||{}).find(k => k.startsWith('commander@'));
- process.stdout.write(k && d.plugins[k][0] ? (d.plugins[k][0].installPath||'n/a') : 'n/a');
- } catch(e) { process.stdout.write('n/a'); }
- " 2>/dev/null || echo "n/a")
- else
- INSTALLED_COMMANDER="installed_plugins.json not found"
- fi
+ PLUGIN_VERSION=$(J "$PLUGIN_SRC/.claude-plugin/plugin.json" version)
+ NODE_VER=$(node --version 2>/dev/null || echo n/a)
+ OS_INFO=$(uname -srm 2>/dev/null || echo n/a)
+ GIT_SHA=n/a; [ "$CLONE" != "n/a" ] && GIT_SHA=$(git -C "$CLONE" rev-parse --short HEAD 2>/dev/null || echo n/a)
+ TEMP_DIRS=$(ls "$P/marketplaces/" 2>/dev/null | /usr/bin/grep '^temp_' | tr '\n' ' '); [ -z "$TEMP_DIRS" ] && TEMP_DIRS=none
- # ── settings.json sanity ────────────────────────────────────────────
+ # settings.json: extraKnownMarketplaces and enabledPlugins may be arrays or objects.
SETTINGS="$HOME/.claude/settings.json"
- if [ -f "$SETTINGS" ]; then
- HAS_MARKETPLACE=$(python3 -c "
- import json
- try:
- d = json.load(open('$SETTINGS'))
- ekm = d.get('extraKnownMarketplaces', [])
- names = [m if isinstance(m, str) else m.get('name','') for m in ekm]
- found = any('commander' in n.lower() for n in names)
- print('yes — commander-hub in extraKnownMarketplaces' if found else 'NO — commander-hub missing')
- except Exception as e:
- print('parse error: ' + str(e))
- " 2>/dev/null || echo "parse error")
+ read -r HAS_MARKETPLACE ENABLED <<EOF
+ $(node -e 'try{const d=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));const keys=x=>Array.isArray(x)?x.map(m=>typeof m==="string"?m:(m&&m.name)||""):Object.keys(x||{});const ep=d.enabledPlugins||{};const k=keys(ep).find(n=>/^commander(@|$)/i.test(n));const en=!k?"no":(!Array.isArray(ep)&&ep[k]===false?"disabled":"yes");process.stdout.write((keys(d.extraKnownMarketplaces).some(n=>/commander/i.test(n))?"yes":"no")+" "+en)}catch(e){process.stdout.write("n/a n/a")}' "$SETTINGS" 2>/dev/null)
+ EOF
- ENABLED=$(python3 -c "
- import json
- try:
- d = json.load(open('$SETTINGS'))
- ep = d.get('enabledPlugins', [])
- found = any('commander' in str(p).lower() for p in ep)
- print('yes — commander in enabledPlugins' if found else 'NO — commander not in enabledPlugins')
- except Exception as e:
- print('parse error: ' + str(e))
- " 2>/dev/null || echo "parse error")
- else
- HAS_MARKETPLACE="settings.json not found"
- ENABLED="settings.json not found"
- fi
+ MCP_SERVERS=$(node -e 'try{const s=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")).mcpServers||{};process.stdout.write(Object.keys(s).join(", ")||"none")}catch(e){process.stdout.write("n/a")}' "$PLUGIN_SRC/.mcp.json" 2>/dev/null)
+ SESSION_COUNT=$(ls "$HOME/.claude/sessions" 2>/dev/null | wc -l | tr -d ' ')
+ LATEST_SESSION=$(ls -t "$HOME/.claude/sessions" 2>/dev/null | head -1); [ -z "$LATEST_SESSION" ] && LATEST_SESSION=none
- # ── MCP servers (list only, no connectivity test) ───────────────────
- MCP_JSON="$PLUGIN_SRC/.mcp.json"
- if [ "$PLUGIN_SRC" != "n/a" ] && [ -f "$MCP_JSON" ]; then
- MCP_SERVERS=$(python3 -c "
- import json
- try:
- d = json.load(open('$MCP_JSON'))
- servers = d.get('mcpServers', {})
- print(', '.join(servers.keys()) if servers else 'none')
- except:
- print('parse error')
- " 2>/dev/null || echo "n/a")
- else
- MCP_SERVERS=".mcp.json not found at $MCP_JSON"
- fi
+ # Update freshness: shared update-check module, else the published manifest.
+ REMOTE_VERSION=n/a
+ if [ "$CLONE" != "n/a" ]; then REMOTE_VERSION=$(node "$CLONE/commander/update-check.js" --remote-only 2>/dev/null || echo n/a)
+ else REMOTE_VERSION=$(curl -fsSL --max-time 8 "https://raw.githubusercontent.com/KevinZai/commander/main/commander/cowork-plugin/.claude-plugin/plugin.json" 2>/dev/null | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const v=JSON.parse(s).version;process.stdout.write(/^\d+\.\d+\.\d+$/.test(v)?v:"n/a")}catch(e){process.stdout.write("n/a")}})' 2>/dev/null || echo n/a); fi
+ CMP=$INSTALLED_VERSION; [ "$CMP" = "n/a" ] && CMP=$PLUGIN_VERSION
+ VERSION_STATUS="unknown — could not reach update server"
+ [ "$REMOTE_VERSION" != "n/a" ] && [ "$CMP" != "n/a" ] && VERSION_STATUS=$(node -e 'const [r,i]=process.argv.slice(1);const a=r.split(".").map(Number),b=i.split(".").map(Number);let gt=false;for(let n=0;n<3;n++){if((a[n]||0)>(b[n]||0)){gt=true;break}if((a[n]||0)<(b[n]||0))break}process.stdout.write(gt?"UPDATE AVAILABLE: "+i+" -> "+r:"up to date ("+i+")")' "$REMOTE_VERSION" "$CMP" 2>/dev/null || echo unknown)
- # ── Recent sessions ─────────────────────────────────────────────────
- SESSION_DIR="$HOME/.claude/sessions"
- if [ -d "$SESSION_DIR" ]; then
- SESSION_COUNT=$(ls "$SESSION_DIR" | wc -l | tr -d ' ')
- LATEST_SESSION=$(ls -t "$SESSION_DIR" 2>/dev/null | head -1 || echo "none")
- else
- SESSION_COUNT="0"
- LATEST_SESSION="sessions dir not found"
- fi
+ # Cache dirs linger by design (no auto-clean); more than one is normal.
+ CACHE_ROOT="$P/cache/commander-hub/commander"
+ CACHE_VERSIONS=$(ls "$CACHE_ROOT" 2>/dev/null | sort -V | tr '\n' ',' | sed 's/,$//'); [ -z "$CACHE_VERSIONS" ] && CACHE_VERSIONS=none
+ CACHE_VERSION_COUNT=$(ls "$CACHE_ROOT" 2>/dev/null | wc -l | tr -d ' ')
+ INLINE_DIR="$P/data/commander-inline"; INLINE_PRESENT=no; INLINE_VERSION=n/a
+ [ -d "$INLINE_DIR" ] && { INLINE_PRESENT=yes; INLINE_VERSION=$(J "$INLINE_DIR/.claude-plugin/plugin.json" version); }
+ CATALOG_AGE_HOURS=$(node -e 'try{process.stdout.write(String(Math.floor((Date.now()-require("fs").statSync(process.argv[1]).mtimeMs)/3600000)))}catch(e){process.stdout.write("n/a")}' "$P/plugin-catalog-cache.json" 2>/dev/null)
+ AUTOUPDATE_FLAG=$(node -e 'try{const m=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"))["commander-hub"];process.stdout.write(m?String(!!m.autoUpdate):"not-registered")}catch(e){process.stdout.write("known_marketplaces.json not found")}' "$P/known_marketplaces.json" 2>/dev/null)
- # ── Update freshness (reuses the SessionStart nudge cache when fresh;
- # falls back to a live check via the shared update-check module) ──
- REMOTE_VERSION="n/a"
- if [ "$CLONE" != "n/a" ]; then
- REMOTE_VERSION=$(node "$CLONE/commander/update-check.js" --remote-only 2>/dev/null || echo "n/a")
- fi
- VERSION_STATUS="unknown — could not reach update server"
- if [ "$REMOTE_VERSION" != "n/a" ] && [ "$INSTALLED_VERSION" != "n/a" ]; then
- VERSION_STATUS=$(node -e "
- const a='$REMOTE_VERSION'.split('.').map(Number), b='$INSTALLED_VERSION'.split('.').map(Number);
- let gt=false;
- for(let i=0;i<3;i++){ if((a[i]||0)>(b[i]||0)){gt=true;break;} if((a[i]||0)<(b[i]||0))break; }
- process.stdout.write(gt ? 'UPDATE AVAILABLE: $INSTALLED_VERSION -> $REMOTE_VERSION' : 'up to date ($INSTALLED_VERSION)');
- " 2>/dev/null || echo "unknown")
+ # Claude Code platform checks (absent CLI is normal for Desktop-managed installs).
+ CLAUDE_VER=n/a; VALIDATE=n/a; DETAILS=n/a; MOD_SUPPORT=n/a
+ if has claude; then
+ CLAUDE_VER=$(claude --version 2>/dev/null | head -1 | awk '{print $1}')
+ if [ "$PLUGIN_SRC" != "n/a" ]; then
+ VALIDATE_OUT=$(claude plugin validate --strict "$PLUGIN_SRC" 2>&1); RC=$?
+ [ $RC -eq 0 ] && VALIDATE=pass || VALIDATE="fail (exit $RC)"
+ fi
+ DETAILS=$(claude plugin details commander 2>&1 | head -40)
+ MOD_SUPPORT=$(node -e 'const v=(process.argv[1]||"").split(".").map(Number);const m=[2,1,287];let ok=true;for(let i=0;i<3;i++){if((v[i]||0)>m[i])break;if((v[i]||0)<m[i]){ok=false;break}}process.stdout.write(ok?"yes":"no (needs 2.1.287+)")' "$CLAUDE_VER" 2>/dev/null)
fi
+ MODS_SHIPPED=$(ls "$PLUGIN_SRC/mods" 2>/dev/null | tr '\n' ' '); [ -z "$MODS_SHIPPED" ] && MODS_SHIPPED=none
+ MODS_INSTALLED=$(node -e 'try{const fs=require("fs");const d=JSON.parse(fs.readFileSync(process.argv[1],"utf8")).plugins||{};const out=[];for(const k of Object.keys(d)){const e=[].concat(d[k])[0];try{if(e&&/"modules"/.test(fs.readFileSync(e.installPath+"/hooks/hooks.json","utf8")))out.push(k)}catch(x){}}process.stdout.write(out.join(" ")||"none")}catch(e){process.stdout.write("n/a")}' "$INSTALLED_JSON" 2>/dev/null)
- # ── Installed-executable-copy cache: cache/<mp>/commander/<version>/ ─
- # No auto-clean — old versions linger. List them, flag if more than one.
- CACHE_ROOT="$HOME/.claude/plugins/cache/commander-hub/commander"
- CACHE_VERSIONS="none"
- CACHE_VERSION_COUNT=0
- if [ -d "$CACHE_ROOT" ]; then
- CACHE_VERSIONS=$(ls "$CACHE_ROOT" 2>/dev/null | sort -V | tr '\n' ',' | sed 's/,$//')
- CACHE_VERSION_COUNT=$(ls "$CACHE_ROOT" 2>/dev/null | wc -l | tr -d ' ')
- [ -z "$CACHE_VERSIONS" ] && CACHE_VERSIONS="none"
+ # Codex (only when installed).
+ CODEX_VER="not installed"; CODEX_DOCTOR=n/a; CODEX_PLUGIN=n/a
+ if has codex; then
+ CODEX_VER=$(codex --version 2>/dev/null | head -1)
+ CODEX_DOCTOR_OUT=$(codex doctor --json 2>&1); RC=$?
+ [ $RC -eq 0 ] && CODEX_DOCTOR=pass || CODEX_DOCTOR="fail (exit $RC)"
+ CODEX_PLUGIN=$(codex plugin list --json 2>/dev/null | /usr/bin/grep -o '"commander[^"]*"' | head -3 | tr '\n' ' '); [ -z "$CODEX_PLUGIN" ] && CODEX_PLUGIN="commander not listed"
fi
- # ── Desktop inline copy (bypasses the marketplace clone on some installs) ─
- INLINE_DIR="$HOME/.claude/plugins/data/commander-inline"
- INLINE_PRESENT="no"
- INLINE_VERSION="n/a"
- if [ -d "$INLINE_DIR" ]; then
- INLINE_PRESENT="yes"
- INLINE_VERSION=$(node -e "
- try { process.stdout.write(JSON.parse(require('fs').readFileSync('$INLINE_DIR/.claude-plugin/plugin.json','utf8')).version || 'n/a'); }
- catch(e) { process.stdout.write('n/a (present but no cached plugin.json found)'); }
- " 2>/dev/null || echo "n/a")
- fi
+ for v in INSTALL_MODE PLUGIN_SRC PLUGIN_VERSION INSTALLED_KEY INSTALLED_VERSION INSTALLED_PATH REMOTE_VERSION VERSION_STATUS \
+ NODE_VER OS_INFO GIT_SHA CLONE TEMP_DIRS HAS_MARKETPLACE ENABLED MCP_SERVERS SESSION_COUNT LATEST_SESSION \
+ CACHE_VERSIONS CACHE_VERSION_COUNT INLINE_PRESENT INLINE_VERSION CATALOG_AGE_HOURS AUTOUPDATE_FLAG \
+ CLAUDE_VER VALIDATE MOD_SUPPORT MODS_SHIPPED MODS_INSTALLED CODEX_VER CODEX_DOCTOR CODEX_PLUGIN; do
+ printf '%s=%s\n' "$v" "${!v}"
+ done
+ echo "--- validate (last 30 lines) ---"; printf '%s\n' "${VALIDATE_OUT:-n/a}" | tail -30
+ echo "--- plugin details ---"; printf '%s\n' "$DETAILS"
+ echo "--- codex doctor --json (first 3000 chars) ---"; printf '%s' "${CODEX_DOCTOR_OUT:-n/a}" | head -c 3000; echo
+ PROBE
+ ```
- # ── Plugin catalog cache age (Desktop's marketplace listing cache) ───
- CATALOG_CACHE="$HOME/.claude/plugins/plugin-catalog-cache.json"
- CATALOG_AGE_HOURS="n/a"
- if [ -f "$CATALOG_CACHE" ]; then
- CATALOG_AGE_HOURS=$(node -e "
- try { process.stdout.write(String(Math.floor((Date.now()-require('fs').statSync('$CATALOG_CACHE').mtimeMs)/3600000))); }
- catch(e) { process.stdout.write('n/a'); }
- " 2>/dev/null || echo "n/a")
- fi
+ If the block itself errors, report the error line and continue with the rows that printed.
- # ── autoUpdate flag for commander-hub ─────────────────────────────────
- KNOWN_MARKETPLACES="$HOME/.claude/plugins/known_marketplaces.json"
- AUTOUPDATE_FLAG="n/a"
- if [ -f "$KNOWN_MARKETPLACES" ]; then
- AUTOUPDATE_FLAG=$(node -e "
- try {
- const j = JSON.parse(require('fs').readFileSync('$KNOWN_MARKETPLACES','utf8'));
- const m = j['commander-hub'];
- process.stdout.write(m ? String(!!m.autoUpdate) : 'not-registered');
- } catch(e) { process.stdout.write('n/a'); }
- " 2>/dev/null || echo "n/a")
- else
- AUTOUPDATE_FLAG="known_marketplaces.json not found"
- fi
+ ### 2. Read the platform rows
- # ── Output ───────────────────────────────────────────────────────────
- echo "PLUGIN_VERSION=$PLUGIN_VERSION"
- echo "NODE_VER=$NODE_VER"
- echo "OS_INFO=$OS_INFO"
- echo "GIT_SHA=$GIT_SHA"
- echo "CLONE_PRESENT=$CLONE_PRESENT"
- echo "TEMP_DIRS=$TEMP_DIRS"
- echo "INSTALLED_COMMANDER=$INSTALLED_COMMANDER"
- echo "INSTALLED_VERSION=$INSTALLED_VERSION"
- echo "INSTALLED_PATH=$INSTALLED_PATH"
- echo "HAS_MARKETPLACE=$HAS_MARKETPLACE"
- echo "ENABLED=$ENABLED"
- echo "MCP_SERVERS=$MCP_SERVERS"
- echo "SESSION_COUNT=$SESSION_COUNT"
- echo "LATEST_SESSION=$LATEST_SESSION"
- echo "REMOTE_VERSION=$REMOTE_VERSION"
- echo "VERSION_STATUS=$VERSION_STATUS"
- echo "CACHE_VERSIONS=$CACHE_VERSIONS"
- echo "CACHE_VERSION_COUNT=$CACHE_VERSION_COUNT"
- echo "INLINE_PRESENT=$INLINE_PRESENT"
- echo "INLINE_VERSION=$INLINE_VERSION"
- echo "CATALOG_AGE_HOURS=$CATALOG_AGE_HOURS"
- echo "AUTOUPDATE_FLAG=$AUTOUPDATE_FLAG"
- ```
+ - `VALIDATE` is the result of `claude plugin validate --strict` on the plugin source. On `fail`, quote the validator lines that name a file or rule; the listing of gating hooks and mod `hooks:`/`calls:` in that output is informational.
+ - The `plugin details` section is the Claude Code view of the installed `commander` entry: version, component inventory, and projected always-on token cost. Flag a version that differs from `INSTALLED_VERSION`, or a note about dropped hooks from a plugin-name collision. Report the always-on token figure in the details row.
+ - Mods: `MODS_SHIPPED` lists mods bundled in the plugin source; `MODS_INSTALLED` lists installed plugins whose hooks file loads a module. A shipped mod that is not installed is optional, not a failure. When a mod is installed and `MOD_SUPPORT` is `no`, that is the failure.
+ - Codex rows appear only when `codex` is on `PATH`. Summarize `codex doctor --json` as its failing checks, if any; `CODEX_PLUGIN` shows whether the Commander mirror is installed.
+ - `/doctor prompt-audit` is an interactive Claude Code command, not something a Bash call can run. Recommend it in the report as the check for older-model prompting patterns in CLAUDE.md, skills, agents and commands.
- ## Format the output
+ ### 3. Render the report
- After running the Bash block, render the following markdown report. Substitute each variable. Determine pass/fail for each check row automatically.
+ Render one markdown block the user can paste as-is. Lead with the update status when an update is available; that is usually why the user came.
```
## CC Commander Diagnostic Report
- **When:** <current UTC datetime> · **By:** $ccc-doctor
+ **When:** <current UTC datetime> · **By:** $ccc-doctor · **Install mode:** <INSTALL_MODE>
| Item | Value |
|------|-------|
- | Plugin version (marketplace clone) | <PLUGIN_VERSION> |
- | Plugin version (installed/running) | <INSTALLED_VERSION> — <INSTALLED_PATH> |
- | Latest on GitHub main | <REMOTE_VERSION> |
| **Update status** | **<VERSION_STATUS>** |
- | Node | <NODE_VER> |
- | OS | <OS_INFO> |
- | Marketplace clone SHA | <GIT_SHA> |
- | Clone directory present | <CLONE_PRESENT> |
- | temp_* dirs (drift indicator) | <TEMP_DIRS> |
- | installed_plugins.json — commander | <INSTALLED_COMMANDER> |
- | Cached executable versions (`plugins/cache/commander-hub/commander/`) | <CACHE_VERSIONS> (<CACHE_VERSION_COUNT> version dir(s) — no auto-clean, old ones linger by design) |
- | Desktop inline copy present | <INLINE_PRESENT> (version: <INLINE_VERSION>) |
+ | Plugin version (plugin source) | <PLUGIN_VERSION> |
+ | Plugin version (installed runtime) | <INSTALLED_VERSION> — <INSTALLED_PATH> |
+ | Latest published | <REMOTE_VERSION> |
+ | Claude Code | <CLAUDE_VER> |
+ | `claude plugin validate --strict` | <VALIDATE> |
+ | `claude plugin details commander` | <one-line summary> |
+ | Mods shipped / installed / supported | <MODS_SHIPPED> / <MODS_INSTALLED> / <MOD_SUPPORT> |
+ | Codex | <CODEX_VER> · doctor: <CODEX_DOCTOR> · plugin: <CODEX_PLUGIN> |
+ | Node / OS | <NODE_VER> / <OS_INFO> |
+ | Marketplace clone | <CLONE> @ <GIT_SHA> |
+ | temp_* dirs | <TEMP_DIRS> |
+ | installed_plugins.json entry | <INSTALLED_KEY> |
+ | Cached versions | <CACHE_VERSIONS> (<CACHE_VERSION_COUNT> dirs; old ones linger by design) |
+ | Desktop inline copy | <INLINE_PRESENT> (version: <INLINE_VERSION>) |
| Plugin catalog cache age | <CATALOG_AGE_HOURS>h |
- | Marketplace `autoUpdate` flag | <AUTOUPDATE_FLAG> |
- | settings.json — extraKnownMarketplaces | <HAS_MARKETPLACE> |
- | settings.json — enabledPlugins | <ENABLED> |
+ | Marketplace `autoUpdate` | <AUTOUPDATE_FLAG> |
+ | settings.json — marketplace / enabled | <HAS_MARKETPLACE> / <ENABLED> |
| Bundled MCP servers | <MCP_SERVERS> |
- | Recent sessions count | <SESSION_COUNT> |
- | Latest session file | <LATEST_SESSION> |
+ | Sessions (count / latest) | <SESSION_COUNT> / <LATEST_SESSION> |
- ### Common issues checklist
+ ### Checklist
+ - [ ] Plugin source found (INSTALL_MODE is not `not-found`)
+ - [ ] No temp_* dirs
+ - [ ] commander in extraKnownMarketplaces (CLI marketplace mode only)
+ - [ ] commander enabled in enabledPlugins (CLI marketplace mode only)
+ - [ ] installed_plugins.json lists commander (CLI marketplace mode only)
+ - [ ] Up to date (VERSION_STATUS does not start with "UPDATE AVAILABLE")
+ - [ ] `claude plugin validate --strict` passes (n/a without the claude CLI)
+ - [ ] Installed mods supported by this Claude Code version
+ - [ ] `codex doctor --json` passes (only when Codex is installed)
- - [<pass/fail>] Marketplace clone present (directory exists)
- - [<pass/fail>] No temp_* dirs (no mid-clone drift)
- - [<pass/fail>] commander in extraKnownMarketplaces
- - [<pass/fail>] commander in enabledPlugins
- - [<pass/fail>] installed_plugins.json references commander
- - [<pass/fail>] Up to date (VERSION_STATUS does not start with "UPDATE AVAILABLE")
- - [<pass/fail>] At most 1 cached executable version (CACHE_VERSION_COUNT ≤ 1 — more means stale versions are lingering, harmless but worth a mental note)
+ Next: run `/doctor prompt-audit` in Claude Code to audit prompts for older-model patterns.
+ ```
- Mark [x] for pass, [ ] for fail.
+ Mark `[x]` for pass. For `desktop-managed`, mark the three CLI-only rows `n/a` and say the mode is healthy without a clone. Notes that keep the report honest:
- ### If an update is available
+ - `INSTALL_MODE=not-found`, no clone: the fix is to re-add the marketplace (Step 4, `references/fixes.md`).
+ - temp_* dirs are the most common cause of "plugin not loading"; flag them prominently.
+ - `CACHE_VERSION_COUNT` above 1 is normal; mention it, do not alarm on it.
- If `VERSION_STATUS` starts with `UPDATE AVAILABLE`, tell the user:
+ ### 4. Hand over the fix
- ```
- ⬆️ CC Commander vREMOTE_VERSION is available (you have vINSTALLED_VERSION).
+ For each failing row, print the matching block from [references/fixes.md](references/fixes.md) for the user to paste. Doctor does not run repair commands.
- claude plugin marketplace update commander-hub
- claude plugin update commander
+ If `VERSION_STATUS` starts with `UPDATE AVAILABLE`:
- Then restart Claude Code / Cowork Desktop to apply.
- ```
+ - CLI marketplace mode: tell the user
- Never suggest `git pull` for a marketplace install — there is no working
- tree to pull into; the marketplace clone at `~/.claude/plugins/marketplaces/commander-hub`
- is Claude Code's own cache, not something the user edits directly.
+ ```
+ 🔄 CC Commander <REMOTE_VERSION> is available (you have <INSTALLED_VERSION>).
- If `AUTOUPDATE_FLAG` is `false` or `not-registered`, offer (do not apply
- without confirmation):
+ claude plugin marketplace update commander-hub
+ claude plugin update commander
- > 💡 Skip the manual `marketplace update` step going forward — enable
- > auto-update for commander-hub?
+ Then restart Claude Code or Desktop, and run $ccc-doctor again (Step 5 verifies the runtime).
+ ```
- Call `AskUserQuestion` with options `Enable auto-update` / `Not now`. On
- "Enable auto-update":
- 1. Back up first: `cp ~/.claude/plugins/known_marketplaces.json ~/.claude/plugins/known_marketplaces.json.backup-$(date +%Y%m%d-%H%M%S)`
- 2. Set `known_marketplaces["commander-hub"].autoUpdate = true`, preserving every other key and every other marketplace entry untouched:
+ - Desktop-managed mode: give the Desktop steps in [../ccc-update/references/install-modes.md](../ccc-update/references/install-modes.md#desktop-managed-account-synced) instead; there is no clone for the CLI commands to act on.
+ - Never suggest `git pull` for a marketplace install: `~/.claude/plugins/marketplaces/commander-hub` is Claude Code's own cache, not a working tree.
+
+ If `AUTOUPDATE_FLAG` is `false` or `not-registered`, ask with the picker in Decision points. On "Enable auto-update":
+
+ 1. Back up: `cp ~/.claude/plugins/known_marketplaces.json ~/.claude/plugins/known_marketplaces.json.backup-$(date +%Y%m%d-%H%M%S)`
+ 2. Set only `known_marketplaces["commander-hub"].autoUpdate = true`, leaving every other key and marketplace untouched:
+
```bash
node -e "
const fs = require('fs');
const p = process.env.HOME + '/.claude/plugins/known_marketplaces.json';
const j = JSON.parse(fs.readFileSync(p, 'utf8'));
if (!j['commander-hub']) { console.log('commander-hub not registered — nothing to change'); process.exit(0); }
j['commander-hub'].autoUpdate = true;
fs.writeFileSync(p, JSON.stringify(j, null, 2));
console.log('autoUpdate enabled for commander-hub');
"
```
- 3. Re-read the file and confirm `autoUpdate === true` before reporting success. If verification fails, restore the backup and say so.
- This is the ONLY mutation `$ccc-doctor` ever performs, it is always
- consent-gated, and it always backs up first — every other finding in this
- report is diagnostic-only (see `$ccc-tuneup` for general remediation).
+ 3. Verify: `node -e "console.log(JSON.parse(require('fs').readFileSync(process.env.HOME+'/.claude/plugins/known_marketplaces.json','utf8'))['commander-hub'].autoUpdate===true)"` must print `true`. If it does not, restore the backup and say so.
- ### Post-update verification (run after the user says they updated)
+ ### 5. Post-update verification (after the user says they updated)
- Verify the **installed runtime**, not the marketplace clone — a successful
- `marketplace update` with a failed/unapplied `plugin update` leaves the clone
- new while the runtime stays old, and probing the clone would falsely report
- success.
+ Verify the installed runtime, not the marketplace clone. A successful `marketplace update` with an unapplied `plugin update` leaves the clone new and the runtime old, so probing the clone would report a false success.
```bash
# Installed runtime source + version (authoritative for what actually runs)
RUNTIME_SRC=$(node -e "
try {
const j=JSON.parse(require('fs').readFileSync(process.env.HOME+'/.claude/plugins/installed_plugins.json','utf8'));
const rows=(j.plugins&&(j.plugins['commander@commander-hub']||j.plugins['commander']))||[];
const r=Array.isArray(rows)?rows[0]:rows;
if(r&&r.installPath) process.stdout.write(r.installPath);
} catch(e) {}
" 2>/dev/null || echo "")
- # NO fallback to $PLUGIN_SRC here — with a marketplace clone present, that
- # fallback would "verify" the CLONE (which a marketplace refresh updates even
- # when the plugin update failed) and falsely report the new version as
- # running. If there is no installed-runtime record, say so honestly instead.
+ # No fallback to the clone here: a marketplace refresh updates the clone even
+ # when the plugin update failed. No installed-runtime record means say so.
{ [ -z "$RUNTIME_SRC" ] || [ ! -d "$RUNTIME_SRC" ]; } && RUNTIME_SRC="n/a"
RUNTIME_VERSION="n/a"
[ "$RUNTIME_SRC" != "n/a" ] && RUNTIME_VERSION=$(node -e "
try { process.stdout.write(JSON.parse(require('fs').readFileSync('$RUNTIME_SRC/.claude-plugin/plugin.json','utf8')).version||'n/a'); }
catch(e) { process.stdout.write('n/a'); }
" 2>/dev/null || echo "n/a")
echo "RUNTIME_VERSION=$RUNTIME_VERSION"
[ "$RUNTIME_SRC" != "n/a" ] && echo '{}' | node "$RUNTIME_SRC/hooks/session-end.js" | head -c 200 || echo "n/a — no installed runtime found to test"
```
- Expect a single JSON line back (e.g. `{"continue":true,...}`). That proves
- the hook chain in the RUNNING install works — not just that the clone's
- version string changed. If `RUNTIME_VERSION` still shows the old version
- while the clone shows the new one, say exactly that: the marketplace
- refreshed but `claude plugin update commander` didn't apply (or the app
- hasn't been restarted) — re-run the update step.
-
- ### Nuclear fix commands (paste into terminal if needed)
-
- If the clone is missing or corrupted:
- ```bash
- # Remove stale marketplace state and re-add
- rm -rf ~/.claude/plugins/marketplaces/commander-hub
- # Then in Claude Code Desktop: Settings → Plugins → Add Marketplace → KevinZai/commander
- ```
+ Expect one JSON line (for example `{"continue":true,...}`): that proves the hook chain in the running install works. If `RUNTIME_VERSION` is still old while the clone is new, say exactly that: the marketplace refreshed but `claude plugin update commander` did not apply, or the app was not fully restarted. Optionally cross-check with `claude plugin details commander`.
- If temp_* dirs exist (mid-clone drift):
- ```bash
- ls ~/.claude/plugins/marketplaces/
- rm -rf ~/.claude/plugins/marketplaces/temp_*
- ```
+ ### 6. Full mode: source-tree drift checks
- If commander not in enabledPlugins:
- ```bash
- # Open Settings → Plugin Marketplace → find commander → Enable
- # Or check ~/.claude/settings.json and add "commander" to enabledPlugins array
- ```
+ Only when `$ARGUMENTS` is `full` or the user asks for a thorough audit. The checks need a Commander repository root: the marketplace clone, or the current directory when it is a Commander checkout. Desktop-managed installs have neither; report the step as `n/a`.
- If MCP servers not responding:
```bash
- # Restart Claude Code Desktop and retry $ccc-doctor
- # Check .mcp.json at: ~/.claude/plugins/marketplaces/commander-hub/commander/cowork-plugin/.mcp.json
- ```
+ ROOT="$HOME/.claude/plugins/marketplaces/commander-hub"
+ [ -d "$ROOT/commander/cowork-plugin" ] || ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
+ [ -n "$ROOT" ] && [ -f "$ROOT/commander/cowork-plugin/skills/ccc-doctor/lib/diagnostics.js" ] && node -e '
+ const diag = require(process.argv[1] + "/commander/cowork-plugin/skills/ccc-doctor/lib/diagnostics");
+ for (const r of diag.runDiagnostics(process.argv[1])) {
+ console.log("[" + (r.status === "ok" ? "OK" : r.status === "warn" ? "WARN" : "FAIL") + "] " + r.category + " — " + r.message);
+ if (r.remediation) console.log(" fix: " + r.remediation);
+ }' "$ROOT" || echo "drift checks n/a — no Commander repository root"
```
- ### Recommended services
-
- After diagnostics, scan for missing infrastructure and suggest relevant partners:
-
- **If `package.json` has no `database` or `db` dependency detected:**
- > 💾 **[Supabase](https://supabase.com?utm_source=ccc&utm_medium=plugin&utm_campaign=ccc-doctor&utm_content=supabase-db)** — Need a database? Postgres + auth + real-time, free tier generous.
-
- **If no `deploy` script or `vercel.json` detected:**
- > 🌐 **[Vercel](https://vercel.com?utm_source=ccc&utm_medium=plugin&utm_campaign=ccc-doctor&utm_content=vercel-deploy)** — Deploy to production. Free tier suitable for most projects.
-
- **If no cache or queue setup detected:**
- > ⚡ **[Upstash](https://upstash.com?utm_source=ccc&utm_medium=plugin&utm_campaign=ccc-doctor&utm_content=upstash-cache)** — Redis + Kafka serverless. Pay only for what you use.
-
- **If using serverless without edge support:**
- > 🔗 **[Cloudflare Workers](https://www.cloudflare.com?utm_source=ccc&utm_medium=plugin&utm_campaign=ccc-doctor&utm_content=cloudflare-edge)** — Deploy globally with edge computing.
-
- [View all partners →](./affiliate-disclosure.mdx)
+ Append the rows under the main report as a `### Drift checks` section. What each category verifies is in [references/drift-checks.md](references/drift-checks.md). A `fail` row is diagnostic output; doctor never blocks on it.
- ## Anti-patterns — DO NOT
+ ### 7. Optional partner suggestions
- - Never test MCP connectivity (just list server names — network calls are slow and flaky)
- - Never crash if any file is missing — graceful n/a for every field
- - Never hardcode the plugin version — always read from plugin.json
- - Never output more than one code block to paste — keep it a single unified markdown block
- - Never tell a marketplace-installed user to `git pull` — there is no working tree to pull into. The remediation is always `claude plugin marketplace update <mp> && claude plugin update <plugin>` + restart.
- - Never write to `known_marketplaces.json` (or any file) without the AskUserQuestion confirmation and a prior backup — the autoUpdate toggle is the one narrow exception, and even it follows backup-then-verify.
- - Never collapse the marketplace-clone root and the plugin-source root into one path — `$CLONE` (`marketplaces/commander-hub`, has `.git`) and `$PLUGIN_SRC` (`$CLONE/commander/cowork-plugin`, has `.claude-plugin/plugin.json`) are different directories with different contents.
+ After the report, and only when the current project shows a gap, print at most two lines from [references/recommended-services.md](references/recommended-services.md). Skip this when the install itself is failing.
- ## Tips
+ ## Decision points
- 1. Run the full Bash block in one call — avoids extra turns.
- 2. The report is self-contained: user pastes it into a GitHub issue as-is.
- 3. If PLUGIN_VERSION is n/a and CLONE_PRESENT is NO, the fix is always: re-add the marketplace.
- 4. temp_* dirs are the #1 cause of "plugin not loading" — always flag them prominently.
- 5. If VERSION_STATUS says UPDATE AVAILABLE, that is usually the actual root cause the user came here for — lead the report with it, don't bury it under Node/OS trivia.
- 6. CACHE_VERSION_COUNT > 1 is normal (no auto-clean) — don't alarm on it, just note it.
+ Auto-update offer (Step 4), only when `AUTOUPDATE_FLAG` is `false` or `not-registered`:
- ## Deeper drift checks (full mode)
+ ```yaml
+ question: "Enable auto-update for the commander-hub marketplace?"
+ header: "Auto-update"
+ multiSelect: false
+ options:
+ - label: "🟢 Enable auto-update"
+ description: "Backs up known_marketplaces.json, sets autoUpdate on commander-hub only, then verifies."
+ - label: "Not now"
+ description: "No change; you keep running the marketplace update command by hand."
+ ```
- When the user passes `full` or asks for a thorough audit, run the source-tree drift checks via the bundled helper module. These run against the cloned plugin source — not the user's `~/.claude/` state.
+ Next step after a report with failures (skip when everything passes):
- ```bash
- PLUGIN_DIR="$HOME/.claude/plugins/marketplaces/commander-hub"
- node -e "
- const diag = require('$PLUGIN_DIR/commander/cowork-plugin/skills/ccc-doctor/lib/diagnostics');
- const results = diag.runDiagnostics('$PLUGIN_DIR');
- for (const r of results) {
- const icon = r.status === 'ok' ? 'OK' : r.status === 'warn' ? 'WARN' : 'FAIL';
- console.log('[' + icon + '] ' + r.category + ' — ' + r.message);
- if (r.remediation) console.log(' fix: ' + r.remediation);
- }
- "
+ ```yaml
+ question: "What next?"
+ header: "Doctor"
+ multiSelect: false
+ options:
+ - label: "🟢 Fix local state with $ccc-tuneup"
+ description: "Archives junk and adds missing settings keys, with backups and a confirm step."
+ - label: "Update with $ccc-update"
+ description: "Walks the update and verifies the installed runtime after restart."
+ - label: "Done"
+ description: "Keep the report; nothing else runs."
```
- The helper runs full-stack categories: Claude settings, contract counts, vendors, bundled MCPs, hooks, agents, tests, display name, version parity, and critical files. Append the table below after the main report when the user requested `full`.
+ ## Done when
- | # | Category | What it verifies |
- |---|----------|------------------|
- | 1 | `license-cleanup` | No `license.json`, `licenseFile`, `tier === 'free'`, or `isPro()` references in plugin hooks. CC Commander's core is free forever — any residue is a red flag. |
- | 2 | `hook-chain` | Every `.js` referenced from `hooks.json` exists on disk and uses ESM `import` (not legacy `require`). Flags unregistered orphan hook files. |
- | 3 | `mcp-availability` | `.mcp.json` lists exactly the 2 bundled servers (context7 + sequential-thinking) and `CONNECTORS.md` advertises 16 opt-in connectors. Drift either way is flagged. |
- | 4 | `agent-models` | All 22 sub-agent `.md` frontmatter has the expected `model:` pin. `architect`, `security-auditor`, `debugger`, `product-manager` must be on `claude-fable-5` (promoted from Opus in v6.0); `designer`, `researcher`, `reviewer` must be on `claude-sonnet-5`. Other agents on legacy aliases (`opus`, `sonnet`, `haiku`) are flagged as upgrade candidates. |
- | 5 | `test-suite` | Required audit scripts (`audit-frontmatter.js`, `audit-counts.js`, `check-version-parity.js`) exist. Doctor doesn't shell out to them — it only verifies presence so the user can run `--check` manually. |
- | 6 | `display-name` | `plugin.json.displayName === "Commander"` AND `marketplace.json.plugins[0].displayName === "Commander"` (per brand commit `0954a3a`). |
- | 7 | `version-parity` | Spot-check that `package.json` and `plugin.json` versions match. Full parity check covered by `scripts/check-version-parity.js`. |
- | 8 | `critical-files` | `CHANGELOG.md`, `README.md`, `LICENSE`, `package.json`, `commander/core/registry.yaml` all present. |
+ - The Step 1 probe ran and every report row holds a value or `n/a`; no row is guessed.
+ - The checklist is marked from probe values, and each failing row has its fix block from `references/fixes.md` or the update commands.
+ - After an auto-update toggle, the Step 4 verification printed `true` (or the backup was restored and reported).
+ - After an update, Step 5 reported `RUNTIME_VERSION` and the hook pipe-test result.
- ### Result shape
+ ## Guardrails
- `runDiagnostics(root)` returns an array of:
+ - Read-only except the `autoUpdate` toggle, which requires the picker answer, a prior backup, and a post-write check. Repair commands are printed for the user, never run.
+ - Never test MCP connectivity; list server names only.
+ - Never hardcode the plugin version; read it from `plugin.json` and `installed_plugins.json`.
+ - Never tell a marketplace-installed user to `git pull`; the remediation is the `claude plugin marketplace update` and `claude plugin update` pair plus a restart.
+ - Never collapse the clone root (`marketplaces/commander-hub`, has `.git`) and the plugin source (`$CLONE/commander/cowork-plugin`, has `.claude-plugin/plugin.json`) into one path.
+ - Never treat a missing clone as broken in `desktop-managed` mode.
+ - Keep the report one paste-ready block.
- ```
- {
- category: string, // e.g. 'hook-chain'
- status: 'ok' | 'warn' | 'fail',
- message: string, // one-line human summary
- remediation?: string // present when status !== 'ok'
- }
- ```
+ ## See also
- Quick mode (default) skips this section. Pass `full` to run it. Doctor never blocks — even a `fail` row is just diagnostic output.
+ - `$ccc-update` — update the plugin and verify the runtime
+ - `$ccc-tuneup` — safe, backed-up remediation of local setup
+ - `$ccc-settings` — Commander preferences and Claude Code settings
+ - `$ccc-upgrade` — maintainer-only vendor submodule updates
---
> ⚙️ **Fable contract:** plan before build · verifier ≠ worker · prove before alarm · loops need gates · leave durable state — `rules/fable-method.md`
> (On Codex, present these options as a numbered list and ask the user to reply with a number — AskUserQuestion is Claude-only.)