Repository navigation
fix(skills): login and plugin install skills through the shared installer - #125
dg-coreylweathers wants to merge 3 commits into
Conversation
cf03649 to
154daca
Compare
GregHolmes
left a comment
There was a problem hiding this comment.
Requesting changes based on one release-compatibility blocker and three follow-ups. This is a content assessment only: the PR remains draft and stacked on #124.
[B1] packages/deepctl-cmd-login/pyproject.toml:24 and sibling command packages allow incompatible core releases
Summary: Login, plugin, and skills now call install_for and warn_install_failure, introduced here in deepctl_core.skill_generator, but each package still permits deepctl-core>=0.1.10. A resolver can install a published older core with these newly released command packages, then fail after login or a plugin operation has completed; the failure handler can also fail because the warning helper is absent.
Expected: Every independently published command package must require the first released deepctl-core version that implements every core API it calls.
Observed: deepctl-cmd-login, deepctl-cmd-plugin, and deepctl-cmd-skills retain their pre-feature deepctl-core>=0.1.10 floors while now depending on APIs first added in this stack.
Recommended fix: In the release PR, raise all three leaf-package floors to the new core release version, publish core before or alongside the command packages, and add an isolated wheel-resolution test using each declared minimum. Do not publish the command packages until that release artifact is complete.
Verified in Docker: Ruff, mypy, and the full test suite passed (1,890 passed, 8 skipped).
| for _gen, _paths, leftover in sg.install_for(plan): | ||
| if leftover: | ||
| print_warning(escape(sg._msg("E12", staging=leftover)), stderr=True) | ||
| console.print("[dim]AI assistant skills updated[/dim]") |
There was a problem hiding this comment.
[S1] Refresh status bypasses quiet mode and writes to stdout
Summary: The new plugin-refresh success message uses a module-level Rich Console directly. That bypasses the output configuration, so dg --quiet plugin install, update, or remove still prints an unrelated skills-refresh message; it also adds status text to stdout.
Expected: A best-effort skills refresh must produce no non-essential output under --quiet; when it is shown, its status output must respect the command's stdout/stderr contract.
Observed: console.print("[dim]AI assistant skills updated[/dim]") always runs after a successful refresh. The new tests assert that output exists but do not cover quiet mode or its output channel.
Recommended fix: Remove the success notification, or send it through a quiet-aware status helper on stderr. Add CLI-level non-agentic tests for successful refresh with --quiet and for stdout/stderr separation.
There was a problem hiding this comment.
Fixed in 1d34a22. The success line is removed, so a successful refresh prints nothing; failures still go through the quiet-aware print_warning on stderr. New CLI test test_successful_refresh_keeps_skills_off_stdout_and_quiet_silent runs plugin install non-agentic with and without quiet and asserts nothing about skills on stdout or stderr, and both empty under quiet.
|
|
||
| deepctl replaces or deletes a folder only if skills.json records it for that tool, it is a real folder directly in that tool's skills folder, it holds deepctl's `.deepctl-skill` marker, and its contents are exactly what deepctl installed. So folders from `npx skills add`, and symlinked skill folders, are left alone. | ||
|
|
||
| `dg login` offers to install the skills, and `dg plugin install`, `update` and `remove` refresh the installed ones, through the same path as `dg skills install` and `update`. A problem prints a warning on stderr and never changes those commands' exit codes. |
There was a problem hiding this comment.
[S2] The README makes cancellation look non-fatal
Summary: The README says a problem during the skills step never changes login or plugin exit codes. But the handlers catch Exception, not KeyboardInterrupt; an interrupt during the shared installer propagates and exits 2, as the PR description itself states.
Expected: The README must distinguish recoverable skills-install failures from user cancellation or process interruption.
Observed: The code intentionally allows Ctrl-C to escape the skills handlers, while the README describes every problem as exit-code-neutral.
Recommended fix: Replace the sentence with: A skills installation failure prints a warning on stderr and does not change those commands' exit codes. Ctrl-C still cancels the command.
There was a problem hiding this comment.
Fixed in 1d34a22 with your wording: "A skills installation failure prints a warning on stderr and does not change those commands' exit codes. Ctrl-C still cancels the command."
|
|
||
| def print_warning(message: str) -> None: | ||
| """Print warning message.""" | ||
| def print_warning(message: str, *, stderr: bool = False) -> None: |
There was a problem hiding this comment.
[S3] Added CRLF lines fail whitespace validation
Summary: The added print_warning lines use CRLF endings. git diff --check origin/goal/bg-2-skills-folder-install...HEAD reports every added line as trailing whitespace, even though Ruff accepts the file.
Expected: The PR diff must pass Git's whitespace validation so review and CI tooling can treat whitespace failures as meaningful.
Observed: git diff --check reports trailing whitespace on lines 372, 373, and 378-380 of this file.
Recommended fix: Normalize the touched file to the repository's expected line endings, then rerun git diff --check.
There was a problem hiding this comment.
output.py is CRLF on main (as are 7 other files), and ruff format rewrites mixed endings back to CRLF, so converting only the added lines can't pass the format check. ec9a7e4 adds a two-line .gitattributes setting whitespace=cr-at-eol for that one file, so git diff --check origin/goal/bg-2-skills-folder-install...HEAD is clean when run from this branch's checkout. File contents are unchanged.
a256b8e to
ff4d7f6
Compare
6da366c to
ec789c5
Compare
3c97bf0 to
4aaf3ac
Compare
98a8176 to
962843f
Compare
4aaf3ac to
6f29e0b
Compare
962843f to
452e584
Compare
d16199f to
451267f
Compare
452e584 to
927b85f
Compare
451267f to
d1cf0f0
Compare
Move the fetch-once, preflight-every-tool, install-tool-by-tool loop from SkillsCommand._install into deepctl_core.skill_generator.install_for so login and the plugin refresh can use the same path. Add warn_install_failure and a stderr flag on print_warning for best-effort callers.
…installer Login's skills step and the skills refresh after 'dg plugin install', 'update' or 'remove' now call install_for, the path 'dg skills install' and 'update' use, instead of the old generator inside a silent except. Every tool is preflighted before anything is written, hint-only tools and existing installed_skills entries are never rewritten, and a failure prints a warning on stderr without changing the command's result. Rerunning login or a plugin command does not retry the skills step, so the warning names the command that does: 'dg skills install' for login, 'dg skills update' for the plugin refresh, in every sentence of a multi-folder ownership error. Tests cover fetch failures, an invalid recorded skills_ref, DEEPCTL_SKILLS_REF, a HOME path containing markup, and point skills.json at a throwaway path so no test touches the real home.
With login and the plugin refresh on install_for, SkillGenerator.install, save_skills_state and the 0.3.x command-metadata, hash and developer-guide renderers have no caller left. Remove them and their tests; the state-file write tests now go through _update_state, the one writer.
927b85f to
63c2be1
Compare
|
On B1 (the deepctl-core floors): same constraint as #124. Rule 2 of |
GregHolmes
left a comment
There was a problem hiding this comment.
Content approved. Corey's dependency-floor note is the correct release sequencing, not a blocker to this stacked PR: the compatible core version cannot exist until the stack lands. Before publishing, the release PR must raise the deepctl-core minimum for deepctl-cmd-login, deepctl-cmd-plugin, and deepctl-cmd-skills, then verify each wheel imports with exactly its declared minimum core.
Stacked on #124 (
d1cf0f0) until it merges.Commits, each passing ruff, mypy and the core, skills, login and plugin tests on its own:
refactor(skills): share one fetch, preflight and install loop from corefix(skills): install skills from login and plugin through the shared installerrefactor(core): delete the install() shim and the code only it usedGreg's review (2026-10-07)
install_fororwarn_install_failure).check_dependency_floors.pyrule 2 fails any floor above the workspace version, so the raise has to happen in the release PR after release-please bumps core. See Release checklist below.print_warning, which respects--quiet. New CLI testtest_successful_refresh_keeps_skills_off_stdout_and_quiet_silentrunsplugin installthroughCliRunnerin non-agentic mode, with and without quiet: nothing about skills on stdout or stderr, and under quiet both are empty.output.pyis CRLF on main (as are 7 other.pyfiles), and ruff format rewrites mixed endings back to CRLF, so converting only the added lines fails the format check. A new one-line.gitattributesrule setswhitespace=cr-at-eolfor that file.git diff --check origin/goal/bg-2-skills-folder-install...HEADis now clean when run from this branch's checkout, which carries the new.gitattributes, and no file is reformatted.What
dg login's skills step and the skills refresh afterdg plugin install,updateandremovenow install throughinstall_for, a new shared helper indeepctl_core.skill_generator. It is the same fetch-once, preflight-every-tool, record-as-each-tool-lands loopdg skills installandupdateuse.SkillsCommand._installnow calls it too, so there is one copy of the loop.The
SkillGenerator.install()compatibility shim from #124 is deleted, along withsave_skills_stateand the 0.3.x code only the shim used.Closes
Greg's earlier B3 on #111 (review of 2026-09-22):
dg skillsall go throughinstall_for.install_tool(record before swap, then settle).except Exception: passis gone from both commands.Behavior
Otherwise, behavior matches
dg skills install(login) anddg skills update(plugin refresh): same records, same per-tool stop on the first failure, same ownership refusals (E1, E22). The differences:print_warning(..., stderr=True)so stdout stays clean outside agentic mode too. If staging was left behind, an E12 line comes first.run 'dg skills install' to try again(login) orrun 'dg skills update' to try again(plugin). It replaces the core message's "then run the command again" in every sentence, so a warning that lists several folders names the retry command for each one.dg skillsmessages are unchanged. For example:⚠ AI assistant skills were not updated: deepctl cannot prove it installed /home/you/.cursor/skills/api, so it will not replace anything there; move or rename what is there, then run 'dg skills update' to try again.install_tool, which holds skills.json.lock for each tool's whole install, so they wait up to 30 seconds for anotherdg skills,dg loginordg plugincommand, then warn (E27) without changing the exit code. The README's lock sentence now names all three.dg skillskeeps feat(skills): install and remove only skill folders deepctl owns #124's wording.installed_skills. Amazon Q's and Aider's entries, and every folder tool's 0.3.xpaths, stay byte-identical.What else users see:
✓ Claude Code → /home/you/.claude/skills (14 skills). Main printed one line per path, which would be 84 lines for six tools.dg skills install.dg skills updatedoes.test_handle_install_git_url_detectionran the real refresh against the developer's HOME.Done when
goal/bg-3-login-plugin-installer, based on feat(skills): install and remove only skill folders deepctl owns #124's branch as a stacked draft.git diff --numstat origin/goal/bg-2-skills-folder-install...HEAD -- . ':(exclude)*/tests/*' ':(exclude)tests/*'.test_login_writes_the_records_dg_skills_install_wouldandtest_refresh_writes_the_records_dg_skills_update_wouldcompare normalized skills.json against a realSkillsCommandrun in a second HOME.test_install_for_second_tool_failure_keeps_first_recorded(core),test_login_second_tool_failure_keeps_first_recorded_warns_once_exit_zero(throughLoginCommand.handle) andtest_refresh_second_tool_failure_keeps_first_recorded_warns_once_exit_zero(through click'sCliRunner). The first tool stays recorded as installed.success, the plugin command exits 0).TestWarningOnStderrcoversprint_warning(stderr=True).test_generator_has_no_install_shimchecks it.git grep -nE "gen\.install\(|\.install\(commands|save_skills_state" packages/*/src srcprints nothing.git grep "\.install(" packages/*/srcprints onlydeepctl-cmd-plugin/.../command.py:761 strategy.install(options). That is the pip/uv plugin install strategy, unrelated to skills.dg skills install --all, in Docker as uid 0 and uid 1000 (details below).Mutation testing. 29 mutants of the new guards and messages, all killed:
install_for's hint-only filter, preflight and ref dedup;stderr=True;auto_update, empty-plan and leftover branches;except Exceptionnarrowed toSkillInstallError(fetch failure and invalid recorded ref);DEEPCTL_SKILLS_REF, and the per-tool line withoutescape();removesuffix), or only the first;Upgrade check
Input: the origin/main HOME archive (8 tools, 0.3.x files, user
mine.md), plus a seeded~/.claude/skills/my-skill,.api.tmp-123and.api.old-1-2. Results were the same as uid 0 and as uid 1000, on this branch on top of #124's earlier head4aaf3ac; since then #124 changed onlyremove's handling of copies left by an interrupted run, which this HOME does not trigger.all): no prompt, becauseinstalled_skillsis non-empty. skills.json is byte-identical and all 16 pre-existing files are sha-identical.~/.cursor/skills/api:installed, withv03set;installed_skillsis equal to the archive's, including Amazon Q's and Aider's entries and every 0.3.xpaths;dg skills status,listandremove --all: remove notes the 0.3.x files for Claude Code and Amazon Q Developer, and every pre-existing file is sha-identical.all):dg skills install --allrun on a copy of the same HOME (normalized).0.x API removal
This PR removes these public
deepctl_core.skill_generatornames, which no command uses after this change:SkillGenerator.installsave_skills_stateCommandMetadatacollect_command_metadataskills_need_updaterender_developer_guiderender_skill_content_commands_hashand_safe_get_argumentsThey are removed in a separate commit. The removal will be noted in the release PR's release notes, with no breaking-change
!marker (deepctl-core is 0.x).Release checklist
For the release PR that ships this stack (B1):
deepctl-corefloor in deepctl-cmd-login, deepctl-cmd-plugin and deepctl-cmd-skills to that new core version. They callinstall_forandwarn_install_failure, which no published core has.check_dependency_floors.py --fixonly updates root floors, so this is by hand.Concurrency check
A race harness using #124's real lock races a plugin refresh,
dg skills update, login anddg skills installagainstdg skills removefor the same tool, with remove starting mid-swap and before the first swap: 8 runs on this branch, in Linux Docker as uid 1000. In every run remove waited on the lock until the install finished, 0 orphaned folders, 0 staging left, and the nextdg skills install --alland plugin refresh succeeded with no warning.Not in this PR
dg skills updatedoes today. So it can bring back a tool removed after the refresh read skills.json and before it reaches that tool (during the download, while earlier tools install, or while it waits for the lock), or overwrite a--refchosen in that window.install_tool, under the lock, closes this window.install_toolre-proves ownership from disk under the lock.🤖 Generated with Claude Code