Skills installed by Codex, Cursor, Gemini CLI, or opencode live under
`.agents/skills/` -- the convention documented at agentskills.io -- so users
had to keep a duplicate copy in `.claude/skills/` before cc-haha could see
them. The spec only constrains SKILL.md itself, which we already parse, so
this is purely a directory-discovery gap.
Scan both conventions at every scope: user (`~/.agents/skills`), project
(every level up to the repo root), `--add-dir` roots, on-demand monorepo
discovery, and the hot-reload watcher. skillRoots.ts is the single source of
truth for those paths; the REST API now reports which convention a skill came
from and the desktop list badges the `.agents` ones.
`.claude` is always ordered first, so first-wins dedup keeps a user's own
skills authoritative rather than letting an externally installed same-named
skill shadow them. Name collisions are collapsed per scope, which covers
copy-based sync -- symlink-based sync was already handled by the existing
realpath dedup. Collisions across different scopes, such as a user skill and
a project skill sharing a name, keep their existing behavior, and the shadowed
file's identity is recorded so overlapping roots (CLAUDE_CONFIG_DIR pointing
inside the project) cannot reintroduce it under the other scope.
getProjectDirsUpToHome is untouched -- it is shared by commands, agents,
output-styles, and workflows. Only its directory walk is extracted so the
skill resolver reuses the same submodule/worktree stop boundary. Writes stay
where they were: market installs and /skillify still target
`~/.claude/skills`.
`.agents` is anchored at $HOME rather than CLAUDE_CONFIG_DIR, since it is a
space shared with other tools that read $HOME. Note that Bun snapshots
os.homedir() at startup while Node re-reads $HOME per call, so the resolver
reads the env var explicitly.
Opt out with `disableAgentSkillsDirectory` in settings.json or
CLAUDE_CODE_DISABLE_AGENT_SKILLS_DIR=1; no `.agents` root is produced at all
then, so there is not even a failed stat.
(cherry picked from commit ba9338f85d6beaba237753e42e823c87b59633d4)
setInteractiveRegions only recorded visibleDragRegion inside the darwin
branch, so on Windows and Linux it stayed null and dragging fell back to
clamping against the whole PET_WINDOW_WIDTH x PET_WINDOW_HEIGHT window. That
window is mostly transparent padding around a mascot anchored bottom center,
so the mascot stopped short of every display edge by the width of that
padding and could not be dragged into a corner. Record the region on all
platforms; the reposition-on-restore step stays darwin-only.
Normalizing the region also clamped against the nominal constants. Renderer
regions are measured against the live viewport, so clamp against the real
content box instead -- the same petWindowContentExtent the shape clamp
already uses -- and let the shape clamp reuse that one normalizer.
The Windows shape clamp used the nominal PET_WINDOW_HEIGHT constant. Renderer
regions are measured against the live viewport, so once the content area was
taller than that constant the mascot -- which sits flush with the viewport
bottom -- got sliced off from below, while the task badge kept its own
unclamped rect and stayed visible. Clamp the shape to the real content box.
Dragging also moved the window with setPosition, which Windows resolves as
getSize() + setBounds(); that DIP round trip grows the window a pixel at a
time on fractional display scaling, and the drag timer fires every 16ms.
Restate the recorded size on every tick so dragging stays size-neutral.