SIGN IN SIGN UP

fix: stop the palette silently dropping and mis-sourcing skills

Three verified defects in slash-command and skill discovery, each with a
test that fails without its fix.

A symlinked skill directory was dropped. scanSkillDirBudget rejected any
entry whose DirEntry.IsDir() was false, which is every symlink, so a skill
symlinked in from a dotfiles repo vanished from the palette with no
diagnostic - not even a suppression entry. Measured against a real
~/.claude/skills: 40 skills on disk, 39 discovered plus 1 legitimately
suppressed only after this change; before it, one was unaccounted for.
scanSkillDirFormat in the same file already followed symlinks and
de-duplicated by resolved path, so this adopts that convention rather than
inventing one, and de-duplicates the same way now that two entries can name
one real skill.

walkDirBudget deliberately keeps skipping symlinks. A command tree is
namespaced by directory, so following a link that resolves inside the same
tree would publish one file twice under two namespaces, with os.ReadDir
order deciding which name wins. TestWalkSymlinkDirSkipped and
TestWalkSymlinkFileSkipped pin that; the comment now says why the two
scanners differ. It does gain a regular-file check: a FIFO named *.md
reached fileFrontmatter, whose os.ReadFile blocks forever on a pipe with no
writer - a permanent hang in a service that polls.

An empty ctx.Home made the relay scan its own working directory. ctx.Home
is the service's os.UserHomeDir() and is empty whenever that lookup fails,
so filepath.Join(ctx.Home, ".claude", "skills") yields a relative path that
os.ReadDir resolves against the headless service's arbitrary cwd - and the
results were labelled "personal" as if they were the user's. agentroots
already refuses a non-absolute base for this exact reason; the providers'
hand-rolled joins bypassed it. omp, Pi and Kimi now reject one, and Kimi no
longer reads a config.toml from there either. A relative omp
customDirectories entry now resolves against the pane's cwd, matching how
Kimi treats extra_skill_dirs, and is skipped when that cwd is unknown.

Two omp config.yml shapes silently discarded a user's bans. A block
sequence written at its parent's own column - legal YAML - cleared
disabledExtensions and then dropped every item, because the indent == 0
branch reset the parser mode before inspecting the line. A trailing comment
on a section header made the value "#..." rather than empty, so neither
"skills:" nor "disabledExtensions:" was recognised as introducing a block,
discarding the whole section including skills.enabled: false. Both left the
palette offering skills the user had explicitly disabled, which is the one
place failing open is wrong.

Not fixed, deliberately: claude.go and qoder.go walk project directories
outermost-first, so a large git-root scope can exhaust the shared file
budget before the pane's own directory is reached. Reversing the loop would
also reverse precedence, because apply() is last-wins, trading a budget
ordering nit for a command precedence regression. It needs its own change.
K
Kiryl Berasneu committed
9191443aed6b98accd57cb4c890fb5582cc36cc9
Parent: 1d092c4