Skip to content

feat(skills): install and remove only skill folders deepctl owns - #124

Draft
dg-coreylweathers wants to merge 7 commits into
goal/bg-1-skills-bundle-fetchfrom
goal/bg-2-skills-folder-install
Draft

dg-coreylweathers wants to merge 7 commits into
goal/bg-1-skills-bundle-fetchfrom
goal/bg-2-skills-folder-install

Conversation

@dg-coreylweathers

@dg-coreylweathers dg-coreylweathers commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Second PR of the five that replace #111. Stacked on #123 (goal 1, the skills bundle fetch). Its base is goal/bg-1-skills-bundle-fetch until #123 merges, then it will be rebased onto main.

What it does

dg skills install, update, setup, remove, status and list now install all 14 Deepgram skills as folders in each tool's skills folder. The skills come from the pinned bundle that goal 1 added. Before this, skills were concatenated into files, and remove deleted by glob, so it could delete a user's own files.

Tool Skills folder
Claude Code ~/.claude/skills
OpenAI Codex ~/.agents/skills
Gemini CLI ~/.gemini/skills
Cursor ~/.cursor/skills
OpenCode ~/.config/opencode/skills
Cline ~/.cline/skills
Amazon Q Developer, Aider hint only

The ownership rule, in plain words

deepctl replaces or deletes a skill folder only if all four of these are true:

  1. skills.json records it for that tool.
  2. It is a real folder directly inside that tool's skills folder, not a symlink.
  3. It holds deepctl's .deepctl-skill marker.
  4. Its content fingerprint matches what deepctl installed.

Anything else is left alone and reported. That covers a user's own skills, folders from npx skills add, symlinked skill folders, and deepctl folders the user has edited. The fingerprint is checked again on the moved-aside copy right before anything is deleted, so an edit made while the operation runs is also caught and put back.

Greg's findings this closes (from the 2026-10-05 review on #111)

  • B5: a folder created after preflight is never replaced. Each destination is claimed with os.mkdir (POSIX) or a plain os.rename (Windows, which refuses an existing destination). Test: the destination is created after install_conflicts() returns, and the user's folder survives.
  • B6: an "installing" record naming the destination is saved before any folder moves. If that save fails, nothing moves. Test: save_skills_state fails on that write, and no new folder is left while the old one stays intact.
  • B7: no code deletes a path because of its name. Staging is a fresh mkdtemp, and leftovers are reported, never swept. Test: a user-created .api.tmp-123 and .api.old-1-2 survive install and remove.
  • S4: the two environment-dependent tests pin agentic detection, and they pass with CI, CLAUDECODE and TTY variables both set and unset.
  • B4 for records: names read back from skills.json go through goal 1's name check.

Compatibility

Login and plugin are unchanged. A compatibility shim keeps SkillGenerator.install() and the six module functions they call. Records live under their own key, and the public save_skills_state() re-reads that key from disk. That way, a login or plugin run that saves a stale copy of the state can't erase the install records. A test runs the read-install-save-stale sequence. Plugin's auto-update keeps a user's --ref.

Upgrade from 0.3.x

I built a HOME with origin/main's real dg skills install --all, then ran this branch's install, update, an edited-folder update, and remove --all against it, as root and as uid 1000. Every 0.3.x file and every seeded user file stayed byte-identical. The 0.3.x files (~/.claude/commands/deepgram/*.md, the marked sections in shared instruction files, and the rules files) stay in place until goal 4. remove drops a tool's 0.3.x installed_skills entry but leaves its files. skill_folders.<tool>.v03 records that a tool's 0.3.x files remain, so remove can say so even after a plugin refresh. Goal 4 must clear it after a successful cleanup.

What is deliberately not here yet

  • Login and plugin moving onto the shared installer, which removes the shim: goal 3.
  • 0.3.x leftover cleanup: goal 4.
  • The skills.json lock and the re-check under it (S5): goal 5. Without the lock, two deepctl runs that overlap can lose one run's records: its folders stay on disk but read as not deepctl's, and the next run refuses them until the user deletes them. No file is deleted.
  • Not planned: pruning, keep-going, -o table/csv, and edit-tolerant fingerprints. Finder's .DS_Store counts as an edit, and the README says so.

How to review

  1. skill_generator.py: ownership helpers (_ownership, _fingerprint, _marker_ok), then install_conflicts, install_tool (stage → mark → swap → settle → cleanup), remove_tool and tool_status.
  2. command.py: the handlers, which are thin over core.
  3. Tests: grouped by B5, B6, B7, ownership, edit protection, Ctrl-C and races, and the S4 matrix.

Verification

  • Budget: 924 added source lines (cap 950), of which 19 are README. The budget counts added lines, per Corey.
  • Docker gate: ruff, mypy strict and the full pytest suite (1851 passed in the Docker gate; CI's package list runs 1855) all pass. Core tests also pass as uid 1000 and on Python 3.10, and the skills command tests pass on Python 3.10.
  • Mutation testing: every non-equivalent mutation of the safety and reporting checks is killed (about 190 mutants across the build and review rounds); six equivalent or unreachable mutants survive (an unreachable TypeError catch, rmdir on a link, a vanished-destination race, empty mirror paths, the marker size cap, and message text compared through the same table).

Stacked series: 2/5. Tracking PR: #111.

🤖 Generated with Claude Code

dg skills install, update, setup, remove, status and list now install
the Deepgram skills as folders in each tool's skills folder. A folder
is replaced or deleted only when skills.json records it for that tool,
it is a real folder directly in the tool's root, it holds the
.deepctl-skill marker, and its content fingerprint matches what deepctl
installed. Edited folders are left alone.

The record is saved before a folder moves into place, a destination
is claimed with mkdir so a folder created after preflight is never
replaced, and staging leftovers are reported, never swept by name.
Login and plugin keep working through a compatibility shim.
…ints

remove keeps a folder's record while its copy waits in staging, and an
interrupted put-back is retried. A file or symlink swapped in before a
move is put back with a no-replace link on POSIX. The MCP hint now
names /setup-mcp and only prints after a Claude Code install, counts
are pluralized, and remove notes 0.3.x files that stay behind.
…eporting

remove marks a folder held before moving it aside and puts it back if
interrupted. A tool upgraded from 0.3.x keeps a v03 flag so remove still
notes the files left behind after a plugin refresh. Messages no longer
assume a count or claim nothing was installed, and the cross-tool halt,
moved-folder report and reporting guards are now covered by tests.
…ve messages

remove now says it no longer tracks a folder it cannot prove, and only
mentions 0.3.x files when the tool really has some. New tests cover an
unreadable folder at settle, remove_tool errors, the v03 condition,
setup --ref, status after a user deletes a folder, and a corrupt state
failing before any prompt. Command tests pin the console width.
The compatibility shim returns a hint-only tool's recorded 0.3.x paths
instead of an empty list, so remove still notes those files after a
plugin refresh. E13 and E18 give remedies that fit every cause, remove's
staging failure (E21) is tested, and the command tests restore Rich's
console width.
… exist

list now says no skill folders are installed, which stays true on a
HOME upgraded from 0.3.x. Tests pin the release label in the install
summary and the Ref column in list.

This branch has not been deployed

No deployments
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.

1 participant