Compare commits

..

1 Commits

Author SHA1 Message Date
shokollm
3a0983c28a fix: add PR merge conflict check to dev agent workflow
Dev agent should check if PR has merge conflicts before asking for review.
This prevents approving a PR that has conflicts, which could introduce
new errors after rebase.

Changes:
- Add conflict check step in workflow for both with/without user message cases
- Use mergeable field from PR API to detect conflicts
- Instructions to rebase and resolve conflicts if mergeable: false
2026-04-08 02:15:37 +00:00
10 changed files with 229 additions and 630 deletions

1
.gitignore vendored
View File

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

View File

@@ -1,61 +0,0 @@
# Exit Codes
This document describes the exit codes used by kugetsu commands.
## Exit Codes
| Exit Code | Meaning |
|-----------|---------|
| 0 | Success - operation completed successfully |
| 1 | General error - worktree/session/validation failed |
| 2 | Max concurrent agents reached (MAX_CONCURRENT_AGENTS limit) |
## Commands
### cmd_continue / cmd_start
The `cmd_continue` command (aliased to `cmd_start`) returns exit codes to indicate the result of the operation:
- **Exit 0**: Success - agent forked successfully
- **Exit 1**: General error - worktree/session/validation failed
- **Exit 2**: Max concurrent agents reached
### Internal Functions
#### fork_agent()
Forks a new agent session for a given worktree.
- **Return 0**: Success - agent forked successfully
- **Return 1**: General error - invalid worktree path or other failure
#### ensure_worktree()
Ensures a worktree exists for the given issue reference.
- **Return 0**: Success - worktree existed or was created
- **Return 1**: General error - base session not found or worktree creation failed
- **Return 2**: Max concurrent agents reached
## Daemon Integration
These exit codes are designed to help the queue daemon distinguish between recoverable and non-recoverable errors:
- **Exit 2 (max agents)**: This is a recoverable error - the daemon can retry later when agents become available
- **Exit 1 (general error)**: Non-recoverable - the task should be marked as failed
## MAX_CONCURRENT_AGENTS
The maximum number of concurrent development agents is controlled by the `MAX_CONCURRENT_AGENTS` configuration variable (default: 3).
Set this in your `config` file:
```bash
MAX_CONCURRENT_AGENTS=5
```
Or via environment variable:
```bash
export MAX_CONCURRENT_AGENTS=5
```

View File

@@ -364,14 +364,10 @@ kugetsu queue-daemon logs # Show recent daemon logs
**Daemon Behavior:** **Daemon Behavior:**
1. Runs at configurable interval (default: 5 minutes) 1. Runs at configurable interval (default: 5 minutes)
2. Checks queue for pending items 2. Checks if active agents < MAX_CONCURRENT_AGENTS
3. For each pending item: 3. Picks 1-N pending items (configurable batch size)
- Acquires lock to prevent duplicate processing 4. Forks PM session for each picked item
- Sources kugetsu-session.sh and calls `cmd_continue` 5. PM decides whether to use `start` or `continue`
- `cmd_continue` handles worktree/session creation and forks dev agent from base session
- Updates queue item state to "notified"
4. Uses per-issue locking to prevent race conditions
5. Implements timeout for tasks that don't complete (marks as "error" after TASK_TIMEOUT_HOURS)
**Queue Directory:** **Queue Directory:**
``` ```
@@ -530,13 +526,6 @@ The script will:
See [docs/kugetsu-setup.md](../../docs/kugetsu-setup.md) for full Tailscale setup documentation. See [docs/kugetsu-setup.md](../../docs/kugetsu-setup.md) for full Tailscale setup documentation.
## Version History
### v0.2.3
- **Queue daemon context drift fix** (issue #156): Daemon now sources kugetsu-session.sh and calls `cmd_continue` directly instead of forking PM session. This fixes context drift where daemon would lose track of task state.
- **Daemon locking**: Added per-issue locking to prevent race conditions when multiple daemon instances run.
- **Timeout handling**: Tasks that don't complete within TASK_TIMEOUT_HOURS are marked as "error".
## Without kugetsu ## Without kugetsu
If kugetsu is not available, use opencode directly: If kugetsu is not available, use opencode directly:

View File

@@ -17,129 +17,77 @@ usage() {
kugetsu - OpenCode Session Manager (Issue-Driven) kugetsu - OpenCode Session Manager (Issue-Driven)
Usage: Usage:
kugetsu <command> [subcommand] [options] kugetsu init [--force] Initialize base + pm-agent sessions (requires TTY)
kugetsu start <issue-ref> <message> [--debug] Start task for issue (forks base session)
Commands: kugetsu continue <issue-ref> [message] [--debug] Continue existing task for issue
init [--force] Initialize base + pm-agent sessions (requires TTY) kugetsu delegate <message> Send message to PM agent (fire-and-forget)
start <issue-ref> <message> [--debug] Start task for issue (forks base session) kugetsu logs [n] Show recent delegation logs (default: 10)
continue <issue-ref> [message] [--debug] Continue existing task for issue kugetsu status Check kugetsu initialization status
delegate <message> Send message to PM agent (fire-and-forget) kugetsu doctor [--fix] Diagnose and fix kugetsu issues
logs [n] Show recent delegation logs (default: 10) kugetsu notify [list|clear] Show or clear notifications
status Check kugetsu initialization status kugetsu list List all tracked sessions
doctor [--fix] Diagnose and fix kugetsu issues kugetsu prune [--force] Remove orphaned sessions (keeps base + pm-agent)
notify [list|clear] Show or clear notifications kugetsu destroy <issue-ref> [-y] Delete session for issue
list List all tracked sessions kugetsu destroy --pm-agent [-y] Delete pm-agent session (not recommended)
prune [--force] Remove orphaned sessions kugetsu destroy --base [-y] Delete base session
destroy <target> [-y] Delete session (issue, pm-agent, or base) kugetsu set-pr <issue-ref> <pr-url> Set PR URL for session (for PR tracking)
set-pr <issue-ref> <pr-url> Set PR URL for session kugetsu context <issue-ref> Show context for issue
context <issue-ref> Show context for issue kugetsu queue [list|stats|clear] Show queue status or statistics
queue [subcommand] Queue management kugetsu queue enqueue <issue-ref> <message> Enqueue a task (normally via delegate)
queue-daemon [subcommand] Queue daemon management kugetsu queue-daemon [start|stop|restart|status|logs] Manage queue daemon
env [subcommand] Environment variable management kugetsu env [get|set|list] Manage agent environment variables
server [subcommand] Git server configuration kugetsu server [list|add|remove|default|get] Manage git server configurations
help [command] Show help for a command kugetsu help Show this help
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
}
usage_queue() { Commands:
cat << 'EOF' init Create base + pm-agent sessions via TUI. Requires terminal access.
kugetsu queue - Queue management Use --force to reinitialize if sessions exist.
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.
Usage: Options:
kugetsu queue [subcommand] --debug Show real-time debug output and capture to debug.log
Subcommands: PM Context:
list [--limit=N] [--format=json] Show pending tasks (default: 10 items, text format) kugetsu reads ~/.kugetsu/pm-agent.md (if exists) and injects it
stats Show queue statistics into the PM agent session at init time. This allows customizing PM
clear Clear all queue items behavior without recreating the session.
enqueue <issue-ref> <message> Enqueue a task
help Show this help
Options for list: Notifications:
--limit=N Number of items to return (default: 10) PM Agent writes task completion notifications to ~/.kugetsu/notifications.json
--format=json Output in JSON format with stats Use 'kugetsu notify list' to see unread notifications.
Examples: Examples:
kugetsu queue list kugetsu init
kugetsu queue list --limit=50 kugetsu status
kugetsu queue list --format=json kugetsu delegate "work on issue #5"
kugetsu queue list --limit=20 --format=json kugetsu logs
kugetsu queue stats kugetsu logs 20
kugetsu queue clear kugetsu doctor
kugetsu queue enqueue github.com/shoko/kugetsu#14 "fix bug" kugetsu doctor --fix
EOF kugetsu notify list
} kugetsu notify clear
kugetsu start github.com/shoko/kugetsu#14 "fix bug"
usage_queue_daemon() { kugetsu continue github.com/shoko/kugetsu#14 "add tests"
cat << 'EOF' kugetsu list
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
} }
@@ -362,25 +310,19 @@ 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
if [ "$format" = "json" ]; then echo "[]"
echo '{"items": [], "total": 0, "pending": 0}'
else
echo "No pending tasks in queue."
fi
return return
fi fi
python3 << PYEOF python3 -c "
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):
@@ -392,27 +334,13 @@ 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
items.sort(key=lambda x: x.get('pending_since', '')) print(json.dumps(items))
" "$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() {
@@ -937,34 +865,18 @@ find_sessions_by_issue_number() {
} }
cmd_queue() { cmd_queue() {
local action="${1:-}" local action="${1:-list}"
shift
case "$action" in case "$action" in
""|help|--help|-h)
usage_queue
;;
help|--help|-h)
usage_queue
;;
list) list)
local limit="10" local pending_tasks=$(get_pending_tasks 10)
local format="text" if [ "$pending_tasks" = "[]" ]; then
echo "No pending tasks in queue."
while [ $# -gt 0 ]; do else
case "$1" in echo "Pending tasks:"
--limit=*) 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="${1#*=}" fi
;;
--format=json)
format="json"
;;
*)
;;
esac
shift
done
get_pending_tasks "$limit" "$format"
;; ;;
stats) stats)
local stats=$(get_queue_stats) local stats=$(get_queue_stats)
@@ -986,8 +898,8 @@ cmd_queue() {
echo "Queue cleared." echo "Queue cleared."
;; ;;
enqueue) enqueue)
local issue_ref="${2:-}" local issue_ref="${1:-}"
local message="${3:-}" local message="${2:-}"
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
@@ -998,20 +910,16 @@ cmd_queue() {
check_task_timeouts check_task_timeouts
;; ;;
*) *)
echo "Unknown queue subcommand: $action" >&2 echo "Usage: kugetsu queue [list|stats|clear|enqueue]" >&2
usage_queue
exit 1 exit 1
;; ;;
esac esac
} }
cmd_queue_daemon() { cmd_queue_daemon() {
local action="${1:-}" local action="${1:-status}"
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")
@@ -1070,8 +978,7 @@ cmd_queue_daemon() {
fi fi
;; ;;
*) *)
echo "Unknown queue-daemon subcommand: $action" >&2 echo "Usage: kugetsu queue-daemon [start|stop|restart|status|logs]" >&2
usage_queue_daemon
exit 1 exit 1
;; ;;
esac esac
@@ -1152,12 +1059,10 @@ set_debug_mode() {
} }
cmd_env() { cmd_env() {
local action="${1:-}" local action="${1:-list}"
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
@@ -1175,7 +1080,7 @@ cmd_env() {
fi fi
;; ;;
get) get)
local key="${2:-}" local key="${1:-}"
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
@@ -1190,8 +1095,8 @@ cmd_env() {
fi fi
;; ;;
set) set)
local key="${2:-}" local key="${1:-}"
local value="${3:-}" local value="${2:-}"
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
@@ -1200,8 +1105,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|remove) rm)
local key="${2:-}" local key="${1:-}"
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
@@ -1212,8 +1117,7 @@ cmd_env() {
fi fi
;; ;;
*) *)
echo "Unknown env subcommand: $action" >&2 echo "Usage: kugetsu env [list|get|set|rm]" >&2
usage_env
exit 1 exit 1
;; ;;
esac esac
@@ -1223,10 +1127,7 @@ cmd_server() {
local action="${1:-}" local action="${1:-}"
case "$action" in case "$action" in
""|help|--help|-h) ""|"list")
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
@@ -1303,8 +1204,14 @@ cmd_server() {
fi fi
;; ;;
*) *)
echo "Unknown server subcommand: $action" >&2 echo "Usage: kugetsu server <list|add|remove|default|get>" >&2
usage_server echo "" >&2
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
@@ -1414,29 +1321,7 @@ main() {
case "$command" in case "$command" in
help|--help|-h) help|--help|-h)
local subcommand="${1:-}" usage
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,10 +26,6 @@ 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}"
KUGETSU_BASE_BRANCH="${KUGETSU_BASE_BRANCH:-origin/main}"
# 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"
@@ -91,24 +87,3 @@ 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,77 +139,6 @@ 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

@@ -45,61 +45,22 @@ cmd_logs() {
find "$LOGS_DIR" -type f -mtime +7 -delete 2>/dev/null find "$LOGS_DIR" -type f -mtime +7 -delete 2>/dev/null
local log_files=() ls -lt "$LOGS_DIR" | head -$((count + 1)) | tail -$count | while read line; do
while IFS= read -r line; do echo "$line"
log_files+=("$line")
done < <(ls -lt "$LOGS_DIR" | head -$((count + 1)) | tail -$count | awk '{print $NF}')
echo "=== Recent Logs ($count files) ==="
echo ""
printf "%-30s %8s %s\n" "FILE" "SIZE" "LAST MODIFIED"
echo "----------------------------------------------------------------"
for log in "${log_files[@]}"; do
if [ -f "$LOGS_DIR/$log" ]; then
local size=$(stat -c %s "$LOGS_DIR/$log" 2>/dev/null || echo "0")
local mtime=$(stat -c %Y "$LOGS_DIR/$log" 2>/dev/null || echo "0")
local date_str=$(date -d "@$mtime" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "unknown")
printf "%-30s %8s %s\n" "$log" "$size" "$date_str"
fi
done done
echo "" echo ""
echo "Log Contents:" echo "Recent log contents:"
echo "================================================================" echo "===================="
for log in "${log_files[@]}"; do for log in $(ls -lt "$LOGS_DIR" | head -$((count + 1)) | tail -$count | awk '{print $NF}'); do
if [ -f "$LOGS_DIR/$log" ]; then if [ -f "$LOGS_DIR/$log" ]; then
echo "" echo ""
echo ">>> $log <<<" echo "--- $log ---"
echo "----------------------------------------------------------------"
tail -20 "$LOGS_DIR/$log" | while read line; do tail -20 "$LOGS_DIR/$log" | while read line; do
line=$(strip_ansi_codes "$line") line=$(strip_ansi_codes "$line")
line=$(mask_sensitive_vars "$line") line=$(mask_sensitive_vars "$line")
echo " $line"
if [[ "$line" =~ \[([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}[^\]]*)\]\ ([a-z]+)\ (.+) ]]; then
local ts="${BASH_REMATCH[1]}"
local level="${BASH_REMATCH[2]}"
local msg="${BASH_REMATCH[3]}"
local color=""
local level_str=""
case "$level" in
error) color="\033[0;31m"; level_str="[ERROR]" ;;
warn) color="\033[0;33m"; level_str="[WARN ]" ;;
info) color="\033[0;32m"; level_str="[INFO ]" ;;
debug) color="\033[0;36m"; level_str="[DEBUG]" ;;
*) color="\033[0;90m"; level_str="[$level]" ;;
esac
if [ -t 1 ]; then
echo -e " \033[0;90m$ts\033[0m ${color}${level_str}\033[0m $msg"
else
echo " $ts ${level_str} $msg"
fi
else
echo " $line"
fi
done done
fi fi
done done

View File

@@ -34,8 +34,6 @@ 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
@@ -43,31 +41,6 @@ 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
@@ -75,7 +48,7 @@ check_task_completion() {
local has_commits=false local has_commits=false
if [ -d "$worktree_path" ] && [ -d "$worktree_path/.git" ]; then if [ -d "$worktree_path" ] && [ -d "$worktree_path/.git" ]; then
if [ -n "$(git -C "$worktree_path" log --oneline "$KUGETSU_BASE_BRANCH..HEAD" 2>/dev/null)" ]; then if [ -n "$(git -C "$worktree_path" log --oneline origin/main..HEAD 2>/dev/null)" ]; then
has_commits=true has_commits=true
fi fi
fi fi
@@ -95,7 +68,7 @@ check_task_completion() {
local has_commits=false local has_commits=false
if [ -d "$worktree_path" ] && [ -d "$worktree_path/.git" ]; then if [ -d "$worktree_path" ] && [ -d "$worktree_path/.git" ]; then
if [ -n "$(git -C "$worktree_path" log --oneline "$KUGETSU_BASE_BRANCH..HEAD" 2>/dev/null)" ]; then if [ -n "$(git -C "$worktree_path" log --oneline origin/main..HEAD 2>/dev/null)" ]; then
has_commits=true has_commits=true
fi fi
fi fi

View File

@@ -13,8 +13,7 @@ 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 local filename=$(basename "$session_file")
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
@@ -240,55 +239,23 @@ create_session() {
return 1 return 1
fi fi
local before_file local before_json=$(opencode session list --format=json 2>/dev/null)
before_file="$KUGETSU_DIR/sessions/before$$.json" 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 "|")
local after_file
after_file="$KUGETSU_DIR/sessions/after$$.json"
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" local after_json=$(opencode session list --format=json 2>/dev/null)
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=""
new_session_id=$(python3 << PYEOF while IFS= read -r sess; do
import json if [[ -n "$sess" ]] && [[ ! "$before_set" =~ \|${sess}\| ]]; then
new_session_id="$sess"
with open("$before_file", 'r') as f: break
before = json.load(f) fi
with open("$after_file", 'r') as f: done <<< "$after_sessions"
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"
} }
@@ -303,37 +270,21 @@ build_dev_agent_message() {
local number=$(echo "$issue_ref" | grep -oE '#[0-9]+$' | tr -d '#') local number=$(echo "$issue_ref" | grep -oE '#[0-9]+$' | tr -d '#')
local worktree_path=$(issue_ref_to_worktree_path "$issue_ref") local worktree_path=$(issue_ref_to_worktree_path "$issue_ref")
local conflict_check=""
local review_notes=""
local delegator_header=""
local delegator_footer=""
if [ -n "$user_message" ]; then if [ -n "$user_message" ]; then
conflict_check=" - CRITICAL: Check if PR has merge conflicts before asking for review: cat <<EOF
- Use: curl -s \"https://$instance/api/v1/repos/$owner/$repo/pulls/$number\" -H \"Authorization: Bearer \$GITEA_TOKEN\"
- If \"mergeable\": false, there ARE conflicts - you MUST resolve them FIRST
- To resolve: cd to worktree, git fetch origin, git rebase origin/main, resolve conflicts, git rebase --continue, git push --force-with-lease
- Only after resolving conflicts (mergeable: true) can you ask for review"
delegator_header="IMPORTANT: Follow the workflow below as your guideline, but prioritize the delegator's message.
Workflow:"
delegator_footer="
Delegator's message:
$user_message"
else
review_notes=" - IMPORTANT: After listing reviews, READ the review comments and incorporate feedback
- Check for review state: \"APPROVED\" means ready to merge, \"COMMENT\" means feedback to address"
delegator_header="Workflow:"
fi
cat <<EOF
You are assigned to work on $issue_ref. You are assigned to work on $issue_ref.
$delegator_header IMPORTANT: Follow the workflow below as your guideline, but prioritize the delegator's message.
Workflow:
1. Read the issue at $instance/$owner/$repo/issues/$number AND all comments on that issue 1. Read the issue at $instance/$owner/$repo/issues/$number AND all comments on that issue
2. Check if a PR already exists for this issue 2. Check if a PR already exists for this issue
- If PR exists and is open, review it and learn from it - If PR exists and is open, review it and learn from it
$conflict_check - CRITICAL: Check if PR has merge conflicts before asking for review:
- Use: curl -s "https://$instance/api/v1/repos/$owner/$repo/pulls/$number" -H "Authorization: Bearer \$GITEA_TOKEN"
- If "mergeable": false, there ARE conflicts - you MUST resolve them FIRST
- To resolve: cd to worktree, git fetch origin, git rebase origin/main, resolve conflicts, git rebase --continue, git push --force-with-lease
- Only after resolving conflicts (mergeable: true) can you ask for review
- If PR makes sense to continue, work on it instead - If PR makes sense to continue, work on it instead
- If PR is not worth continuing, create a new branch/PR but explain in PR description why you're creating a new one instead of continuing the existing PR - If PR is not worth continuing, create a new branch/PR but explain in PR description why you're creating a new one instead of continuing the existing PR
3. Read README.md (if exists) to understand the general concept of this repository 3. Read README.md (if exists) to understand the general concept of this repository
@@ -351,14 +302,52 @@ Tools for PR interaction:
- Post issue/PR comment: curl -X POST "https://$instance/api/v1/repos/$owner/$repo/issues/$number/comments" -H "Authorization: Bearer \$GITEA_TOKEN" -H "Content-Type: application/json" -d '{"body":"Your comment"}' - Post issue/PR comment: curl -X POST "https://$instance/api/v1/repos/$owner/$repo/issues/$number/comments" -H "Authorization: Bearer \$GITEA_TOKEN" -H "Content-Type: application/json" -d '{"body":"Your comment"}'
- List PR comments: curl -s "https://$instance/api/v1/repos/$owner/$repo/issues/$number/comments" -H "Authorization: Bearer \$GITEA_TOKEN" - List PR comments: curl -s "https://$instance/api/v1/repos/$owner/$repo/issues/$number/comments" -H "Authorization: Bearer \$GITEA_TOKEN"
- List PR reviews: curl -s "https://$instance/api/v1/repos/$owner/$repo/pulls/$number/reviews" -H "Authorization: Bearer \$GITEA_TOKEN" - List PR reviews: curl -s "https://$instance/api/v1/repos/$owner/$repo/pulls/$number/reviews" -H "Authorization: Bearer \$GITEA_TOKEN"
$review_notes
- Merge PR (only with approval): tea pr merge --repo $owner/$repo $number --style merge - Merge PR (only with approval): tea pr merge --repo $owner/$repo $number --style merge
- MERGING requires approval first! Check for: approval in reviews, OR "lgtm"/"approved" in comments - MERGING requires approval first! Check for: approval in reviews, OR "lgtm"/"approved" in comments
- If no approval, ask reviewer to approve first before merging - If no approval, ask reviewer to approve first before merging
$delegator_footer
Delegator's message:
$user_message
Work directory: $worktree_path Work directory: $worktree_path
EOF EOF
else
cat <<EOF
You are assigned to work on $issue_ref.
Workflow:
1. Read the issue at $instance/$owner/$repo/issues/$number AND all comments on that issue
2. Check if a PR already exists for this issue
- If PR exists and is open, review it and learn from it
- CRITICAL: Check if PR has merge conflicts before asking for review:
- Use: curl -s "https://$instance/api/v1/repos/$owner/$repo/pulls/$number" -H "Authorization: Bearer \$GITEA_TOKEN"
- If "mergeable": false, there ARE conflicts - you MUST resolve them FIRST
- To resolve: cd to worktree, git fetch origin, git rebase origin/main, resolve conflicts, git rebase --continue, git push --force-with-lease
- Only after resolving conflicts (mergeable: true) can you ask for review
- If PR makes sense to continue, work on it instead
- If PR is not worth continuing, create a new branch/PR but explain in PR description why you're creating a new one instead of continuing the existing PR
3. Read README.md (if exists) to understand the general concept of this repository
4. Read CONTRIBUTING.md (if exists) to understand how to contribute
- If CONTRIBUTING.md doesn't exist, follow steps 5-9 as your guideline
5. Explore the repository to understand the codebase
6. If anything is unclear, post a comment on the issue asking for clarification before implementing
7. Implement the solution
8. Create a branch named fix/issue-$number and implement the fix
9. Create a PR when the implementation is complete using: tea pr create --repo $owner/$repo --title "Your PR title" --body "PR description"
- Make sure you are logged in with: tea login add --name gitea --token \$GITEA_TOKEN --url https://$instance
- If tea is not available, use: curl -X POST "https://$instance/api/v1/repos/$owner/$repo/pulls" -H "Authorization: Bearer \$GITEA_TOKEN" -H "Content-Type: application/json" -d '{"title":"PR Title","head":"branch-name","base":"main","body":"PR description"}'
Tools for PR interaction:
- Post issue/PR comment: curl -X POST "https://$instance/api/v1/repos/$owner/$repo/issues/$number/comments" -H "Authorization: Bearer \$GITEA_TOKEN" -H "Content-Type: application/json" -d '{"body":"Your comment"}'
- List PR comments: curl -s "https://$instance/api/v1/repos/$owner/$repo/issues/$number/comments" -H "Authorization: Bearer \$GITEA_TOKEN"
- List PR reviews: curl -s "https://$instance/api/v1/repos/$owner/$repo/pulls/$number/reviews" -H "Authorization: Bearer \$GITEA_TOKEN"
- Merge PR (only with approval): tea pr merge --repo $owner/$repo $number --style merge
- MERGING requires approval first! Check for: approval in reviews, OR "lgtm"/"approved" in comments
- If no approval, ask reviewer to approve first before merging
Work directory: $worktree_path
EOF
fi
} }
ensure_worktree() { ensure_worktree() {
@@ -380,8 +369,8 @@ ensure_worktree() {
local active_count=$(count_active_dev_sessions) local active_count=$(count_active_dev_sessions)
if [ "$active_count" -ge "${MAX_CONCURRENT_AGENTS:-3}" ]; then if [ "$active_count" -ge "${MAX_CONCURRENT_AGENTS:-3}" ]; then
log "error" "ensure_worktree" "Max concurrent agents reached for $issue_ref" log "error" "ensure_worktree" "Max concurrent agents reached for $issue_ref"
echo "max_agents" echo "error"
return 2 return 1
fi fi
if create_worktree "$issue_ref" "$WORKTREES_DIR" 2>&1 | tee >(cat >&2); then if create_worktree "$issue_ref" "$WORKTREES_DIR" 2>&1 | tee >(cat >&2); then
@@ -411,20 +400,20 @@ ensure_session() {
session_exists=true session_exists=true
fi fi
if [ "$worktree_exists" = true ] && [ "$session_exists" = true ]; then if $worktree_exists && $session_exists; 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" = false ] && [ "$session_exists" = true ]; then if ! $worktree_exists && $session_exists; 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" = false ]; then if ! $worktree_exists; 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"
@@ -467,6 +456,7 @@ fork_agent() {
if [ -z "$worktree_path" ] || [ ! -d "$worktree_path" ]; then if [ -z "$worktree_path" ] || [ ! -d "$worktree_path" ]; then
log "error" "fork_agent" "Invalid worktree path: $worktree_path" log "error" "fork_agent" "Invalid worktree path: $worktree_path"
echo "error"
return 1 return 1
fi fi
@@ -475,7 +465,7 @@ fork_agent() {
cd "$worktree_path" cd "$worktree_path"
local sanitized_id=$(echo "$session_id" | sed 's/[^a-zA-Z0-9_-]/_/g') local sanitized_id=$(echo "$session_id" | sed 's/[^a-zA-Z0-9_-]/_/g')
mkdir -p "$worktree_path/.kugetsu" mkdir -p "$worktree_path/.kugetsu"
if [ ! -f "$worktree_path/.gitignore" ] || ! grep -q "^.kugetsu/" "$worktree_path/.gitignore" ]; then if [ ! -f "$worktree_path/.gitignore" ] || ! grep -q "^.kugetsu/" "$worktree_path/.gitignore"; then
echo ".kugetsu/" >> "$worktree_path/.gitignore" 2>/dev/null || true echo ".kugetsu/" >> "$worktree_path/.gitignore" 2>/dev/null || true
fi fi
local msg_file="$worktree_path/.kugetsu/msg.txt" local msg_file="$worktree_path/.kugetsu/msg.txt"
@@ -483,6 +473,7 @@ fork_agent() {
nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '@$msg_file' --session '$session_id'" >> "$LOGS_DIR/dev-$sanitized_id.log" 2>&1 & nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '@$msg_file' --session '$session_id'" >> "$LOGS_DIR/dev-$sanitized_id.log" 2>&1 &
log "info" "fork_agent" "Forked agent for session $session_id in $worktree_path" log "info" "fork_agent" "Forked agent for session $session_id in $worktree_path"
echo "forked"
return 0 return 0
} }
@@ -497,10 +488,10 @@ cmd_continue() {
if [ -z "$issue_ref" ]; then if [ -z "$issue_ref" ]; then
echo "Error: issue ref is required" >&2 echo "Error: issue ref is required" >&2
echo "Usage: kugetsu continue <issue-ref> [message]" >&2 echo "Usage: kugetsu continue <issue-ref> [message]" >&2
return 1 exit 1
fi fi
validate_issue_ref "$issue_ref" || return 1 validate_issue_ref "$issue_ref"
if [ -z "$message" ]; then if [ -z "$message" ]; then
message=$(build_dev_agent_message "$issue_ref" "") message=$(build_dev_agent_message "$issue_ref" "")
@@ -509,35 +500,33 @@ cmd_continue() {
fi fi
local worktree_status=$(ensure_worktree "$issue_ref") local worktree_status=$(ensure_worktree "$issue_ref")
if [ "$worktree_status" = "max_agents" ]; then
echo "Error: Max concurrent agents reached for '$issue_ref'" >&2
return 2
fi
if [ "$worktree_status" = "error" ]; then if [ "$worktree_status" = "error" ]; then
echo "Error: Failed to ensure worktree for '$issue_ref'" >&2 echo "Error: Failed to ensure worktree for '$issue_ref'" >&2
return 1 exit 1
fi fi
local session_status=$(ensure_session "$issue_ref") local session_status=$(ensure_session "$issue_ref")
if [ "$session_status" = "error" ]; then if [ "$session_status" = "error" ]; then
echo "Error: Failed to ensure session for '$issue_ref'" >&2 echo "Error: Failed to ensure session for '$issue_ref'" >&2
return 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 "")
local worktree_path=$(python3 -c "import json; print(json.load(open('$session_path')).get('worktree_path', ''))" 2>/dev/null || echo "") local worktree_path=$(python3 -c "import json; print(json.load(open('$session_path')).get('worktree_path', ''))" 2>/dev/null || echo "")
fork_agent "$opencode_session_id" "$worktree_path" "$message" || return 1 local fork_status=$(fork_agent "$opencode_session_id" "$worktree_path" "$message")
if [ "$fork_status" = "error" ]; then
echo "Error: Failed to fork agent for '$issue_ref'" >&2
exit 1
fi
log "info" "cmd_continue" "Result for $issue_ref: worktree=$worktree_status session=$session_status fork=forked" log "info" "cmd_continue" "Result for $issue_ref: worktree=$worktree_status session=$session_status fork=$fork_status"
echo "Session continued for '$issue_ref': $opencode_session_id" echo "Session continued for '$issue_ref': $opencode_session_id"
echo "Worktree: $worktree_path" echo "Worktree: $worktree_path"
echo "${worktree_status}-${session_status}-forked" echo "${worktree_status}-${session_status}-${fork_status}"
} }
cmd_list() { cmd_list() {
@@ -559,71 +548,55 @@ cmd_list() {
fi fi
echo "" echo ""
echo "Issue sessions:"
local issue_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=$(basename "$session_file")
if [ "$filename" != "base.json" ] && [ "$filename" != "pm-agent.json" ]; then if [ "$filename" != "base.json" ] && [ "$filename" != "pm-agent.json" ]; then
local mtime=$(stat -c %Y "$session_file" 2>/dev/null || echo "0")
local issue_ref=$(filename_to_issue_ref "$filename") local issue_ref=$(filename_to_issue_ref "$filename")
local opencode_sid=$(python3 -c "import json; print(json.load(open('$session_file')).get('opencode_session_id', 'unknown'))" 2>/dev/null || echo "unknown") local opencode_sid=$(python3 -c "import json; print(json.load(open('$session_file')).get('opencode_session_id', 'unknown'))" 2>/dev/null || echo "unknown")
local state=$(python3 -c "import json; print(json.load(open('$session_file')).get('state', 'unknown'))" 2>/dev/null || echo "unknown") local state=$(python3 -c "import json; print(json.load(open('$session_file')).get('state', 'unknown'))" 2>/dev/null || echo "unknown")
local worktree_path=$(python3 -c "import json; print(json.load(open('$session_file')).get('worktree_path', ''))" 2>/dev/null || echo "") local worktree_path=$(python3 -c "import json; print(json.load(open('$session_file')).get('worktree_path', ''))" 2>/dev/null || echo "")
issue_sessions+=("$mtime|$filename|$issue_ref|$opencode_sid|$state|$worktree_path") local worktree_status=""
if [ -n "$worktree_path" ]; then
if [ -d "$worktree_path" ]; then
worktree_status="(worktree exists)"
else
worktree_status="(worktree MISSING)"
fi
fi
echo " $filename"
echo " Issue: $issue_ref"
echo " Session: $opencode_sid"
echo " State: $state"
echo " $worktree_status"
fi fi
fi fi
done done
fi fi
if [ ${#issue_sessions[@]} -gt 0 ]; then
printf "%-45s %-10s %-20s %s\n" "ISSUE_REF" "STATE" "LAST_UPDATED" "SESSION_ID"
echo "-------------------------------------------------------------------------------------------------------------"
while IFS='|' read -r mtime filename issue_ref opencode_sid state worktree_path; do
local last_updated=$(date -d "@$mtime" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "unknown")
local worktree_status=""
if [ -n "$worktree_path" ] && [ ! -d "$worktree_path" ]; then
worktree_status=" [worktree MISSING]"
fi
printf "%-45s %-10s %-20s %s%s\n" "$issue_ref" "$state" "$last_updated" "$opencode_sid" "$worktree_status"
done < <(printf '%s\n' "${issue_sessions[@]}" | sort -t'|' -k1 -rn)
else
echo "No issue sessions found."
fi
echo ""
local orphaned_worktrees=()
if [ -d "$WORKTREES_DIR" ]; then if [ -d "$WORKTREES_DIR" ]; then
echo ""
echo "Worktrees without sessions:"
for worktree in $(find "$WORKTREES_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null); do for worktree in $(find "$WORKTREES_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null); do
local worktree_name=$(basename "$worktree") local worktree_name=$(basename "$worktree")
local has_session=false local has_session=false
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 wt_path=$(python3 -c "import json; print(json.load(open('$session_file')).get('worktree_path', ''))" 2>/dev/null || echo "")
local wt_path=$(python3 -c "import json; print(json.load(open('$session_file')).get('worktree_path', ''))" 2>/dev/null || echo "") if [ "$wt_path" = "$worktree" ]; then
if [ "$wt_path" = "$worktree" ]; then has_session=true
has_session=true break
break
fi
fi fi
done fi
fi done
if [ "$has_session" = false ]; then if [ "$has_session" = false ]; then
orphaned_worktrees+=("$worktree_name") echo " $worktree_name (no session)"
fi fi
done done
fi fi
if [ ${#orphaned_worktrees[@]} -gt 0 ]; then
echo "Worktrees without sessions:"
for wt in "${orphaned_worktrees[@]}"; do
echo " $wt"
done
fi
} }
cmd_prune() { cmd_prune() {

View File

@@ -76,8 +76,7 @@ create_worktree() {
exit 1 exit 1
fi fi
local worktree_parent_dir local worktree_parent_dir=$(dirname "$worktree_path")
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
@@ -86,36 +85,15 @@ 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 || {
local clone_success=false echo "Error: Failed to clone repository" >&2
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'..."
if git -C "$worktree_path" checkout -b "$branch_name" "$KUGETSU_BASE_BRANCH" 2>/dev/null; then (cd "$worktree_path" && git checkout -b "$branch_name" origin/main 2>/dev/null || git checkout -b "$branch_name" main 2>/dev/null) || {
:
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"
} }
@@ -167,17 +145,15 @@ check_pr_status() {
token="${GITEA_TOKEN:-}" token="${GITEA_TOKEN:-}"
fi fi
local response_file="$KUGETSU_DIR/.pr_status_response_$$.json" local response
if [ -n "$token" ]; then if [ -n "$token" ]; then
curl -s -H "Authorization: token $token" "$api_url" > "$response_file" 2>/dev/null || printf '{}' > "$response_file" response=$(curl -s -H "Authorization: token $token" "$api_url" 2>/dev/null || echo "{}")
else else
curl -s "$api_url" > "$response_file" 2>/dev/null || printf '{}' > "$response_file" response=$(curl -s "$api_url" 2>/dev/null || echo "{}")
fi fi
local state=$(python3 -c "import json; print(json.load(open('$response_file')).get('state', 'unknown'))" 2>/dev/null || echo "unknown") 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=$(python3 -c "import json; print('true' if json.load(open('$response_file')).get('merged', False) else 'false')" 2>/dev/null || echo "false") 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")
rm -f "$response_file"
if [ "$merged" = "true" ]; then if [ "$merged" = "true" ]; then
echo "merged" echo "merged"