From 7dd20efb7f0e30cd962f4d1d7b513b385d28b125 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Mon, 3 Aug 2026 14:52:59 +0800 Subject: [PATCH] fix(skills): Preserve local lock during sync Run deployment syncs against a temporary copy of the committed skills lock so generated re-locking cannot overwrite user-owned checkout changes. Refs shunkakinokisoftware-hnnt --- Makefile | 9 ++- README.md | 7 ++- package.json | 5 +- scripts/with-committed-skills-lock.sh | 30 +++++++++ tests/with-committed-skills-lock.sh | 91 +++++++++++++++++++++++++++ 5 files changed, 136 insertions(+), 6 deletions(-) create mode 100755 scripts/with-committed-skills-lock.sh create mode 100755 tests/with-committed-skills-lock.sh diff --git a/Makefile b/Makefile index 566c159..7936423 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ SKILLS_SRC_DIR := $(dir $(lastword $(MAKEFILE_LIST)))skills SKILLS_RULER_DIR := $(dir $(lastword $(MAKEFILE_LIST))).ruler/skills SKILLS_TARGET_DIRS := $(HOME)/.claude/skills $(HOME)/.cursor/skills $(HOME)/.codex/skills $(HOME)/.roo/skills $(HOME)/.gemini/skills $(HOME)/.agents/skills $(HOME)/.vibe/skills $(HOME)/.config/opencode/skills SKILLS_FILE := $(dir $(lastword $(MAKEFILE_LIST)))SKILLS.txt -SKILLS_LOCK_FILE := $(dir $(lastword $(MAKEFILE_LIST)))skills-lock.json +SKILLS_LOCK_FILE ?= $(dir $(lastword $(MAKEFILE_LIST)))skills-lock.json SKILLS_EXTERNAL_SOURCE_DIR := $(HOME)/.agents/skills SKILLS_GLOBAL_LOCK := $(HOME)/.agents/.skill-lock.json @@ -32,8 +32,11 @@ DOTDIRS_SRC_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) # ==================================================================================== ifeq ($(DOTAGENTS_SKIP_SYNC),) -.PHONY: sync -sync: ruler-prepare ## Sync project commands, skills, and MCP configuration to assistant-specific directories. +.PHONY: sync sync-internal +sync: ## Sync project commands, skills, and MCP configuration without rewriting the committed lock. + @./scripts/with-committed-skills-lock.sh $(MAKE) sync-internal + +sync-internal: ruler-prepare @$(MAKE) ruler-apply-global @$(MAKE) commands-sync @$(MAKE) skills-install diff --git a/README.md b/README.md index d6af106..62f362a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,12 @@ Rules for Agents make sync ``` -This runs the full pipeline: prepares `.ruler/`, generates agent instruction files (`~/.claude/CLAUDE.md`, etc.) via Ruler, installs external skills from `skills-lock.json`, then syncs commands, local repo skills, MCP config, and dot directories to `$HOME`. +This runs the full pipeline: prepares `.ruler/`, generates agent instruction +files (`~/.claude/CLAUDE.md`, etc.) via Ruler, installs external skills from +the committed `skills-lock.json`, then syncs commands, local repo skills, MCP +config, and dot directories to `$HOME`. The sync uses a temporary lock so +deployment does not rewrite a user-modified checkout; use the explicit skills +maintenance targets below when intentionally updating the committed lock. ## External skills diff --git a/package.json b/package.json index 0284cee..12a3682 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@shunkakinoki/dotagents", "scripts": { - "check": "bun run ruler:check && bun run biome:check", + "check": "bun run test && bun run ruler:check && bun run biome:check", "lint": "bun run ruler:apply && bun run check && bun run format", "format": "bun run biome:format", "biome:check": "bun biome check --write .", @@ -11,7 +11,8 @@ "lefthook:install": "lefthook install", "lefthook:uninstall": "lefthook uninstall", "lefthook:run": "lefthook run", - "prepare": "make ruler-prepare" + "prepare": "make ruler-prepare", + "test": "bash tests/with-committed-skills-lock.sh" }, "devDependencies": { "@biomejs/biome": "2.3.11", diff --git a/scripts/with-committed-skills-lock.sh b/scripts/with-committed-skills-lock.sh new file mode 100755 index 0000000..c72cf41 --- /dev/null +++ b/scripts/with-committed-skills-lock.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if (($# == 0)); then + echo "Usage: $0 [args...]" >&2 + exit 2 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DOTAGENTS_ROOT="${DOTAGENTS_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}" +CURRENT_LOCK="${SKILLS_LOCK_FILE:-$DOTAGENTS_ROOT/skills-lock.json}" +TEMP_LOCK="$(mktemp "${TMPDIR:-/tmp}/dotagents-skills-lock.XXXXXX")" + +cleanup() { + local status=$? + trap - EXIT + rm -f "$TEMP_LOCK" "$TEMP_LOCK.tmp" + exit "$status" +} +trap cleanup EXIT + +if git -C "$DOTAGENTS_ROOT" show HEAD:skills-lock.json >"$TEMP_LOCK" 2>/dev/null; then + echo "Using committed skills-lock.json for synchronization." +else + echo "Git metadata unavailable; using the current skills lock for synchronization." + cp -f "$CURRENT_LOCK" "$TEMP_LOCK" +fi + +SKILLS_LOCK_FILE="$TEMP_LOCK" "$@" diff --git a/tests/with-committed-skills-lock.sh b/tests/with-committed-skills-lock.sh new file mode 100755 index 0000000..87004e3 --- /dev/null +++ b/tests/with-committed-skills-lock.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SCRIPT="$REPO_ROOT/scripts/with-committed-skills-lock.sh" +TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/dotagents-lock-test.XXXXXX")" + +cleanup() { + rm -rf "$TEST_ROOT" +} +trap cleanup EXIT + +fail() { + echo "not ok - $1" >&2 + exit 1 +} + +assert_eq() { + local expected=$1 + local actual=$2 + local message=$3 + + [[ "$actual" == "$expected" ]] || fail "$message: expected '$expected', got '$actual'" +} + +FIXTURE="$TEST_ROOT/repo" +CAPTURE="$TEST_ROOT/capture" +MOCK_COMMAND="$TEST_ROOT/capture-lock" +mkdir -p "$FIXTURE" +git -C "$FIXTURE" init --quiet +git -C "$FIXTURE" config user.email test@example.com +git -C "$FIXTURE" config user.name 'Dotagents Test' +printf 'committed\n' >"$FIXTURE/skills-lock.json" +git -C "$FIXTURE" add skills-lock.json +git -C "$FIXTURE" commit --quiet -m initial +printf 'user-owned\n' >"$FIXTURE/skills-lock.json" + +cat >"$MOCK_COMMAND" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +printf '%s\n' "$SKILLS_LOCK_FILE" >"$CAPTURE.path" +cat "$SKILLS_LOCK_FILE" >"$CAPTURE.contents" +printf 'generated\n' >"$SKILLS_LOCK_FILE" +exit "${MOCK_STATUS:-0}" +EOF +chmod +x "$MOCK_COMMAND" + +DOTAGENTS_ROOT="$FIXTURE" \ + CAPTURE="$CAPTURE" \ + "$SCRIPT" "$MOCK_COMMAND" + +assert_eq 'committed' "$(cat "$CAPTURE.contents")" 'sync command should receive committed lock' +assert_eq 'user-owned' "$(cat "$FIXTURE/skills-lock.json")" 'working lock should remain unchanged' +TEMP_LOCK="$(cat "$CAPTURE.path")" +[[ ! -e "$TEMP_LOCK" ]] || fail 'temporary lock should be removed after success' + +cat >"$FIXTURE/Makefile" <<'EOF' +SKILLS_LOCK_FILE ?= missing + +.PHONY: capture +capture: + @printf '%s\n' "$(SKILLS_LOCK_FILE)" >"$$CAPTURE.path" + @cat "$(SKILLS_LOCK_FILE)" >"$$CAPTURE.contents" + @printf 'generated\n' >"$(SKILLS_LOCK_FILE)" +EOF + +DOTAGENTS_ROOT="$FIXTURE" \ + CAPTURE="$CAPTURE" \ + "$SCRIPT" make -C "$FIXTURE" capture + +assert_eq 'committed' "$(cat "$CAPTURE.contents")" 'make should inherit temporary lock override' +assert_eq 'user-owned' "$(cat "$FIXTURE/skills-lock.json")" 'make should preserve working lock' +TEMP_LOCK="$(cat "$CAPTURE.path")" +[[ ! -e "$TEMP_LOCK" ]] || fail 'make temporary lock should be removed' + +set +e +DOTAGENTS_ROOT="$FIXTURE" \ + CAPTURE="$CAPTURE" \ + MOCK_STATUS=42 \ + "$SCRIPT" "$MOCK_COMMAND" +status=$? +set -e + +assert_eq '42' "$status" 'sync failure should propagate' +assert_eq 'user-owned' "$(cat "$FIXTURE/skills-lock.json")" 'failure should preserve working lock' +TEMP_LOCK="$(cat "$CAPTURE.path")" +[[ ! -e "$TEMP_LOCK" ]] || fail 'temporary lock should be removed after failure' + +echo 'ok - committed skills lock isolation'