Compare commits

..

4 Commits

Author SHA1 Message Date
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
4 changed files with 246 additions and 101 deletions

View File

@@ -17,77 +17,122 @@ 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 Show pending tasks (default)
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
Notifications: help Show this help
PM Agent writes task completion notifications to ~/.kugetsu/notifications.json
Use 'kugetsu notify list' to see unread notifications.
Examples: Examples:
kugetsu init kugetsu queue list
kugetsu status kugetsu queue stats
kugetsu delegate "work on issue #5" kugetsu queue clear
kugetsu logs kugetsu queue enqueue github.com/shoko/kugetsu#14 "fix bug"
kugetsu logs 20 EOF
kugetsu doctor }
kugetsu doctor --fix
kugetsu notify list usage_queue_daemon() {
kugetsu notify clear cat << 'EOF'
kugetsu start github.com/shoko/kugetsu#14 "fix bug" kugetsu queue-daemon - Queue daemon management
kugetsu continue github.com/shoko/kugetsu#14 "add tests"
kugetsu list 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
} }
@@ -865,10 +910,15 @@ 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 pending_tasks=$(get_pending_tasks 10)
if [ "$pending_tasks" = "[]" ]; then if [ "$pending_tasks" = "[]" ]; then
@@ -898,8 +948,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 +960,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 +1032,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 +1114,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 +1137,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 +1152,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 +1162,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 +1174,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 +1185,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 +1265,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,8 +1376,30 @@ main() {
case "$command" in case "$command" in
help|--help|-h) help|--help|-h)
local subcommand="${1:-}"
case "$subcommand" in
queue|"")
usage_queue
;;
queue-daemon)
usage_queue_daemon
;;
env)
usage_env
;;
server)
usage_server
;;
"")
usage 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

@@ -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,18 +240,39 @@ create_session() {
return 1 return 1
fi fi
local before_file="$KUGETSU_DIR/sessions/before$$.json" local before_file
local after_file="$KUGETSU_DIR/sessions/after$$.json" before_file="$KUGETSU_DIR/sessions/before$$.json"
local after_file
after_file="$KUGETSU_DIR/sessions/after$$.json"
opencode session list --format=json > "$before_file" 2>/dev/null || printf '{}' > "$before_file" opencode session list --format=json > "$before_file" 2>/dev/null || printf '{}' > "$before_file"
opencode run --fork --session "$base_session" "new session" >/dev/null 2>&1 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
opencode session list --format=json > "$after_file" 2>/dev/null || printf '{}' > "$after_file" opencode session list --format=json > "$after_file" 2>/dev/null || printf '{}' > "$after_file"
local new_session_id=$(python3 << PYEOF local new_session_id
new_session_id=$(python3 << PYEOF
import json import json
with open("$before_file", 'r') as f: with open("$before_file", 'r') as f:
@@ -389,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"

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"
} }