Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions .agents/skills/merge-and-release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,17 @@ skill covers all of it, scoped down by what the maintainer actually asks for.
retried.
7. In the hub, when the chosen scope includes a release, bring this checkout to the merged
content without discarding or mixing in anything local. First assert `git status --porcelain
--untracked-files=all --ignored` is empty, and stop and report rather than proceeding over any
uncommitted content, tracked, untracked, or gitignored, since `skills_install.py` installs
each skill directory with `shutil.copytree()`, which copies a gitignored stray file the same
as any other, so the plain porcelain form (silent on ignored paths) would pass this preflight
while one still rides along into the install. Then `git fetch origin main`, `git checkout main`
--untracked-files=all --ignored -- .agents/skills/ .claude-plugin/` is empty, and stop and
report rather than proceeding over any uncommitted content there, tracked, untracked, or
gitignored, since `skills_install.py` reads both paths: `shutil.copytree()` installs each
`.agents/skills/` skill directory for Codex/opencode, and `claude plugin marketplace add`
installs from `.claude-plugin/` for Claude Code, so a gitignored stray file under either rides
along the same as any other, and the plain porcelain form (silent on ignored paths) would pass
this preflight while one still rides into an install. Scoped to those two paths rather than
the whole tree, matching `skills_install.py`'s own `source_ref()` dirty check (`watched =
[SKILLS_SRC, CLAUDE_PLUGIN_DIR]`), since an ignored file elsewhere in the checkout (a build
cache, a lockfile) is not this preflight's concern and should not block the refresh on it.
Then `git fetch origin main`, `git checkout main`
(or `git checkout -b main origin/main` the first time this checkout carries no local `main` at
all, `checkout` rather than `switch` since the fleet's own `git` floor is undeclared and
`checkout` needs no minimum version for this), and `git merge --ff-only origin/main`.
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/fleet-skills/.source-digest
Original file line number Diff line number Diff line change
@@ -1 +1 @@
332c16d2c066383b
86607328dc15eded
16 changes: 11 additions & 5 deletions .claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,17 @@ skill covers all of it, scoped down by what the maintainer actually asks for.
retried.
7. In the hub, when the chosen scope includes a release, bring this checkout to the merged
content without discarding or mixing in anything local. First assert `git status --porcelain
--untracked-files=all --ignored` is empty, and stop and report rather than proceeding over any
uncommitted content, tracked, untracked, or gitignored, since `skills_install.py` installs
each skill directory with `shutil.copytree()`, which copies a gitignored stray file the same
as any other, so the plain porcelain form (silent on ignored paths) would pass this preflight
while one still rides along into the install. Then `git fetch origin main`, `git checkout main`
--untracked-files=all --ignored -- .agents/skills/ .claude-plugin/` is empty, and stop and
report rather than proceeding over any uncommitted content there, tracked, untracked, or
gitignored, since `skills_install.py` reads both paths: `shutil.copytree()` installs each
`.agents/skills/` skill directory for Codex/opencode, and `claude plugin marketplace add`
installs from `.claude-plugin/` for Claude Code, so a gitignored stray file under either rides
along the same as any other, and the plain porcelain form (silent on ignored paths) would pass
this preflight while one still rides into an install. Scoped to those two paths rather than
the whole tree, matching `skills_install.py`'s own `source_ref()` dirty check (`watched =
[SKILLS_SRC, CLAUDE_PLUGIN_DIR]`), since an ignored file elsewhere in the checkout (a build
cache, a lockfile) is not this preflight's concern and should not block the refresh on it.
Then `git fetch origin main`, `git checkout main`
(or `git checkout -b main origin/main` the first time this checkout carries no local `main` at
all, `checkout` rather than `switch` since the fleet's own `git` floor is undeclared and
`checkout` needs no minimum version for this), and `git merge --ff-only origin/main`.
Expand Down
16 changes: 11 additions & 5 deletions .github/skills/merge-and-release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,17 @@ skill covers all of it, scoped down by what the maintainer actually asks for.
retried.
7. In the hub, when the chosen scope includes a release, bring this checkout to the merged
content without discarding or mixing in anything local. First assert `git status --porcelain
--untracked-files=all --ignored` is empty, and stop and report rather than proceeding over any
uncommitted content, tracked, untracked, or gitignored, since `skills_install.py` installs
each skill directory with `shutil.copytree()`, which copies a gitignored stray file the same
as any other, so the plain porcelain form (silent on ignored paths) would pass this preflight
while one still rides along into the install. Then `git fetch origin main`, `git checkout main`
--untracked-files=all --ignored -- .agents/skills/ .claude-plugin/` is empty, and stop and
report rather than proceeding over any uncommitted content there, tracked, untracked, or
gitignored, since `skills_install.py` reads both paths: `shutil.copytree()` installs each
`.agents/skills/` skill directory for Codex/opencode, and `claude plugin marketplace add`
installs from `.claude-plugin/` for Claude Code, so a gitignored stray file under either rides
along the same as any other, and the plain porcelain form (silent on ignored paths) would pass
this preflight while one still rides into an install. Scoped to those two paths rather than
the whole tree, matching `skills_install.py`'s own `source_ref()` dirty check (`watched =
[SKILLS_SRC, CLAUDE_PLUGIN_DIR]`), since an ignored file elsewhere in the checkout (a build
cache, a lockfile) is not this preflight's concern and should not block the refresh on it.
Then `git fetch origin main`, `git checkout main`
(or `git checkout -b main origin/main` the first time this checkout carries no local `main` at
all, `checkout` rather than `switch` since the fleet's own `git` floor is undeclared and
`checkout` needs no minimum version for this), and `git merge --ff-only origin/main`.
Expand Down
6 changes: 3 additions & 3 deletions scripts/skills_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,9 @@ def main():
stamp_path.write_text(
json.dumps(build_stamp(claude_registered), indent=2) + "\n", encoding="utf-8"
)
print(
f"Installed to {home / 'skills'}. Claude Code marketplace registered: {claude_registered}."
)
# Two independent operations get separate lines, not one combined sentence.
print(f"Skills materialized to {home / 'skills'}.")
print(f"Claude Code marketplace registered: {claude_registered}.")
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# `claude` missing is a partial-but-expected install (a Codex/opencode-only machine).
# `claude` present but registration failing is a real failure.
Expand Down
12 changes: 12 additions & 0 deletions scripts/tests/test_skills_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

from __future__ import annotations

import contextlib
import io
import json
import os
import shutil
Expand Down Expand Up @@ -399,6 +401,16 @@ def test_claude_absent_is_a_partial_install_not_a_failure(self) -> None:
mock.patch("skills_install.claude_available", return_value=False).start()
self.assertEqual(skills_install.main(), 0)

def test_skills_and_marketplace_outcomes_print_on_separate_lines(self) -> None:
mock.patch("skills_install.claude_available", return_value=True).start()
mock.patch("skills_install.register_claude_marketplace", return_value=True).start()
out = io.StringIO()
with contextlib.redirect_stdout(out):
skills_install.main()
lines = out.getvalue().splitlines()
self.assertIn(f"Skills materialized to {self.tmp / 'skills'}.", lines)
self.assertIn("Claude Code marketplace registered: True.", lines)


LINUX_WRAPPER = (
Path(__file__).resolve().parent.parent.parent / "host-setup" / "linux" / "install-skills.sh"
Expand Down