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"