From fd7a98b2638221c7f4daf70254ae93b9a02f3319 Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Sun, 5 Apr 2026 12:10:55 +0000 Subject: [PATCH 1/5] fix: validate sessions in cmd_status + use isolated test environment 1. cmd_status now validates session IDs against opencode session list - Reports 'error: base session X not found in opencode' if missing - Reports 'error: pm_agent session X not found in opencode' if missing 2. Test suite now uses isolated KUGETSU_DIR=/tmp/test-kugetsu-$$ - All tests use separate test directory instead of ~/.kugetsu - Prevents test suite from corrupting real user data - Cleanup removes test directory entirely Fixes #148 --- CONTRIBUTING.md | 9 +- skills/kugetsu/scripts/kugetsu | 12 +++ skills/kugetsu/tests/test-kugetsu-v2.sh | 108 ++++++++++++------------ 3 files changed, 73 insertions(+), 56 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ae2f1e8..0b69f4a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,10 +2,10 @@ ## Workflow -1. Create a branch for your work: `git checkout -b fix/issue-N-name` or `git checkout -b docs/topic-name` +1. Create a branch for your work: `git checkout -b fix/issue-N-name` or `git checkout -b feat/issue-N-feature-name` 2. Make changes and commit with clear messages 3. Open a Pull Request for review -4. Do not merge directly to `master` for reviewable changes +4. Do not merge directly to `main` for reviewable changes 5. After approval, squash and merge ## Guidelines @@ -17,7 +17,10 @@ ## Branches -- `master` — stable, reviewed content only +- `main` — stable, reviewed content only +- `develop` — experimental work for 0.2.x - `fix/*` — bug fixes +- `feat/*` — new features - `docs/*` — documentation updates +- `refactor/*` — refactoring - `research/*` — new research notes diff --git a/skills/kugetsu/scripts/kugetsu b/skills/kugetsu/scripts/kugetsu index 5095955..bd0ba76 100755 --- a/skills/kugetsu/scripts/kugetsu +++ b/skills/kugetsu/scripts/kugetsu @@ -1075,6 +1075,18 @@ cmd_status() { return fi + local opencode_sessions=$(opencode session list 2>/dev/null | grep -oP '^ses_\w+' || true) + + if ! echo "$opencode_sessions" | grep -q "^${base}$"; then + echo "error: base session '$base' not found in opencode" + return + fi + + if ! echo "$opencode_sessions" | grep -q "^${pm_agent}$"; then + echo "error: pm_agent session '$pm_agent' not found in opencode" + return + fi + echo "ok" } diff --git a/skills/kugetsu/tests/test-kugetsu-v2.sh b/skills/kugetsu/tests/test-kugetsu-v2.sh index 70835a7..4675436 100644 --- a/skills/kugetsu/tests/test-kugetsu-v2.sh +++ b/skills/kugetsu/tests/test-kugetsu-v2.sh @@ -7,6 +7,8 @@ set -euo pipefail KUGETSU="./skills/kugetsu/scripts/kugetsu" +TEST_KUGETSU_DIR="/tmp/test-kugetsu-$$" +export KUGETSU_DIR="$TEST_KUGETSU_DIR" TEST_ISSUE_REF="github.com/shoko/kugetsu#14" TEST_DISCUSS_REF="github.com/shoko/kugetsu#-discuss" TEST_BASE_SESSION_ID="ses_test_base_123" @@ -18,28 +20,28 @@ PASS=0 FAIL=0 cleanup() { - rm -rf ~/.kugetsu/sessions/* ~/.kugetsu/worktrees/* ~/.kugetsu/index.json 2>/dev/null || true + rm -rf "$TEST_KUGETSU_DIR" 2>/dev/null || true } setup_mock_base() { - mkdir -p ~/.kugetsu/sessions ~/.kugetsu/worktrees - cat > ~/.kugetsu/index.json << EOF + mkdir -p "$TEST_KUGETSU_DIR/sessions" "$TEST_KUGETSU_DIR/worktrees" + cat > "$TEST_KUGETSU_DIR/index.json" << EOF { "base": "$TEST_BASE_SESSION_ID", "pm_agent": "$TEST_PM_AGENT_SESSION_ID", "issues": {} } EOF - cat > ~/.kugetsu/sessions/$TEST_BASE_SESSION_FILE << EOF + cat > "$TEST_KUGETSU_DIR/sessions/$TEST_BASE_SESSION_FILE" << EOF {"type": "base", "opencode_session_id": "$TEST_BASE_SESSION_ID", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"} EOF - cat > ~/.kugetsu/sessions/$TEST_PM_AGENT_SESSION_FILE << EOF + cat > "$TEST_KUGETSU_DIR/sessions/$TEST_PM_AGENT_SESSION_FILE" << EOF {"type": "pm_agent", "opencode_session_id": "$TEST_PM_AGENT_SESSION_ID", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"} EOF } setup_mock_forked() { - cat > ~/.kugetsu/index.json << EOF + cat > "$TEST_KUGETSU_DIR/index.json" << EOF { "base": "$TEST_BASE_SESSION_ID", "pm_agent": "$TEST_PM_AGENT_SESSION_ID", @@ -48,7 +50,7 @@ setup_mock_forked() { } } EOF - cat > ~/.kugetsu/sessions/$TEST_FORKED_SESSION_FILE << EOF + cat > "$TEST_KUGETSU_DIR/sessions/$TEST_FORKED_SESSION_FILE" << EOF {"type": "forked", "issue_ref": "$TEST_ISSUE_REF", "opencode_session_id": "ses_forked_789", "worktree_path": "/tmp/test-worktree", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"} EOF } @@ -112,16 +114,16 @@ echo "" # Test 3b: start fails without pm-agent echo "--- Test: start without pm-agent session ---" -rm -f ~/.kugetsu/index.json ~/.kugetsu/sessions/* -mkdir -p ~/.kugetsu/sessions -cat > ~/.kugetsu/index.json << EOF +rm -f $TEST_KUGETSU_DIR/index.json $TEST_KUGETSU_DIR/sessions/* +mkdir -p $TEST_KUGETSU_DIR/sessions +cat > $TEST_KUGETSU_DIR/index.json << EOF { "base": "$TEST_BASE_SESSION_ID", "pm_agent": null, "issues": {} } EOF -cat > ~/.kugetsu/sessions/$TEST_BASE_SESSION_FILE << EOF +cat > $TEST_KUGETSU_DIR/sessions/$TEST_BASE_SESSION_FILE << EOF {"type": "base", "opencode_session_id": "$TEST_BASE_SESSION_ID", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"} EOF OUTPUT=$($KUGETSU start github.com/shoko/kugetsu#14 "test" 2>&1 || true) @@ -176,7 +178,7 @@ echo "" # Test 6c: index.json has pm_agent field echo "--- Test: index.json has pm_agent field ---" -if grep -q '"pm_agent"' ~/.kugetsu/index.json; then +if grep -q '"pm_agent"' $TEST_KUGETSU_DIR/index.json; then pass "index.json has pm_agent field" else fail "index.json missing pm_agent field" @@ -227,12 +229,12 @@ echo "" echo "--- Test: destroy --pm-agent -y ---" setup_mock_base OUTPUT=$($KUGETSU destroy --pm-agent -y 2>&1 || true) -if [ -f ~/.kugetsu/sessions/$TEST_PM_AGENT_SESSION_FILE ]; then +if [ -f $TEST_KUGETSU_DIR/sessions/$TEST_PM_AGENT_SESSION_FILE ]; then fail "destroy --pm-agent -y removes pm-agent file" else pass "destroy --pm-agent -y removes pm-agent file" fi -if grep -q '"pm_agent": null' ~/.kugetsu/index.json; then +if grep -q '"pm_agent": null' $TEST_KUGETSU_DIR/index.json; then pass "destroy --pm-agent -y sets pm_agent to null in index" else fail "destroy --pm-agent -y should set pm_agent to null" @@ -243,7 +245,7 @@ echo "" echo "--- Test: destroy --base -y ---" setup_mock_base OUTPUT=$($KUGETSU destroy --base -y 2>&1 || true) -if [ -f ~/.kugetsu/sessions/$TEST_BASE_SESSION_FILE ]; then +if [ -f $TEST_KUGETSU_DIR/sessions/$TEST_BASE_SESSION_FILE ]; then fail "destroy --base -y removes base file" else pass "destroy --base -y removes base file" @@ -292,7 +294,7 @@ echo "" # Test 15: worktree path in session file echo "--- Test: worktree_path in session file ---" -if grep -q "worktree_path" ~/.kugetsu/sessions/$TEST_FORKED_SESSION_FILE; then +if grep -q "worktree_path" $TEST_KUGETSU_DIR/sessions/$TEST_FORKED_SESSION_FILE; then pass "session file contains worktree_path" else fail "session file missing worktree_path" @@ -303,7 +305,7 @@ echo "" echo "--- Test: prune with orphaned worktree ---" cleanup setup_mock_base -mkdir -p ~/.kugetsu/worktrees/orphaned-worktree +mkdir -p $TEST_KUGETSU_DIR/worktrees/orphaned-worktree OUTPUT=$($KUGETSU prune 2>&1 || true) if echo "$OUTPUT" | grep -q "orphaned worktree"; then pass "prune detects orphaned worktree" @@ -315,7 +317,7 @@ echo "" # Test 17: prune --force removes orphaned worktrees echo "--- Test: prune --force removes orphaned worktrees ---" OUTPUT=$($KUGETSU prune --force 2>&1 || true) -if [ -d ~/.kugetsu/worktrees/orphaned-worktree ]; then +if [ -d $TEST_KUGETSU_DIR/worktrees/orphaned-worktree ]; then fail "prune --force should remove orphaned worktree" else pass "prune --force removes orphaned worktree" @@ -332,10 +334,10 @@ echo "" echo "--- Test: destroy removes worktree ---" cleanup setup_mock_forked -# remove_worktree_for_issue derives path from issue ref: ~/.kugetsu/worktrees/github.com-shoko-kugetsu-14 -mkdir -p ~/.kugetsu/worktrees/github.com-shoko-kugetsu-14 +# remove_worktree_for_issue derives path from issue ref: $TEST_KUGETSU_DIR/worktrees/github.com-shoko-kugetsu-14 +mkdir -p $TEST_KUGETSU_DIR/worktrees/github.com-shoko-kugetsu-14 OUTPUT=$($KUGETSU destroy github.com/shoko/kugetsu#14 -y 2>&1 || true) -if [ -d ~/.kugetsu/worktrees/github.com-shoko-kugetsu-14 ]; then +if [ -d $TEST_KUGETSU_DIR/worktrees/github.com-shoko-kugetsu-14 ]; then fail "destroy should remove worktree" else pass "destroy removes worktree" @@ -345,7 +347,7 @@ echo "" # Test 20: session file properly formatted for v2.2 echo "--- Test: session file format v2.2 ---" setup_mock_forked -SESSION_CONTENT=$(cat ~/.kugetsu/sessions/$TEST_FORKED_SESSION_FILE) +SESSION_CONTENT=$(cat $TEST_KUGETSU_DIR/sessions/$TEST_FORKED_SESSION_FILE) if echo "$SESSION_CONTENT" | grep -q '"type": "forked"' && \ echo "$SESSION_CONTENT" | grep -q '"worktree_path"'; then pass "session file has v2.2 format" @@ -367,8 +369,8 @@ echo "" # Test 22: status when base missing echo "--- Test: status (base missing) ---" -mkdir -p ~/.kugetsu/sessions -cat > ~/.kugetsu/index.json << EOF +mkdir -p $TEST_KUGETSU_DIR/sessions +cat > $TEST_KUGETSU_DIR/index.json << EOF { "base": null, "pm_agent": "$TEST_PM_AGENT_SESSION_ID", @@ -385,7 +387,7 @@ echo "" # Test 23: status when pm-agent missing echo "--- Test: status (pm-agent missing) ---" -cat > ~/.kugetsu/index.json << EOF +cat > $TEST_KUGETSU_DIR/index.json << EOF { "base": "$TEST_BASE_SESSION_ID", "pm_agent": null, @@ -402,7 +404,7 @@ echo "" # Test 24: status when pm-agent is "None" (Python None output) echo "--- Test: status (pm-agent is Python None) ---" -cat > ~/.kugetsu/index.json << EOF +cat > $TEST_KUGETSU_DIR/index.json << EOF { "base": "$TEST_BASE_SESSION_ID", "pm_agent": "None", @@ -445,8 +447,8 @@ echo "" # Test 27: delegate when pm-agent missing echo "--- Test: delegate (pm-agent missing) ---" cleanup -mkdir -p ~/.kugetsu/sessions ~/.kugetsu/worktrees -cat > ~/.kugetsu/index.json << EOF +mkdir -p $TEST_KUGETSU_DIR/sessions $TEST_KUGETSU_DIR/worktrees +cat > $TEST_KUGETSU_DIR/index.json << EOF { "base": "$TEST_BASE_SESSION_ID", "pm_agent": null, @@ -508,7 +510,7 @@ echo "" # Test 32: delegate is fire-and-forget (returns immediately) echo "--- Test: delegate is fire-and-forget ---" setup_mock_base -mkdir -p ~/.kugetsu/logs +mkdir -p $TEST_KUGETSU_DIR/logs START=$(date +%s) OUTPUT=$($KUGETSU delegate "test fire-and-forget" 2>&1 || true) END=$(date +%s) @@ -527,10 +529,10 @@ echo "" # Test 33: delegate creates log file echo "--- Test: delegate creates log file ---" setup_mock_base -LOG_COUNT_BEFORE=$(ls ~/.kugetsu/logs/*.log 2>/dev/null | wc -l) +LOG_COUNT_BEFORE=$(ls $TEST_KUGETSU_DIR/logs/*.log 2>/dev/null | wc -l) $KUGETSU delegate "test log file" 2>&1 || true sleep 1 -LOG_COUNT_AFTER=$(ls ~/.kugetsu/logs/*.log 2>/dev/null | wc -l) +LOG_COUNT_AFTER=$(ls $TEST_KUGETSU_DIR/logs/*.log 2>/dev/null | wc -l) if [ $LOG_COUNT_AFTER -gt $LOG_COUNT_BEFORE ]; then pass "delegate creates log file" else @@ -558,10 +560,10 @@ echo "" # Test E2: env set creates file echo "--- Test: env set creates env file ---" -mkdir -p ~/.kugetsu/env -rm -f ~/.kugetsu/env/pm-agent.env +mkdir -p $TEST_KUGETSU_DIR/env +rm -f $TEST_KUGETSU_DIR/env/pm-agent.env $KUGETSU env set TEST_VAR "test_value" pm-agent 2>&1 || true -if [ -f ~/.kugetsu/env/pm-agent.env ]; then +if [ -f $TEST_KUGETSU_DIR/env/pm-agent.env ]; then pass "env set creates pm-agent.env file" else fail "env set did not create pm-agent.env" @@ -570,7 +572,7 @@ echo "" # Test E3: env show masks sensitive values echo "--- Test: env show masks sensitive values ---" -cat > ~/.kugetsu/env/pm-agent.env << 'ENVEOF' +cat > $TEST_KUGETSU_DIR/env/pm-agent.env << 'ENVEOF' export GITEA_TOKEN="secret_token_123" export MY_VAR="visible_value" ENVEOF @@ -584,14 +586,14 @@ echo "" # Test E4: Variables exported to child processes via set -a echo "--- Test: set -a exports variables to children ---" -mkdir -p ~/.kugetsu/env -cat > ~/.kugetsu/env/test.env << 'ENVEOF' +mkdir -p $TEST_KUGETSU_DIR/env +cat > $TEST_KUGETSU_DIR/env/test.env << 'ENVEOF' export EXPORT_TEST="exported_value" SIMPLE_TEST="not_exported" ENVEOF # Simulate what cmd_delegate does -ENV_FILE="~/.kugetsu/env/test.env" +ENV_FILE="$TEST_KUGETSU_DIR/env/test.env" env_sh="set -a; source '$ENV_FILE'; set +a; " result=$(bash -c "${env_sh}bash -c 'echo \$EXPORT_TEST'") @@ -604,11 +606,11 @@ echo "" # Test E5: pm-agent.env takes precedence echo "--- Test: pm-agent.env takes precedence over default ---" -mkdir -p ~/.kugetsu/env -cat > ~/.kugetsu/env/default.env << 'ENVEOF' +mkdir -p $TEST_KUGETSU_DIR/env +cat > $TEST_KUGETSU_DIR/env/default.env << 'ENVEOF' export GITEA_TOKEN="default_token" ENVEOF -cat > ~/.kugetsu/env/pm-agent.env << 'ENVEOF' +cat > $TEST_KUGETSU_DIR/env/pm-agent.env << 'ENVEOF' export GITEA_TOKEN="pm_agent_token" ENVEOF @@ -644,7 +646,7 @@ fi echo "" # Cleanup env files -rm -rf ~/.kugetsu/env 2>/dev/null || true +rm -rf $TEST_KUGETSU_DIR/env 2>/dev/null || true # Test E7: fix_session_permissions function exists echo "--- Test: fix_session_permissions function exists ---" @@ -736,7 +738,7 @@ PASS=0 FAIL=0 test_cleanup() { - rm -rf ~/.kugetsu/sessions/* ~/.kugetsu/worktrees/* ~/.kugetsu/index.json ~/.kugetsu/logs/* ~/.kugetsu/.agent_count ~/.kugetsu/.agent_lock 2>/dev/null || true + rm -rf $TEST_KUGETSU_DIR/sessions/* $TEST_KUGETSU_DIR/worktrees/* $TEST_KUGETSU_DIR/index.json $TEST_KUGETSU_DIR/logs/* $TEST_KUGETSU_DIR/.agent_count $TEST_KUGETSU_DIR/.agent_lock 2>/dev/null || true } pass() { @@ -750,25 +752,25 @@ fail() { } setup_mock_sessions() { - mkdir -p ~/.kugetsu/sessions ~/.kugetsu/worktrees ~/.kugetsu/logs - cat > ~/.kugetsu/index.json << INDEX + mkdir -p $TEST_KUGETSU_DIR/sessions $TEST_KUGETSU_DIR/worktrees $TEST_KUGETSU_DIR/logs + cat > $TEST_KUGETSU_DIR/index.json << INDEX { "base": "ses_test_base_123", "pm_agent": "ses_test_pm_456", "issues": {} } INDEX - echo '{"type": "base", "opencode_session_id": "ses_test_base_123", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"}' > ~/.kugetsu/sessions/base.json - echo '{"type": "pm_agent", "opencode_session_id": "ses_test_pm_456", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"}' > ~/.kugetsu/sessions/pm-agent.json + echo '{"type": "base", "opencode_session_id": "ses_test_base_123", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"}' > $TEST_KUGETSU_DIR/sessions/base.json + echo '{"type": "pm_agent", "opencode_session_id": "ses_test_pm_456", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"}' > $TEST_KUGETSU_DIR/sessions/pm-agent.json } # Test C1: Agent count file is initialized to 0 echo "--- Test: agent count file initialized ---" test_cleanup -mkdir -p ~/.kugetsu/sessions ~/.kugetsu/worktrees +mkdir -p $TEST_KUGETSU_DIR/sessions $TEST_KUGETSU_DIR/worktrees $KUGETSU list > /dev/null 2>&1 || true -if [ -f ~/.kugetsu/.agent_count ]; then - COUNT=$(cat ~/.kugetsu/.agent_count) +if [ -f $TEST_KUGETSU_DIR/.agent_count ]; then + COUNT=$(cat $TEST_KUGETSU_DIR/.agent_count) if [ "$COUNT" = "0" ]; then pass "agent count file initialized to 0" else @@ -795,10 +797,10 @@ test_cleanup setup_mock_sessions # Initialize count to 0 -echo 0 > ~/.kugetsu/.agent_count +echo 0 > $TEST_KUGETSU_DIR/.agent_count # Verify initial state -INITIAL=$(cat ~/.kugetsu/.agent_count) +INITIAL=$(cat $TEST_KUGETSU_DIR/.agent_count) if [ "$INITIAL" = "0" ]; then pass "agent count starts at 0" else @@ -809,7 +811,7 @@ fi $KUGETSU list > /dev/null 2>&1 # Verify count is still 0 (no slot leak) -AFTER=$(cat ~/.kugetsu/.agent_count) +AFTER=$(cat $TEST_KUGETSU_DIR/.agent_count) if [ "$AFTER" = "0" ]; then pass "agent count stays 0 after list (no leak)" else From ab0c4e14487d32c211e7de628d516920c7af1dcb Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Sun, 5 Apr 2026 12:45:59 +0000 Subject: [PATCH 2/5] fix: detect task completion by checking if session ended and has commits --- .../kugetsu/scripts/kugetsu-queue-daemon.sh | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/skills/kugetsu/scripts/kugetsu-queue-daemon.sh b/skills/kugetsu/scripts/kugetsu-queue-daemon.sh index 7cae460..0a32c3a 100755 --- a/skills/kugetsu/scripts/kugetsu-queue-daemon.sh +++ b/skills/kugetsu/scripts/kugetsu-queue-daemon.sh @@ -8,8 +8,52 @@ source "$SCRIPT_DIR/kugetsu-index.sh" source "$SCRIPT_DIR/kugetsu-worktree.sh" source "$SCRIPT_DIR/kugetsu-log.sh" +# Check if a notified task has completed (session ended or has new commits) +check_task_completion() { + local item="$1" + local queue_id=$(basename "$item" .json) + local state=$(python3 -c "import json; print(json.load(open('$item')).get('state', ''))" 2>/dev/null) + + [ "$state" = "notified" ] || return 0 + + local session_id=$(python3 -c "import json; print(json.load(open('$item')).get('session_id', ''))" 2>/dev/null) + local issue_ref=$(python3 -c "import json; print(json.load(open('$item')).get('issue_ref', ''))" 2>/dev/null) + + # If no session tracked, skip + [ -n "$session_id" ] || return 0 + + # Check if session still exists in opencode + if ! opencode session list 2>/dev/null | grep -q "$session_id"; then + # Session ended — check if work was done + local worktree_path=$(issue_ref_to_worktree_path "$issue_ref" "$HOME/.kugetsu-worktrees") + local has_commits=false + + if [ -d "$worktree_path" ] && [ -d "$worktree_path/.git" ]; then + # Check if worktree has new commits beyond origin/main + if [ -n "$(git -C "$worktree_path" log --oneline origin/main..HEAD 2>/dev/null)" ]; then + has_commits=true + fi + fi + + if [ "$has_commits" = true ]; then + update_queue_item_state "$queue_id" "completed" + echo "Task $queue_id ($issue_ref) completed — new commits found" + else + update_queue_item_state "$queue_id" "error" + echo "Task $queue_id ($issue_ref) marked error — no commits found after session ended" + fi + fi +} + while true; do + # Check completion of notified tasks if [ -d "$QUEUE_ITEMS_DIR" ]; then + for item in "$QUEUE_ITEMS_DIR"/*.json; do + [ -f "$item" ] || continue + check_task_completion "$item" + done + + # Process pending tasks for item in "$QUEUE_ITEMS_DIR"/*.json; do [ -f "$item" ] || continue state=$(python3 -c "import json; print(json.load(open('$item')).get('state', ''))" 2>/dev/null) @@ -21,7 +65,7 @@ while true; do pm_session=$(get_pm_agent_session_id) if [ -n "$pm_session" ] && [ "$pm_session" != "null" ]; then log_file="$LOGS_DIR/delegate-$(date +%s).log" - GITEA_TOKEN="${GITEA_TOKEN:-}" nohup sh -c "opencode run '$message' --continue --session '$pm_session' >> '$log_file' 2>&1" > /dev/null 2>&1 & + GITEA_TOKEN="***" nohup sh -c "opencode run '$message' --continue --session '$pm_session' >> '$log_file' 2>&1" > /dev/null 2>&1 & pid=$! update_queue_item_state "$queue_id" "notified" "$pm_session" "$pid" fi From 1d4f190d97c35be64e86b4f1174c1733bbfa6fd0 Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Sun, 5 Apr 2026 13:09:08 +0000 Subject: [PATCH 3/5] fix(kugetsu): pass GITEA_TOKEN via env to subprocess instead of hardcoded value --- skills/kugetsu/scripts/kugetsu-queue-daemon.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/kugetsu/scripts/kugetsu-queue-daemon.sh b/skills/kugetsu/scripts/kugetsu-queue-daemon.sh index 0a32c3a..d1d1e9f 100755 --- a/skills/kugetsu/scripts/kugetsu-queue-daemon.sh +++ b/skills/kugetsu/scripts/kugetsu-queue-daemon.sh @@ -65,7 +65,7 @@ while true; do pm_session=$(get_pm_agent_session_id) if [ -n "$pm_session" ] && [ "$pm_session" != "null" ]; then log_file="$LOGS_DIR/delegate-$(date +%s).log" - GITEA_TOKEN="***" nohup sh -c "opencode run '$message' --continue --session '$pm_session' >> '$log_file' 2>&1" > /dev/null 2>&1 & + nohup env GITEA_TOKEN="$GITEA_TOKEN" sh -c "opencode run '$message' --continue --session '$pm_session' >> '$log_file' 2>&1" > /dev/null 2>&1 & pid=$! update_queue_item_state "$queue_id" "notified" "$pm_session" "$pid" fi From 7fa669b4c38efdba34183ccc21395cc17c717527 Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Sun, 5 Apr 2026 20:57:51 +0000 Subject: [PATCH 4/5] fix(kugetsu): queue daemon runs PM agent in correct worktree with proper token - Load GITEA_TOKEN from ~/.kugetsu/env/default.env at daemon startup - Use --fork --session --dir instead of --continue to run in correct directory - Create worktree if it doesn't exist for the issue - Track forked session ID (not parent pm_session) for completion detection - Forked session ends when task completes, parent pm_session continues Fixes #156 --- .../kugetsu/scripts/kugetsu-queue-daemon.sh | 55 +++++++++++++++++-- 1 file changed, 49 insertions(+), 6 deletions(-) mode change 100755 => 100644 skills/kugetsu/scripts/kugetsu-queue-daemon.sh diff --git a/skills/kugetsu/scripts/kugetsu-queue-daemon.sh b/skills/kugetsu/scripts/kugetsu-queue-daemon.sh old mode 100755 new mode 100644 index d1d1e9f..77fd413 --- a/skills/kugetsu/scripts/kugetsu-queue-daemon.sh +++ b/skills/kugetsu/scripts/kugetsu-queue-daemon.sh @@ -8,7 +8,12 @@ source "$SCRIPT_DIR/kugetsu-index.sh" source "$SCRIPT_DIR/kugetsu-worktree.sh" source "$SCRIPT_DIR/kugetsu-log.sh" -# Check if a notified task has completed (session ended or has new commits) +# Load GITEA_TOKEN from default.env +if [ -f "$HOME/.kugetsu/env/default.env" ]; then + source "$HOME/.kugetsu/env/default.env" +fi + +# Check if a notified task has completed (forked session ended or has new commits) check_task_completion() { local item="$1" local queue_id=$(basename "$item" .json) @@ -16,15 +21,16 @@ check_task_completion() { [ "$state" = "notified" ] || return 0 - local session_id=$(python3 -c "import json; print(json.load(open('$item')).get('session_id', ''))" 2>/dev/null) + # Use opencode_session_id (the forked session, not the parent pm_session) + 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) # If no session tracked, skip [ -n "$session_id" ] || return 0 - # Check if session still exists in opencode + # Check if forked session still exists in opencode if ! opencode session list 2>/dev/null | grep -q "$session_id"; then - # Session ended — check if work was done + # Forked session ended — check if work was done local worktree_path=$(issue_ref_to_worktree_path "$issue_ref" "$HOME/.kugetsu-worktrees") local has_commits=false @@ -64,10 +70,47 @@ while true; do pm_session=$(get_pm_agent_session_id) if [ -n "$pm_session" ] && [ "$pm_session" != "null" ]; then + # Compute worktree path for this issue + worktree_path=$(issue_ref_to_worktree_path "$issue_ref" "$HOME/.kugetsu-worktrees") + + # Ensure worktree exists, create if needed + if [ ! -d "$worktree_path" ]; then + echo "Creating worktree for $issue_ref at $worktree_path..." + create_worktree "$issue_ref" "$HOME/.kugetsu-worktrees" + fi + + # Capture sessions before fork to identify the new forked session + sessions_before=$(opencode session list 2>/dev/null || echo "") + log_file="$LOGS_DIR/delegate-$(date +%s).log" - nohup env GITEA_TOKEN="$GITEA_TOKEN" sh -c "opencode run '$message' --continue --session '$pm_session' >> '$log_file' 2>&1" > /dev/null 2>&1 & + # Use --fork --dir to run in the correct worktree directory + # The forked session will end when the task completes, while parent pm_session continues + (cd "$worktree_path" && nohup bash -c "export GITEA_TOKEN=$GITEA_TOKEN; opencode run '$message' --fork --session '$pm_session' --dir '$worktree_path'" >> "$log_file" 2>&1) & pid=$! - update_queue_item_state "$queue_id" "notified" "$pm_session" "$pid" + + # Wait for fork to initialize and capture new session + sleep 3 + + # Find the forked session ID by comparing session lists + sessions_after=$(opencode session list 2>/dev/null || echo "") + forked_session="" + while IFS= read -r line; do + session=$(echo "$line" | awk '{print $1}' | tr -d '[:space:]') + if [ -n "$session" ] && ! echo "$sessions_before" | grep -q "$session"; then + forked_session="$session" + break + fi + done <<< "$sessions_after" + + # Store the forked session ID (not the parent pm_session) for completion detection + if [ -n "$forked_session" ]; then + update_queue_item_state "$queue_id" "notified" "$forked_session" "$pid" + echo "Task $queue_id notified with forked session $forked_session for $issue_ref" + else + # Fallback: still update with empty session so task isn't stuck + update_queue_item_state "$queue_id" "notified" "" "$pid" + echo "Task $queue_id notified but could not capture forked session for $issue_ref" + fi fi fi done From cbfc8a064614375598770820a0def4c678c65f78 Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Sun, 5 Apr 2026 21:29:34 +0000 Subject: [PATCH 5/5] refactor(kugetsu): daemon uses cmd_start/cmd_continue instead of direct opencode calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move issue_ref_to_filename and filename_to_issue_ref to kugetsu-index.sh (where they logically belong, instead of in main kugetsu script) - Refactor queue daemon to use cmd_start/cmd_continue for session management - Daemon now checks if worktree/session exists → cmd_continue, else → cmd_start - Removes ~40 lines of direct opencode session forking logic from daemon - cmd_start/cmd_continue handle worktree creation, session forking, and tracking This simplifies the daemon significantly and centralizes session management in kugetsu-session.sh where it belongs. --- skills/kugetsu/scripts/kugetsu | 11 ---- skills/kugetsu/scripts/kugetsu-index.sh | 15 ++++- .../kugetsu/scripts/kugetsu-queue-daemon.sh | 56 +++++-------------- 3 files changed, 29 insertions(+), 53 deletions(-) diff --git a/skills/kugetsu/scripts/kugetsu b/skills/kugetsu/scripts/kugetsu index 43524c2..ab2e6bd 100755 --- a/skills/kugetsu/scripts/kugetsu +++ b/skills/kugetsu/scripts/kugetsu @@ -99,17 +99,6 @@ ensure_worktree_dir() { mkdir -p "$WORKTREES_DIR" } -issue_ref_to_filename() { - local issue_ref="$1" - echo "$issue_ref" | sed 's/[\/:]/-/g' | sed 's/#/-/' -} - -filename_to_issue_ref() { - local filename="$1" - local name="${filename%.json}" - echo "$name" | sed 's/-\([0-9]*\)$/#\1/' | sed 's/-/\//g' -} - issue_ref_to_context_file() { local issue_ref="$1" local context_filename=$(issue_ref_to_filename "$issue_ref") diff --git a/skills/kugetsu/scripts/kugetsu-index.sh b/skills/kugetsu/scripts/kugetsu-index.sh index 0be7814..eb85d26 100755 --- a/skills/kugetsu/scripts/kugetsu-index.sh +++ b/skills/kugetsu/scripts/kugetsu-index.sh @@ -130,6 +130,19 @@ session['pr_url'] = pr_url with open(session_path, 'w') as f: json.dump(session, f, indent=2) -print(f"Updated PR URL for $issue_ref: $pr_url") + print(f"Updated PR URL for $issue_ref: $pr_url") PYEOF } + +# Convert issue ref to session filename +issue_ref_to_filename() { + local issue_ref="$1" + echo "$issue_ref" | sed 's/[\/:]/-/g' | sed 's/#/-/' +} + +# Convert session filename back to issue ref +filename_to_issue_ref() { + local filename="$1" + local name="${filename%.json}" + echo "$name" | sed 's-\([0-9]*\)$-#\1-' | sed 's/-/\//g' +} diff --git a/skills/kugetsu/scripts/kugetsu-queue-daemon.sh b/skills/kugetsu/scripts/kugetsu-queue-daemon.sh index 77fd413..d1471f3 100644 --- a/skills/kugetsu/scripts/kugetsu-queue-daemon.sh +++ b/skills/kugetsu/scripts/kugetsu-queue-daemon.sh @@ -68,49 +68,23 @@ while true; do issue_ref=$(python3 -c "import json; print(json.load(open('$item')).get('issue_ref', ''))" 2>/dev/null) message=$(python3 -c "import json; print(json.load(open('$item')).get('message', ''))" 2>/dev/null) - pm_session=$(get_pm_agent_session_id) - if [ -n "$pm_session" ] && [ "$pm_session" != "null" ]; then - # Compute worktree path for this issue - worktree_path=$(issue_ref_to_worktree_path "$issue_ref" "$HOME/.kugetsu-worktrees") - - # Ensure worktree exists, create if needed - if [ ! -d "$worktree_path" ]; then - echo "Creating worktree for $issue_ref at $worktree_path..." - create_worktree "$issue_ref" "$HOME/.kugetsu-worktrees" - fi - - # Capture sessions before fork to identify the new forked session - sessions_before=$(opencode session list 2>/dev/null || echo "") - + # Source session management and use cmd_start/cmd_continue + source "$SCRIPT_DIR/kugetsu-session.sh" + + if worktree_exists "$issue_ref" "$HOME/.kugetsu-worktrees" || [ -f "$SESSIONS_DIR/$(issue_ref_to_filename "$issue_ref").json" ]; then + # Continue existing session log_file="$LOGS_DIR/delegate-$(date +%s).log" - # Use --fork --dir to run in the correct worktree directory - # The forked session will end when the task completes, while parent pm_session continues - (cd "$worktree_path" && nohup bash -c "export GITEA_TOKEN=$GITEA_TOKEN; opencode run '$message' --fork --session '$pm_session' --dir '$worktree_path'" >> "$log_file" 2>&1) & + cmd_continue "$issue_ref" "$message" >> "$log_file" 2>&1 & pid=$! - - # Wait for fork to initialize and capture new session - sleep 3 - - # Find the forked session ID by comparing session lists - sessions_after=$(opencode session list 2>/dev/null || echo "") - forked_session="" - while IFS= read -r line; do - session=$(echo "$line" | awk '{print $1}' | tr -d '[:space:]') - if [ -n "$session" ] && ! echo "$sessions_before" | grep -q "$session"; then - forked_session="$session" - break - fi - done <<< "$sessions_after" - - # Store the forked session ID (not the parent pm_session) for completion detection - if [ -n "$forked_session" ]; then - update_queue_item_state "$queue_id" "notified" "$forked_session" "$pid" - echo "Task $queue_id notified with forked session $forked_session for $issue_ref" - else - # Fallback: still update with empty session so task isn't stuck - update_queue_item_state "$queue_id" "notified" "" "$pid" - echo "Task $queue_id notified but could not capture forked session for $issue_ref" - fi + update_queue_item_state "$queue_id" "notified" "" "$pid" + echo "Task $queue_id continued for $issue_ref" + else + # Start new session + log_file="$LOGS_DIR/delegate-$(date +%s).log" + cmd_start "$issue_ref" "$message" >> "$log_file" 2>&1 & + pid=$! + update_queue_item_state "$queue_id" "notified" "" "$pid" + echo "Task $queue_id started for $issue_ref" fi fi done