From 860bf9295f566d993eba18301b7ebf4fd8e4c658 Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Mon, 6 Apr 2026 09:42:37 +0000 Subject: [PATCH 1/4] fix: use ${GITEA_TOKEN:-} to handle unset token and initialize env during init With set -u, expanding $GITEA_TOKEN fails if not set. Changed to ${GITEA_TOKEN:-} to provide empty default. Also initializes $ENV_DIR/default.env during kugetsu init so users are aware of the env file structure. --- skills/kugetsu/scripts/kugetsu-session.sh | 27 +++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/skills/kugetsu/scripts/kugetsu-session.sh b/skills/kugetsu/scripts/kugetsu-session.sh index 3755c0a..16b9b96 100755 --- a/skills/kugetsu/scripts/kugetsu-session.sh +++ b/skills/kugetsu/scripts/kugetsu-session.sh @@ -58,6 +58,25 @@ EOF echo "Created config file: $KUGETSU_DIR/config" fi + mkdir -p "$ENV_DIR" + if [ ! -f "$ENV_DIR/default.env" ]; then + cat > "$ENV_DIR/default.env" << 'EOF' +# Environment variables for agents +# Copy this file to .env (e.g., pm-agent.env, dev.env) +# and set your tokens and configuration + +# Required: Gitea token for API access +# GITEA_TOKEN=your_gitea_token_here + +# Optional: GitHub token (if using GitHub) +# GITHUB_TOKEN=your_github_token_here + +# Optional: GitLab token (if using GitLab) +# GITLAB_TOKEN=your_gitlab_token_here +EOF + echo "Created env template: $ENV_DIR/default.env" + fi + local existing_base=$(get_base_session_id) local existing_pm=$(get_pm_agent_session_id) @@ -195,7 +214,7 @@ cmd_delegate() { mkdir -p "$LOGS_DIR" local log_file="$LOGS_DIR/delegate-$(date +%s).log" load_agent_env "pm-agent" - nohup sh -c "GITEA_TOKEN='$GITEA_TOKEN' opencode run '$message' --continue --session '$pm_session'" >> "$log_file" 2>&1 & + nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '$message' --continue --session '$pm_session'" >> "$log_file" 2>&1 & echo "Delegated to PM agent (logged to $(basename "$log_file"))" } @@ -314,7 +333,7 @@ cmd_start() { load_agent_env "dev" cd "$worktree_path" - nohup sh -c "GITEA_TOKEN='$GITEA_TOKEN' opencode run '$dev_message' --continue --session '$new_session_id'" >> "$LOGS_DIR/dev-$new_session_id.log" 2>&1 & + nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '$dev_message' --continue --session '$new_session_id'" >> "$LOGS_DIR/dev-$new_session_id.log" 2>&1 & echo "Session started for '$issue_ref': $new_session_id" echo "Worktree: $worktree_path" @@ -369,9 +388,9 @@ cmd_continue() { if [ -n "$worktree_path" ] && [ -d "$worktree_path" ]; then cd "$worktree_path" - nohup sh -c "GITEA_TOKEN='$GITEA_TOKEN' opencode run '$message' --continue --session '$opencode_session_id'" >> "$LOGS_DIR/dev-$opencode_session_id.log" 2>&1 & + nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '$message' --continue --session '$opencode_session_id'" >> "$LOGS_DIR/dev-$opencode_session_id.log" 2>&1 & else - nohup sh -c "GITEA_TOKEN='$GITEA_TOKEN' opencode run '$message' --continue --session '$opencode_session_id'" >> "$LOGS_DIR/dev-$opencode_session_id.log" 2>&1 & + nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '$message' --continue --session '$opencode_session_id'" >> "$LOGS_DIR/dev-$opencode_session_id.log" 2>&1 & fi } From 08b633400d2f36730150728a9e93f849df1c369a Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Tue, 7 Apr 2026 00:13:06 +0000 Subject: [PATCH 2/4] refactor: add create_session() and use --session instead of --continue - Add create_session() function that forks from base session using JSON session detection - cmd_delegate: fork new session from base instead of using pm_agent - cmd_start: use create_session() instead of broken before/after detection - cmd_continue: use --session instead of --continue (no need to continue existing session) - Remove pm_agent check from cmd_start (no longer needed) --- skills/kugetsu/scripts/kugetsu-session.sh | 72 ++++++++++++++--------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/skills/kugetsu/scripts/kugetsu-session.sh b/skills/kugetsu/scripts/kugetsu-session.sh index 16b9b96..644ec9f 100755 --- a/skills/kugetsu/scripts/kugetsu-session.sh +++ b/skills/kugetsu/scripts/kugetsu-session.sh @@ -204,18 +204,52 @@ cmd_delegate() { return fi - # No issue ref detected — delegate directly to PM agent (legacy path) - local pm_session=$(get_pm_agent_session_id) - if [ -z "$pm_session" ] || [ "$pm_session" = "null" ] || [ "$pm_session" = "None" ]; then - echo "Error: PM agent session not found. Run 'kugetsu init' first." >&2 + # No issue ref detected — fork a new session from base session + local base_session=$(get_base_session_id) + if [ -z "$base_session" ] || [ "$base_session" = "null" ]; then + echo "Error: Base session not found. Run 'kugetsu init' first." >&2 exit 1 fi mkdir -p "$LOGS_DIR" local log_file="$LOGS_DIR/delegate-$(date +%s).log" load_agent_env "pm-agent" - nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '$message' --continue --session '$pm_session'" >> "$log_file" 2>&1 & - echo "Delegated to PM agent (logged to $(basename "$log_file"))" + + local new_session=$(create_session "$base_session") + if [ -z "$new_session" ]; then + echo "Error: Failed to create session" >&2 + exit 1 + fi + + nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '$message' --session '$new_session'" >> "$log_file" 2>&1 & + echo "Delegated to new session (logged to $(basename "$log_file"))" +} + +create_session() { + local base_session="${1:-$base_session_id}" + + if [ -z "$base_session" ] || [ "$base_session" = "null" ]; then + echo "Error: base session not found. Run 'kugetsu init' first." >&2 + return 1 + fi + + local before_json=$(opencode session list --format=json 2>/dev/null) + local before_ids=$(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 run --fork --session "$base_session" "new session" 2>/dev/null + + local after_json=$(opencode session list --format=json 2>/dev/null) + local after_ids=$(echo "$after_json" | python3 -c "import sys,json; sessions=json.load(sys.stdin); print(' '.join(s['id'] for s in sessions))" 2>/dev/null || echo "") + + local new_session_id="" + for sess in $after_ids; do + if [[ ! " $before_ids " =~ " $sess " ]] && [[ "$sess" != "$base_session" ]]; then + new_session_id="$sess" + break + fi + done + + echo "$new_session_id" } build_dev_agent_message() { @@ -275,12 +309,6 @@ cmd_start() { exit 1 fi - local pm_agent_session_id=$(get_pm_agent_session_id) - if [ -z "$pm_agent_session_id" ] || [ "$pm_agent_session_id" = "null" ]; then - echo "Error: PM agent session not found. Run 'kugetsu init' first." >&2 - exit 1 - fi - if worktree_exists "$issue_ref"; then echo "Issue '$issue_ref' already has a worktree. Use 'kugetsu continue' instead." exit 1 @@ -301,22 +329,12 @@ cmd_start() { exit 1 fi - local before_sessions=$(opencode session list 2>/dev/null | grep -oP '^ses_\w+' | sort) - local before_set="|$before_sessions|" - create_worktree "$issue_ref" "$WORKTREES_DIR" - local after_sessions=$(opencode session list 2>/dev/null | grep -oP '^ses_\w+' | sort) - local new_session_id="" - while IFS= read -r sess; do - if [[ ! "$before_set" =~ \|${sess}\| ]] && [[ "$sess" != "$base_session_id" ]] && [[ "$sess" != "$pm_agent_session_id" ]]; then - new_session_id="$sess" - break - fi - done <<< "$after_sessions" + local new_session_id=$(create_session "$base_session_id") if [ -z "$new_session_id" ]; then - echo "Error: Could not find newly created session" >&2 + echo "Error: Could not create session" >&2 remove_worktree_for_issue "$issue_ref" exit 1 fi @@ -333,7 +351,7 @@ cmd_start() { load_agent_env "dev" cd "$worktree_path" - nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '$dev_message' --continue --session '$new_session_id'" >> "$LOGS_DIR/dev-$new_session_id.log" 2>&1 & + nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '$dev_message' --session '$new_session_id'" >> "$LOGS_DIR/dev-$new_session_id.log" 2>&1 & echo "Session started for '$issue_ref': $new_session_id" echo "Worktree: $worktree_path" @@ -388,9 +406,9 @@ cmd_continue() { if [ -n "$worktree_path" ] && [ -d "$worktree_path" ]; then cd "$worktree_path" - nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '$message' --continue --session '$opencode_session_id'" >> "$LOGS_DIR/dev-$opencode_session_id.log" 2>&1 & + nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '$message' --session '$opencode_session_id'" >> "$LOGS_DIR/dev-$opencode_session_id.log" 2>&1 & else - nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '$message' --continue --session '$opencode_session_id'" >> "$LOGS_DIR/dev-$opencode_session_id.log" 2>&1 & + nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '$message' --session '$opencode_session_id'" >> "$LOGS_DIR/dev-$opencode_session_id.log" 2>&1 & fi } From b595411a07a9bb02786a429511b4d8c58c274959 Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Tue, 7 Apr 2026 00:24:03 +0000 Subject: [PATCH 3/4] test: add test suite for create_session function Tests run sequentially to avoid memory exhaustion with too many opencode sessions: - JSON session list parsing - Session ID format validation - create_session returns valid session ID - create_session creates NEW session (different from base) - create_session creates different sessions on multiple calls - create_session accepts optional base session parameter - Created session visible in opencode session list --- skills/kugetsu/tests/test-create-session.sh | 181 ++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 skills/kugetsu/tests/test-create-session.sh diff --git a/skills/kugetsu/tests/test-create-session.sh b/skills/kugetsu/tests/test-create-session.sh new file mode 100644 index 0000000..cb18aa4 --- /dev/null +++ b/skills/kugetsu/tests/test-create-session.sh @@ -0,0 +1,181 @@ +#!/bin/bash +# Tests for create_session function +# +# Run with: bash skills/kugetsu/tests/test-create-session.sh +# +# NOTE: These tests MUST be run sequentially (not in parallel) +# to avoid exhausting memory with too many opencode sessions. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/../scripts/kugetsu-config.sh" +source "$SCRIPT_DIR/../scripts/kugetsu-index.sh" +source "$SCRIPT_DIR/../scripts/kugetsu-session.sh" + +PASS=0 +FAIL=0 +RUN=0 + +pass() { + echo "PASS: $1" + PASS=$((PASS + 1)) +} + +fail() { + echo "FAIL: $1" + echo " Expected: $2" + echo " Got: $3" + FAIL=$((FAIL + 1)) +} + +run_test() { + local name="$1" + local test_func="$2" + RUN=$((RUN + 1)) + echo "" + echo "=== Test $RUN: $name ===" + echo "--- $name ---" + $test_func +} + +echo "=== create_session Test Suite ===" +echo "NOTE: Running sequentially to avoid memory exhaustion" +echo "" + +# Test 1: create_session requires base session +test_create_session_requires_base() { + local base_id=$(get_base_session_id) + if [ -z "$base_id" ] || [ "$base_id" = "null" ]; then + skip "Base session not initialized - run 'kugetsu init' first" + return + fi + + local result=$(create_session "$base_id") + if [ -n "$result" ] && [[ "$result" =~ ^ses_ ]]; then + pass "create_session returns valid session ID" + else + fail "create_session returns valid session ID" "ses_xxx" "$result" + fi +} + +# Test 2: create_session creates a NEW session (different from base) +test_create_session_is_new() { + local base_id=$(get_base_session_id) + if [ -z "$base_id" ] || [ "$base_id" = "null" ]; then + skip "Base session not initialized - run 'kugetsu init' first" + return + fi + + local new_id=$(create_session "$base_id") + if [ "$new_id" != "$base_id" ]; then + pass "create_session returns NEW session (not same as base)" + else + fail "create_session returns NEW session" "different from base_id" "$new_id" + fi +} + +# Test 3: create_session can be called multiple times (creates different sessions) +test_create_session_multiple_calls() { + local base_id=$(get_base_session_id) + if [ -z "$base_id" ] || [ "$base_id" = "null" ]; then + skip "Base session not initialized - run 'kugetsu init' first" + return + fi + + local id1=$(create_session "$base_id") + sleep 1 + local id2=$(create_session "$base_id") + + if [ "$id1" != "$id2" ]; then + pass "create_session creates different sessions on each call" + else + fail "create_session creates different sessions" "$id1 != $id2" "both equal: $id1" + fi +} + +# Test 4: JSON session list parsing +test_session_json_parsing() { + local json='[{"id": "ses_abc123", "title": "test"}, {"id": "ses_def456", "title": "test2"}]' + local ids=$(echo "$json" | python3 -c "import sys,json; sessions=json.load(sys.stdin); print(' '.join(s['id'] for s in sessions))" 2>/dev/null) + + if [ "$ids" = "ses_abc123 ses_def456" ]; then + pass "JSON session list parsing extracts IDs correctly" + else + fail "JSON session list parsing" "ses_abc123 ses_def456" "$ids" + fi +} + +# Test 5: Session ID format validation +test_session_id_format() { + local json='[{"id": "ses_2b4814406ffe3AxcpbrP7FknDr", "title": "test"}]' + local ids=$(echo "$json" | python3 -c "import sys,json; sessions=json.load(sys.stdin); print(' '.join(s['id'] for s in sessions))" 2>/dev/null) + + if [[ "$ids" =~ ^ses_ ]]; then + pass "Session ID format is valid (starts with ses_)" + else + fail "Session ID format" "ses_xxx" "$ids" + fi +} + +# Test 6: create_session accepts optional base session parameter +test_create_session_with_param() { + local base_id=$(get_base_session_id) + if [ -z "$base_id" ] || [ "$base_id" = "null" ]; then + skip "Base session not initialized - run 'kugetsu init' first" + return + fi + + local result=$(create_session "$base_id") + if [ -n "$result" ] && [[ "$result" =~ ^ses_ ]]; then + pass "create_session accepts base session parameter" + else + fail "create_session accepts base session parameter" "ses_xxx" "$result" + fi +} + +# Test 7: Verify session appears in opencode session list after creation +test_session_visible_in_list() { + local base_id=$(get_base_session_id) + if [ -z "$base_id" ] || [ "$base_id" = "null" ]; then + skip "Base session not initialized - run 'kugetsu init' first" + return + fi + + local new_id=$(create_session "$base_id") + sleep 1 + + local all_sessions=$(opencode session list --format=json 2>/dev/null) + if echo "$all_sessions" | grep -q "$new_id"; then + pass "Created session appears in opencode session list" + else + fail "Created session appears in session list" "should contain $new_id" "$all_sessions" + fi +} + +skip() { + echo "SKIP: $1" +} + +# Run tests sequentially +run_test "create_session requires base session" test_create_session_requires_base +run_test "create_session creates NEW session" test_create_session_is_new +run_test "create_session creates different sessions on multiple calls" test_create_session_multiple_calls +run_test "JSON session list parsing" test_session_json_parsing +run_test "Session ID format validation" test_session_id_format +run_test "create_session accepts optional base session parameter" test_create_session_with_param +run_test "Created session visible in opencode session list" test_session_visible_in_list + +echo "" +echo "=== Test Results ===" +echo "Passed: $PASS" +echo "Failed: $FAIL" +echo "Total: $RUN" + +if [ $FAIL -eq 0 ]; then + echo "All tests passed!" + exit 0 +else + echo "Some tests failed!" + exit 1 +fi \ No newline at end of file From 998f7a4f441aac9878ea0d106f073b07ee27bf8d Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Tue, 7 Apr 2026 00:35:38 +0000 Subject: [PATCH 4/4] refactor: remove duplicate functions from kugetsu, use kugetsu-index.sh exclusively - Remove duplicate write_index, get_base_session_id, get_pm_agent_session_id, get_session_for_issue, set_base_in_index, set_pm_agent_in_index, add_issue_to_index, remove_issue_from_index from kugetsu - These functions are already defined in kugetsu-index.sh which is sourced earlier - The kugetsu versions were shadowing the kugetsu-index.sh ones unnecessarily - This removes code duplication and ensures consistent behavior --- skills/kugetsu/scripts/kugetsu | 91 ------------------------- skills/kugetsu/scripts/kugetsu-index.sh | 7 ++ 2 files changed, 7 insertions(+), 91 deletions(-) diff --git a/skills/kugetsu/scripts/kugetsu b/skills/kugetsu/scripts/kugetsu index 3e847d0..87fbccb 100755 --- a/skills/kugetsu/scripts/kugetsu +++ b/skills/kugetsu/scripts/kugetsu @@ -475,97 +475,6 @@ read_index() { fi } -write_index() { - local base="$1" - local pm_agent="$2" - local issues_json="$3" - local temp_file="$INDEX_FILE.tmp.$$" - printf '{"base": %s, "pm_agent": %s, "issues": %s}\n' "$base" "$pm_agent" "$issues_json" > "$temp_file" - mv "$temp_file" "$INDEX_FILE" -} - -get_base_session_id() { - local index=$(read_index) - echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('base') or '')" -} - -get_pm_agent_session_id() { - local index=$(read_index) - echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('pm_agent') or '')" -} - -get_session_for_issue() { - local issue_ref="$1" - local index=$(read_index) - echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d['issues'].get('$issue_ref') or '')" -} - -set_base_in_index() { - local base_session_id="$1" - local pm_agent=$(get_pm_agent_session_id) - local issues_json=$(read_index | python3 -c "import sys, json; d=json.load(sys.stdin); print(json.dumps(d['issues']))") - if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ]; then - write_index "\"$base_session_id\"" "null" "$issues_json" - else - write_index "\"$base_session_id\"" "\"$pm_agent\"" "$issues_json" - fi -} - -set_pm_agent_in_index() { - local pm_agent_session_id="$1" - local base=$(get_base_session_id) - local issues_json=$(read_index | python3 -c "import sys, json; d=json.load(sys.stdin); print(json.dumps(d['issues']))") - if [ -z "$base" ] || [ "$base" = "null" ]; then - write_index "null" "\"$pm_agent_session_id\"" "$issues_json" - else - write_index "\"$base\"" "\"$pm_agent_session_id\"" "$issues_json" - fi -} - -add_issue_to_index() { - local issue_ref="$1" - local session_file="$2" - local index=$(read_index) - local base=$(get_base_session_id) - local pm_agent=$(get_pm_agent_session_id) - local issues=$(echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); print(json.dumps(d['issues']))") - local new_issues=$(echo "$issues" | python3 -c "import sys, json; d=json.load(sys.stdin); d['$issue_ref']='$session_file'; print(json.dumps(d))") - if [ -z "$base" ] || [ "$base" = "null" ]; then - if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ]; then - write_index "null" "null" "$new_issues" - else - write_index "null" "\"$pm_agent\"" "$new_issues" - fi - else - if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ]; then - write_index "\"$base\"" "null" "$new_issues" - else - write_index "\"$base\"" "\"$pm_agent\"" "$new_issues" - fi - fi -} - -remove_issue_from_index() { - local issue_ref="$1" - local index=$(read_index) - local base=$(get_base_session_id) - local pm_agent=$(get_pm_agent_session_id) - local new_issues=$(echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); d['issues'].pop('$issue_ref', None); print(json.dumps(d['issues']))") - if [ -z "$base" ] || [ "$base" = "null" ]; then - if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ]; then - write_index "null" "null" "$new_issues" - else - write_index "null" "\"$pm_agent\"" "$new_issues" - fi - else - if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ]; then - write_index "\"$base\"" "null" "$new_issues" - else - write_index "\"$base\"" "\"$pm_agent\"" "$new_issues" - fi - fi -} - validate_issue_ref() { local issue_ref="$1" if [[ ! "$issue_ref" =~ ^[^/]+/[^/]+/[^#]+#[0-9]+$ ]]; then diff --git a/skills/kugetsu/scripts/kugetsu-index.sh b/skills/kugetsu/scripts/kugetsu-index.sh index 8eec8c2..f1e6695 100755 --- a/skills/kugetsu/scripts/kugetsu-index.sh +++ b/skills/kugetsu/scripts/kugetsu-index.sh @@ -15,6 +15,13 @@ write_index() { local issues_json="$3" local temp_file="$INDEX_FILE.tmp.$$" printf '{"base": %s, "pm_agent": %s, "issues": %s}\n' "$base" "$pm_agent" "$issues_json" > "$temp_file" + + if ! python3 -c "import json; json.load(open('$temp_file'))" 2>/dev/null; then + echo "Error: write_index would create malformed JSON, aborting. base=$base, pm_agent=$pm_agent, issues_json=$issues_json" >&2 + rm -f "$temp_file" + return 1 + fi + mv "$temp_file" "$INDEX_FILE" }