Compare commits

...

13 Commits

Author SHA1 Message Date
shokollm
b28ad78bd8 fix(queue): add --format=json and --limit flags to queue list 2026-04-08 06:20:52 +00:00
d152421d3f Merge pull request 'refactor(cli): restructure help text to show parent commands' (#255) from fix/issue-244 into main 2026-04-08 07:50:08 +02:00
shokollm
9f7da84915 refactor(cli): restructure help text to show parent commands
- kugetsu: show only parent commands, not all subcommands inline
- kugetsu <command> help: show help for specific command
- kugetsu queue: show queue subcommands (list, stats, clear, enqueue)
- kugetsu queue-daemon: show daemon subcommands (start, stop, restart, status, logs)
- kugetsu env: show env subcommands (list, get, set, rm)
- kugetsu server: show server subcommands (list, add, remove, default, get)

Closes #244
2026-04-08 05:12:51 +00:00
ab99647193 Merge pull request 'fix(shell): address shellcheck warnings, standardize error handling, add retry logic' (#253) from fix/issue-121 into main 2026-04-08 07:04:26 +02:00
shokollm
3deae2dd81 fix(shell): address shellcheck warnings, standardize error handling, add retry logic
- Fix shellcheck SC2155 (separate variable assignment from declaration)
- Replace ! bool && bool pattern with [ "$bool" = true ] && [ "$bool2" = true ]
- Add retry logic for git clone with configurable attempts
- Add retry logic for opencode session fork
- Add NETWORK_RETRY_ATTEMPTS and NETWORK_RETRY_DELAY_SECONDS config vars
- Add retry_with_backoff helper function in kugetsu-config.sh
- Replace subshell cd with git -C for safer directory handling
2026-04-08 04:47:01 +00:00
41c56a859c Merge pull request 'refactor: use JSON file exchange instead of stdout parsing' (#234) from fix/issue-119 into main 2026-04-08 05:35:11 +02:00
shokollm
dd903bb8aa refactor: use JSON file exchange instead of stdout parsing
Replace fragile patterns like:
  python3 -c "import json; print(json.load(...))" | grep
  echo "$json" | python3 -c "import sys,json; ...
2026-04-08 03:11:18 +00:00
efb1e34a7b Merge pull request 'fix: add PR merge conflict check to dev agent workflow' (#233) from fix/issue-229-pr-conflict-check into main 2026-04-08 04:56:02 +02:00
44c84280f8 Merge pull request 'fix(queue-daemon): implement timeout handling for long-running tasks' (#228) from fix/issue-166 into main 2026-04-08 04:51:54 +02:00
shokollm
fa8b8467ee fix(queue-daemon): use kugetsu-log for timeout messages
Use log_warn and log_error instead of echo for unified log formatting.
2026-04-08 02:39:40 +00:00
a65e9d6d28 Merge pull request 'feat(session): integrate kugetsu_context_dump into delegation flow' (#229) from fix/issue-212 into main 2026-04-08 04:22:00 +02:00
shokollm
c9eb8badea feat(session): add kugetsu_context_dump call in cmd_continue
Integrates the existing kugetsu_context_dump() function to capture
the initial user prompt before forking the agent.

Closes #212
2026-04-08 02:17:27 +00:00
shokollm
51ec844365 fix(queue-daemon): implement timeout handling for long-running tasks
Check notified_at timestamp in check_task_completion() and mark tasks
as error if they exceed TASK_TIMEOUT_HOURS (defaults to 1 hour).

When timeout is detected:
- Kill the task process (PID) if running
- Stop the opencode session if exists
- Mark queue item as error state

Fixes #166
2026-04-07 12:53:35 +00:00
7 changed files with 423 additions and 126 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ results/
*/results/ */results/
*.pyc *.pyc
.kugetsu/

View File

@@ -17,77 +17,129 @@ usage() {
kugetsu - OpenCode Session Manager (Issue-Driven) kugetsu - OpenCode Session Manager (Issue-Driven)
Usage: Usage:
kugetsu init [--force] Initialize base + pm-agent sessions (requires TTY) kugetsu <command> [subcommand] [options]
kugetsu start <issue-ref> <message> [--debug] Start task for issue (forks base session)
kugetsu continue <issue-ref> [message] [--debug] Continue existing task for issue Commands:
kugetsu delegate <message> Send message to PM agent (fire-and-forget) init [--force] Initialize base + pm-agent sessions (requires TTY)
kugetsu logs [n] Show recent delegation logs (default: 10) start <issue-ref> <message> [--debug] Start task for issue (forks base session)
kugetsu status Check kugetsu initialization status continue <issue-ref> [message] [--debug] Continue existing task for issue
kugetsu doctor [--fix] Diagnose and fix kugetsu issues delegate <message> Send message to PM agent (fire-and-forget)
kugetsu notify [list|clear] Show or clear notifications logs [n] Show recent delegation logs (default: 10)
kugetsu list List all tracked sessions status Check kugetsu initialization status
kugetsu prune [--force] Remove orphaned sessions (keeps base + pm-agent) doctor [--fix] Diagnose and fix kugetsu issues
kugetsu destroy <issue-ref> [-y] Delete session for issue notify [list|clear] Show or clear notifications
kugetsu destroy --pm-agent [-y] Delete pm-agent session (not recommended) list List all tracked sessions
kugetsu destroy --base [-y] Delete base session prune [--force] Remove orphaned sessions
kugetsu set-pr <issue-ref> <pr-url> Set PR URL for session (for PR tracking) destroy <target> [-y] Delete session (issue, pm-agent, or base)
kugetsu context <issue-ref> Show context for issue set-pr <issue-ref> <pr-url> Set PR URL for session
kugetsu queue [list|stats|clear] Show queue status or statistics context <issue-ref> Show context for issue
kugetsu queue enqueue <issue-ref> <message> Enqueue a task (normally via delegate) queue [subcommand] Queue management
kugetsu queue-daemon [start|stop|restart|status|logs] Manage queue daemon queue-daemon [subcommand] Queue daemon management
kugetsu env [get|set|list] Manage agent environment variables env [subcommand] Environment variable management
kugetsu server [list|add|remove|default|get] Manage git server configurations server [subcommand] Git server configuration
kugetsu help Show this help help [command] Show help for a command
Use 'kugetsu <command> help' for subcommand help.
Example: kugetsu queue help, kugetsu queue-daemon help
Issue Ref Format: Issue Ref Format:
instance/user/repo#number instance/user/repo#number
Example: github.com/shoko/kugetsu#14 Example: github.com/shoko/kugetsu#14
EOF
}
Commands: usage_queue() {
init Create base + pm-agent sessions via TUI. Requires terminal access. cat << 'EOF'
Use --force to reinitialize if sessions exist. kugetsu queue - Queue management
start Fork new session from base for specific issue.
Requires pm-agent to be running (created by init).
continue Continue work on existing issue session.
delegate Send message to PM agent for task coordination.
Fire-and-forget: returns immediately, runs in background.
Use 'kugetsu logs' to check output.
logs Show recent delegation logs.
Default: 10 most recent. Use 'kugetsu logs 20' for more.
status Check if kugetsu is initialized and PM agent is active.
doctor Diagnose kugetsu issues. Use --fix to attempt repairs.
notify Show or clear notifications from PM agent.
Use 'kugetsu notify list' to see unread notifications.
list Show all sessions (base + pm-agent + forked issues).
prune Remove sessions not in index (orphaned from opencode).
Use --force to skip confirmation.
destroy Delete specific issue, pm-agent, or base session.
Options: Usage:
--debug Show real-time debug output and capture to debug.log kugetsu queue [subcommand]
PM Context: Subcommands:
kugetsu reads ~/.kugetsu/pm-agent.md (if exists) and injects it list [--limit=N] [--format=json] Show pending tasks (default: 10 items, text format)
into the PM agent session at init time. This allows customizing PM stats Show queue statistics
behavior without recreating the session. clear Clear all queue items
enqueue <issue-ref> <message> Enqueue a task
help Show this help
Notifications: Options for list:
PM Agent writes task completion notifications to ~/.kugetsu/notifications.json --limit=N Number of items to return (default: 10)
Use 'kugetsu notify list' to see unread notifications. --format=json Output in JSON format with stats
Examples: Examples:
kugetsu init kugetsu queue list
kugetsu status kugetsu queue list --limit=50
kugetsu delegate "work on issue #5" kugetsu queue list --format=json
kugetsu logs kugetsu queue list --limit=20 --format=json
kugetsu logs 20 kugetsu queue stats
kugetsu doctor kugetsu queue clear
kugetsu doctor --fix kugetsu queue enqueue github.com/shoko/kugetsu#14 "fix bug"
kugetsu notify list EOF
kugetsu notify clear }
kugetsu start github.com/shoko/kugetsu#14 "fix bug"
kugetsu continue github.com/shoko/kugetsu#14 "add tests" usage_queue_daemon() {
kugetsu list cat << 'EOF'
kugetsu queue-daemon - Queue daemon management
Usage:
kugetsu queue-daemon [subcommand]
Subcommands:
start Start the queue daemon
stop Stop the queue daemon
restart Restart the queue daemon
status Check daemon status
logs Show recent daemon logs
help Show this help
Examples:
kugetsu queue-daemon start
kugetsu queue-daemon status
kugetsu queue-daemon logs
EOF
}
usage_env() {
cat << 'EOF'
kugetsu env - Environment variable management
Usage:
kugetsu env [subcommand]
Subcommands:
list List all environment variables
get <key> Get a specific variable
set <key> <value> Set a variable
rm <key> Remove a variable
help Show this help
Examples:
kugetsu env list
kugetsu env get GITEA_TOKEN
kugetsu env set CUSTOM_VAR "value"
kugetsu env rm CUSTOM_VAR
EOF
}
usage_server() {
cat << 'EOF'
kugetsu server - Git server configuration
Usage:
kugetsu server [subcommand]
Subcommands:
list List all configured servers (default)
add <name> <url> Add a new server
remove <name> Remove a server
default [<name>] Get or set default server
get [<name>] Get server URL
help Show this help
Examples:
kugetsu server list
kugetsu server add github.com https://github.com
kugetsu server default github.com
EOF EOF
} }
@@ -310,19 +362,25 @@ PYEOF
get_pending_tasks() { get_pending_tasks() {
local limit="${1:-10}" local limit="${1:-10}"
local format="${2:-text}"
if [ ! -d "$QUEUE_ITEMS_DIR" ]; then if [ ! -d "$QUEUE_ITEMS_DIR" ]; then
echo "[]" if [ "$format" = "json" ]; then
echo '{"items": [], "total": 0, "pending": 0}'
else
echo "No pending tasks in queue."
fi
return return
fi fi
python3 -c " python3 << PYEOF
import json import json
import os import os
import sys import sys
queue_dir = os.environ.get('QUEUE_ITEMS_DIR', '') queue_dir = os.environ.get('QUEUE_ITEMS_DIR', '')
limit = int(sys.argv[1]) if len(sys.argv) > 1 else 10 limit = int(sys.argv[1]) if len(sys.argv) > 1 else 10
format_type = sys.argv[2] if len(sys.argv) > 2 else 'text'
items = [] items = []
if os.path.isdir(queue_dir): if os.path.isdir(queue_dir):
@@ -334,13 +392,27 @@ if os.path.isdir(queue_dir):
data = json.load(f) data = json.load(f)
if data.get('state') == 'pending': if data.get('state') == 'pending':
items.append(data) items.append(data)
if len(items) >= limit:
break
except: except:
pass pass
print(json.dumps(items)) items.sort(key=lambda x: x.get('pending_since', ''))
" "$limit"
if format_type == 'json':
output = {
"items": items[:limit],
"total": len(items),
"pending": len(items)
}
print(json.dumps(output))
else:
if not items:
print("No pending tasks in queue.")
else:
print("Pending tasks:")
for t in items[:limit]:
msg = t.get('message', '')[:50]
print(f" {t.get('id')}: {t.get('issue_ref')} - {msg}...")
PYEOF
} }
get_queue_stats() { get_queue_stats() {
@@ -865,18 +937,34 @@ find_sessions_by_issue_number() {
} }
cmd_queue() { cmd_queue() {
local action="${1:-list}" local action="${1:-}"
shift
case "$action" in case "$action" in
""|help|--help|-h)
usage_queue
;;
help|--help|-h)
usage_queue
;;
list) list)
local pending_tasks=$(get_pending_tasks 10) local limit="10"
if [ "$pending_tasks" = "[]" ]; then local format="text"
echo "No pending tasks in queue."
else while [ $# -gt 0 ]; do
echo "Pending tasks:" case "$1" in
echo "$pending_tasks" | python3 -c "import sys, json; [print(f\" {t.get('id')}: {t.get('issue_ref')} - {t.get('message', '')[:50]}...\") for t in json.load(sys.stdin)]" --limit=*)
fi limit="${1#*=}"
;;
--format=json)
format="json"
;;
*)
;;
esac
shift
done
get_pending_tasks "$limit" "$format"
;; ;;
stats) stats)
local stats=$(get_queue_stats) local stats=$(get_queue_stats)
@@ -898,8 +986,8 @@ cmd_queue() {
echo "Queue cleared." echo "Queue cleared."
;; ;;
enqueue) enqueue)
local issue_ref="${1:-}" local issue_ref="${2:-}"
local message="${2:-}" local message="${3:-}"
if [ -z "$issue_ref" ] || [ -z "$message" ]; then if [ -z "$issue_ref" ] || [ -z "$message" ]; then
echo "Usage: kugetsu queue enqueue <issue-ref> <message>" >&2 echo "Usage: kugetsu queue enqueue <issue-ref> <message>" >&2
exit 1 exit 1
@@ -910,16 +998,20 @@ cmd_queue() {
check_task_timeouts check_task_timeouts
;; ;;
*) *)
echo "Usage: kugetsu queue [list|stats|clear|enqueue]" >&2 echo "Unknown queue subcommand: $action" >&2
usage_queue
exit 1 exit 1
;; ;;
esac esac
} }
cmd_queue_daemon() { cmd_queue_daemon() {
local action="${1:-status}" local action="${1:-}"
case "$action" in case "$action" in
""|help|--help|-h)
usage_queue_daemon
;;
start) start)
if [ -f "$QUEUE_DAEMON_PID_FILE" ]; then if [ -f "$QUEUE_DAEMON_PID_FILE" ]; then
local old_pid=$(cat "$QUEUE_DAEMON_PID_FILE") local old_pid=$(cat "$QUEUE_DAEMON_PID_FILE")
@@ -978,7 +1070,8 @@ cmd_queue_daemon() {
fi fi
;; ;;
*) *)
echo "Usage: kugetsu queue-daemon [start|stop|restart|status|logs]" >&2 echo "Unknown queue-daemon subcommand: $action" >&2
usage_queue_daemon
exit 1 exit 1
;; ;;
esac esac
@@ -1059,10 +1152,12 @@ set_debug_mode() {
} }
cmd_env() { cmd_env() {
local action="${1:-list}" local action="${1:-}"
shift
case "$action" in case "$action" in
""|help|--help|-h)
usage_env
;;
list) list)
echo "Agent environment variables:" echo "Agent environment variables:"
if [ -d "$ENV_DIR" ]; then if [ -d "$ENV_DIR" ]; then
@@ -1080,7 +1175,7 @@ cmd_env() {
fi fi
;; ;;
get) get)
local key="${1:-}" local key="${2:-}"
if [ -z "$key" ]; then if [ -z "$key" ]; then
echo "Usage: kugetsu env get <key>" >&2 echo "Usage: kugetsu env get <key>" >&2
exit 1 exit 1
@@ -1095,8 +1190,8 @@ cmd_env() {
fi fi
;; ;;
set) set)
local key="${1:-}" local key="${2:-}"
local value="${2:-}" local value="${3:-}"
if [ -z "$key" ] || [ -z "$value" ]; then if [ -z "$key" ] || [ -z "$value" ]; then
echo "Usage: kugetsu env set <key> <value>" >&2 echo "Usage: kugetsu env set <key> <value>" >&2
exit 1 exit 1
@@ -1105,8 +1200,8 @@ cmd_env() {
echo "${key}=${value}" >> "$ENV_DIR/default.env" echo "${key}=${value}" >> "$ENV_DIR/default.env"
echo "Set $key in $ENV_DIR/default.env" echo "Set $key in $ENV_DIR/default.env"
;; ;;
rm) rm|remove)
local key="${1:-}" local key="${2:-}"
if [ -z "$key" ]; then if [ -z "$key" ]; then
echo "Usage: kugetsu env rm <key>" >&2 echo "Usage: kugetsu env rm <key>" >&2
exit 1 exit 1
@@ -1117,7 +1212,8 @@ cmd_env() {
fi fi
;; ;;
*) *)
echo "Usage: kugetsu env [list|get|set|rm]" >&2 echo "Unknown env subcommand: $action" >&2
usage_env
exit 1 exit 1
;; ;;
esac esac
@@ -1127,7 +1223,10 @@ cmd_server() {
local action="${1:-}" local action="${1:-}"
case "$action" in case "$action" in
""|"list") ""|help|--help|-h)
usage_server
;;
"list")
if [ -z "${GIT_SERVERS+x}" ]; then if [ -z "${GIT_SERVERS+x}" ]; then
echo "No git servers configured" echo "No git servers configured"
return return
@@ -1204,14 +1303,8 @@ cmd_server() {
fi fi
;; ;;
*) *)
echo "Usage: kugetsu server <list|add|remove|default|get>" >&2 echo "Unknown server subcommand: $action" >&2
echo "" >&2 usage_server
echo "Commands:" >&2
echo " list List all configured git servers" >&2
echo " add <name> <url> Add a new git server" >&2
echo " remove <name> Remove a git server" >&2
echo " default [<name>] Get or set default server" >&2
echo " get [<name>] Get URL for a server (default: current default)" >&2
exit 1 exit 1
;; ;;
esac esac
@@ -1321,7 +1414,29 @@ main() {
case "$command" in case "$command" in
help|--help|-h) help|--help|-h)
usage local subcommand="${1:-}"
case "$subcommand" in
queue|"")
usage_queue
;;
queue-daemon)
usage_queue_daemon
;;
env)
usage_env
;;
server)
usage_server
;;
"")
usage
;;
*)
echo "Help not available for '$subcommand'" >&2
usage
exit 1
;;
esac
;; ;;
init) init)
cmd_init "$@" cmd_init "$@"

View File

@@ -26,6 +26,9 @@ QUEUE_DAEMON_INTERVAL_MINUTES="${QUEUE_DAEMON_INTERVAL_MINUTES:-5}"
QUEUE_CLEANUP_AGE_DAYS="${QUEUE_CLEANUP_AGE_DAYS:-7}" QUEUE_CLEANUP_AGE_DAYS="${QUEUE_CLEANUP_AGE_DAYS:-7}"
TASK_TIMEOUT_HOURS="${TASK_TIMEOUT_HOURS:-1}" TASK_TIMEOUT_HOURS="${TASK_TIMEOUT_HOURS:-1}"
NETWORK_RETRY_ATTEMPTS="${NETWORK_RETRY_ATTEMPTS:-3}"
NETWORK_RETRY_DELAY_SECONDS="${NETWORK_RETRY_DELAY_SECONDS:-5}"
# Load user config overrides (~/.kugetsu/config) # Load user config overrides (~/.kugetsu/config)
if [ -f "$KUGETSU_DIR/config" ]; then if [ -f "$KUGETSU_DIR/config" ]; then
source "$KUGETSU_DIR/config" source "$KUGETSU_DIR/config"
@@ -87,3 +90,24 @@ set_debug_mode() {
echo "${filtered_args[@]}" echo "${filtered_args[@]}"
} }
retry_with_backoff() {
local max_attempts="${1:-$NETWORK_RETRY_ATTEMPTS}"
local delay_seconds="${2:-$NETWORK_RETRY_DELAY_SECONDS}"
local command="$3"
local remaining_attempts=$max_attempts
while [ $remaining_attempts -gt 0 ]; do
if eval "$command"; then
return 0
fi
remaining_attempts=$((remaining_attempts - 1))
if [ $remaining_attempts -gt 0 ]; then
log "warn" "retry_with_backoff" "Command failed, $remaining_attempts retries remaining. Waiting ${delay_seconds}s..."
sleep "$delay_seconds"
delay_seconds=$((delay_seconds * 2))
fi
done
log "error" "retry_with_backoff" "Command failed after $max_attempts attempts"
return 1
}

View File

@@ -139,6 +139,77 @@ validate_issue_ref() {
fi fi
} }
read_json_file() {
local file_path="$1"
if [ -f "$file_path" ]; then
cat "$file_path"
else
echo "{}"
fi
}
write_json_file() {
local file_path="$1"
local json_content="$2"
local temp_file="$file_path.tmp.$$"
printf '%s' "$json_content" > "$temp_file"
if ! python3 -c "import json; json.load(open('$temp_file'))" 2>/dev/null; then
echo "Error: write_json_file would create malformed JSON: $file_path" >&2
rm -f "$temp_file"
return 1
fi
mv "$temp_file" "$file_path"
}
get_json_value() {
local file_path="$1"
local key="$2"
local default="${3:-}"
if [ ! -f "$file_path" ]; then
echo "$default"
return
fi
python3 -c "import json; print(json.load(open('$file_path')).get('$key', '$default'))" 2>/dev/null || echo "$default"
}
set_json_value() {
local file_path="$1"
local key="$2"
local value="$3"
if [ ! -f "$file_path" ]; then
printf '{"%s": "%s"}\n' "$key" "$value" > "$file_path"
return
fi
python3 << PYEOF
import json
import sys
file_path = "$file_path"
key = "$key"
value = "$value"
try:
with open(file_path, 'r') as f:
data = json.load(f)
except:
data = {}
data[key] = value
with open(file_path, 'w') as f:
json.dump(data, f, indent=2)
print(f"Set $key = $value in $file_path")
PYEOF
}
update_session_pr_url() { update_session_pr_url() {
local issue_ref="$1" local issue_ref="$1"
local pr_url="$2" local pr_url="$2"

View File

@@ -34,6 +34,8 @@ release_lock() {
check_task_completion() { check_task_completion() {
local item="$1" local item="$1"
local queue_id=$(basename "$item" .json) local queue_id=$(basename "$item" .json)
local item_data=$(read_json_file "$item")
local state=$(python3 -c "import json; print(json.load(open('$item')).get('state', ''))" 2>/dev/null) local state=$(python3 -c "import json; print(json.load(open('$item')).get('state', ''))" 2>/dev/null)
[ "$state" = "notified" ] || return 0 [ "$state" = "notified" ] || return 0
@@ -41,6 +43,31 @@ check_task_completion() {
local session_id=$(python3 -c "import json; print(json.load(open('$item')).get('opencode_session_id', ''))" 2>/dev/null) local session_id=$(python3 -c "import json; print(json.load(open('$item')).get('opencode_session_id', ''))" 2>/dev/null)
local issue_ref=$(python3 -c "import json; print(json.load(open('$item')).get('issue_ref', ''))" 2>/dev/null) local issue_ref=$(python3 -c "import json; print(json.load(open('$item')).get('issue_ref', ''))" 2>/dev/null)
local pid=$(python3 -c "import json; print(json.load(open('$item')).get('pid', ''))" 2>/dev/null) local pid=$(python3 -c "import json; print(json.load(open('$item')).get('pid', ''))" 2>/dev/null)
local notified_at=$(python3 -c "import json; print(json.load(open('$item')).get('notified_at', ''))" 2>/dev/null)
local timed_out=false
if [ -n "$notified_at" ]; then
local notified_epoch=$(date -d "$notified_at" +%s 2>/dev/null || echo "0")
local now_epoch=$(date +%s)
local hours_elapsed=$(( (now_epoch - notified_epoch) / 3600 ))
if [ "$hours_elapsed" -ge "${TASK_TIMEOUT_HOURS:-1}" ]; then
timed_out=true
log_warn "queue-daemon" "Task $queue_id ($issue_ref) timed out after ${hours_elapsed}h"
fi
fi
if [ "$timed_out" = true ]; then
if [ -n "$pid" ] && [ "$pid" != "None" ]; then
kill "$pid" 2>/dev/null || true
fi
if [ -n "$session_id" ]; then
opencode session stop "$session_id" 2>/dev/null || true
fi
update_queue_item_state "$queue_id" "error"
log_error "queue-daemon" "Task $queue_id ($issue_ref) marked error — timeout after ${hours_elapsed}h"
release_lock "$issue_ref"
return
fi
if [ -n "$pid" ] && [ "$pid" != "None" ]; then if [ -n "$pid" ] && [ "$pid" != "None" ]; then
if ! kill -0 "$pid" 2>/dev/null; then if ! kill -0 "$pid" 2>/dev/null; then

View File

@@ -13,7 +13,8 @@ count_active_dev_sessions() {
if [ -d "$SESSIONS_DIR" ]; then if [ -d "$SESSIONS_DIR" ]; then
for session_file in "$SESSIONS_DIR"/*.json; do for session_file in "$SESSIONS_DIR"/*.json; do
if [ -f "$session_file" ]; then if [ -f "$session_file" ]; then
local filename=$(basename "$session_file") local filename
filename=$(basename "$session_file")
if [ "$filename" != "base.json" ] && [ "$filename" != "pm-agent.json" ]; then if [ "$filename" != "base.json" ] && [ "$filename" != "pm-agent.json" ]; then
count=$((count + 1)) count=$((count + 1))
fi fi
@@ -239,23 +240,55 @@ create_session() {
return 1 return 1
fi fi
local before_json=$(opencode session list --format=json 2>/dev/null) local before_file
local before_set=$(echo "$before_json" | python3 -c "import sys,json; sessions=json.load(sys.stdin); print('|'.join(s['id'] for s in sessions))" 2>/dev/null || echo "|") before_file="$KUGETSU_DIR/sessions/before$$.json"
local after_file
after_file="$KUGETSU_DIR/sessions/after$$.json"
opencode run --fork --session "$base_session" "new session" >/dev/null 2>&1 opencode session list --format=json > "$before_file" 2>/dev/null || printf '{}' > "$before_file"
local fork_success=false
local attempt=0
local max_attempts="${NETWORK_RETRY_ATTEMPTS:-3}"
while [ $attempt -lt $max_attempts ] && [ "$fork_success" = false ]; do
attempt=$((attempt + 1))
if opencode run --fork --session "$base_session" "new session" >/dev/null 2>&1; then
fork_success=true
elif [ $attempt -lt $max_attempts ]; then
log "warn" "create_session" "Fork attempt $attempt failed, retrying..."
sleep "$((attempt * 2))"
fi
done
if [ "$fork_success" = false ]; then
log "error" "create_session" "Failed to fork session after $max_attempts attempts"
rm -f "$before_file" "$after_file"
return 1
fi
sleep 1 sleep 1
local after_json=$(opencode session list --format=json 2>/dev/null) opencode session list --format=json > "$after_file" 2>/dev/null || printf '{}' > "$after_file"
local after_sessions=$(echo "$after_json" | python3 -c "import sys,json; sessions=json.load(sys.stdin); [print(s['id']) for s in sessions]" 2>/dev/null || true)
local new_session_id="" local new_session_id
while IFS= read -r sess; do new_session_id=$(python3 << PYEOF
if [[ -n "$sess" ]] && [[ ! "$before_set" =~ \|${sess}\| ]]; then import json
new_session_id="$sess"
break with open("$before_file", 'r') as f:
fi before = json.load(f)
done <<< "$after_sessions" with open("$after_file", 'r') as f:
after = json.load(f)
before_ids = set(s['id'] for s in before)
for s in after:
if s['id'] not in before_ids:
print(s['id'])
break
PYEOF
)
rm -f "$before_file" "$after_file"
echo "$new_session_id" echo "$new_session_id"
} }
@@ -378,20 +411,20 @@ ensure_session() {
session_exists=true session_exists=true
fi fi
if $worktree_exists && $session_exists; then if [ "$worktree_exists" = true ] && [ "$session_exists" = true ]; then
log "info" "ensure_session" "Session already exists for $issue_ref" log "info" "ensure_session" "Session already exists for $issue_ref"
echo "continued" echo "continued"
return 0 return 0
fi fi
if ! $worktree_exists && $session_exists; then if [ "$worktree_exists" = false ] && [ "$session_exists" = true ]; then
log "warn" "ensure_session" "Session exists but worktree is missing. Removing stale session..." log "warn" "ensure_session" "Session exists but worktree is missing. Removing stale session..."
rm -f "$session_path" rm -f "$session_path"
remove_issue_from_index "$issue_ref" remove_issue_from_index "$issue_ref"
session_exists=false session_exists=false
fi fi
if ! $worktree_exists; then if [ "$worktree_exists" = false ]; then
local wt_status=$(ensure_worktree "$issue_ref") local wt_status=$(ensure_worktree "$issue_ref")
if [ "$wt_status" != "created" ] && [ "$wt_status" != "existed" ]; then if [ "$wt_status" != "created" ] && [ "$wt_status" != "existed" ]; then
log "error" "ensure_session" "Failed to ensure worktree for $issue_ref" log "error" "ensure_session" "Failed to ensure worktree for $issue_ref"
@@ -489,6 +522,8 @@ cmd_continue() {
exit 1 exit 1
fi fi
kugetsu_context_dump "$issue_ref" "$message" "$(issue_ref_to_branch_name "$issue_ref")"
local session_file=$(issue_ref_to_filename "$issue_ref") local session_file=$(issue_ref_to_filename "$issue_ref")
local session_path="$SESSIONS_DIR/$session_file" local session_path="$SESSIONS_DIR/$session_file"
local opencode_session_id=$(python3 -c "import json; print(json.load(open('$session_path')).get('opencode_session_id', ''))" 2>/dev/null || echo "") local opencode_session_id=$(python3 -c "import json; print(json.load(open('$session_path')).get('opencode_session_id', ''))" 2>/dev/null || echo "")

View File

@@ -76,7 +76,8 @@ create_worktree() {
exit 1 exit 1
fi fi
local worktree_parent_dir=$(dirname "$worktree_path") local worktree_parent_dir
worktree_parent_dir=$(dirname "$worktree_path")
mkdir -p "$worktree_parent_dir" mkdir -p "$worktree_parent_dir"
if worktree_exists "$issue_ref" "$parent_dir"; then if worktree_exists "$issue_ref" "$parent_dir"; then
@@ -85,15 +86,36 @@ create_worktree() {
fi fi
echo "Creating worktree at '$worktree_path'..." echo "Creating worktree at '$worktree_path'..."
git clone "$repo_url" "$worktree_path" 2>/dev/null || {
echo "Error: Failed to clone repository" >&2 local clone_success=false
local attempt=0
local max_attempts="${NETWORK_RETRY_ATTEMPTS:-3}"
while [ $attempt -lt $max_attempts ] && [ "$clone_success" = false ]; do
attempt=$((attempt + 1))
if [ $attempt -gt 1 ]; then
echo "Clone attempt $attempt of $max_attempts..."
sleep "$((attempt * 2))"
fi
if git clone "$repo_url" "$worktree_path" 2>/dev/null; then
clone_success=true
fi
done
if [ "$clone_success" = false ]; then
echo "Error: Failed to clone repository after $max_attempts attempts" >&2
exit 1 exit 1
} fi
echo "Creating branch '$branch_name'..." echo "Creating branch '$branch_name'..."
(cd "$worktree_path" && git checkout -b "$branch_name" origin/main 2>/dev/null || git checkout -b "$branch_name" main 2>/dev/null) || { if git -C "$worktree_path" checkout -b "$branch_name" origin/main 2>/dev/null; then
:
elif git -C "$worktree_path" checkout -b "$branch_name" main 2>/dev/null; then
:
else
echo "Warning: Could not checkout branch (may need to run from within worktree after session)" >&2 echo "Warning: Could not checkout branch (may need to run from within worktree after session)" >&2
} fi
echo "Worktree created at: $worktree_path" echo "Worktree created at: $worktree_path"
} }
@@ -145,15 +167,17 @@ check_pr_status() {
token="${GITEA_TOKEN:-}" token="${GITEA_TOKEN:-}"
fi fi
local response local response_file="$KUGETSU_DIR/.pr_status_response_$$.json"
if [ -n "$token" ]; then if [ -n "$token" ]; then
response=$(curl -s -H "Authorization: token $token" "$api_url" 2>/dev/null || echo "{}") curl -s -H "Authorization: token $token" "$api_url" > "$response_file" 2>/dev/null || printf '{}' > "$response_file"
else else
response=$(curl -s "$api_url" 2>/dev/null || echo "{}") curl -s "$api_url" > "$response_file" 2>/dev/null || printf '{}' > "$response_file"
fi 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 state=$(python3 -c "import json; print(json.load(open('$response_file')).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") local merged=$(python3 -c "import json; print('true' if json.load(open('$response_file')).get('merged', False) else 'false')" 2>/dev/null || echo "false")
rm -f "$response_file"
if [ "$merged" = "true" ]; then if [ "$merged" = "true" ]; then
echo "merged" echo "merged"