Compare commits

..

7 Commits

Author SHA1 Message Date
shokollm
87434d1bca fix: always use base workflow and add user message to it
Previously when a user message was provided, cmd_continue would only
use the user message without the base agent workflow. Now both cases
build the full workflow and append the user message.

Changes:
- cmd_continue now always calls build_dev_agent_message even when
  message is provided
- User message is appended at the end with 'Delegator's message:'
- Both with/without message cases now use the same workflow structure

Fixes #229
2026-04-08 00:11:03 +00:00
ae8f1433a7 Merge pull request 'fix(session): remove incorrect worktree removal in ensure_session' (#231) from fix/issue-229-ensure-session-worktree-bug into main 2026-04-08 01:52:08 +02:00
shokollm
b16a97514e fix(session): remove incorrect worktree removal in ensure_session
When worktree exists but session is missing, ensure_session was
incorrectly removing the worktree before recreating. This caused
issues when cmd_continue called ensure_worktree first (creating the
worktree) then ensure_session (which wrongly removed it).

The fix removes the block that removes worktree when session is missing.
If worktree exists, just create the session without touching the worktree.

Fixes #229
2026-04-07 23:39:18 +00:00
b09fe8eabc Merge pull request 'refactor(session): make cmd_continue idempotent with ensure_* functions' (#230) from fix/issue-168 into main 2026-04-08 00:32:16 +02:00
shokollm
d240000088 refactor(session): make cmd_continue idempotent with ensure_* functions
- Add ensure_worktree() - creates worktree if missing, returns status
- Add ensure_session() - creates session if missing, handles inconsistent states
- Add fork_agent() - extracted agent forking logic
- Refactor cmd_continue() to use ensure_* functions (idempotent)
- Make cmd_start() a thin wrapper calling cmd_continue()
- Simplify daemon to always call cmd_continue (no existence check)

This makes cmd_continue truly idempotent - it will:
- Continue existing session if it exists
- Create session and worktree if they don't exist
- Clean and recreate if state is inconsistent

Closes #168
2026-04-07 22:25:53 +00:00
05683ea4c6 Merge pull request 'fix: accumulate message words in cmd_continue for loop' (#227) from fix/issue-225-cmd-continue-message-truncation into main 2026-04-07 15:57:40 +02:00
shokollm
2800e140ac fix: accumulate message words instead of overwriting in cmd_continue
The for loop was overwriting message each iteration, causing only
the last word to survive. Changed to accumulate all words with
proper spacing.

Fixes #225
2026-04-07 13:55:47 +00:00
2 changed files with 156 additions and 141 deletions

View File

@@ -109,7 +109,6 @@ process_task() {
source "$SCRIPT_DIR/kugetsu-session.sh" source "$SCRIPT_DIR/kugetsu-session.sh"
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" log_file="$LOGS_DIR/delegate-$(date +%s).log"
if cmd_continue "$issue_ref" "$message" >> "$log_file" 2>&1; then if cmd_continue "$issue_ref" "$message" >> "$log_file" 2>&1; then
sleep 1 sleep 1
@@ -120,18 +119,6 @@ process_task() {
update_queue_item_state "$queue_id" "error" update_queue_item_state "$queue_id" "error"
echo "Task $queue_id ($issue_ref) failed to continue" echo "Task $queue_id ($issue_ref) failed to continue"
fi fi
else
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" release_lock "$issue_ref"
} }

View File

@@ -272,29 +272,39 @@ build_dev_agent_message() {
if [ -n "$user_message" ]; then if [ -n "$user_message" ]; then
cat <<EOF cat <<EOF
You are continuing work on $issue_ref. A PR likely already exists. You are assigned to work on $issue_ref.
IMPORTANT - Review workflow: IMPORTANT: Follow the workflow below as your guideline, but prioritize the delegator's message.
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: Workflow:
- Make code changes and push to the same branch 1. Read the issue at $instance/$owner/$repo/issues/$number AND all comments on that issue
- 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"}' 2. Check if a PR already exists for this issue
- Or do both - If PR exists and is open, review it and learn from it
- 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"}'
MERGING: If instructed to merge, you MUST confirm approval first before merging: Tools for PR interaction:
- Check for PR approval via: curl -s "https://$instance/api/v1/repos/$owner/$repo/pulls/$number/reviews" -H "Authorization: Bearer \$GITEA_TOKEN" - 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"}'
- Check for "lgtm" or "approved" in 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"
- Only merge if you see approval OR the instruction explicitly says to merge (e.g., "merge the PR", "please merge", "go ahead and merge") - List PR reviews: curl -s "https://$instance/api/v1/repos/$owner/$repo/pulls/$number/reviews" -H "Authorization: Bearer \$GITEA_TOKEN"
- To merge: tea pr merge --repo $owner/$repo $number --style merge - Merge PR (only with approval): tea pr merge --repo $owner/$repo $number --style merge
- If no approval yet, reply asking for review/approval first - 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's message: Delegator's message:
$user_message $user_message
Work directory: $worktree_path (already on the fix branch) Work directory: $worktree_path
EOF EOF
else else
cat <<EOF cat <<EOF
@@ -330,29 +340,48 @@ EOF
fi fi
} }
cmd_start() { ensure_worktree() {
local issue_ref="${1:-}" local issue_ref="$1"
local message="${2:-}"
if [ -z "$issue_ref" ]; then if worktree_exists "$issue_ref" "$WORKTREES_DIR"; then
echo "Error: issue ref is required" >&2 log "info" "ensure_worktree" "Worktree already exists for $issue_ref"
echo "Usage: kugetsu start <issue-ref> [message]" >&2 echo "existed"
exit 1 return 0
fi fi
validate_issue_ref "$issue_ref"
local base_session_id=$(get_base_session_id) local base_session_id=$(get_base_session_id)
if [ -z "$base_session_id" ] || [ "$base_session_id" = "null" ]; then if [ -z "$base_session_id" ] || [ "$base_session_id" = "null" ]; then
echo "Error: Base session not found. Run 'kugetsu init' first." >&2 log "error" "ensure_worktree" "Base session not found for $issue_ref"
exit 1 echo "error"
return 1
fi 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_file=$(issue_ref_to_filename "$issue_ref")
local session_path="$SESSIONS_DIR/$session_file" local session_path="$SESSIONS_DIR/$session_file"
local worktree_exists=false
if worktree_exists "$issue_ref"; then local worktree_exists=false
if worktree_exists "$issue_ref" "$WORKTREES_DIR"; then
worktree_exists=true worktree_exists=true
fi fi
@@ -362,38 +391,40 @@ cmd_start() {
fi fi
if $worktree_exists && $session_exists; then if $worktree_exists && $session_exists; then
echo "Issue '$issue_ref' already has a worktree and session." >&2 log "info" "ensure_session" "Session already exists for $issue_ref"
echo "Use 'kugetsu continue $issue_ref' to continue work." >&2 echo "continued"
exit 1 return 0
fi
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 fi
if ! $worktree_exists && $session_exists; then if ! $worktree_exists && $session_exists; then
echo "Warning: Session exists but worktree is missing. Removing stale session to recreate both..." >&2 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
local active_count=$(count_active_dev_sessions) if ! $worktree_exists; then
if [ "$active_count" -ge "${MAX_CONCURRENT_AGENTS:-3}" ]; then local wt_status=$(ensure_worktree "$issue_ref")
echo "Error: Max concurrent agents (${MAX_CONCURRENT_AGENTS:-3}) reached. Use 'kugetsu continue' or wait for an agent to finish." >&2 if [ "$wt_status" != "created" ] && [ "$wt_status" != "existed" ]; then
exit 1 log "error" "ensure_session" "Failed to ensure worktree for $issue_ref"
echo "error"
return 1
fi
fi fi
create_worktree "$issue_ref" "$WORKTREES_DIR" 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
local new_session_id=$(create_session "$base_session_id") local new_session_id=$(create_session "$base_session_id")
if [ -z "$new_session_id" ]; then if [ -z "$new_session_id" ]; then
echo "Error: Could not create session" >&2 log "error" "ensure_session" "Could not create session for $issue_ref"
remove_worktree_for_issue "$issue_ref" echo "error"
exit 1 return 1
fi fi
local worktree_path=$(issue_ref_to_worktree_path "$issue_ref") local worktree_path=$(issue_ref_to_worktree_path "$issue_ref")
@@ -403,93 +434,90 @@ cmd_start() {
add_issue_to_index "$issue_ref" "$session_file" add_issue_to_index "$issue_ref" "$session_file"
local dev_message=$(build_dev_agent_message "$issue_ref" "$message") log "info" "ensure_session" "Created session for $issue_ref: $new_session_id"
echo "created"
kugetsu_context_dump "$issue_ref" "$dev_message" "$(issue_ref_to_branch_name "$issue_ref")" return 0
load_agent_env "dev"
cd "$worktree_path"
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() { fork_agent() {
local session_name="" local session_id="$1"
local message="" local worktree_path="$2"
local args=("$@") local message="$3"
args=$(set_debug_mode "${args[@]}") if [ -z "$worktree_path" ] || [ ! -d "$worktree_path" ]; then
log "error" "fork_agent" "Invalid worktree path: $worktree_path"
for arg in $args; do echo "error"
if [ -z "$session_name" ]; then return 1
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 fi
load_agent_env "dev" 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" cd "$worktree_path"
local sanitized_id=$(echo "$opencode_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"
printf '%s' "$message" > "$msg_file" printf '%s' "$message" > "$msg_file"
nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '@$msg_file' --session '$opencode_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"
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
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
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}"
} }
cmd_list() { cmd_list() {