Skip to content
Closed
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
9 changes: 6 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: make -f /path/to/Makefile sync fails outside the checkout because this wrapper is resolved from the caller’s working directory. Resolve the script relative to the Makefile, matching the other project paths.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Makefile, line 37:

<comment>`make -f /path/to/Makefile sync` fails outside the checkout because this wrapper is resolved from the caller’s working directory. Resolve the script relative to the Makefile, matching the other project paths.</comment>

<file context>
@@ -32,8 +32,11 @@ DOTDIRS_SRC_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
-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
</file context>
Suggested change
@./scripts/with-committed-skills-lock.sh $(MAKE) sync-internal
@"$(dir $(lastword $(MAKEFILE_LIST)))scripts/with-committed-skills-lock.sh" $(MAKE) sync-internal


sync-internal: ruler-prepare
@$(MAKE) ruler-apply-global
@$(MAKE) commands-sync
@$(MAKE) skills-install
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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 .",
Expand All @@ -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",
Expand Down
30 changes: 30 additions & 0 deletions scripts/with-committed-skills-lock.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env bash

set -euo pipefail

if (($# == 0)); then
echo "Usage: $0 <command> [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" "$@"
91 changes: 91 additions & 0 deletions tests/with-committed-skills-lock.sh
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The three scenarios (direct command, make, failure) all run against the committed-git fixture, so the script's fallback path (git show failing → copy CURRENT_LOCK) is never exercised. Consider adding a case where DOTAGENTS_ROOT has no git metadata (or a repo without a committed skills-lock) and assert the temporary lock then mirrors the working lock while the working file is still left untouched.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/with-committed-skills-lock.sh, line 31:

<comment>The three scenarios (direct command, make, failure) all run against the committed-git fixture, so the script's fallback path (git show failing → copy CURRENT_LOCK) is never exercised. Consider adding a case where DOTAGENTS_ROOT has no git metadata (or a repo without a committed skills-lock) and assert the temporary lock then mirrors the working lock while the working file is still left untouched.</comment>

<file context>
@@ -0,0 +1,91 @@
+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'
</file context>

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'