From 3fa9bee1667a59aae89691ae0cbe78e4b2338e3a Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Wed, 1 Apr 2026 23:26:22 +0000 Subject: [PATCH] feat(kugetsu): add lock mechanism for worktree coordination Add lock mechanism to prevent concurrent access to worktrees: - Add LOCKS_DIR constant (\~/.kugetsu/locks) - Add acquire_lock() - acquires lock file with PID, session_id, timestamp - Add release_lock() - releases lock if held by current process - Add release_all_locks() - releases all locks for a session - Add check_lock() - check if issue is locked - Modify cmd_start to acquire lock before creating worktree - Modify cmd_destroy to release lock when destroying session - Add trap for cleanup on EXIT/INT/TERM Lock files are named after issue ref with format: .lock Contains: PID:session_id:timestamp Stale lock detection: if PID no longer exists, lock is considered stale. This prevents concurrent processes from modifying the same worktree. Fixes #71 --- skills/kugetsu/scripts/kugetsu | 96 ++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/skills/kugetsu/scripts/kugetsu b/skills/kugetsu/scripts/kugetsu index 24152c1..9200e40 100755 --- a/skills/kugetsu/scripts/kugetsu +++ b/skills/kugetsu/scripts/kugetsu @@ -8,6 +8,7 @@ REPOS_CONFIG="$KUGETSU_DIR/repos.json" INDEX_FILE="$KUGETSU_DIR/index.json" NOTIFICATIONS_FILE="$KUGETSU_DIR/notifications.json" LOGS_DIR="$KUGETSU_DIR/logs" +LOCKS_DIR="$KUGETSU_DIR/locks" MAX_CONCURRENT_AGENTS="${MAX_CONCURRENT_AGENTS:-3}" # Load user config overrides (~/.kugetsu/config) @@ -30,6 +31,89 @@ count_active_dev_sessions() { echo "$count" } +acquire_lock() { + local issue_ref="$1" + local session_id="$2" + local lock_file="$LOCKS_DIR/${issue_ref//[^a-zA-Z0-9._-]/_}.lock" + + mkdir -p "$LOCKS_DIR" + + if [ -f "$lock_file" ]; then + local lock_pid=$(cat "$lock_file" 2>/dev/null | cut -d: -f1) + local lock_session=$(cat "$lock_file" 2>/dev/null | cut -d: -f2) + local lock_time=$(cat "$lock_file" 2>/dev/null | cut -d: -f3) + + if [ -n "$lock_pid" ] && ! kill -0 "$lock_pid" 2>/dev/null; then + echo "Stale lock detected, removing..." + rm -f "$lock_file" + elif [ "$lock_session" = "$session_id" ]; then + echo "Already holding lock for $issue_ref" + return 0 + else + echo "Error: $issue_ref is locked by session $lock_session (PID $lock_pid)" >&2 + return 1 + fi + fi + + echo "${BASHPID}:${session_id}:$(date +%s)" > "$lock_file" + echo "Lock acquired for $issue_ref: $lock_file" + return 0 +} + +release_lock() { + local issue_ref="$1" + local lock_file="$LOCKS_DIR/${issue_ref//[^a-zA-Z0-9._-]/_}.lock" + + if [ ! -f "$lock_file" ]; then + return 0 + fi + + local lock_pid=$(cat "$lock_file" 2>/dev/null | cut -d: -f1) + + if [ "$lock_pid" = "$BASHPID" ]; then + rm -f "$lock_file" + echo "Lock released for $issue_ref" + else + echo "Error: Cannot release lock held by PID $lock_pid (current: $BASHPID)" >&2 + return 1 + fi +} + +release_all_locks() { + local session_id="$1" + if [ ! -d "$LOCKS_DIR" ]; then + return 0 + fi + for lock_file in "$LOCKS_DIR"/*.lock; do + [ -f "$lock_file" ] || continue + local lock_session=$(cat "$lock_file" 2>/dev/null | cut -d: -f2) + if [ "$lock_session" = "$session_id" ]; then + rm -f "$lock_file" + echo "Released stale lock: $(basename "$lock_file")" + fi + done +} + +check_lock() { + local issue_ref="$1" + local lock_file="$LOCKS_DIR/${issue_ref//[^a-zA-Z0-9._-]/_}.lock" + + if [ -f "$lock_file" ]; then + local lock_pid=$(cat "$lock_file" 2>/dev/null | cut -d: -f1) + local lock_session=$(cat "$lock_file" 2>/dev/null | cut -d: -f2) + local lock_time=$(cat "$lock_file" 2>/dev/null | cut -d: -f3) + + if [ -n "$lock_pid" ] && ! kill -0 "$lock_pid" 2>/dev/null; then + echo "Stale lock detected, removing..." + rm -f "$lock_file" + return 1 + fi + echo "Locked by session $lock_session (PID $lock_pid)" + return 0 + fi + return 1 +} + usage() { cat << 'EOF' kugetsu - OpenCode Session Manager (Issue-Driven) @@ -825,6 +909,14 @@ cmd_start() { fi local worktree_path=$(issue_ref_to_worktree_path "$issue_ref") + + trap 'release_lock "$issue_ref" 2>/dev/null; exit' EXIT INT TERM + + if ! acquire_lock "$issue_ref" "$base_session_id"; then + echo "Error: Could not acquire lock for '$issue_ref'" >&2 + exit 1 + fi + create_worktree "$issue_ref" local session_file="$(issue_ref_to_filename "$issue_ref").json" @@ -869,6 +961,8 @@ cmd_start() { add_issue_to_index "$issue_ref" "$session_file" + release_lock "$issue_ref" + echo "Session started for '$issue_ref': $new_session_id" echo "Worktree: $worktree_path" } @@ -1126,6 +1220,7 @@ cmd_destroy() { remove_worktree_for_issue "$target" rm -f "$session_path" remove_issue_from_index "$target" + release_lock "$target" 2>/dev/null || true echo "Session for '$target' destroyed" else echo "Delete session and worktree for '$target'? [y/N] " @@ -1135,6 +1230,7 @@ cmd_destroy() { remove_worktree_for_issue "$target" rm -f "$session_path" remove_issue_from_index "$target" + release_lock "$target" 2>/dev/null || true echo "Session for '$target' destroyed" else echo "Aborted" -- 2.49.1