Compare commits

..

13 Commits

Author SHA1 Message Date
shokollm
ce4116bcb1 fix(worktree-lifecycle): use github.com as example in set-pr help 2026-04-05 03:52:41 +00:00
shokollm
3107dbf1e5 fix(worktree-lifecycle): use GIT_SERVERS config for check_pr_status
- Extract hostname from pr_url instead of hardcoding domains
- Look up server base URL from GIT_SERVERS config
- Append /api/v1 to derive API URL (configurable per server)
- Works with any server configured in GIT_SERVERS
2026-04-05 03:41:41 +00:00
shokollm
b8b97e3c09 fix(worktree-lifecycle): address PR review feedback
- Rename update-pr to set-pr for clarity (it's setting the PR URL, not updating PR)
- Add optional pr-url argument to kugetsu start command
  Usage: kugetsu start <issue-ref> <message> [pr-url]
- If pr-url is provided at start, it's stored directly in session file
2026-04-05 03:16:05 +00:00
shokollm
d8af560e6d feat(worktree-lifecycle): add PR tracking and safe destroy
- Add WORKTREE_CHECK_PR_STATUS config (default: true)
- Add pr_url and branch_name fields to session files
- Add check_pr_status() to query PR status via API (Gitea/GitHub)
- Add update_session_pr_url() to update PR URL in session
- Add kugetsu update-pr command to set PR URL
- Modify cmd_destroy to check PR status before destroying worktree

Closes #135
2026-04-05 02:50:09 +00:00
5d12f6ca42 Merge pull request 'feat(kugetsu): smart delegate with worktree awareness' (#130) from feature/smart-delegate-worktree-awareness into main 2026-04-03 16:31:30 +02:00
shokollm
91505345a2 feat(kugetsu): smart delegate with worktree awareness
- Parse issue refs from message (gitserver.com/owner/repo/issues/123 or owner/repo#123)
- Find existing worktrees/sessions by issue number
- Ask user to confirm which worktree to use, or delegate anyway
- Inject missing info context to PM agent
- Inject selected worktree context to PM agent

Fixes #128
2026-04-03 14:20:48 +00:00
f7fe22de25 Merge pull request 'fix(kugetsu): wrap cmd_continue in subshell with cd for correct worktree dir' (#129) from fix/cmd-continue-worktree-dir into main 2026-04-03 15:58:19 +02:00
shokollm
3ce43ffa65 fix(kugetsu): wrap cmd_continue in subshell with cd for correct worktree dir
The --dir flag only sets directory for the subprocess, not the session's
stored directory in opencode's SQLite DB. This was already fixed for
cmd_start in v0.1.10, but cmd_continue still had the bug.

Fixes #127
2026-04-03 13:06:02 +00:00
shokollm
416e8e5757 fix(kugetsu): destroy --base now also deletes PM agent session
When destroying base session, we now also delete the PM agent session
and all issue session files. This ensures clean slate on re-init.
2026-04-02 14:47:40 +00:00
1c1d18b9ae Merge pull request 'fix(kugetsu): init creates base session in ~/.kugetsu-worktrees and adds context to forked sessions' (#114) from fix/session-context-and-init-worktree into main 2026-04-02 16:35:12 +02:00
shokollm
8c639e2928 fix(kugetsu): init creates base session in ~/.kugetsu-worktrees, adds context to forked sessions, and clears logs
1. Init: cd to ~/.kugetsu-worktrees before creating base session
   This keeps all worktrees inside a predictable directory structure
   and avoids external_directory permission issues

2. Init: Clear old logs but keep repos.json, config, and env files

3. Fork context: Add kugetsu_get_fork_context() that provides:
   - Important working rules (stop on error, don't pivot)
   - Repository configuration from repos.json
   - Environment file location info

4. Fork message: Prepend context to user message when forking session
2026-04-02 14:32:07 +00:00
c4c3556247 Merge pull request 'fix(kugetsu): destroy --base and --pm-agent actually delete opencode sessions' (#113) from fix/destroy-removes-opencode-session into main 2026-04-02 15:30:48 +02:00
shokollm
4342347ac6 fix(kugetsu): destroy --base and --pm-agent actually delete opencode sessions
Previously destroy only removed local session files but didn't delete
the sessions from opencode's database. This caused init to reuse the
same session with old context.

Now destroy calls 'opencode session delete <id>' to properly remove
the session from opencode.
2026-04-02 13:28:47 +00:00
2 changed files with 383 additions and 18 deletions

Submodule .kugetsu-worktrees/git.fbrns.co-shoko-jigaido-2 added at 332d7fc60a

View File

@@ -13,6 +13,7 @@ VERBOSITY_DIR="$KUGETSU_DIR/verbosity"
MAX_CONCURRENT_AGENTS="${MAX_CONCURRENT_AGENTS:-3}" MAX_CONCURRENT_AGENTS="${MAX_CONCURRENT_AGENTS:-3}"
KUGETSU_VERBOSITY="${KUGETSU_VERBOSITY:-default}" KUGETSU_VERBOSITY="${KUGETSU_VERBOSITY:-default}"
WORKTREE_CHECK_PR_STATUS="${WORKTREE_CHECK_PR_STATUS:-true}"
# Load user config overrides (~/.kugetsu/config) # Load user config overrides (~/.kugetsu/config)
if [ -f "$KUGETSU_DIR/config" ]; then if [ -f "$KUGETSU_DIR/config" ]; then
@@ -76,7 +77,8 @@ Usage:
kugetsu prune [--force] Remove orphaned sessions (keeps base + pm-agent) kugetsu prune [--force] Remove orphaned sessions (keeps base + pm-agent)
kugetsu destroy <issue-ref> [-y] Delete session for issue kugetsu destroy <issue-ref> [-y] Delete session for issue
kugetsu destroy --pm-agent [-y] Delete pm-agent session (not recommended) kugetsu destroy --pm-agent [-y] Delete pm-agent session (not recommended)
kugetsu destroy --base [-y] Delete base session kugetsu destroy --base [-y] Delete base session
kugetsu set-pr <issue-ref> <pr-url> Set PR URL for session (for PR tracking)
kugetsu help Show this help kugetsu help Show this help
Issue Ref Format: Issue Ref Format:
@@ -256,6 +258,54 @@ get_worktree_path_for_session() {
fi fi
} }
check_pr_status() {
local pr_url="$1"
if [ -z "$pr_url" ]; then
echo "no_pr_url"
return 1
fi
local hostname=$(echo "$pr_url" | sed -E 's|https://([^/]+)/.*|\1|')
local server_base="${GIT_SERVERS[$hostname]:-}"
if [ -z "$server_base" ]; then
echo "unknown_server"
return 1
fi
local api_base="${server_base}/api/v1"
local api_url=$(echo "$pr_url" | sed -E 's|https://[^/]+/([^/]+)/([^/]+)/(pulls|merge_requests)/([0-9]+)|'"${api_base}"'/repos/\1/\2/\3/\4|')
local token=""
if [[ "$hostname" == "github.com" ]]; then
token="${GITHUB_TOKEN:-}"
else
token="${GITEA_TOKEN:-}"
fi
local response
if [ -n "$token" ]; then
response=$(curl -s -H "Authorization: token $token" "$api_url" 2>/dev/null || echo "{}")
else
response=$(curl -s "$api_url" 2>/dev/null || echo "{}")
fi
local state=$(echo "$response" | python3 -c "import json, sys; d=json.load(sys.stdin); print(d.get('state', 'unknown'))" 2>/dev/null || echo "unknown")
local merged=$(echo "$response" | python3 -c "import json, sys; d=json.load(sys.stdin); print('true' if d.get('merged', False) else 'false')" 2>/dev/null || echo "false")
if [ "$merged" = "true" ]; then
echo "merged"
elif [ "$state" = "closed" ]; then
echo "closed"
elif [ "$state" = "open" ]; then
echo "open"
else
echo "unknown"
fi
}
issue_ref_to_filename() { issue_ref_to_filename() {
local issue_ref="$1" local issue_ref="$1"
echo "$issue_ref" | sed 's/[\/:]/-/g' | sed 's/#/-/' echo "$issue_ref" | sed 's/[\/:]/-/g' | sed 's/#/-/'
@@ -267,6 +317,46 @@ filename_to_issue_ref() {
echo "$name" | sed 's/-\([0-9]*\)$/#\1' | sed 's/-/\//g' echo "$name" | sed 's/-\([0-9]*\)$/#\1' | sed 's/-/\//g'
} }
update_session_pr_url() {
local issue_ref="$1"
local pr_url="$2"
if [ -z "$issue_ref" ] || [ -z "$pr_url" ]; then
echo "Error: update_session_pr_url requires <issue-ref> and <pr-url>" >&2
return 1
fi
local session_file=$(get_session_for_issue "$issue_ref")
if [ -z "$session_file" ] || [ "$session_file" = "null" ]; then
echo "Error: No session found for '$issue_ref'" >&2
return 1
fi
local session_path="$SESSIONS_DIR/$session_file"
if [ ! -f "$session_path" ]; then
echo "Error: Session file not found: $session_path" >&2
return 1
fi
python3 << PYEOF
import json
session_path = "$session_path"
pr_url = "$pr_url"
with open(session_path, 'r') as f:
session = json.load(f)
session['pr_url'] = pr_url
with open(session_path, 'w') as f:
json.dump(session, f, indent=2)
print(f"Updated pr_url to: {pr_url}")
PYEOF
}
read_index() { read_index() {
if [ -f "$INDEX_FILE" ]; then if [ -f "$INDEX_FILE" ]; then
cat "$INDEX_FILE" cat "$INDEX_FILE"
@@ -394,6 +484,41 @@ kugetsu_get_pm_context() {
fi fi
} }
kugetsu_get_fork_context() {
local issue_ref="$1"
local context=""
context="## IMPORTANT WORKING RULES
1. You are working on issue: $issue_ref
2. If you encounter ANY error, blocker, or cannot complete the task:
- STOP immediately
- Log what happened and why you cannot proceed
- Do NOT switch to other work or try alternative approaches
3. Do NOT work on other issues or PRs unless explicitly asked
4. Environment variables are available in ~/.kugetsu/env/
"
if [ -f "$REPOS_CONFIG" ]; then
context="${context}
## REPOSITORIES CONFIG
$(cat "$REPOS_CONFIG")
"
fi
if [ -f "$ENV_DIR/default.env" ]; then
context="${context}
## ENVIRONMENT (available at ~/.kugetsu/env/)
Environment file exists at: $ENV_DIR/default.env
Source it with: source ~/.kugetsu/env/default.env
"
fi
echo "$context"
}
kugetsu_add_notification() { kugetsu_add_notification() {
local type="$1" local type="$1"
local message="$2" local message="$2"
@@ -606,6 +731,99 @@ EOF
fi fi
} }
parse_issue_ref_from_message() {
local message="$1"
local gitserver=""
local owner=""
local repo=""
local issue_number=""
if echo "$message" | grep -qE '[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/(issues|pull)/[0-9]+'; then
gitserver=$(echo "$message" | grep -oE '[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+' | head -1 | sed 's/\/[^/]*\/[^/]*$//')
local full_path=$(echo "$message" | grep -oE '[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/(issues|pull)/[0-9]+' | head -1)
owner=$(echo "$full_path" | cut -d'/' -f2)
repo=$(echo "$full_path" | cut -d'/' -f3)
issue_number=$(echo "$full_path" | grep -oE '[0-9]+$' | head -1)
elif echo "$message" | grep -qE '[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+#([0-9]+)'; then
owner=$(echo "$message" | grep -oE '[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+#' | sed 's/#$//' | cut -d'/' -f1)
repo=$(echo "$message" | grep -oE '[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+#' | sed 's/#$//' | cut -d'/' -f2)
issue_number=$(echo "$message" | grep -oE '#[0-9]+' | grep -oE '[0-9]+' | head -1)
fi
echo "${gitserver}|${owner}|${repo}|${issue_number}"
}
get_missing_info() {
local parsed="$1"
local gitserver=$(echo "$parsed" | cut -d'|' -f1)
local owner=$(echo "$parsed" | cut -d'|' -f2)
local repo=$(echo "$parsed" | cut -d'|' -f3)
local issue_number=$(echo "$parsed" | cut -d'|' -f4)
local missing=""
[ -z "$gitserver" ] && missing="${missing}git server, "
[ -z "$owner" ] && missing="${missing}owner, "
[ -z "$repo" ] && missing="${missing}repository, "
[ -z "$issue_number" ] && missing="${missing}issue number, "
echo "$missing" | sed 's/, $//'
}
build_missing_info_context() {
local missing="$1"
if [ -n "$missing" ]; then
echo ""
echo "NOTE: This task delegation has no information about: ${missing}."
echo "We need them if user wants to work on a specific issue. Otherwise we don't need it."
fi
}
find_worktrees_by_issue_number() {
local issue_number="$1"
local results=""
if [ ! -d "$WORKTREES_DIR/.kugetsu-worktrees" ]; then
echo ""
return
fi
for wt in "$WORKTREES_DIR/.kugetsu-worktrees"/*; do
if [ -d "$wt" ]; then
local wt_issue_number=$(echo "$wt" | grep -oE '#?[0-9]+$' | grep -oE '[0-9]+' | head -1)
if [ "$wt_issue_number" = "$issue_number" ]; then
results="${results}${wt}:worktree
"
fi
fi
done
echo "$results"
}
find_sessions_by_issue_number() {
local issue_number="$1"
local results=""
if [ ! -d "$SESSIONS_DIR" ]; then
echo ""
return
fi
for session_file in "$SESSIONS_DIR"/*.json; do
if [ -f "$session_file" ]; then
local session_issue_ref=$(basename "$session_file" .json | sed 's/_/\//g')
local session_issue_number=$(echo "$session_issue_ref" | grep -oE '#?[0-9]+$' | grep -oE '[0-9]+' | head -1)
if [ "$session_issue_number" = "$issue_number" ]; then
results="${results}${session_file}:session
"
fi
fi
done
echo "$results"
}
cmd_delegate() { cmd_delegate() {
local message="${1:-}" local message="${1:-}"
local verbosity="${KUGETSU_VERBOSITY:-default}" local verbosity="${KUGETSU_VERBOSITY:-default}"
@@ -625,6 +843,70 @@ cmd_delegate() {
mkdir -p "$LOGS_DIR" mkdir -p "$LOGS_DIR"
local log_file="$LOGS_DIR/delegate-$(date +%s).log" local log_file="$LOGS_DIR/delegate-$(date +%s).log"
local parsed=$(parse_issue_ref_from_message "$message")
local gitserver=$(echo "$parsed" | cut -d'|' -f1)
local owner=$(echo "$parsed" | cut -d'|' -f2)
local repo=$(echo "$parsed" | cut -d'|' -f3)
local issue_number=$(echo "$parsed" | cut -d'|' -f4)
local missing_info=$(get_missing_info "$parsed")
local context_injection=""
if [ -n "$missing_info" ]; then
context_injection=$(build_missing_info_context "$missing_info")
echo "NOTE: Delegation missing information: ${missing_info}"
fi
local candidates=""
local candidate_count=0
if [ -n "$issue_number" ]; then
local worktrees=$(find_worktrees_by_issue_number "$issue_number")
local sessions=$(find_sessions_by_issue_number "$issue_number")
while IFS=: read -r path type; do
if [ -n "$path" ]; then
candidate_count=$((candidate_count + 1))
candidates="${candidates}${candidate_count}) ${path} (${type})
"
fi
done <<< "$worktrees"
while IFS=: read -r path type; do
if [ -n "$path" ]; then
candidate_count=$((candidate_count + 1))
candidates="${candidates}${candidate_count}) ${path} (${type})
"
fi
done <<< "$sessions"
fi
local use_worktree=""
if [ $candidate_count -gt 0 ]; then
echo "Found $candidate_count existing worktree(s)/session(s) for issue #${issue_number}:"
echo "$candidates"
echo "r) Delegate anyway (without routing)"
echo "Which one to use? [1-${candidate_count}/r]: "
read -r choice
if [ "$choice" = "r" ] || [ -z "$choice" ]; then
use_worktree=""
elif [ "$choice" -ge 1 ] && [ "$choice" -le "$candidate_count" ]; then
local selected=$(echo "$candidates" | sed -n "${choice}p")
use_worktree=$(echo "$selected" | sed 's/) .*//')
fi
fi
local final_message="${message}${context_injection}"
if [ -n "$use_worktree" ]; then
if [ -d "$use_worktree" ]; then
echo "Using worktree: $use_worktree"
final_message="${final_message}
NOTE: Worktree selected: ${use_worktree}"
fi
fi
local temp_dir="${KUGETSU_TEMP_DIR:-$HOME/.local/share/opencode/tool-output}" local temp_dir="${KUGETSU_TEMP_DIR:-$HOME/.local/share/opencode/tool-output}"
mkdir -p "$ENV_DIR" mkdir -p "$ENV_DIR"
@@ -636,7 +918,7 @@ cmd_delegate() {
fi fi
env_sh="${env_sh}set +a; " env_sh="${env_sh}set +a; "
nohup sh -c "${env_sh}opencode run '$message' --continue --session '$pm_session' >> '$log_file' 2>&1" > /dev/null 2>&1 & nohup sh -c "${env_sh}opencode run '${final_message}' --continue --session '$pm_session' >> '$log_file' 2>&1" > /dev/null 2>&1 &
disown disown
echo "Delegated to PM agent (logged to $(basename "$log_file"))" echo "Delegated to PM agent (logged to $(basename "$log_file"))"
echo "Verbosity: $verbosity" echo "Verbosity: $verbosity"
@@ -1088,6 +1370,11 @@ EOF
echo "Created pm-agent env file: $ENV_DIR/pm-agent.env" echo "Created pm-agent env file: $ENV_DIR/pm-agent.env"
fi fi
if [ -d "$LOGS_DIR" ]; then
echo "Cleaning up old logs..."
rm -rf "$LOGS_DIR"/*.log 2>/dev/null || true
fi
local existing_base=$(get_base_session_id) local existing_base=$(get_base_session_id)
local existing_pm=$(get_pm_agent_session_id) local existing_pm=$(get_pm_agent_session_id)
@@ -1107,12 +1394,19 @@ EOF
exit 1 exit 1
fi fi
local init_worktree_dir="$HOME/.kugetsu-worktrees"
mkdir -p "$init_worktree_dir"
cd "$init_worktree_dir"
echo "Initialized kugetsu worktrees directory: $init_worktree_dir"
echo "Base session will be created in this directory."
echo ""
local cwd_files=$(ls -A "$PWD" 2>/dev/null | wc -l) local cwd_files=$(ls -A "$PWD" 2>/dev/null | wc -l)
local cwd_git=$(git rev-parse --is-inside-work-tree 2>/dev/null || echo "false") local cwd_git=$(git rev-parse --is-inside-work-tree 2>/dev/null || echo "false")
if [ "$cwd_files" -gt 0 ] || [ "$cwd_git" = "true" ]; then if [ "$cwd_files" -gt 0 ] || [ "$cwd_git" = "true" ]; then
echo "Warning: Current directory is not empty: $PWD" >&2 echo "Warning: Worktrees directory is not empty: $PWD" >&2
echo "This may cause project context to contaminate the base session." >&2 echo "This may cause project context to contaminate the base session." >&2
echo "Consider running kugetsu init from an empty directory." >&2 echo "Consider running kugetsu destroy --base -y and reinitializing." >&2
echo "" >&2 echo "" >&2
echo "Files in current directory: $cwd_files" >&2 echo "Files in current directory: $cwd_files" >&2
if [ "$cwd_git" = "true" ]; then if [ "$cwd_git" = "true" ]; then
@@ -1197,6 +1491,7 @@ EOF
cmd_start() { cmd_start() {
local issue_ref="" local issue_ref=""
local message="" local message=""
local pr_url=""
local args=("$@") local args=("$@")
args=$(set_debug_mode "${args[@]}") args=$(set_debug_mode "${args[@]}")
@@ -1206,11 +1501,14 @@ cmd_start() {
issue_ref="$arg" issue_ref="$arg"
elif [ -z "$message" ]; then elif [ -z "$message" ]; then
message="$arg" message="$arg"
elif [ -z "$pr_url" ]; then
pr_url="$arg"
fi fi
done done
if [ -z "$issue_ref" ] || [ -z "$message" ]; then if [ -z "$issue_ref" ] || [ -z "$message" ]; then
echo "Error: start requires <issue-ref> and <message>" >&2 echo "Error: start requires <issue-ref> and <message>" >&2
echo "Usage: kugetsu start <issue-ref> <message> [pr-url]" >&2
exit 1 exit 1
fi fi
@@ -1258,12 +1556,18 @@ cmd_start() {
> "$fork_log" > "$fork_log"
local fork_context=$(kugetsu_get_fork_context "$issue_ref")
local full_message="${fork_context}
## YOUR TASK
$message"
fix_session_permissions fix_session_permissions
if [ "$DEBUG_MODE" = true ]; then if [ "$DEBUG_MODE" = true ]; then
(cd "$worktree_path" && opencode run "$message" --fork --session "$base_session_id" --dir "$worktree_path" 2>&1) | tee "$fork_log" & (cd "$worktree_path" && opencode run "$full_message" --fork --session "$base_session_id" --dir "$worktree_path" 2>&1) | tee "$fork_log" &
else else
(cd "$worktree_path" && opencode run "$message" --fork --session "$base_session_id" --dir "$worktree_path" 2>&1) >> "$fork_log" & (cd "$worktree_path" && opencode run "$full_message" --fork --session "$base_session_id" --dir "$worktree_path" 2>&1) >> "$fork_log" &
fi fi
local fork_pid=$! local fork_pid=$!
@@ -1333,8 +1637,25 @@ for row in cursor.fetchall():
" 2>/dev/null || echo " (failed to query DB)" " 2>/dev/null || echo " (failed to query DB)"
fi fi
printf '{"type": "forked", "issue_ref": "%s", "opencode_session_id": "%s", "worktree_path": "%s", "created_at": "%s", "state": "idle"}\n' \ local branch_name=$(issue_ref_to_branch_name "$issue_ref")
"$issue_ref" "$new_session_id" "$worktree_path" "$(date -Iseconds)" > "$SESSIONS_DIR/$session_file"
python3 << PYEOF > "$SESSIONS_DIR/$session_file"
import json
session = {
"type": "forked",
"issue_ref": "$issue_ref",
"opencode_session_id": "$new_session_id",
"worktree_path": "$worktree_path",
"created_at": "$(date -Iseconds)",
"state": "idle",
"branch_name": "$branch_name",
"pr_url": "$pr_url" if "$pr_url" else None
}
with open("$SESSIONS_DIR/$session_file", "w") as f:
json.dump(session, f, indent=2)
PYEOF
add_issue_to_index "$issue_ref" "$session_file" add_issue_to_index "$issue_ref" "$session_file"
@@ -1386,12 +1707,13 @@ cmd_continue() {
echo "Continuing session for '$session_name'..." echo "Continuing session for '$session_name'..."
# Note: --continue always allowed (existing sessions don't count toward limit) # Note: --continue always allowed (existing sessions don't count toward limit)
# Wrap in subshell with cd to ensure worktree directory is set correctly in session DB
if [ -n "$worktree_path" ] && [ -d "$worktree_path" ]; then if [ -n "$worktree_path" ] && [ -d "$worktree_path" ]; then
echo "Using worktree: $worktree_path" echo "Using worktree: $worktree_path"
if [ "$DEBUG_MODE" = true ]; then if [ "$DEBUG_MODE" = true ]; then
opencode run "$message" --continue --session "$opencode_session_id" --dir "$worktree_path" 2>&1 | tee "$session_path.debug.log" & (cd "$worktree_path" && opencode run "$message" --continue --session "$opencode_session_id" --dir "$worktree_path" 2>&1) | tee "$session_path.debug.log" &
else else
opencode run "$message" --continue --session "$opencode_session_id" --dir "$worktree_path" 2>&1 & (cd "$worktree_path" && opencode run "$message" --continue --session "$opencode_session_id" --dir "$worktree_path" 2>&1) &
fi fi
else else
if [ "$DEBUG_MODE" = true ]; then if [ "$DEBUG_MODE" = true ]; then
@@ -1548,15 +1870,22 @@ cmd_destroy() {
if [ "$target" = "base" ]; then if [ "$target" = "base" ]; then
if [ "$force" = true ]; then if [ "$force" = true ]; then
local base_session_id=$(get_base_session_id)
local pm_agent_session_id=$(get_pm_agent_session_id)
rm -f "$SESSIONS_DIR/base.json" rm -f "$SESSIONS_DIR/base.json"
local pm_agent=$(get_pm_agent_session_id) rm -f "$SESSIONS_DIR/pm-agent.json"
if [ -n "$pm_agent" ] && [ "$pm_agent" != "null" ]; then rm -f "$SESSIONS_DIR/issue-"*.json 2>/dev/null || true
rm -f "$SESSIONS_DIR/pm-agent.json" echo '{"base": null, "pm_agent": null, "issues": {}}' > "$INDEX_FILE"
echo '{"base": null, "pm_agent": null, "issues": {}}' > "$INDEX_FILE"
else if [ -n "$base_session_id" ] && [ "$base_session_id" != "null" ]; then
echo '{"base": null, "pm_agent": null, "issues": {}}' > "$INDEX_FILE" echo "Deleting base session: $base_session_id"
opencode session delete "$base_session_id" 2>/dev/null || echo "Warning: Could not delete base session"
fi fi
echo "Base session destroyed" if [ -n "$pm_agent_session_id" ] && [ "$pm_agent_session_id" != "null" ]; then
echo "Deleting PM agent session: $pm_agent_session_id"
opencode session delete "$pm_agent_session_id" 2>/dev/null || echo "Warning: Could not delete PM agent session"
fi
echo "Base and PM agent sessions destroyed"
else else
echo "Error: destroying base session requires --base -y" >&2 echo "Error: destroying base session requires --base -y" >&2
exit 1 exit 1
@@ -1566,6 +1895,7 @@ cmd_destroy() {
if [ "$target" = "pm-agent" ]; then if [ "$target" = "pm-agent" ]; then
if [ "$force" = true ]; then if [ "$force" = true ]; then
local pm_session_id=$(get_pm_agent_session_id)
rm -f "$SESSIONS_DIR/pm-agent.json" rm -f "$SESSIONS_DIR/pm-agent.json"
local base=$(get_base_session_id) local base=$(get_base_session_id)
if [ -n "$base" ] && [ "$base" != "null" ]; then if [ -n "$base" ] && [ "$base" != "null" ]; then
@@ -1573,6 +1903,10 @@ cmd_destroy() {
else else
write_index "null" "null" "{}" write_index "null" "null" "{}"
fi fi
if [ -n "$pm_session_id" ] && [ "$pm_session_id" != "null" ]; then
echo "Deleting opencode session: $pm_session_id"
opencode session delete "$pm_session_id" 2>/dev/null || echo "Warning: Could not delete session from opencode (may already be deleted)"
fi
echo "PM agent session destroyed" echo "PM agent session destroyed"
else else
echo "Error: destroying pm-agent session requires --pm-agent -y" >&2 echo "Error: destroying pm-agent session requires --pm-agent -y" >&2
@@ -1597,6 +1931,25 @@ cmd_destroy() {
remove_issue_from_index "$target" remove_issue_from_index "$target"
echo "Session for '$target' destroyed" echo "Session for '$target' destroyed"
else else
if [ "$WORKTREE_CHECK_PR_STATUS" = "true" ]; then
local pr_url=$(python3 -c "import json; print(json.load(open('$session_path')).get('pr_url', '') or '')" 2>/dev/null || echo "")
if [ -n "$pr_url" ] && [ "$pr_url" != "None" ]; then
echo "Checking PR status at '$pr_url'..."
local pr_status=$(check_pr_status "$pr_url")
if [ "$pr_status" = "open" ]; then
echo "Error: PR is still open at $pr_url" >&2
echo "Use --force to destroy anyway, or close the PR first" >&2
exit 1
elif [ "$pr_status" = "merged" ]; then
echo "PR has been merged. Safe to destroy."
elif [ "$pr_status" = "closed" ]; then
echo "PR has been closed. Safe to destroy."
else
echo "Warning: Could not determine PR status (got: $pr_status). Proceeding anyway." >&2
fi
fi
fi
echo "Delete session and worktree for '$target'? [y/N] " echo "Delete session and worktree for '$target'? [y/N] "
local reply local reply
read reply read reply
@@ -1664,6 +2017,17 @@ main() {
destroy) destroy)
cmd_destroy "$@" cmd_destroy "$@"
;; ;;
set-pr)
local issue_ref="${1:-}"
local pr_url="${2:-}"
if [ -z "$issue_ref" ] || [ -z "$pr_url" ]; then
echo "Usage: kugetsu set-pr <issue-ref> <pr-url>" >&2
echo "Example: kugetsu set-pr github.com/shoko/kugetsu#14 https://github.com/shoko/kugetsu/pulls/123" >&2
exit 1
fi
validate_issue_ref "$issue_ref"
update_session_pr_url "$issue_ref" "$pr_url"
;;
*) *)
echo "Error: unknown command '$command'" >&2 echo "Error: unknown command '$command'" >&2
usage usage