Compare commits

..

1 Commits

Author SHA1 Message Date
shokollm
3014fc303e feat(session): integrate kugetsu_context_dump into delegation flow
- Call kugetsu_context_dump() in cmd_start() before forking agent
- Call kugetsu_context_dump() in cmd_continue() before forking agent
- Captures initial user prompt in context JSON for session resumption

Closes #212
2026-04-07 12:59:02 +00:00
7 changed files with 266 additions and 527 deletions

1
.gitignore vendored
View File

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

View File

@@ -17,122 +17,77 @@ usage() {
kugetsu - OpenCode Session Manager (Issue-Driven)
Usage:
kugetsu <command> [subcommand] [options]
Commands:
init [--force] Initialize base + pm-agent sessions (requires TTY)
start <issue-ref> <message> [--debug] Start task for issue (forks base session)
continue <issue-ref> [message] [--debug] Continue existing task for issue
delegate <message> Send message to PM agent (fire-and-forget)
logs [n] Show recent delegation logs (default: 10)
status Check kugetsu initialization status
doctor [--fix] Diagnose and fix kugetsu issues
notify [list|clear] Show or clear notifications
list List all tracked sessions
prune [--force] Remove orphaned sessions
destroy <target> [-y] Delete session (issue, pm-agent, or base)
set-pr <issue-ref> <pr-url> Set PR URL for session
context <issue-ref> Show context for issue
queue [subcommand] Queue management
queue-daemon [subcommand] Queue daemon management
env [subcommand] Environment variable management
server [subcommand] Git server configuration
help [command] Show help for a command
Use 'kugetsu <command> help' for subcommand help.
Example: kugetsu queue help, kugetsu queue-daemon help
kugetsu init [--force] Initialize base + pm-agent sessions (requires TTY)
kugetsu start <issue-ref> <message> [--debug] Start task for issue (forks base session)
kugetsu continue <issue-ref> [message] [--debug] Continue existing task for issue
kugetsu delegate <message> Send message to PM agent (fire-and-forget)
kugetsu logs [n] Show recent delegation logs (default: 10)
kugetsu status Check kugetsu initialization status
kugetsu doctor [--fix] Diagnose and fix kugetsu issues
kugetsu notify [list|clear] Show or clear notifications
kugetsu list List all tracked sessions
kugetsu prune [--force] Remove orphaned sessions (keeps base + pm-agent)
kugetsu destroy <issue-ref> [-y] Delete session for issue
kugetsu destroy --pm-agent [-y] Delete pm-agent session (not recommended)
kugetsu destroy --base [-y] Delete base session
kugetsu set-pr <issue-ref> <pr-url> Set PR URL for session (for PR tracking)
kugetsu context <issue-ref> Show context for issue
kugetsu queue [list|stats|clear] Show queue status or statistics
kugetsu queue enqueue <issue-ref> <message> Enqueue a task (normally via delegate)
kugetsu queue-daemon [start|stop|restart|status|logs] Manage queue daemon
kugetsu env [get|set|list] Manage agent environment variables
kugetsu server [list|add|remove|default|get] Manage git server configurations
kugetsu help Show this help
Issue Ref Format:
instance/user/repo#number
Example: github.com/shoko/kugetsu#14
EOF
}
usage_queue() {
cat << 'EOF'
kugetsu queue - Queue management
Commands:
init Create base + pm-agent sessions via TUI. Requires terminal access.
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:
kugetsu queue [subcommand]
Options:
--debug Show real-time debug output and capture to debug.log
Subcommands:
list Show pending tasks (default)
stats Show queue statistics
clear Clear all queue items
enqueue <issue-ref> <message> Enqueue a task
help Show this help
PM Context:
kugetsu reads ~/.kugetsu/pm-agent.md (if exists) and injects it
into the PM agent session at init time. This allows customizing PM
behavior without recreating the session.
Notifications:
PM Agent writes task completion notifications to ~/.kugetsu/notifications.json
Use 'kugetsu notify list' to see unread notifications.
Examples:
kugetsu queue list
kugetsu queue stats
kugetsu queue clear
kugetsu queue enqueue github.com/shoko/kugetsu#14 "fix bug"
EOF
}
usage_queue_daemon() {
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
kugetsu init
kugetsu status
kugetsu delegate "work on issue #5"
kugetsu logs
kugetsu logs 20
kugetsu doctor
kugetsu doctor --fix
kugetsu notify list
kugetsu notify clear
kugetsu start github.com/shoko/kugetsu#14 "fix bug"
kugetsu continue github.com/shoko/kugetsu#14 "add tests"
kugetsu list
EOF
}
@@ -910,15 +865,10 @@ find_sessions_by_issue_number() {
}
cmd_queue() {
local action="${1:-}"
local action="${1:-list}"
shift
case "$action" in
""|help|--help|-h)
usage_queue
;;
help|--help|-h)
usage_queue
;;
list)
local pending_tasks=$(get_pending_tasks 10)
if [ "$pending_tasks" = "[]" ]; then
@@ -948,8 +898,8 @@ cmd_queue() {
echo "Queue cleared."
;;
enqueue)
local issue_ref="${2:-}"
local message="${3:-}"
local issue_ref="${1:-}"
local message="${2:-}"
if [ -z "$issue_ref" ] || [ -z "$message" ]; then
echo "Usage: kugetsu queue enqueue <issue-ref> <message>" >&2
exit 1
@@ -960,20 +910,16 @@ cmd_queue() {
check_task_timeouts
;;
*)
echo "Unknown queue subcommand: $action" >&2
usage_queue
echo "Usage: kugetsu queue [list|stats|clear|enqueue]" >&2
exit 1
;;
esac
}
cmd_queue_daemon() {
local action="${1:-}"
local action="${1:-status}"
case "$action" in
""|help|--help|-h)
usage_queue_daemon
;;
start)
if [ -f "$QUEUE_DAEMON_PID_FILE" ]; then
local old_pid=$(cat "$QUEUE_DAEMON_PID_FILE")
@@ -1032,8 +978,7 @@ cmd_queue_daemon() {
fi
;;
*)
echo "Unknown queue-daemon subcommand: $action" >&2
usage_queue_daemon
echo "Usage: kugetsu queue-daemon [start|stop|restart|status|logs]" >&2
exit 1
;;
esac
@@ -1114,12 +1059,10 @@ set_debug_mode() {
}
cmd_env() {
local action="${1:-}"
local action="${1:-list}"
shift
case "$action" in
""|help|--help|-h)
usage_env
;;
list)
echo "Agent environment variables:"
if [ -d "$ENV_DIR" ]; then
@@ -1137,7 +1080,7 @@ cmd_env() {
fi
;;
get)
local key="${2:-}"
local key="${1:-}"
if [ -z "$key" ]; then
echo "Usage: kugetsu env get <key>" >&2
exit 1
@@ -1152,8 +1095,8 @@ cmd_env() {
fi
;;
set)
local key="${2:-}"
local value="${3:-}"
local key="${1:-}"
local value="${2:-}"
if [ -z "$key" ] || [ -z "$value" ]; then
echo "Usage: kugetsu env set <key> <value>" >&2
exit 1
@@ -1162,8 +1105,8 @@ cmd_env() {
echo "${key}=${value}" >> "$ENV_DIR/default.env"
echo "Set $key in $ENV_DIR/default.env"
;;
rm|remove)
local key="${2:-}"
rm)
local key="${1:-}"
if [ -z "$key" ]; then
echo "Usage: kugetsu env rm <key>" >&2
exit 1
@@ -1174,8 +1117,7 @@ cmd_env() {
fi
;;
*)
echo "Unknown env subcommand: $action" >&2
usage_env
echo "Usage: kugetsu env [list|get|set|rm]" >&2
exit 1
;;
esac
@@ -1185,10 +1127,7 @@ cmd_server() {
local action="${1:-}"
case "$action" in
""|help|--help|-h)
usage_server
;;
"list")
""|"list")
if [ -z "${GIT_SERVERS+x}" ]; then
echo "No git servers configured"
return
@@ -1265,8 +1204,14 @@ cmd_server() {
fi
;;
*)
echo "Unknown server subcommand: $action" >&2
usage_server
echo "Usage: kugetsu server <list|add|remove|default|get>" >&2
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
;;
esac
@@ -1376,29 +1321,7 @@ main() {
case "$command" in
help|--help|-h)
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
usage
;;
init)
cmd_init "$@"

View File

@@ -26,9 +26,6 @@ QUEUE_DAEMON_INTERVAL_MINUTES="${QUEUE_DAEMON_INTERVAL_MINUTES:-5}"
QUEUE_CLEANUP_AGE_DAYS="${QUEUE_CLEANUP_AGE_DAYS:-7}"
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)
if [ -f "$KUGETSU_DIR/config" ]; then
source "$KUGETSU_DIR/config"
@@ -90,24 +87,3 @@ set_debug_mode() {
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
}
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() {
local issue_ref="$1"
local pr_url="$2"

View File

@@ -34,8 +34,6 @@ release_lock() {
check_task_completion() {
local item="$1"
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)
[ "$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 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 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 ! kill -0 "$pid" 2>/dev/null; then
@@ -136,15 +109,28 @@ process_task() {
source "$SCRIPT_DIR/kugetsu-session.sh"
log_file="$LOGS_DIR/delegate-$(date +%s).log"
if cmd_continue "$issue_ref" "$message" >> "$log_file" 2>&1; then
sleep 1
local session_id=$(get_session_id_for_issue "$issue_ref")
update_queue_item_state "$queue_id" "notified" "$session_id" ""
echo "Task $queue_id continued for $issue_ref"
if worktree_exists "$issue_ref" "$WORKTREES_DIR" || [ -f "$SESSIONS_DIR/$(issue_ref_to_filename "$issue_ref").json" ]; then
log_file="$LOGS_DIR/delegate-$(date +%s).log"
if cmd_continue "$issue_ref" "$message" >> "$log_file" 2>&1; then
sleep 1
local session_id=$(get_session_id_for_issue "$issue_ref")
update_queue_item_state "$queue_id" "notified" "$session_id" ""
echo "Task $queue_id continued for $issue_ref"
else
update_queue_item_state "$queue_id" "error"
echo "Task $queue_id ($issue_ref) failed to continue"
fi
else
update_queue_item_state "$queue_id" "error"
echo "Task $queue_id ($issue_ref) failed to continue"
log_file="$LOGS_DIR/delegate-$(date +%s).log"
if cmd_start "$issue_ref" "$message" >> "$log_file" 2>&1; then
sleep 1
local session_id=$(get_session_id_for_issue "$issue_ref")
update_queue_item_state "$queue_id" "notified" "$session_id" ""
echo "Task $queue_id started for $issue_ref"
else
update_queue_item_state "$queue_id" "error"
echo "Task $queue_id ($issue_ref) failed to start"
fi
fi
release_lock "$issue_ref"

View File

@@ -13,8 +13,7 @@ count_active_dev_sessions() {
if [ -d "$SESSIONS_DIR" ]; then
for session_file in "$SESSIONS_DIR"/*.json; do
if [ -f "$session_file" ]; then
local filename
filename=$(basename "$session_file")
local filename=$(basename "$session_file")
if [ "$filename" != "base.json" ] && [ "$filename" != "pm-agent.json" ]; then
count=$((count + 1))
fi
@@ -240,55 +239,23 @@ create_session() {
return 1
fi
local before_file
before_file="$KUGETSU_DIR/sessions/before$$.json"
local after_file
after_file="$KUGETSU_DIR/sessions/after$$.json"
local before_json=$(opencode session list --format=json 2>/dev/null)
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 "|")
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
opencode run --fork --session "$base_session" "new session" >/dev/null 2>&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
new_session_id=$(python3 << PYEOF
import json
with open("$before_file", 'r') as f:
before = json.load(f)
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"
local new_session_id=""
while IFS= read -r sess; do
if [[ -n "$sess" ]] && [[ ! "$before_set" =~ \|${sess}\| ]]; then
new_session_id="$sess"
break
fi
done <<< "$after_sessions"
echo "$new_session_id"
}
@@ -303,37 +270,40 @@ build_dev_agent_message() {
local number=$(echo "$issue_ref" | grep -oE '#[0-9]+$' | tr -d '#')
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
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"
delegator_header="IMPORTANT: Follow the workflow below as your guideline, but prioritize the delegator's message.
cat <<EOF
You are continuing work on $issue_ref. A PR likely already exists.
Workflow:"
delegator_footer="
IMPORTANT - Review workflow:
1. First, check if PR exists: curl -s "https://$instance/api/v1/repos/$owner/$repo/pulls?state=open" -H "Authorization: Bearer \$GITEA_TOKEN" | grep -i "$number"
2. Get PR comments: curl -s "https://$instance/api/v1/repos/$owner/$repo/issues/$number/comments" -H "Authorization: Bearer \$GITEA_TOKEN"
3. Get PR reviews: curl -s "https://$instance/api/v1/repos/$owner/$repo/pulls/$number/reviews" -H "Authorization: Bearer \$GITEA_TOKEN"
You may need to:
- Make code changes and push to the same branch
- Reply to PR comments using: 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 reply here"}'
- Or do both
MERGING: If instructed to merge, you MUST confirm approval first before merging:
- Check for PR approval via: curl -s "https://$instance/api/v1/repos/$owner/$repo/pulls/$number/reviews" -H "Authorization: Bearer \$GITEA_TOKEN"
- Check for "lgtm" or "approved" in comments: curl -s "https://$instance/api/v1/repos/$owner/$repo/issues/$number/comments" -H "Authorization: Bearer \$GITEA_TOKEN"
- Only merge if you see approval OR the instruction explicitly says to merge (e.g., "merge the PR", "please merge", "go ahead and merge")
- To merge: tea pr merge --repo $owner/$repo $number --style merge
- If no approval yet, reply asking for review/approval first
Delegator's message:
$user_message"
$user_message
Work directory: $worktree_path (already on the fix branch)
EOF
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
cat <<EOF
You are assigned to work on $issue_ref.
$delegator_header
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
$conflict_check
- 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
@@ -351,58 +321,38 @@ 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"
$review_notes
- 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
$delegator_footer
Work directory: $worktree_path
EOF
fi
}
ensure_worktree() {
local issue_ref="$1"
cmd_start() {
local issue_ref="${1:-}"
local message="${2:-}"
if worktree_exists "$issue_ref" "$WORKTREES_DIR"; then
log "info" "ensure_worktree" "Worktree already exists for $issue_ref"
echo "existed"
return 0
if [ -z "$issue_ref" ]; then
echo "Error: issue ref is required" >&2
echo "Usage: kugetsu start <issue-ref> [message]" >&2
exit 1
fi
validate_issue_ref "$issue_ref"
local base_session_id=$(get_base_session_id)
if [ -z "$base_session_id" ] || [ "$base_session_id" = "null" ]; then
log "error" "ensure_worktree" "Base session not found for $issue_ref"
echo "error"
return 1
echo "Error: Base session not found. Run 'kugetsu init' first." >&2
exit 1
fi
local active_count=$(count_active_dev_sessions)
if [ "$active_count" -ge "${MAX_CONCURRENT_AGENTS:-3}" ]; then
log "error" "ensure_worktree" "Max concurrent agents reached for $issue_ref"
echo "error"
return 1
fi
if create_worktree "$issue_ref" "$WORKTREES_DIR" 2>&1 | tee >(cat >&2); then
log "info" "ensure_worktree" "Created worktree for $issue_ref"
echo "created"
return 0
else
log "error" "ensure_worktree" "Failed to create worktree for $issue_ref"
echo "error"
return 1
fi
}
ensure_session() {
local issue_ref="$1"
local session_file=$(issue_ref_to_filename "$issue_ref")
local session_path="$SESSIONS_DIR/$session_file"
local worktree_exists=false
if worktree_exists "$issue_ref" "$WORKTREES_DIR"; then
if worktree_exists "$issue_ref"; then
worktree_exists=true
fi
@@ -411,41 +361,39 @@ ensure_session() {
session_exists=true
fi
if [ "$worktree_exists" = true ] && [ "$session_exists" = true ]; then
log "info" "ensure_session" "Session already exists for $issue_ref"
echo "continued"
return 0
if $worktree_exists && $session_exists; then
echo "Issue '$issue_ref' already has a worktree and session." >&2
echo "Use 'kugetsu continue $issue_ref' to continue work." >&2
exit 1
fi
if [ "$worktree_exists" = false ] && [ "$session_exists" = true ]; then
log "warn" "ensure_session" "Session exists but worktree is missing. Removing stale session..."
if $worktree_exists && ! $session_exists; then
echo "Warning: Worktree exists but session is missing. Removing worktree to recreate both..." >&2
remove_worktree_for_issue "$issue_ref"
worktree_exists=false
fi
if ! $worktree_exists && $session_exists; then
echo "Warning: Session exists but worktree is missing. Removing stale session to recreate both..." >&2
rm -f "$session_path"
remove_issue_from_index "$issue_ref"
session_exists=false
fi
if [ "$worktree_exists" = false ]; then
local wt_status=$(ensure_worktree "$issue_ref")
if [ "$wt_status" != "created" ] && [ "$wt_status" != "existed" ]; then
log "error" "ensure_session" "Failed to ensure worktree for $issue_ref"
echo "error"
return 1
fi
local active_count=$(count_active_dev_sessions)
if [ "$active_count" -ge "${MAX_CONCURRENT_AGENTS:-3}" ]; then
echo "Error: Max concurrent agents (${MAX_CONCURRENT_AGENTS:-3}) reached. Use 'kugetsu continue' or wait for an agent to finish." >&2
exit 1
fi
local base_session_id=$(get_base_session_id)
if [ -z "$base_session_id" ] || [ "$base_session_id" = "null" ]; then
log "error" "ensure_session" "Base session not found for $issue_ref"
echo "error"
return 1
fi
create_worktree "$issue_ref" "$WORKTREES_DIR"
local new_session_id=$(create_session "$base_session_id")
if [ -z "$new_session_id" ]; then
log "error" "ensure_session" "Could not create session for $issue_ref"
echo "error"
return 1
echo "Error: Could not create session" >&2
remove_worktree_for_issue "$issue_ref"
exit 1
fi
local worktree_path=$(issue_ref_to_worktree_path "$issue_ref")
@@ -455,91 +403,93 @@ ensure_session() {
add_issue_to_index "$issue_ref" "$session_file"
log "info" "ensure_session" "Created session for $issue_ref: $new_session_id"
echo "created"
return 0
}
fork_agent() {
local session_id="$1"
local worktree_path="$2"
local message="$3"
local dev_message=$(build_dev_agent_message "$issue_ref" "$message")
if [ -z "$worktree_path" ] || [ ! -d "$worktree_path" ]; then
log "error" "fork_agent" "Invalid worktree path: $worktree_path"
echo "error"
return 1
fi
kugetsu_context_dump "$issue_ref" "$dev_message" "$(issue_ref_to_branch_name "$issue_ref")"
load_agent_env "dev"
cd "$worktree_path"
local sanitized_id=$(echo "$session_id" | sed 's/[^a-zA-Z0-9_-]/_/g')
local sanitized_id=$(echo "$new_session_id" | sed 's/[^a-zA-Z0-9_-]/_/g')
mkdir -p "$worktree_path/.kugetsu"
if [ ! -f "$worktree_path/.gitignore" ] || ! grep -q "^.kugetsu/" "$worktree_path/.gitignore"; then
echo ".kugetsu/" >> "$worktree_path/.gitignore" 2>/dev/null || true
fi
local msg_file="$worktree_path/.kugetsu/msg.txt"
printf '%s' "$dev_message" > "$msg_file"
nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '@$msg_file' --session '$new_session_id'" >> "$LOGS_DIR/dev-$sanitized_id.log" 2>&1 &
echo "Session started for '$issue_ref': $new_session_id"
echo "Worktree: $worktree_path"
}
cmd_continue() {
local session_name=""
local message=""
local args=("$@")
args=$(set_debug_mode "${args[@]}")
for arg in $args; do
if [ -z "$session_name" ]; then
session_name="$arg"
else
message="$arg"
fi
done
if [ -z "$session_name" ]; then
echo "Error: issue ref is required" >&2
echo "Usage: kugetsu continue <issue-ref> [message]" >&2
exit 1
fi
validate_issue_ref "$session_name"
local session_file=$(get_session_for_issue "$session_name")
if [ -z "$session_file" ] || [ "$session_file" = "null" ]; then
echo "Error: No session found for '$session_name'" >&2
echo "Use 'kugetsu start $session_name' to create a new session." >&2
exit 1
fi
local session_path="$SESSIONS_DIR/$session_file"
if [ ! -f "$session_path" ]; then
echo "Error: Session file not found: $session_path" >&2
exit 1
fi
load_agent_env "dev"
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 issue_ref=$(python3 -c "import json; print(json.load(open('$session_path')).get('issue_ref', ''))" 2>/dev/null || echo "")
if [ -z "$worktree_path" ] || [ ! -d "$worktree_path" ]; then
echo "Warning: Worktree is missing for '$session_name'. Recovering..." >&2
rm -f "$session_path"
remove_issue_from_index "$session_name"
echo "Calling cmd_start to create new session and worktree..." >&2
cmd_start "$session_name" "$message"
return $?
fi
if [ -z "$message" ]; then
message=$(build_dev_agent_message "$issue_ref" "")
fi
kugetsu_context_dump "$issue_ref" "$message" "$(issue_ref_to_branch_name "$issue_ref")"
cd "$worktree_path"
local sanitized_id=$(echo "$opencode_session_id" | sed 's/[^a-zA-Z0-9_-]/_/g')
mkdir -p "$worktree_path/.kugetsu"
if [ ! -f "$worktree_path/.gitignore" ] || ! grep -q "^.kugetsu/" "$worktree_path/.gitignore"; then
echo ".kugetsu/" >> "$worktree_path/.gitignore" 2>/dev/null || true
fi
local msg_file="$worktree_path/.kugetsu/msg.txt"
printf '%s' "$message" > "$msg_file"
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"
echo "forked"
return 0
}
cmd_start() {
cmd_continue "$@"
}
cmd_continue() {
local issue_ref="${1:-}"
local message="${2:-}"
if [ -z "$issue_ref" ]; then
echo "Error: issue ref is required" >&2
echo "Usage: kugetsu continue <issue-ref> [message]" >&2
exit 1
fi
validate_issue_ref "$issue_ref"
if [ -z "$message" ]; then
message=$(build_dev_agent_message "$issue_ref" "")
else
message=$(build_dev_agent_message "$issue_ref" "$message")
fi
local worktree_status=$(ensure_worktree "$issue_ref")
if [ "$worktree_status" = "error" ]; then
echo "Error: Failed to ensure worktree for '$issue_ref'" >&2
exit 1
fi
local session_status=$(ensure_session "$issue_ref")
if [ "$session_status" = "error" ]; then
echo "Error: Failed to ensure session for '$issue_ref'" >&2
exit 1
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_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 worktree_path=$(python3 -c "import json; print(json.load(open('$session_path')).get('worktree_path', ''))" 2>/dev/null || echo "")
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=$fork_status"
echo "Session continued for '$issue_ref': $opencode_session_id"
echo "Worktree: $worktree_path"
echo "${worktree_status}-${session_status}-${fork_status}"
nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '@$msg_file' --session '$opencode_session_id'" >> "$LOGS_DIR/dev-$sanitized_id.log" 2>&1 &
}
cmd_list() {

View File

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