diff --git a/.agents/skills/merge-and-release/SKILL.md b/.agents/skills/merge-and-release/SKILL.md index dcf4e34d..3eb31c30 100644 --- a/.agents/skills/merge-and-release/SKILL.md +++ b/.agents/skills/merge-and-release/SKILL.md @@ -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`. diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index 93518134..db22d6f2 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -332c16d2c066383b +86607328dc15eded diff --git a/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md b/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md index dcf4e34d..3eb31c30 100644 --- a/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md @@ -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`. diff --git a/.github/skills/merge-and-release/SKILL.md b/.github/skills/merge-and-release/SKILL.md index dcf4e34d..3eb31c30 100644 --- a/.github/skills/merge-and-release/SKILL.md +++ b/.github/skills/merge-and-release/SKILL.md @@ -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`. diff --git a/scripts/skills_install.py b/scripts/skills_install.py index 1b521648..bc38d9cf 100755 --- a/scripts/skills_install.py +++ b/scripts/skills_install.py @@ -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}.") # `claude` missing is a partial-but-expected install (a Codex/opencode-only machine). # `claude` present but registration failing is a real failure. diff --git a/scripts/tests/test_skills_install.py b/scripts/tests/test_skills_install.py index 15bc0f75..058b4a98 100755 --- a/scripts/tests/test_skills_install.py +++ b/scripts/tests/test_skills_install.py @@ -7,6 +7,8 @@ from __future__ import annotations +import contextlib +import io import json import os import shutil @@ -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"