From 45e91d563e983f3d350907e3c23111b49b34d3fa Mon Sep 17 00:00:00 2001 From: Thomas Christory Date: Thu, 25 Jun 2026 22:54:18 +0200 Subject: [PATCH] feat(skill): resolve gemini/copilot install paths + quarterly convention re-check Gemini and GitHub Copilot CLI both confirmed (2026-06-25) user-scoped Skill paths: ~/.gemini/skills//SKILL.md and ~/.copilot/skills//SKILL.md (both also accept the agent-neutral ~/.agents/skills/ interop alias). The gemini/copilot resolvers now return a real path instead of manual instructions. Adds .github/workflows/skill-convention-review.yml: a quarterly scheduled job that opens a tracking issue to re-check the four canonical convention URLs. Closes #7 Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/skill-convention-review.yml | 48 +++++++++++++++++++ CHANGELOG.md | 6 +++ docs/guides/using-with-ai-agents.md | 19 ++++---- nsc/cli/skill_commands.py | 25 +++++----- tests/cli/test_skill_commands.py | 38 +++++++++++---- 5 files changed, 104 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/skill-convention-review.yml diff --git a/.github/workflows/skill-convention-review.yml b/.github/workflows/skill-convention-review.yml new file mode 100644 index 0000000..c53f4b3 --- /dev/null +++ b/.github/workflows/skill-convention-review.yml @@ -0,0 +1,48 @@ +name: skill-convention-review + +# Quarterly reminder to re-check the four per-platform Skill-install conventions +# hard-coded in nsc/cli/skill_commands.py `_RESOLVERS`. Opens a tracking issue so +# a maintainer re-reads each canonical source and updates code+docs on drift. + +on: + schedule: + # 09:00 UTC on the 1st of Feb/May/Aug/Nov. + - cron: "0 9 1 2,5,8,11 *" + workflow_dispatch: + +permissions: + contents: read + issues: write + +jobs: + open-tracking-issue: + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} + steps: + - name: Open quarterly convention re-check issue + run: | + gh issue create \ + --repo "${{ github.repository }}" \ + --title "Quarterly: re-check Skill install conventions" \ + --body "$(cat <<'EOF' + Quarterly re-check of the four per-platform Skill-install conventions + hard-coded in `nsc/cli/skill_commands.py` `_RESOLVERS`. Re-read each + canonical source below and confirm the documented user-scoped path is + still current. + + - [ ] **claude-code** — https://code.claude.com/docs/en/skills + - [ ] **codex** — https://developers.openai.com/codex/skills + - [ ] **gemini** — https://geminicli.com/docs/cli/skills/ + - [ ] **copilot** — https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-skills + + If any convention drifted, update all of: + + - `nsc/cli/skill_commands.py` (the relevant `_resolve_*` resolver) + - `docs/guides/using-with-ai-agents.md` (per-target path table) + - `skills/netbox-super-cli/SKILL.md` (if it lists per-target paths) + - `CHANGELOG.md` (note the change under the unreleased section) + + Close this issue once all four are confirmed (and any drift is shipped). + EOF + )" diff --git a/CHANGELOG.md b/CHANGELOG.md index 22d427a..c66fcea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,11 +48,17 @@ mode — plus a round of state-directory, workflow, and supply-chain hardening. (github-actions ecosystem, weekly). - **E2E CI matrix over NetBox v4.5 and v4.6** ([#4]). The end-to-end job now runs against both NetBox lines (`fail-fast: false`, per-version labels). +- **`nsc skill install` now supports Gemini and Copilot CLI** ([#7]). Their + user-scoped Skill paths (`~/.gemini/skills/`, `~/.copilot/skills/`) are now + confirmed and documented, so `--target gemini` / `--target copilot` resolve to + a real install path instead of printing manual instructions. A quarterly CI + job opens a tracking issue to re-check the four per-platform conventions. [#6]: https://github.com/thomaschristory/netbox-super-cli/issues/6 [#3]: https://github.com/thomaschristory/netbox-super-cli/issues/3 [#2]: https://github.com/thomaschristory/netbox-super-cli/issues/2 [#5]: https://github.com/thomaschristory/netbox-super-cli/issues/5 +[#7]: https://github.com/thomaschristory/netbox-super-cli/issues/7 [#90]: https://github.com/thomaschristory/netbox-super-cli/issues/90 [#9]: https://github.com/thomaschristory/netbox-super-cli/issues/9 [#12]: https://github.com/thomaschristory/netbox-super-cli/issues/12 diff --git a/docs/guides/using-with-ai-agents.md b/docs/guides/using-with-ai-agents.md index 3e6da58..7d7c3b7 100644 --- a/docs/guides/using-with-ai-agents.md +++ b/docs/guides/using-with-ai-agents.md @@ -45,16 +45,15 @@ the resolved destination path. |--------------|------------|----------------------------------------------------------------| | claude-code | confirmed | `~/.claude/skills/netbox-super-cli/SKILL.md` | | codex | confirmed | `~/.agents/skills/netbox-super-cli/SKILL.md` | -| gemini | manual | (no programmatic install; prints manual instructions) | -| copilot | manual | (no programmatic install; prints manual instructions) | - -For targets marked `manual`, `nsc skill install --target ` prints -actionable instructions (exit 0) instead of writing a guessed path. -The bundled Skill content is at `skills/netbox-super-cli/SKILL.md` in -this repo and inside the installed wheel; for `manual` targets, paste -its content into your tool's project-scoped configuration (e.g. -`GEMINI.md` for Gemini CLI, `.github/copilot-instructions.md` for -GitHub Copilot CLI). +| gemini | confirmed | `~/.gemini/skills/netbox-super-cli/SKILL.md` | +| copilot | confirmed | `~/.copilot/skills/netbox-super-cli/SKILL.md` | + +Gemini CLI and GitHub Copilot CLI also accept the agent-neutral +`~/.agents/skills/` interop alias (the same prefix Codex uses), so a +single export to `~/.agents/skills/` can be shared across all three; +`nsc skill install` writes to each tool's primary path above. The +bundled Skill content is at `skills/netbox-super-cli/SKILL.md` in this +repo and inside the installed wheel. ## Agent prompts that work well diff --git a/nsc/cli/skill_commands.py b/nsc/cli/skill_commands.py index 8bdd4da..22ced66 100644 --- a/nsc/cli/skill_commands.py +++ b/nsc/cli/skill_commands.py @@ -64,28 +64,25 @@ def _resolve_codex() -> _Resolution: def _resolve_gemini() -> _Resolution: + # Per geminicli.com/docs/cli/skills/ (confirmed 2026-06-25): Gemini CLI + # loads user-scoped skills from $HOME/.gemini/skills//SKILL.md. It + # also accepts the agent-neutral ~/.agents/skills/ interop alias. return _Resolution( target=_Target.GEMINI, - path=None, - manual_instructions=( - "Gemini CLI does not document a programmatic Skill install path " - "as of this nsc release. To use the bundled Skill: paste its " - "content into a project-scoped GEMINI.md or your Gemini system " - "prompt." - ), + path=_home() / ".gemini" / "skills" / BUNDLE_NAME / "SKILL.md", + manual_instructions=None, ) def _resolve_copilot() -> _Resolution: + # Per docs.github.com/.../copilot-cli/customize-copilot/add-skills (confirmed + # 2026-06-25): GitHub Copilot CLI loads user-scoped skills from + # $HOME/.copilot/skills//SKILL.md. It also accepts the agent-neutral + # ~/.agents/skills/ interop alias. return _Resolution( target=_Target.COPILOT, - path=None, - manual_instructions=( - "GitHub Copilot CLI does not document a stable user-scoped Skill " - "install path as of this nsc release. To use the bundled Skill: " - "paste its content into .github/copilot-instructions.md or your " - "team's Copilot configuration." - ), + path=_home() / ".copilot" / "skills" / BUNDLE_NAME / "SKILL.md", + manual_instructions=None, ) diff --git a/tests/cli/test_skill_commands.py b/tests/cli/test_skill_commands.py index d9fc0d8..c3ad64d 100644 --- a/tests/cli/test_skill_commands.py +++ b/tests/cli/test_skill_commands.py @@ -50,13 +50,34 @@ def test_install_apply_codex_writes_to_agents_skills(home: Path) -> None: assert dest.exists() -def test_install_dry_run_gemini_prints_manual_instructions(home: Path) -> None: +def test_install_apply_gemini_writes_to_gemini_skills(home: Path) -> None: + """Gemini CLI loads from $HOME/.gemini/skills/ (confirmed 2026-06-25).""" + runner = CliRunner() + result = runner.invoke(app, ["skill", "install", "--target", "gemini", "--apply"]) + + assert result.exit_code == 0, result.output + dest = home / ".gemini" / "skills" / "netbox-super-cli" / "SKILL.md" + assert dest.exists() + + +def test_install_apply_copilot_writes_to_copilot_skills(home: Path) -> None: + """Copilot CLI loads from $HOME/.copilot/skills/ (confirmed 2026-06-25).""" + runner = CliRunner() + result = runner.invoke(app, ["skill", "install", "--target", "copilot", "--apply"]) + + assert result.exit_code == 0, result.output + dest = home / ".copilot" / "skills" / "netbox-super-cli" / "SKILL.md" + assert dest.exists() + + +def test_install_dry_run_gemini_prints_would_write(home: Path) -> None: runner = CliRunner() result = runner.invoke(app, ["skill", "install", "--target", "gemini"]) assert result.exit_code == 0, result.output - assert "GEMINI.md" in result.output or "Gemini" in result.output - assert "would write to" not in result.output + assert "would write to" in result.output + assert ".gemini/skills/netbox-super-cli/SKILL.md" in result.output + assert not (home / ".gemini" / "skills" / "netbox-super-cli" / "SKILL.md").exists() def test_install_apply_overwrites_existing_file(home: Path) -> None: @@ -96,15 +117,16 @@ def test_install_json_output_apply(home: Path) -> None: assert Path(payload["destination"]).exists() -def test_install_json_output_manual_target(home: Path) -> None: +@pytest.mark.parametrize("target", ["gemini", "copilot"]) +def test_install_json_output_resolves_path(home: Path, target: str) -> None: runner = CliRunner() - result = runner.invoke(app, ["skill", "install", "--target", "copilot", "--output", "json"]) + result = runner.invoke(app, ["skill", "install", "--target", target, "--output", "json"]) assert result.exit_code == 0, result.output payload = json.loads(result.output) - assert payload["manual"] is True - assert payload["destination"] is None - assert "instructions" in payload + assert payload["manual"] is False + assert payload["destination"].endswith("skills/netbox-super-cli/SKILL.md") + assert "instructions" not in payload @pytest.mark.parametrize("target", ["claude-code", "codex", "gemini", "copilot"])