Skip to content

fix(health-check): resolve the skills dir via claude_home_path, not a bare ~/.claude - #2313

Merged
john-the-dev merged 5 commits into
mainfrom
fix/health-check-skills-dir
Jul 27, 2026
Merged

john-the-dev merged 5 commits into
mainfrom
fix/health-check-skills-dir

Conversation

@qingyun-wu

Copy link
Copy Markdown
Collaborator

Problem

check_skill_symlinks hardcodes Path.home()/".claude"/"skills", ignoring the workspace-scoped CLAUDE_CONFIG_DIR — the same bug class #1454 already fixed for _default_memory_dir() in this same file. On a migrated install the check scans a stale ~/.claude/skills/, producing both false positives and false negatives.

Before/after (live run on a migrated host, parent commit vs this head)

Before — scans stale ~/.claude/skills:

⚠ skill-symlinks  warn  1 unlinked: meeting-scheduler; 14 dangling not in this repo: calendar-prep, catchup-after-startup, ...

meeting-scheduler is a FALSE positive — its symlink exists and loads under the workspace claude-home ($CLAUDE_CONFIG_DIR/skills/meeting-scheduler → <repo>/skills/meeting-scheduler, verified). The 14 "dangling" entries are leftovers in a directory Claude Code no longer reads on this host.

After — scans the real claude-home via claude_home_path("skills"):

⚠ skill-symlinks  warn  5 unlinked: gws-gmail-voice, learned-skills, plugin-patches, pointer-teacher-tracer...; 1 dangling not in this repo: skill-usage-report

The false positive is gone, and the check now surfaces skills that are genuinely unlinked in the directory Claude Code actually loads from — findings the stale path was hiding.

Change

  • skills_dst = claude_home_path("skills") (helper already imported at line 43; resolution order CLAUDE_CONFIG_DIR → CLAUDE_HOME → ~/.claude preserves ad-hoc-launch behavior).
  • Docstring documents the parallel to the feat(workspace-revamp): staging→main rollup (M0+M1+M2) #1454 memory-dir fix.
  • tests/health-check-dangling-symlinks.test.py: harness redirects claude_home_path instead of monkeypatching Path.home — all 7 cases pass, assertions unchanged.

Single concern; no behavior change on installs without CLAUDE_CONFIG_DIR set.

🤖 Generated with Claude Code

https://claude.ai/code/session_01F4UsWQjcFqNQmaTHzdEibX

… bare ~/.claude

check_skill_symlinks hardcoded Path.home()/.claude/skills, ignoring the
workspace-scoped CLAUDE_CONFIG_DIR — the exact bug class #1454 fixed for
the memory-dir probe in this same file. On a migrated install the check
scanned a stale ~/.claude/skills: it warned 'unlinked' about skills whose
symlinks exist (and load) under the workspace claude-home, while hiding
the skills that are genuinely unlinked there.

Test harness now redirects claude_home_path() instead of monkeypatching
Path.home; all 7 cases pass unchanged in behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4UsWQjcFqNQmaTHzdEibX
@github-actions

github-actions Bot commented Jul 25, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 65%.

Diff Coverage

Diff: origin/main...HEAD, staged and unstaged changes

  • src/health-check.py (100%)

Summary

  • Total: 4 lines
  • Missing: 0 lines
  • Coverage: 100%

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed current head 5f7f78db56eadbc424b818d2435613ab9fb87cb6.

No blocking findings. check_skill_symlinks() now resolves the active Claude home through the existing claude_home_path("skills") helper, preserving the fallback behavior while fixing migrated/workspace-scoped installs. The focused test harness now redirects that same resolver, so its seven existing dangling/unlinked/fix cases exercise the production path.

Validation: all remote CI checks are green. Locally, git diff --check, the 7-case dangling-symlink suite, Python compilation, scripts/gen-src-map.py --check, and an explicit CLAUDE_CONFIG_DIR skills-path resolution check passed.

Approval signal: looks good to merge. Posted as a comment review because the authenticated account is the PR author.

Reviewed by Qingyun's Personal Codex.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Reviewed — clean, LGTM. Same bug class + fix pattern as the merged #1454 (_default_memory_dir in this same file): Path.home()/'.claude' ignored CLAUDE_CONFIG_DIR, so a migrated install scanned a stale ~/.claude/skills → false warn/ok. claude_home_path('skills') resolves it correctly, and the docstring + not-found detail now reflect the resolved path. CI green, MERGEABLE, live before/after on a migrated host. No blockers from me (shared gh identity → comment-LGTM, not a formal approve; needs a formal reviewer + owner merge).

…probe

The installer only links dirs containing SKILL.md; manifest-loaded and
scripts-only skills (gws-gmail-voice, learned-skills, plugin-patches,
pointer-teacher-tracer) have none, are correctly never symlinked, and
must not be reported 'unlinked'. New test covers the filter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4UsWQjcFqNQmaTHzdEibX
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Pushed a second commit (same concern — probe accuracy): the check now applies skills/install.sh's own SKILL.md filter, so manifest-loaded / scripts-only skills (no SKILL.md, deliberately never symlinked) stop being reported as 'unlinked'. The earlier 'after' output's 5 'unlinked' entries were exactly this class — verified on-host: all lack SKILL.md and install.sh skips them by design. Live output with both commits is now precisely the real findings: warn — 1 dangling not in this repo: skill-usage-report (a genuinely dead symlink). 8/8 tests pass (new case: SKILL.md-less dir ignored, real skill still warned).

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Blocking finding on current head 82e458a (formal self-request-changes is unavailable): the implementation change now mirrors skills/install.sh by requiring SKILL.md, but the shipped live coverage in tests/health-check-skill-symlinks-live.test.py still creates foo/bar/zap as bare directories. As a result, CI now fails both tsc + tests (clean install) and diff coverage >= 95% (python): the live test expects bar and zap to be reported/fixed as unlinked skills, but check_skill_symlinks correctly skips them because they have no SKILL.md.

Please update the live test fixtures to write SKILL.md for the directories that are meant to behave as installable slash skills (foo/bar/zap), and keep/add a separate no-SKILL.md fixture for the manifest/scripts-only skip behavior. Until that lands, the PR is red and the shipped behavioral coverage is inconsistent with the new contract.

Validation in an isolated /private/tmp checkout:

  • git diff --check origin/main...HEAD passed
  • PYTHONPYCACHEPREFIX=/private/tmp/pr-monitor-20260725-1913/pycache python3 -m py_compile src/health-check.py tests/health-check-dangling-symlinks.test.py passed
  • python3 tests/health-check-dangling-symlinks.test.py passed (8 tests)
  • python3 tests/health-check-skill-symlinks-live.test.py fails locally with the same five failed checks reported by CI

Reviewed by Qingyun's Personal Codex.

…e symlink suite

The live suite's fixtures lacked SKILL.md, tripping the new installer-filter
(CI red on commit 2). Also harden isolation: the probe now resolves via
claude_home_path(), so the suite must clear CLAUDE_CONFIG_DIR/CLAUDE_HOME —
not just redirect HOME — or its fix cases write real symlinks into a dev
machine's live skills dir (observed live before this fix).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4UsWQjcFqNQmaTHzdEibX

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed current head 25bc73e9b642660afd766452b82b39190e69be38.

No remaining blocking findings. The new commit resolves the prior blocker by giving the live suite's installable foo/bar/zap fixtures SKILL.md, and it also closes the more serious isolation gap by clearing CLAUDE_CONFIG_DIR and CLAUDE_HOME before redirecting HOME; the shipped test can no longer create symlinks in a developer's active Claude home.

Focused validation passed in a detached checkout: git diff --check, Python compilation, the 8-case dangling/unlinked suite, the shipped live symlink suite, and scripts/gen-src-map.py --check. All completed remote checks are green; the refreshed clean-install and diff-coverage jobs are still running.

Approval signal: the code is ready once those two refreshed CI jobs pass. Posted as a comment review because the authenticated account is the PR author.

Reviewed by Qingyun's Personal Codex.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Reviewed current head 25bc73e. No blocking findings. The follow-up fixes the previous CI blocker: the live skill-symlink fixtures now mark installable test skills with SKILL.md, while the no-SKILL.md case remains covered by the structural suite. I also like the added env isolation in the live test — clearing CLAUDE_CONFIG_DIR and CLAUDE_HOME before exercising check_skill_symlinks() keeps the test from writing into a real workspace Claude-home when run on a migrated dev host.

Validation in an isolated /private/tmp checkout:

  • git diff --check origin/main...HEAD passed
  • PYTHONPYCACHEPREFIX=/private/tmp/pr-monitor-20260725-1913/pycache-new python3 -m py_compile src/health-check.py tests/health-check-dangling-symlinks.test.py tests/health-check-skill-symlinks-live.test.py passed
  • python3 tests/health-check-dangling-symlinks.test.py passed (8 tests)
  • CLAUDE_CONFIG_DIR=/private/tmp/pr-monitor-20260725-1913/real-ccd-guard CLAUDE_HOME=/private/tmp/pr-monitor-20260725-1913/real-chome-guard python3 tests/health-check-skill-symlinks-live.test.py passed
  • Remote CI is green, including diff coverage and tsc + tests (clean install)

Approval signal: looks good to merge from my review. Posted as a regular review comment because the authenticated account is the PR author.

Reviewed by Qingyun's Personal Codex.

sonichi added a commit that referenced this pull request Jul 25, 2026
The scanner skipped `#`/`//` comment lines but not triple-quoted docstring
prose, so a PR that ADDS a docstring *describing* a legacy path it removes got
false-flagged (found live: #2313's `claude_home_path` fix documents the old
`~/.claude/skills/` it replaces, and the runner flagged the doc line).

Track triple-quote (`"""`/`'''`) state per hunk (reset at each @@ to avoid
carrying stale state across a gap) and skip the flag-check for any line that
STARTS inside a docstring — the opening `"""` line is still checked, and real
code after the docstring closes is still scanned. Approximate by design (a path
on the SAME line as a one-line docstring is still checked), but it clears the
common multi-line case. Verified: the real #2313 diff now PASSES.

3 new cases in review-checks-py.test.py (docstring-prose skipped / code-after
still flags / even-count triple-quote doesn't corrupt parity); 21/21 py + 16/16
shell green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rq18tfPmFcgzHQFgoSpsUN
Stand: Echo Act IV Pro
sonichi added a commit that referenced this pull request Jul 25, 2026
…real docstrings (Qingyun review, #2281)

Two blocking findings from Qingyun's review on head 4a6b1aa:

1. CI review-checks step fails open. `gh pr diff N | bash review-checks.sh`
   reports only the pipeline's last exit status, so a failed `gh pr diff`
   (auth/rate-limit/network) fed an empty diff to the scanner, which printed
   "empty diff — nothing to check" and exited 0 — the gate PASSED without ever
   scanning the PR. Now: `set -euo pipefail`, materialize the diff to a file,
   and scan it via `--diff` so a `gh pr diff` failure fails the step closed.

2. Triple-quote suppression was too broad. The #2313 docstring-skip toggled on
   ANY line with an odd triple-quote count, so an assignment like
   `COMMAND = <triple-quote> ... rsync /Users/alice/data ... <triple-quote>`
   exempted the hardcoded path inside it — a scanner bypass that could merge
   host-specific paths under a false-green gate. Replaced the count-only toggle
   with `_doc_transition()`, which opens the exempt span only when the stripped
   line STARTS with a triple-quote (a bare string-expression = real docstring);
   an assigned/called triple-quoted literal stays executable code and is still
   scanned. Added regressions: multi-line assigned string IS flagged; prefixed
   (r) docstring prose is NOT. All existing docstring tests still pass
   (23 py-internal + 16 shell checks green).

AGENTS.md already mirrors the CLAUDE.md change on this head (sync check passes);
the RC_DIFF-via-env finding was already resolved by the stdin-streaming path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rq18tfPmFcgzHQFgoSpsUN
Stand: Echo Act IV Pro

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved current head 25bc73e9.

No blocking findings. The production path now uses the existing claude_home_path("skills") resolver, the focused harness redirects the same seam, and the live symlink suite now isolates both CLAUDE_CONFIG_DIR and CLAUDE_HOME, so it cannot mutate the developer’s active Claude home.

Exact-head verification:

  • python3 tests/health-check-dangling-symlinks.test.py — 8/8 pass.
  • python3 tests/health-check-skill-symlinks-live.test.py — pass.
  • Python compile for all touched Python files — pass.
  • python3 scripts/gen-src-map.py --check — pass.
  • git diff --check origin/main...HEAD — pass.
  • Added-line host-path scan found no production host-specific path.
  • Current CI and CLA are green.

Reviewed by John’s Codex.

john-the-dev added a commit that referenced this pull request Jul 25, 2026
… + CI gate (#2281)

* feat(review): repo-scoped REVIEW_GUIDE drives a generic checks runner + CI gate

Splits generic review MECHANICS from repo-specific CRITERIA (design v3):

- .github/REVIEW_GUIDE.md — single source of truth: prose lessons (fed to the
  codex reviewer) + a machine-readable `checks:` block (hardcoded-path flag/allow
  patterns). Add a lesson/check by editing this file, not the tooling.
- scripts/review-checks.sh (+ review-checks.py) — standalone, codex-free runner:
  reads the guide checks: block and scans a diff. Generic-default fallback when a
  repo has no guide. Skips docs/tests/self (path literals are data there).
- .github/workflows/ci.yml — runs the runner on every PR diff, so the guide
  gates ALL PRs unconditionally (not just codex-tool reviews). The big win.
- review-pr.sh — calls the shared runner (supersedes the baked-in scanner), so
  the codex path surfaces the same mechanical findings.
- tests/review-checks.test.sh — folds the #2229 hardcoded-path fixtures + guide
  patterns, fallback, per-token allow anchoring, file-skip (15 checks).

Closes #2229 (its hardcoded-path scan now lives in the shared, guide-driven
runner). Robust on macOS bash 3.2 (avoids heredoc-in-$() and O(pathological)
${var//…/} on large diffs).

Stand: Echo Act IV Pro
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rq18tfPmFcgzHQFgoSpsUN

* test(review-checks): Python unit tests for the scanner (diff-coverage)

review-checks.py is exercised via the shell runner, so the python diff-coverage
gate saw its new lines as uncovered. Add tests/review-checks-py.test.py: imports
the module and pins token_at / allowed / main across all diff-parsing branches
(+++/@@/-/+ , skip, comment, flag-found + allow anchoring, empty). pragma the
__main__ guard.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rq18tfPmFcgzHQFgoSpsUN

* fix(review-checks): advance line numbers across context

* refactor(review): relocate REVIEW_GUIDE.md -> root REVIEW.md so Claude Code's built-in reviewers consume it

Claude Code's managed GitHub-App PR reviews read a root REVIEW.md (injected into
every review agent's prompt); the in-session /code-review reads CLAUDE.md but NOT
REVIEW.md. Neither built-in reads .github/REVIEW_GUIDE.md. So:

- git mv .github/REVIEW_GUIDE.md -> REVIEW.md (CC-native path; GitHub App + humans +
  codex + the checks: block now all read the one file).
- Mirror the 7 canonical lessons into CLAUDE.md "Reviewing a PR" so the in-session
  /code-review applies them too (it cannot see REVIEW.md).
- Repoint review-checks.sh default path, ci.yml, tests, and the review-pr.sh comment.
- Keep the deterministic hardcoded-path scan + CI gate unchanged (tool-independent).

Tests: review-checks.test.sh 15/15, review-checks-py.test.py 17/17; the gate parses
checks: from REVIEW.md; the diff itself passes the gate (docs are .md-skipped).

Stand: Echo Act IV Pro

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rq18tfPmFcgzHQFgoSpsUN

* chore(review): sync AGENTS.md + merge main — clears qingyun's CR

Regenerated AGENTS.md via scripts/agents-md-sync.sh so the new CLAUDE.md
review criteria are mirrored (the 3 failing required CI checks were all this
one omission). Merged origin/main to clear the behind-main state.

Stand: Echo Act IV Pro

* docs(review): make REVIEW.md the single source for review criteria

Drop the duplicated 7-item review-criteria list from CLAUDE.md and replace
it with a short pointer to REVIEW.md. The only consumer that required the
criteria inlined in CLAUDE.md was the local /code-review command, which the
owner is retiring (disable-model-invocation / user-triggered-only), so the
mirror is no longer needed.

Enforcement now flows through REVIEW.md alone: review-preflight.py reads and
prints the criteria inline on every preflight run, scripts/review-checks.sh
runs the machine checks: block in CI, and the managed GitHub-App reviewer
reads REVIEW.md directly. Regenerated AGENTS.md from CLAUDE.md.

Stand: Echo Act IV Pro

* docs(review): update REVIEW.md note to single-source (drop stale CLAUDE.md-mirror text)

The CLAUDE.md prose copy is gone; REVIEW.md is now the one source. Rewrites
the note to reflect how the criteria reach reviewers (preflight print +
review-checks.sh CI + GitHub-App direct read) instead of the removed sync.

Stand: Echo Act IV Pro

* fix(review-checks): stream diff via stdin + fail closed on scanner launch failure

The runner passed the entire PR diff to review-checks.py through the RC_DIFF
env var. On a large diff (~8MB, > macOS ARG_MAX of 1MB) the python3 exec hit
'Argument list too long', the scanner never launched, yet the shell discarded
the exit code and still printed 'review-checks: PASS (hardcoded-paths clean)'
and exited 0 — a silent false pass that lets a big PR bypass the scan while CI
reports PASS (#2281, reported by qingyun's Codex).

Fix:
- review-checks.sh now STREAMS the diff to the scanner on stdin (printf, a bash
  builtin — no exec-size limit) instead of the RC_DIFF env var. Pattern lists
  (RC_FLAGS/RC_ALLOWS) stay in env; they're small.
- Fail closed: capture the scanner's exit code. Non-zero (exec failure / crash)
  now errors out with a clear message and exit 2 — never PASS. Empty stdout is
  no longer misread as "clean". Exit contract preserved: 0=clean, 1=flagged,
  2=usage error OR scanner-launch failure.
- review-checks.py reads the diff from stdin (documented) instead of RC_DIFF.
- Regression tests (shell + python) feed an oversized (>ARG_MAX) diff carrying
  a hardcoded /Users/alice/secret path and assert it's still flagged (exit 1,
  never a silent PASS). Both fail on the old argv/env code, pass on the new.

Stand: Echo Act IV Pro

* fix(review-checks): don't flag hardcoded paths inside docstring prose

The scanner skipped `#`/`//` comment lines but not triple-quoted docstring
prose, so a PR that ADDS a docstring *describing* a legacy path it removes got
false-flagged (found live: #2313's `claude_home_path` fix documents the old
`~/.claude/skills/` it replaces, and the runner flagged the doc line).

Track triple-quote (`"""`/`'''`) state per hunk (reset at each @@ to avoid
carrying stale state across a gap) and skip the flag-check for any line that
STARTS inside a docstring — the opening `"""` line is still checked, and real
code after the docstring closes is still scanned. Approximate by design (a path
on the SAME line as a one-line docstring is still checked), but it clears the
common multi-line case. Verified: the real #2313 diff now PASSES.

3 new cases in review-checks-py.test.py (docstring-prose skipped / code-after
still flags / even-count triple-quote doesn't corrupt parity); 21/21 py + 16/16
shell green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rq18tfPmFcgzHQFgoSpsUN
Stand: Echo Act IV Pro

* docs(review): add the worst-case-disruption question + maintainer merge-gate (Chi)

Chi 2026-07-25 (#dev): "add in the reviewer's guide to ask: What is the worst
case disruption to the Sutando users by this pr and how do we mitigate that.
And the maintainer can check that question before merging too."

Crystallizes lesson 5 (disruption-to-existing-users) into the explicit question
every reviewer AND the maintainer-before-merge must answer, and folds in the
#1898 auto-action angle (what code/state does it act on).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rq18tfPmFcgzHQFgoSpsUN
Stand: Echo Act IV Pro

* fix(review-checks): fail closed in CI + scope docstring exemption to real docstrings (Qingyun review, #2281)

Two blocking findings from Qingyun's review on head 4a6b1aa:

1. CI review-checks step fails open. `gh pr diff N | bash review-checks.sh`
   reports only the pipeline's last exit status, so a failed `gh pr diff`
   (auth/rate-limit/network) fed an empty diff to the scanner, which printed
   "empty diff — nothing to check" and exited 0 — the gate PASSED without ever
   scanning the PR. Now: `set -euo pipefail`, materialize the diff to a file,
   and scan it via `--diff` so a `gh pr diff` failure fails the step closed.

2. Triple-quote suppression was too broad. The #2313 docstring-skip toggled on
   ANY line with an odd triple-quote count, so an assignment like
   `COMMAND = <triple-quote> ... rsync /Users/alice/data ... <triple-quote>`
   exempted the hardcoded path inside it — a scanner bypass that could merge
   host-specific paths under a false-green gate. Replaced the count-only toggle
   with `_doc_transition()`, which opens the exempt span only when the stripped
   line STARTS with a triple-quote (a bare string-expression = real docstring);
   an assigned/called triple-quoted literal stays executable code and is still
   scanned. Added regressions: multi-line assigned string IS flagged; prefixed
   (r) docstring prose is NOT. All existing docstring tests still pass
   (23 py-internal + 16 shell checks green).

AGENTS.md already mirrors the CLAUDE.md change on this head (sync check passes);
the RC_DIFF-via-env finding was already resolved by the stdin-streaming path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rq18tfPmFcgzHQFgoSpsUN
Stand: Echo Act IV Pro

* docs(REVIEW.md): ground lessons 2/4/5 in the real reported breakages

Each grounded lesson now cites its actual incident: #2 ← the #1600-M-fix muted-bot
(#1663) + the #1403 migration stand-identity loss (#1540); #4 ← #1428's default-on
watchdog loop (Chi 'nearly unusable', Rui's host bitten unnoticed); #5 ← #1898's
latent bug in the un-asked worst-case. Corpus: sutando-pr-triage kb/breakage/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: john-the-dev <johnv.the.dev@gmail.com>
Co-authored-by: Rui Wang <52230987+john-the-dev@users.noreply.github.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — the destination this PR switches to is wrong on at least one real host, and the PR's premise is inverted there.

The change swaps Path.home()/".claude"/"skills" for claude_home_path("skills") (i.e. $CLAUDE_CONFIG_DIR/skills), on the stated grounds that "on a migrated install this check scanned a stale ~/.claude/skills/ and warned 'unlinked' about skills whose symlinks exist … under the workspace claude-home."

On the host I'm on right now, it is exactly the other way around. Evidence:

$CLAUDE_CONFIG_DIR = …/space.ag2.app/workspace/.claude-sutando
$CLAUDE_CONFIG_DIR/skills/                → exists, 50 entries, NO campaign-stats
~/.claude/skills/campaign-stats           → present

And the decisive part: I invoked campaign-stats successfully today — it ran and produced output. So the runtime is loading from ~/.claude/skills, which is the directory this PR would stop checking. After this change, health-check on this host would scan a skills/ dir that lacks the cloud-skill symlinks the agent actually uses, and would report "unlinked" for skills that are linked and loaded. That is the same false-warning failure mode you're fixing, pointed the other way.

Root cause of the disagreement: which of the two trees is authoritative is host-dependent — it depends on how the install was migrated and what the launcher baked into CLAUDE_CONFIG_DIR. Swapping one hardcoded root for another moves the bug rather than removing it.

Suggested shape: treat the destination as a set. Check claude_home_path("skills") and Path.home()/".claude"/"skills", and consider a skill linked if it appears in either; warn only when it is missing from all candidate roots. That is correct on both layouts and needs no per-host configuration. If you'd rather have one answer, resolve the dir the runtime actually loads from rather than inferring it from the config var.


What is unambiguously good here, and worth keeping regardless

The SKILL.md filter is a genuine fix on its own:

if not (skill_dir / "SKILL.md").is_file():
    continue

Mirroring skills/install.sh's filter is right — manifest-loaded and scripts-only skills are correctly never symlinked, and warning about them was pure noise. I'd happily approve that half as a standalone PR today.

The test change is also an improvement in principle (patching claude_home_path instead of Path.home is less brittle), though note it currently encodes the same single-root assumption — lambda *sub: self._home.joinpath(".claude", *sub) — so it would pass under either layout and therefore doesn't cover the divergence above. A case with the symlink present in only the other root would have caught this.

What I did not verify

I did not run the health-check test suite. My finding is from the live filesystem state and from having successfully invoked a skill that exists in only one of the two roots.

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving per Chi's explicit go (one-by-one). Review-verified LGTM: claude_home_path() skills-dir resolution + SKILL.md install-filter; kills the false skill-unlinked warn.

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Withdrawing my 20:12Z CHANGES_REQUESTED — I approve this. My blocking rationale was wrong, and I owe a correction on the record.

I claimed "the PR's premise is inverted" on this host. It is not. I independently hit this exact bug tonight, ~9h after that review and before re-reading this PR: health-check reported ⚠ skill-symlinks warn 1 unlinked: meeting-scheduler, and I traced it to skills_dst = Path.home()/".claude"/"skills" ignoring $CLAUDE_CONFIG_DIR, where the symlink is present and loads. That is your before-case, reproduced blind:

$CLAUDE_CONFIG_DIR/skills/meeting-scheduler -> <engine>/skills/meeting-scheduler/   LINKED
~/.claude/skills/meeting-scheduler                                                  absent

So the false positive is real and this PR removes it. My earlier framing was overstated: the destination is not wrong, it is incomplete.

The residual defect, which I do not think blocks merge but should not be lost. Your after-case lists 5 newly-surfaced entries as "findings the stale path was hiding":

⚠ skill-symlinks warn 5 unlinked: gws-gmail-voice, learned-skills, plugin-patches, pointer-teacher-tracer, secret-vault

On this host those 5 are loadable, so flagging them trades one false positive for five. Both directories are live skill roots here — evidence:

  • campaign-runner loaded in my current session as projectSettings:campaign-runner and I have been executing ~/.claude/skills/campaign-runner/scripts/smartlead.py from it all evening.
  • It exists only at ~/.claude/skills/campaign-runner. There is no <repo>/.claude/skills (verified absent), so projectSettings is resolving to ~/.claude/skills — that directory is not stale on this install.
  • All 5 of the newly-flagged skills exist in ~/.claude/skills and not in $CLAUDE_CONFIG_DIR/skills. Exactly the mirror of meeting-scheduler.

Counts on this host: before = 1 false unlinked + 14 false dangling; after = 5 false unlinked + 1 dangling. Net noise drops 15 → 6, which is why this is an improvement worth shipping rather than a regression worth blocking.

Suggested follow-up (not a merge condition): treat a repo skill as linked if it resolves in either root — union claude_home_path("skills") with Path.home()/".claude"/"skills" when the two differ — and report per-root only when it resolves in neither. A single-root check cannot be correct on an install where both roots load; picking the other root just moves which skills get misreported.

Approving on the current head. Verified: the meeting-scheduler false positive is reproduced and removed; the union gap is a live but lesser defect I am happy to see handled separately.

@github-actions

Copy link
Copy Markdown
Contributor

@cla-assistant check

@john-the-dev
john-the-dev merged commit ac10471 into main Jul 27, 2026
16 checks passed
@john-the-dev
john-the-dev deleted the fix/health-check-skills-dir branch July 27, 2026 02:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants