Compare commits

..

1 Commits

Author SHA1 Message Date
shokollm
a5f6071485 Merge origin/main into fix/issue-116-modularize-script 2026-04-05 10:56:35 +00:00
12 changed files with 350 additions and 1280 deletions

View File

@@ -6,25 +6,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
## [v0.2.4] - 2026-04-06
### Fixed
- Queue daemon: Locking to prevent daemon vs manual conflicts
- Queue daemon: Proper error handling for failed tasks
- Queue daemon: Fix GITEA_TOKEN loading from pm-agent.env
- cmd_delegate: Enqueue tasks instead of bypassing queue
- Notifications: Call kugetsu_add_notification from bash instead of os.system()
- kugetsu: Remove duplicate update_queue_item_state that overwrote fixed version
### Added
- Queue functions moved to kugetsu-index.sh for daemon access
- kugetsu-session.sh sources required modules for daemon use
## [v0.2.3] - 2026-04-06
### Fixed
- get_pending_tasks() returns proper JSON array instead of concatenated JSON objects
## [v0.2.1] - 2026-04-03 ## [v0.2.1] - 2026-04-03
### Fixed ### Fixed

View File

@@ -2,33 +2,6 @@ You are a PM (Project Manager) for software development.
Your role is COORDINATOR. You break down requests, delegate work, monitor progress, and report results. You NEVER write code. Not even small fixes. Not even one-liners. Not even documentation. If asked to write code: delegate it using `kugetsu start`. Your role is COORDINATOR. You break down requests, delegate work, monitor progress, and report results. You NEVER write code. Not even small fixes. Not even one-liners. Not even documentation. If asked to write code: delegate it using `kugetsu start`.
## Response Modes
You have TWO response modes. Choose based on the user's request:
### Mode 1: Task Mode (Delegate)
When the user asks for something that requires coding, implementation, or file changes:
- "work on issue #81"
- "close PR #42"
- "create a PR for issue #91"
- "fix the bug in login.js"
- "add tests for the API"
- Any request that modifies code or creates commits
**Action:** Delegate using `kugetsu start` or `kugetsu continue`.
### Mode 2: Conversation Mode (Answer Directly)
When the user asks a question that doesn't require code changes:
- "show open issues"
- "what is the current state of repo"
- "hi"
- "show me recent commits"
- "what issues are being worked on"
- "what branches exist"
- Any informational query
**Action:** Answer directly from available context or API calls.
## Write Permissions: Strict Boundary ## Write Permissions: Strict Boundary
PM has EXPLICIT write boundaries. You can ONLY write to two specific locations. PM has EXPLICIT write boundaries. You can ONLY write to two specific locations.
@@ -45,116 +18,75 @@ PM has EXPLICIT write boundaries. You can ONLY write to two specific locations.
- Any `.md` files, config files, scripts, or code - Any `.md` files, config files, scripts, or code
### If Asked to Write Outside ~/.kugetsu/: ### If Asked to Write Outside ~/.kugetsu/:
You MUST delegate to a dev agent using Task Mode. You MUST delegate to a dev agent:
```
kugetsu start <domain>/<user>/<repo>#<issue> <task description>
```
Where:
- `<domain>` = git server (e.g., `github.com`, `gitlab.com`, `git.fbrns.co`)
- `<user>` = git username (from `git config user.name`)
- `<repo>` = repository name (from `git remote -v`)
- `<issue>` = issue number to address
## Tools for Delegation ### New Kugetsu Scripts:
Do NOT write new kugetsu scripts yourself (even for internal use). Delegate to a dev agent via the normal workflow:
1. Create an issue describing the needed script
2. Delegate: `kugetsu start <domain>/<user>/<repo>#<issue> Create new kugetsu script`
3. After PR is merged, you may test the new script
### kugetsu start **Example violations (DO NOT DO THESE):**
Create a NEW dev agent session for an issue that has no existing session/worktree. - "Update SKILL.md" → DELEGATE, don't edit it yourself
- "Fix the bug in login.js" → DELEGATE, don't write to repositories/
- "Add a new script for queue management" → DELEGATE via issue/PR workflow
## Critical: How to Delegate
Use `kugetsu start` to create dev agent sessions:
``` ```
kugetsu start <issue-ref> <task description> kugetsu start <domain>/<user>/<repo>#<issue> <task description>
``` ```
**Params:** **Domain/User/Repo**: Pull from `git remote -v` and `git config user.name` to make this agnostic to any git server.
- `issue-ref`: Format `instance/user/repo#number`
- Example: `github.com/shoko/kugetsu#81`
- Example: `git.fbrns.co/shoko/kugetsu#118`
- `task description`: What the agent should do (be specific)
**Returns:** Creates new worktree and dev session, returns session ID **NOT `kugetsu delegate`** - that routes back to the PM (you). Use `kugetsu start` to create a NEW dev agent.
**When to use:** When there is NO existing session or worktree for this issue.
---
### kugetsu continue
Continue an EXISTING dev agent session that already has a worktree/session.
```
kugetsu continue <issue-ref> [additional instructions]
```
**Params:**
- `issue-ref`: Format `instance/user/repo#number`
- `additional instructions`: (optional) Extra context or changed instructions
**Returns:** Continues existing session, returns session ID
**When to use:** When a worktree/session already exists for this issue (check with `kugetsu list`).
---
### How to Choose: start vs continue
| Scenario | Tool |
|----------|------|
| First time working on issue | `kugetsu start` |
| Issue already has worktree/session | `kugetsu continue` |
| Session exists but needs new task | `kugetsu continue <issue-ref> <new task>` |
| Not sure if session exists | Check `kugetsu list` first, or use `kugetsu continue` (it will error if no session) |
**NOT `kugetsu delegate`** - that routes back to the PM (you). Use `kugetsu start` or `kugetsu continue` to create a NEW dev agent.
## Your Identity ## Your Identity
You are the PM. Your job is to coordinate, not to code. You are the PM. Your job is to coordinate, not to code.
- You delegate ALL implementation tasks to dev agents using Task Mode - You delegate ALL implementation tasks to dev agents using `kugetsu start`
- You answer informational queries directly in Conversation Mode
- You review PRs but do not edit code yourself - You review PRs but do not edit code yourself
- You break down complex requests into delegate-able tasks - You break down complex requests into delegate-able tasks
- You monitor progress and keep stakeholders informed - You monitor progress and keep stakeholders informed
## Delegation is Your Default Behavior for Tasks ## Delegation is Your Default Behavior
When a request comes in: When a request comes in:
1. **Identify Mode** - Is this a task (code change needed) or a conversation (info request)? 1. **Understand** - What needs to be built? What's the repo and issue?
2. **For Tasks:** 2. **Delegate** - Use `kugetsu start <issue-ref> <task>` to create a dev agent task
- **Understand** - What needs to be built? What's the repo and issue? 3. **Monitor** - Watch for PR creation and review
- **Choose Tool** - Use `kugetsu start` (new) or `kugetsu continue` (existing)? 4. **Report** - Post final results to the issue
- **Delegate** - Call the appropriate tool with issue-ref and task
- **Monitor** - Watch for PR creation and review
- **Report** - Post final results to the issue
3. **For Conversations:**
- Answer directly using available context
## Few-Shot Examples ## Few-Shot Examples
**User:** "Fix the bug in login.js" **User:** "Fix the bug in login.js"
**Mode:** Task
**You:** `kugetsu start <domain>/<user>/<repo>#123 Investigate and fix the login bug in login.js` **You:** `kugetsu start <domain>/<user>/<repo>#123 Investigate and fix the login bug in login.js`
**User:** "Add tests for the API" **User:** "Add tests for the API"
**Mode:** Task
**You:** `kugetsu start <domain>/<user>/<repo>#124 Write tests for the API module` **You:** `kugetsu start <domain>/<user>/<repo>#124 Write tests for the API module`
**User:** "Can you write a quick script to parse this JSON?" **User:** "Can you write a quick script to parse this JSON?"
**Mode:** Task
**You:** `kugetsu start <domain>/<user>/<repo>#125 Create a script to parse the JSON file` **You:** `kugetsu start <domain>/<user>/<repo>#125 Create a script to parse the JSON file`
**User:** "Update the README with installation instructions" **User:** "Update the README with installation instructions"
**Mode:** Task
**You:** `kugetsu start <domain>/<user>/<repo>#126 Update README with installation instructions` **You:** `kugetsu start <domain>/<user>/<repo>#126 Update README with installation instructions`
**User:** "Create a file at /tmp/test.txt" **User:** "Create a file at /tmp/test.txt"
**Mode:** Task
**You:** `kugetsu start <domain>/<user>/<repo>#127 Create a file at /tmp/test.txt` **You:** `kugetsu start <domain>/<user>/<repo>#127 Create a file at /tmp/test.txt`
**User:** "What open issues do we have?" Notice: In every example, the correct response is to DELEGATE using `kugetsu start`, not to do it yourself.
**Mode:** Conversation
**You:** (Answer directly about open issues from the repository)
**User:** "Show me recent commits"
**Mode:** Conversation
**You:** (Answer directly about recent commits)
**User:** "Hi, how are you?"
**Mode:** Conversation
**You:** (Answer greeting directly)
---
## You Are the PM. You Coordinate. You Do Not Write Code. ## You Are the PM. You Coordinate. You Do Not Write Code.
@@ -162,4 +94,4 @@ This is not just a rule - it is your identity. The code you coordinate is built
--- ---
*PM Agent v5 - Coordinators coordinate. Delegation is for tasks, conversation is for questions. Strict write boundary: ONLY ~/.kugetsu/.* *PM Agent v4 - Coordinators coordinate, we do not code. Strict write boundary: ONLY ~/.kugetsu/.*

View File

@@ -93,16 +93,23 @@ EOF
ensure_dirs() { ensure_dirs() {
mkdir -p "$SESSIONS_DIR" mkdir -p "$SESSIONS_DIR"
mkdir -p "$LOGS_DIR"
mkdir -p "$WORKTREES_DIR"
mkdir -p "$QUEUE_DIR"
mkdir -p "$QUEUE_ITEMS_DIR"
} }
ensure_worktree_dir() { ensure_worktree_dir() {
mkdir -p "$WORKTREES_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() { issue_ref_to_context_file() {
local issue_ref="$1" local issue_ref="$1"
local context_filename=$(issue_ref_to_filename "$issue_ref") local context_filename=$(issue_ref_to_filename "$issue_ref")
@@ -261,9 +268,7 @@ PYEOF
} }
ensure_queue_dirs() { ensure_queue_dirs() {
mkdir -p "$QUEUE_DIR"
mkdir -p "$QUEUE_ITEMS_DIR" mkdir -p "$QUEUE_ITEMS_DIR"
mkdir -p "$LOGS_DIR"
} }
generate_queue_id() { generate_queue_id() {
@@ -316,31 +321,12 @@ get_pending_tasks() {
return return
fi fi
python3 -c " find "$QUEUE_ITEMS_DIR" -name "*.json" -type f 2>/dev/null | while read -r file; do
import json local state=$(python3 -c "import json; print(json.load(open('$file')).get('state', ''))" 2>/dev/null || echo "")
import os if [ "$state" = "pending" ]; then
import sys cat "$file"
fi
queue_dir = os.environ.get('QUEUE_ITEMS_DIR', '') done | head -"$limit"
limit = int(sys.argv[1]) if len(sys.argv) > 1 else 10
items = []
if os.path.isdir(queue_dir):
for filename in os.listdir(queue_dir):
if filename.endswith('.json'):
filepath = os.path.join(queue_dir, filename)
try:
with open(filepath) as f:
data = json.load(f)
if data.get('state') == 'pending':
items.append(data)
if len(items) >= limit:
break
except:
pass
print(json.dumps(items))
" "$limit"
} }
get_queue_stats() { get_queue_stats() {
@@ -367,6 +353,55 @@ get_queue_stats() {
echo "{\"total\": $total, \"pending\": $pending, \"notified\": $notified, \"completed\": $completed, \"error\": $error}" echo "{\"total\": $total, \"pending\": $pending, \"notified\": $notified, \"completed\": $completed, \"error\": $error}"
} }
update_queue_item_state() {
local queue_id="$1"
local new_state="$2"
local session_id="${3:-}"
local pid="${4:-}"
local item_file="$QUEUE_ITEMS_DIR/${queue_id}.json"
if [ ! -f "$item_file" ]; then
echo "Error: Queue item not found: $queue_id" >&2
return 1
fi
python3 << PYEOF
import json
import os
from datetime import datetime
item_file = "$item_file"
new_state = "$new_state"
session_id = "$session_id"
pid = "$pid"
with open(item_file, 'r') as f:
item = json.load(f)
issue_ref = item.get('issue_ref', '')
item['state'] = new_state
if new_state == "notified":
item['notified_at'] = datetime.now().isoformat() + "Z"
if session_id:
item['opencode_session_id'] = session_id
if pid:
item['pid'] = int(pid) if pid.isdigit() else None
elif new_state == "completed":
item['completed_at'] = datetime.now().isoformat() + "Z"
os.system(f"kugetsu_add_notification 'task_completed' 'Task completed: {issue_ref}' '{issue_ref}'")
elif new_state == "error":
item['error'] = datetime.now().isoformat() + "Z"
os.system(f"kugetsu_add_notification 'task_error' 'Task error: {issue_ref}' '{issue_ref}'")
with open(item_file, 'w') as f:
json.dump(item, f, indent=2)
print(f"Updated $queue_id to state: $new_state")
PYEOF
}
check_task_timeouts() { check_task_timeouts() {
if [ ! -d "$QUEUE_ITEMS_DIR" ]; then if [ ! -d "$QUEUE_ITEMS_DIR" ]; then
return return
@@ -475,6 +510,97 @@ read_index() {
fi 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() { validate_issue_ref() {
local issue_ref="$1" local issue_ref="$1"
if [[ ! "$issue_ref" =~ ^[^/]+/[^/]+/[^#]+#[0-9]+$ ]]; then if [[ ! "$issue_ref" =~ ^[^/]+/[^/]+/[^#]+#[0-9]+$ ]]; then
@@ -694,18 +820,6 @@ cmd_status() {
return return
fi 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" echo "ok"
} }
@@ -763,11 +877,6 @@ EOF
} }
parse_issue_ref_from_message() { parse_issue_ref_from_message() {
# DEPRECATED: This function is not called anywhere.
# The active implementation is extract_issue_ref_from_message()
# in kugetsu-session.sh which is used by cmd_delegate.
# This function is kept for backwards compatibility and will
# be removed in a future release.
local message="$1" local message="$1"
local gitserver="" local gitserver=""
@@ -775,20 +884,21 @@ parse_issue_ref_from_message() {
local repo="" local repo=""
local issue_number="" local issue_number=""
if [[ "$message" =~ (https?://)?([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/([a-zA-Z0-9._-]+)/([a-zA-Z0-9._-]+)/(issues|pull)/([0-9]+) ]]; then if echo "$message" | grep -qE '[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/(issues|pull)/[0-9]+'; then
gitserver="${BASH_REMATCH[2]}" gitserver=$(echo "$message" | grep -oE '[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+' | head -1 | sed 's/\/[^/]*\/[^/]*$//')
owner="${BASH_REMATCH[3]}" local full_path=$(echo "$message" | grep -oE '[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/(issues|pull)/[0-9]+' | head -1)
repo="${BASH_REMATCH[4]}" owner=$(echo "$full_path" | cut -d'/' -f2)
issue_number="${BASH_REMATCH[6]}" repo=$(echo "$full_path" | cut -d'/' -f3)
elif [[ "$message" =~ (https?://)?([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/([a-zA-Z0-9._-]+)/([a-zA-Z0-9._-]+)#([0-9]+) ]]; then issue_number=$(echo "$full_path" | grep -oE '[0-9]+$' | head -1)
gitserver="${BASH_REMATCH[2]}" elif echo "$message" | grep -qE '[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+#[0-9]+'; then
owner="${BASH_REMATCH[3]}" gitserver=$(echo "$message" | grep -oE '[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+' | head -1)
repo="${BASH_REMATCH[4]}" owner=$(echo "$gitserver" | cut -d'/' -f2)
issue_number="${BASH_REMATCH[5]}" repo=$(echo "$gitserver" | cut -d'/' -f3)
elif [[ "$message" =~ ([a-zA-Z0-9._-]+)/([a-zA-Z0-9._-]+)#([0-9]+) ]]; then issue_number=$(echo "$message" | grep -oE '#[0-9]+' | grep -oE '[0-9]+' | head -1)
owner="${BASH_REMATCH[1]}" elif echo "$message" | grep -qE '[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+#([0-9]+)'; then
repo="${BASH_REMATCH[2]}" owner=$(echo "$message" | grep -oE '[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+#' | sed 's/#$//' | cut -d'/' -f1)
issue_number="${BASH_REMATCH[3]}" repo=$(echo "$message" | grep -oE '[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+#' | sed 's/#$//' | cut -d'/' -f2)
issue_number=$(echo "$message" | grep -oE '#[0-9]+' | grep -oE '[0-9]+' | head -1)
fi fi
echo "${gitserver}|${owner}|${repo}|${issue_number}" echo "${gitserver}|${owner}|${repo}|${issue_number}"
@@ -864,6 +974,7 @@ find_sessions_by_issue_number() {
echo "$results" echo "$results"
} }
>>>>>>> origin/main
cmd_queue() { cmd_queue() {
local action="${1:-list}" local action="${1:-list}"
shift shift

View File

@@ -32,7 +32,7 @@ if [ -f "$KUGETSU_DIR/config" ]; then
fi fi
mask_sensitive_vars() { mask_sensitive_vars() {
local line="${1:-}" local line="$1"
for var in GITEA_TOKEN GITHUB_TOKEN GITLAB_TOKEN API_KEY PASSWORD TOKEN SECRET; do for var in GITEA_TOKEN GITHUB_TOKEN GITLAB_TOKEN API_KEY PASSWORD TOKEN SECRET; do
if [[ "$line" =~ $var ]]; then if [[ "$line" =~ $var ]]; then
line=$(echo "$line" | sed -E "s/=.*/=***MASKED***/") line=$(echo "$line" | sed -E "s/=.*/=***MASKED***/")
@@ -41,11 +41,6 @@ mask_sensitive_vars() {
echo "$line" echo "$line"
} }
strip_ansi_codes() {
local line="${1:-}"
echo "$line" | sed 's/\x1b\[[0-9;]*m//g' | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g'
}
load_agent_env() { load_agent_env() {
local agent_type="${1:-base}" local agent_type="${1:-base}"
local env_file="$ENV_DIR/${agent_type}.env" local env_file="$ENV_DIR/${agent_type}.env"
@@ -58,32 +53,5 @@ load_agent_env() {
set -a set -a
source "$ENV_DIR/default.env" source "$ENV_DIR/default.env"
set +a set +a
elif [ -f "$ENV_DIR/pm-agent.env" ]; then
set -a
source "$ENV_DIR/pm-agent.env"
set +a
fi fi
} }
set_debug_mode() {
local filtered_args=()
local debug_mode=false
for arg in "$@"; do
case "$arg" in
--debug)
debug_mode=true
;;
*)
filtered_args+=("$arg")
;;
esac
done
if [ "$debug_mode" = true ]; then
export KUGETSU_VERBOSITY="debug"
echo "[DEBUG] Debug mode enabled" >&2
fi
echo "${filtered_args[@]}"
}

View File

@@ -15,13 +15,6 @@ write_index() {
local issues_json="$3" local issues_json="$3"
local temp_file="$INDEX_FILE.tmp.$$" local temp_file="$INDEX_FILE.tmp.$$"
printf '{"base": %s, "pm_agent": %s, "issues": %s}\n' "$base" "$pm_agent" "$issues_json" > "$temp_file" 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" mv "$temp_file" "$INDEX_FILE"
} }
@@ -50,11 +43,7 @@ set_base_in_index() {
if [ "$session_id" = "null" ]; then if [ "$session_id" = "null" ]; then
write_index "null" "$pm_agent" "$issues" write_index "null" "$pm_agent" "$issues"
else else
if [ "$pm_agent" = "null" ]; then write_index "\"$session_id\"" "$pm_agent" "$issues"
write_index "\"$session_id\"" "null" "$issues"
else
write_index "\"$session_id\"" "\"$pm_agent\"" "$issues"
fi
fi fi
} }
@@ -67,11 +56,7 @@ set_pm_agent_in_index() {
if [ "$session_id" = "null" ]; then if [ "$session_id" = "null" ]; then
write_index "$base" "null" "$issues" write_index "$base" "null" "$issues"
else else
if [ "$base" = "null" ]; then write_index "$base" "\"$session_id\"" "$issues"
write_index "null" "\"$session_id\"" "$issues"
else
write_index "\"$base\"" "\"$session_id\"" "$issues"
fi
fi fi
} }
@@ -87,19 +72,7 @@ add_issue_to_index() {
issues=$(python3 -c "import sys, json; d=json.load(sys.stdin); d['$issue_ref']='$session_file'; print(json.dumps(d))" <<< "$issues") issues=$(python3 -c "import sys, json; d=json.load(sys.stdin); d['$issue_ref']='$session_file'; print(json.dumps(d))" <<< "$issues")
if [ "$base" = "null" ]; then write_index "$base" "$pm_agent" "$issues"
if [ "$pm_agent" = "null" ]; then
write_index "null" "null" "$issues"
else
write_index "null" "\"$pm_agent\"" "$issues"
fi
else
if [ "$pm_agent" = "null" ]; then
write_index "\"$base\"" "null" "$issues"
else
write_index "\"$base\"" "\"$pm_agent\"" "$issues"
fi
fi
} }
remove_issue_from_index() { remove_issue_from_index() {
@@ -113,19 +86,7 @@ remove_issue_from_index() {
issues=$(python3 -c "import sys, json; d=json.load(sys.stdin); d.pop('$issue_ref', None); print(json.dumps(d))" <<< "$issues") issues=$(python3 -c "import sys, json; d=json.load(sys.stdin); d.pop('$issue_ref', None); print(json.dumps(d))" <<< "$issues")
if [ "$base" = "null" ]; then write_index "$base" "$pm_agent" "$issues"
if [ "$pm_agent" = "null" ]; then
write_index "null" "null" "$issues"
else
write_index "null" "\"$pm_agent\"" "$issues"
fi
else
if [ "$pm_agent" = "null" ]; then
write_index "\"$base\"" "null" "$issues"
else
write_index "\"$base\"" "\"$pm_agent\"" "$issues"
fi
fi
} }
validate_issue_ref() { validate_issue_ref() {
@@ -169,114 +130,6 @@ session['pr_url'] = pr_url
with open(session_path, 'w') as f: with open(session_path, 'w') as f:
json.dump(session, f, indent=2) 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 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'
}
# Add notification to notifications file
kugetsu_add_notification() {
local type="$1"
local message="$2"
local issue_ref="${3:-}"
local gitea_url="${4:-}"
mkdir -p "$(dirname "$NOTIFICATIONS_FILE")"
python3 << PYEOF
import json
import os
from datetime import datetime
notification = {
"type": "$type",
"message": "$message",
"issue_ref": "$issue_ref" if "$issue_ref" else None,
"gitea_url": "$gitea_url" if "$gitea_url" else None,
"timestamp": datetime.now().isoformat(),
"read": False
}
file_path = os.path.expanduser("$NOTIFICATIONS_FILE")
notifications = []
if os.path.exists(file_path):
try:
with open(file_path, 'r') as f:
notifications = json.load(f)
except:
notifications = []
notifications.append(notification)
with open(file_path, 'w') as f:
json.dump(notifications, f, indent=2)
print("Notification added")
PYEOF
}
# Update queue item state
update_queue_item_state() {
local queue_id="$1"
local new_state="$2"
local session_id="${3:-}"
local pid="${4:-}"
local item_file="$QUEUE_ITEMS_DIR/${queue_id}.json"
if [ ! -f "$item_file" ]; then
echo "Error: Queue item not found: $queue_id" >&2
return 1
fi
local issue_ref=$(python3 -c "import json; print(json.load(open('$item_file')).get('issue_ref', ''))" 2>/dev/null || echo "")
python3 << PYEOF
import json
from datetime import datetime
item_file = "$item_file"
new_state = "$new_state"
session_id = "$session_id"
pid = "$pid"
with open(item_file, 'r') as f:
item = json.load(f)
item['state'] = new_state
if new_state == "notified":
item['notified_at'] = datetime.now().isoformat() + "Z"
if session_id:
item['opencode_session_id'] = session_id
if pid:
item['pid'] = int(pid) if pid.isdigit() else None
elif new_state == "completed":
item['completed_at'] = datetime.now().isoformat() + "Z"
elif new_state == "error":
item['error'] = datetime.now().isoformat() + "Z"
with open(item_file, 'w') as f:
json.dump(item, f, indent=2)
print(f"Updated $queue_id to state: $new_state")
PYEOF
if [ "$new_state" = "completed" ]; then
kugetsu_add_notification "task_completed" "Task completed: $issue_ref" "$issue_ref"
elif [ "$new_state" = "error" ]; then
kugetsu_add_notification "task_error" "Task error: $issue_ref" "$issue_ref"
fi
}

View File

@@ -24,9 +24,7 @@ cmd_logs() {
echo "" echo ""
echo "--- $log ---" echo "--- $log ---"
tail -20 "$LOGS_DIR/$log" | while read line; do tail -20 "$LOGS_DIR/$log" | while read line; do
line=$(strip_ansi_codes "$line") echo " $(mask_sensitive_vars "$line")"
line=$(mask_sensitive_vars "$line")
echo " $line"
done done
fi fi
done done
@@ -45,24 +43,15 @@ kugetsu_add_notification() {
notifications=$(cat "$NOTIFICATIONS_FILE") notifications=$(cat "$NOTIFICATIONS_FILE")
fi fi
notifications=$(echo "$notifications" | python3 -c " local new_notification=$(python3 -c "import json; print(json.dumps({
import json 'type': '$notification_type',
import sys 'message': '$message',
'issue_ref': '$issue_ref',
notifications = json.load(sys.stdin) 'timestamp': '$timestamp',
new_notification = { 'read': False
'type': '$notification_type', }))")
'message': '''$message'''.replace('\"', '\"'),
'issue_ref': '$issue_ref' if '$issue_ref' else None, notifications=$(python3 -c "import json; n=json.loads('$notifications'); n.append(json.loads('$new_notification')); print(json.dumps(n[-50:] if len(n)>50 else n, indent=2))")
'timestamp': '$timestamp',
'read': False
}
notifications.append(new_notification)
notifications = notifications[-50:] if len(notifications) > 50 else notifications
print(json.dumps(notifications, indent=2))
")
echo "$notifications" > "$NOTIFICATIONS_FILE" echo "$notifications" > "$NOTIFICATIONS_FILE"
} }

147
skills/kugetsu/scripts/kugetsu-queue-daemon.sh Normal file → Executable file
View File

@@ -8,148 +8,25 @@ source "$SCRIPT_DIR/kugetsu-index.sh"
source "$SCRIPT_DIR/kugetsu-worktree.sh" source "$SCRIPT_DIR/kugetsu-worktree.sh"
source "$SCRIPT_DIR/kugetsu-log.sh" source "$SCRIPT_DIR/kugetsu-log.sh"
load_agent_env "pm-agent"
acquire_lock() {
local issue_ref="$1"
local lock_file="$QUEUE_DIR/locks/$(echo "$issue_ref" | sed 's/[\/:]/-/g' | sed 's/#/-/').lock"
mkdir -p "$(dirname "$lock_file")"
if [ -f "$lock_file" ]; then
local pid=$(cat "$lock_file" 2>/dev/null || echo "")
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
return 1
fi
rm -f "$lock_file"
fi
echo $$ > "$lock_file"
return 0
}
release_lock() {
local issue_ref="$1"
local lock_file="$QUEUE_DIR/locks/$(echo "$issue_ref" | sed 's/[\/:]/-/g' | sed 's/#/-/').lock"
rm -f "$lock_file"
}
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('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)
if [ -n "$pid" ] && [ "$pid" != "None" ]; then
if ! kill -0 "$pid" 2>/dev/null; then
local worktree_path=$(issue_ref_to_worktree_path "$issue_ref" "$WORKTREES_DIR")
local has_commits=false
if [ -d "$worktree_path" ] && [ -d "$worktree_path/.git" ]; then
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
release_lock "$issue_ref"
fi
else
if [ -n "$session_id" ] && ! opencode session list 2>/dev/null | grep -q "$session_id"; then
local worktree_path=$(issue_ref_to_worktree_path "$issue_ref" "$WORKTREES_DIR")
local has_commits=false
if [ -d "$worktree_path" ] && [ -d "$worktree_path/.git" ]; then
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
release_lock "$issue_ref"
fi
fi
}
get_session_id_for_issue() {
local issue_ref="$1"
local session_file=$(issue_ref_to_filename "$issue_ref")
local session_path="$SESSIONS_DIR/$session_file"
if [ -f "$session_path" ]; then
python3 -c "import json; print(json.load(open('$session_path')).get('opencode_session_id', ''))" 2>/dev/null || echo ""
else
echo ""
fi
}
process_task() {
local item="$1"
local queue_id=$(basename "$item" .json)
local issue_ref=$(python3 -c "import json; print(json.load(open('$item')).get('issue_ref', ''))" 2>/dev/null)
local message=$(python3 -c "import json; print(json.load(open('$item')).get('message', ''))" 2>/dev/null)
if ! acquire_lock "$issue_ref"; then
echo "Task $queue_id ($issue_ref) skipped — another process is handling it"
return
fi
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"
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
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"
}
while true; do while true; do
if [ -d "$QUEUE_ITEMS_DIR" ]; then if [ -d "$QUEUE_ITEMS_DIR" ]; then
for item in "$QUEUE_ITEMS_DIR"/*.json; do
[ -f "$item" ] || continue
check_task_completion "$item"
done
for item in "$QUEUE_ITEMS_DIR"/*.json; do for item in "$QUEUE_ITEMS_DIR"/*.json; do
[ -f "$item" ] || continue [ -f "$item" ] || continue
state=$(python3 -c "import json; print(json.load(open('$item')).get('state', ''))" 2>/dev/null) state=$(python3 -c "import json; print(json.load(open('$item')).get('state', ''))" 2>/dev/null)
if [ "$state" = "pending" ]; then if [ "$state" = "pending" ]; then
process_task "$item" queue_id=$(basename "$item" .json)
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
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 &
pid=$!
update_queue_item_state "$queue_id" "notified" "$pm_session" "$pid"
fi
fi fi
done done
fi fi
sleep "${QUEUE_DAEMON_INTERVAL_MINUTES:-5}m" sleep "${QUEUE_DAEMON_INTERVAL_MINUTES:-5}m"
done done

View File

@@ -1,13 +1,6 @@
#!/bin/bash #!/bin/bash
set -euo pipefail set -euo pipefail
# Source required modules for session management functions
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/kugetsu-config.sh"
source "$SCRIPT_DIR/kugetsu-index.sh"
source "$SCRIPT_DIR/kugetsu-worktree.sh"
source "$SCRIPT_DIR/kugetsu-log.sh"
count_active_dev_sessions() { count_active_dev_sessions() {
local count=0 local count=0
if [ -d "$SESSIONS_DIR" ]; then if [ -d "$SESSIONS_DIR" ]; then
@@ -58,35 +51,12 @@ EOF
echo "Created config file: $KUGETSU_DIR/config" echo "Created config file: $KUGETSU_DIR/config"
fi 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 <agent-type>.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_base=$(get_base_session_id)
local existing_pm=$(get_pm_agent_session_id) local existing_pm=$(get_pm_agent_session_id)
if [ -n "$existing_base" ] && [ "$existing_base" != "null" ]; then if [ -n "$existing_base" ] && [ "$existing_base" != "null" ]; then
if [ "$force" = true ]; then if [ "$force" = true ]; then
echo "Warning: Reinitializing sessions (force mode)" >&2 echo "Warning: Reinitializing sessions (force mode)" >&2
echo "Destroying all sessions, worktrees, and logs..." >&2
cmd_destroy --base -y 2>/dev/null || true
cmd_destroy --pm-agent -y 2>/dev/null || true
rm -f "$LOGS_DIR"/*.log 2>/dev/null || true
else else
echo "Error: Base session already exists: $existing_base" >&2 echo "Error: Base session already exists: $existing_base" >&2
echo "Use --force to reinitialize" >&2 echo "Use --force to reinitialize" >&2
@@ -104,20 +74,9 @@ EOF
echo "Press Ctrl+C to cancel or wait for session to be created" echo "Press Ctrl+C to cancel or wait for session to be created"
sleep 2 sleep 2
local before_sessions=$(opencode session list 2>/dev/null | grep -E '^ses_' | awk '{print $1}' || true)
opencode opencode
local after_sessions=$(opencode session list 2>/dev/null | grep -E '^ses_' | awk '{print $1}' || true) local session_ids=$(opencode session list 2>/dev/null | grep -E '^ses_' | awk '{print $1}' | tail -1)
local session_ids=""
while IFS= read -r line; do
local sid=$(echo "$line" | awk '{print $1}')
if [ -n "$sid" ] && ! echo "$before_sessions" | grep -q "^${sid}$"; then
session_ids="$sid"
break
fi
done <<< "$after_sessions"
if [ -z "$session_ids" ]; then if [ -z "$session_ids" ]; then
echo "Error: Could not find newly created session" >&2 echo "Error: Could not find newly created session" >&2
exit 1 exit 1
@@ -129,20 +88,9 @@ EOF
echo "Base session created: $session_ids" echo "Base session created: $session_ids"
echo "Starting PM agent..." echo "Starting PM agent..."
before_sessions="$after_sessions"
opencode opencode
after_sessions=$(opencode session list 2>/dev/null | grep -E '^ses_' | awk '{print $1}' || true) local pm_session_ids=$(opencode session list 2>/dev/null | grep -E '^ses_' | grep -v "$session_ids" | tail -1)
local pm_session_ids=""
while IFS= read -r line; do
local sid=$(echo "$line" | awk '{print $1}')
if [ -n "$sid" ] && ! echo "$before_sessions" | grep -q "^${sid}$"; then
pm_session_ids="$sid"
break
fi
done <<< "$after_sessions"
if [ -z "$pm_session_ids" ]; then if [ -z "$pm_session_ids" ]; then
echo "Warning: Could not find separate PM agent session" >&2 echo "Warning: Could not find separate PM agent session" >&2
pm_session_ids="$session_ids" pm_session_ids="$session_ids"
@@ -179,11 +127,13 @@ extract_issue_ref_from_message() {
return return
fi fi
if [[ "$message" =~ (https?://)?([a-zA-Z0-9.-]+)/([a-zA-Z0-9._-]+)/([a-zA-Z0-9._-]+)/(issues|pull)/([0-9]+) ]]; then if [[ "$message" =~ (https?://[a-zA-Z0-9.-]+/[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+/(issues|pull)/[0-9]+) ]]; then
local instance="${BASH_REMATCH[2]}" local url="${BASH_REMATCH[1]}"
local owner="${BASH_REMATCH[3]}" local path=$(echo "$url" | sed 's|https\?://||' | cut -d'/' -f2-)
local repo="${BASH_REMATCH[4]}" local instance=$(echo "$path" | cut -d'/' -f1)
local num="${BASH_REMATCH[6]}" local owner=$(echo "$path" | cut -d'/' -f2)
local repo=$(echo "$path" | cut -d'/' -f3)
local num=$(echo "$path" | grep -oE '[0-9]+$')
echo "${instance}/${owner}/${repo}#${num}" echo "${instance}/${owner}/${repo}#${num}"
return return
fi fi
@@ -203,117 +153,21 @@ cmd_delegate() {
local issue_ref=$(extract_issue_ref_from_message "$message") local issue_ref=$(extract_issue_ref_from_message "$message")
if [ -n "$issue_ref" ] && [[ "$issue_ref" =~ \#[0-9]+$ ]]; then if [ -n "$issue_ref" ] && [[ "$issue_ref" =~ \#[0-9]+$ ]]; then
# Enqueue for daemon to process via cmd_start/cmd_continue cmd_start "$issue_ref" "$message"
enqueue_task "$issue_ref" "$message"
return return
fi fi
# No issue ref detected — fork a new session from base session local pm_session=$(get_pm_agent_session_id)
local base_session=$(get_base_session_id) if [ -z "$pm_session" ] || [ "$pm_session" = "null" ] || [ "$pm_session" = "None" ]; then
if [ -z "$base_session" ] || [ "$base_session" = "null" ]; then echo "Error: PM agent session not found. Run 'kugetsu init' first." >&2
echo "Error: Base session not found. Run 'kugetsu init' first." >&2
exit 1 exit 1
fi fi
mkdir -p "$LOGS_DIR" mkdir -p "$LOGS_DIR"
local log_file="$LOGS_DIR/delegate-$(date +%s).log" 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" > /dev/null 2>&1 &
disown
local new_session=$(create_session "$base_session") echo "Delegated to PM agent (logged to $(basename "$log_file"))"
if [ -z "$new_session" ]; then
echo "Error: Failed to create session" >&2
exit 1
fi
local msg_file="$LOGS_DIR/msg-$new_session.txt"
printf '%s' "$message" > "$msg_file"
nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '@$msg_file' --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_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 run --fork --session "$base_session" "new session" >/dev/null 2>&1
sleep 1
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=""
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"
}
build_dev_agent_message() {
local issue_ref="$1"
local user_message="${2:-}"
local instance=$(echo "$issue_ref" | cut -d'/' -f1 | cut -d'#' -f1)
local owner=$(echo "$issue_ref" | cut -d'/' -f2)
local repo=$(echo "$issue_ref" | cut -d'/' -f3 | cut -d'#' -f1)
local number=$(echo "$issue_ref" | grep -oE '#[0-9]+$' | tr -d '#')
local worktree_path=$(issue_ref_to_worktree_path "$issue_ref")
local base_message="You are assigned to work on $issue_ref.
Workflow:
1. Read the issue at $instance/$owner/$repo/issues/$number AND all comments on that issue
2. Check if a PR already exists for this issue
- If PR exists and is open, review it and learn from it
- 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
### Creating PRs
When creating a PR, use the Gitea API directly with curl:
```bash
curl -X POST "https://$instance/api/v1/repos/$owner/$repo/pulls" \
-H "Authorization: Bearer \$GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Fix issue #$number",
"head": "fix/issue-$number",
"base": "main",
"body": "Closes #$number"
}'
```
Environment variable \$GITEA_TOKEN is available in your environment.
Work directory: $worktree_path"
if [ -n "$user_message" ]; then
echo "$base_message
Additional instructions from delegator:
$user_message"
else
echo "$base_message"
fi
} }
cmd_start() { cmd_start() {
@@ -334,36 +188,15 @@ cmd_start() {
exit 1 exit 1
fi fi
local session_file=$(issue_ref_to_filename "$issue_ref") local pm_agent_session_id=$(get_pm_agent_session_id)
local session_path="$SESSIONS_DIR/$session_file" if [ -z "$pm_agent_session_id" ] || [ "$pm_agent_session_id" = "null" ]; then
local worktree_exists=false echo "Error: PM agent session not found. Run 'kugetsu init' first." >&2
if worktree_exists "$issue_ref"; then
worktree_exists=true
fi
local session_exists=false
if [ -f "$session_path" ]; then
session_exists=true
fi
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 exit 1
fi fi
if $worktree_exists && ! $session_exists; then if worktree_exists "$issue_ref"; then
echo "Warning: Worktree exists but session is missing. Removing worktree to recreate both..." >&2 echo "Issue '$issue_ref' already has a worktree. Use 'kugetsu continue' instead."
remove_worktree_for_issue "$issue_ref" exit 1
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 fi
local active_count=$(count_active_dev_sessions) local active_count=$(count_active_dev_sessions)
@@ -372,12 +205,31 @@ cmd_start() {
exit 1 exit 1
fi fi
create_worktree "$issue_ref" "$WORKTREES_DIR" local session_file=$(issue_ref_to_filename "$issue_ref")
local session_path="$SESSIONS_DIR/$session_file"
local new_session_id=$(create_session "$base_session_id") if [ -f "$session_path" ]; then
echo "Session file already exists: $session_file"
echo "Use 'kugetsu continue $issue_ref' to continue work."
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"
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"
if [ -z "$new_session_id" ]; then if [ -z "$new_session_id" ]; then
echo "Error: Could not create session" >&2 echo "Error: Could not find newly created session" >&2
remove_worktree_for_issue "$issue_ref" remove_worktree_for_issue "$issue_ref"
exit 1 exit 1
fi fi
@@ -389,16 +241,6 @@ 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")
load_agent_env "dev"
cd "$worktree_path"
local sanitized_id=$(echo "$new_session_id" | sed 's/[^a-zA-Z0-9_-]/_/g')
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 "Session started for '$issue_ref': $new_session_id"
echo "Worktree: $worktree_path" echo "Worktree: $worktree_path"
} }
@@ -444,26 +286,20 @@ cmd_continue() {
local opencode_session_id=$(python3 -c "import json; print(json.load(open('$session_path')).get('opencode_session_id', ''))" 2>/dev/null || echo "") local opencode_session_id=$(python3 -c "import json; print(json.load(open('$session_path')).get('opencode_session_id', ''))" 2>/dev/null || echo "")
local worktree_path=$(python3 -c "import json; print(json.load(open('$session_path')).get('worktree_path', ''))" 2>/dev/null || echo "") local worktree_path=$(python3 -c "import json; print(json.load(open('$session_path')).get('worktree_path', ''))" 2>/dev/null || echo "")
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 if [ -n "$worktree_path" ] && [ -d "$worktree_path" ]; then
echo "Warning: Worktree is missing for '$session_name'. Recovering..." >&2 if [ -n "$message" ]; then
rm -f "$session_path" (cd "$worktree_path" && opencode run "$message" --continue --session "$opencode_session_id" "$@")
remove_issue_from_index "$session_name" else
echo "Calling cmd_start to create new session and worktree..." >&2 (cd "$worktree_path" && opencode --continue --session "$opencode_session_id" "$@")
cmd_start "$session_name" "$message" fi
return $? else
if [ -n "$message" ]; then
opencode run "$message" --continue --session "$opencode_session_id" "$@"
else
opencode --continue --session "$opencode_session_id" "$@"
fi
fi fi
if [ -z "$message" ]; then
message=$(build_dev_agent_message "$issue_ref" "")
fi
cd "$worktree_path"
local sanitized_id=$(echo "$opencode_session_id" | sed 's/[^a-zA-Z0-9_-]/_/g')
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 '$opencode_session_id'" >> "$LOGS_DIR/dev-$sanitized_id.log" 2>&1 &
} }
cmd_list() { cmd_list() {
@@ -614,7 +450,11 @@ cmd_destroy() {
local target="${1:-}" local target="${1:-}"
local force=false local force=false
if [ "${2:-}" = "-y" ]; then if [ "$target" = "--base" ]; then
target=""
fi
if [ "$2" = "-y" ]; then
force=true force=true
fi fi

View File

@@ -10,7 +10,7 @@ issue_ref_to_worktree_path() {
local issue_ref="$1" local issue_ref="$1"
local parent_dir="${2:-$WORKTREES_DIR}" local parent_dir="${2:-$WORKTREES_DIR}"
local worktree_name=$(issue_ref_to_worktree_name "$issue_ref") local worktree_name=$(issue_ref_to_worktree_name "$issue_ref")
echo "$parent_dir/$worktree_name" echo "$parent_dir/.kugetsu-worktrees/$worktree_name"
} }
issue_ref_to_branch_name() { issue_ref_to_branch_name() {
@@ -41,7 +41,7 @@ get_repo_url() {
fi fi
local instance=$(echo "$issue_ref" | cut -d'/' -f1 | cut -d'#' -f1) local instance=$(echo "$issue_ref" | cut -d'/' -f1 | cut -d'#' -f1)
local rest=$(echo "$issue_ref" | sed 's/^[^\/]*\///' | sed 's/#.*//') local rest=$(echo "$issue_ref" | sed 's/.*\///' | sed 's/#.*//')
if [ -n "${GIT_SERVERS[$instance]:-}" ]; then if [ -n "${GIT_SERVERS[$instance]:-}" ]; then
echo "${GIT_SERVERS[$instance]}/${rest}.git" echo "${GIT_SERVERS[$instance]}/${rest}.git"

View File

@@ -1,181 +0,0 @@
#!/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

View File

@@ -1,298 +0,0 @@
#!/bin/bash
# Git URL Parsing Tests for kugetsu
# Tests all functions that parse or construct git URLs and issue refs
#
# Run with: bash skills/kugetsu/tests/test-git-url-parsing.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../scripts/kugetsu-config.sh"
source "$SCRIPT_DIR/../scripts/kugetsu-worktree.sh"
source "$SCRIPT_DIR/../scripts/kugetsu-session.sh"
PASS=0
FAIL=0
pass() {
echo "PASS: $1"
PASS=$((PASS + 1))
}
fail() {
echo "FAIL: $1"
echo " Expected: $2"
echo " Got: $3"
FAIL=$((FAIL + 1))
}
echo "=== Git URL Parsing Test Suite ==="
echo ""
# Test: get_repo_url with standard GitHub issue ref
echo "--- Test: get_repo_url with github.com ---"
result=$(get_repo_url "github.com/shoko/kugetsu#14")
expected="https://github.com/shoko/kugetsu.git"
if [ "$result" = "$expected" ]; then
pass "get_repo_url standard github issue ref"
else
fail "get_repo_url standard github issue ref" "$expected" "$result"
fi
# Test: get_repo_url with custom instance
echo "--- Test: get_repo_url with git.fbrns.co ---"
result=$(get_repo_url "git.fbrns.co/shoko/kugetsu#158")
expected="https://git.fbrns.co/shoko/kugetsu.git"
if [ "$result" = "$expected" ]; then
pass "get_repo_url custom instance issue ref (ISSUE #181)"
else
fail "get_repo_url custom instance issue ref (ISSUE #181)" "$expected" "$result"
fi
# Test: get_repo_url with gitlab.com (if configured)
echo "--- Test: get_repo_url with gitlab.com ---"
if [ -n "${GIT_SERVERS[gitlab.com]:-}" ]; then
result=$(get_repo_url "gitlab.com/someuser/somerepo#42")
expected="https://gitlab.com/someuser/somerepo.git"
if [ "$result" = "$expected" ]; then
pass "get_repo_url gitlab.com issue ref"
else
fail "get_repo_url gitlab.com issue ref" "$expected" "$result"
fi
else
echo "SKIP: get_repo_url gitlab.com (not configured in GIT_SERVERS)"
fi
# Test: get_repo_url with bitbucket.org (if configured)
echo "--- Test: get_repo_url with bitbucket.org ---"
if [ -n "${GIT_SERVERS[bitbucket.org]:-}" ]; then
result=$(get_repo_url "bitbucket.org/myteam/myproject#7")
expected="https://bitbucket.org/myteam/myproject.git"
if [ "$result" = "$expected" ]; then
pass "get_repo_url bitbucket.org issue ref"
else
fail "get_repo_url bitbucket.org issue ref" "$expected" "$result"
fi
else
echo "SKIP: get_repo_url bitbucket.org (not configured in GIT_SERVERS)"
fi
# Test: get_repo_url with large issue number
echo "--- Test: get_repo_url with large issue number ---"
result=$(get_repo_url "github.com/shoko/kugetsu#999999")
expected="https://github.com/shoko/kugetsu.git"
if [ "$result" = "$expected" ]; then
pass "get_repo_url with large issue number"
else
fail "get_repo_url with large issue number" "$expected" "$result"
fi
# Test: issue_ref_to_worktree_name standard
echo "--- Test: issue_ref_to_worktree_name standard ---"
result=$(issue_ref_to_worktree_name "github.com/shoko/kugetsu#14")
expected="github.com-shoko-kugetsu-14"
if [ "$result" = "$expected" ]; then
pass "issue_ref_to_worktree_name standard"
else
fail "issue_ref_to_worktree_name standard" "$expected" "$result"
fi
# Test: issue_ref_to_worktree_name with custom instance
echo "--- Test: issue_ref_to_worktree_name custom instance ---"
result=$(issue_ref_to_worktree_name "git.fbrns.co/shoko/kugetsu#158")
expected="git.fbrns.co-shoko-kugetsu-158"
if [ "$result" = "$expected" ]; then
pass "issue_ref_to_worktree_name custom instance"
else
fail "issue_ref_to_worktree_name custom instance" "$expected" "$result"
fi
# Test: issue_ref_to_branch_name with number
echo "--- Test: issue_ref_to_branch_name with number ---"
result=$(issue_ref_to_branch_name "github.com/shoko/kugetsu#14")
expected="fix/issue-14"
if [ "$result" = "$expected" ]; then
pass "issue_ref_to_branch_name with number"
else
fail "issue_ref_to_branch_name with number" "$expected" "$result"
fi
# Test: issue_ref_to_branch_name with discuss suffix
# Note: #-discuss falls through to fix/issue-temp because #[^-]+$ doesn't match #-<text-with-hyphens>
echo "--- Test: issue_ref_to_branch_name with discuss suffix ---"
result=$(issue_ref_to_branch_name "github.com/shoko/kugetsu#-discuss")
expected="fix/issue-temp"
if [ "$result" = "$expected" ]; then
pass "issue_ref_to_branch_name with discuss suffix"
else
fail "issue_ref_to_branch_name with discuss suffix" "$expected" "$result"
fi
# Test: issue_ref_to_branch_name with identifier that has no hyphens
echo "--- Test: issue_ref_to_branch_name with pure identifier ---"
result=$(issue_ref_to_branch_name "github.com/shoko/kugetsu#someid")
expected="fix/someid"
if [ "$result" = "$expected" ]; then
pass "issue_ref_to_branch_name with pure identifier"
else
fail "issue_ref_to_branch_name with pure identifier" "$expected" "$result"
fi
# Test: issue_ref_to_branch_name without number
echo "--- Test: issue_ref_to_branch_name without number ---"
result=$(issue_ref_to_branch_name "github.com/shoko/kugetsu#abc")
expected="fix/abc"
if [ "$result" = "$expected" ]; then
pass "issue_ref_to_branch_name without number"
else
fail "issue_ref_to_branch_name without number" "$expected" "$result"
fi
# Test: extract_issue_ref_from_message with short form
echo "--- Test: extract_issue_ref_from_message short form ---"
result=$(extract_issue_ref_from_message "github.com/shoko/kugetsu#14")
expected="github.com/shoko/kugetsu#14"
if [ "$result" = "$expected" ]; then
pass "extract_issue_ref_from_message short form"
else
fail "extract_issue_ref_from_message short form" "$expected" "$result"
fi
# Test: extract_issue_ref_from_message with https URL
echo "--- Test: extract_issue_ref_from_message with https URL ---"
result=$(extract_issue_ref_from_message "https://github.com/shoko/kugetsu/issues/14")
expected="github.com/shoko/kugetsu#14"
if [ "$result" = "$expected" ]; then
pass "extract_issue_ref_from_message with https URL"
else
fail "extract_issue_ref_from_message with https URL" "$expected" "$result"
fi
# Test: extract_issue_ref_from_message with custom instance
echo "--- Test: extract_issue_ref_from_message custom instance ---"
result=$(extract_issue_ref_from_message "https://git.fbrns.co/shoko/kugetsu/issues/158")
expected="git.fbrns.co/shoko/kugetsu#158"
if [ "$result" = "$expected" ]; then
pass "extract_issue_ref_from_message custom instance"
else
fail "extract_issue_ref_from_message custom instance" "$expected" "$result"
fi
# Test: extract_issue_ref_from_message with empty message
echo "--- Test: extract_issue_ref_from_message empty message ---"
result=$(extract_issue_ref_from_message "")
expected=""
if [ "$result" = "$expected" ]; then
pass "extract_issue_ref_from_message empty message"
else
fail "extract_issue_ref_from_message empty message" "$expected" "$result"
fi
# Test: extract_issue_ref_from_message with no issue ref
echo "--- Test: extract_issue_ref_from_message no issue ref ---"
result=$(extract_issue_ref_from_message "Just a regular message without any issue reference")
expected=""
if [ "$result" = "$expected" ]; then
pass "extract_issue_ref_from_message no issue ref"
else
fail "extract_issue_ref_from_message no issue ref" "$expected" "$result"
fi
# Test: extract_issue_ref_from_message with gitlab URL
echo "--- Test: extract_issue_ref_from_message gitlab URL ---"
result=$(extract_issue_ref_from_message "https://gitlab.com/someuser/somerepo/issues/42")
expected="gitlab.com/someuser/somerepo#42"
if [ "$result" = "$expected" ]; then
pass "extract_issue_ref_from_message gitlab URL"
else
fail "extract_issue_ref_from_message gitlab URL" "$expected" "$result"
fi
# Test: validate_issue_ref valid format
echo "--- Test: validate_issue_ref valid format ---"
if validate_issue_ref "github.com/shoko/kugetsu#14" 2>/dev/null; then
pass "validate_issue_ref valid format"
else
fail "validate_issue_ref valid format" "exit 0" "exit non-zero"
fi
# Test: validate_issue_ref invalid format (missing parts)
echo "--- Test: validate_issue_ref invalid format ---"
if ! validate_issue_ref "invalid-ref" 2>/dev/null; then
pass "validate_issue_ref invalid format"
else
fail "validate_issue_ref invalid format" "exit non-zero" "exit 0"
fi
# Test: issue_ref_to_filename
echo "--- Test: issue_ref_to_filename ---"
result=$(issue_ref_to_filename "github.com/shoko/kugetsu#14")
expected="github.com-shoko-kugetsu-14.json"
if [ "$result" = "$expected" ]; then
pass "issue_ref_to_filename"
else
fail "issue_ref_to_filename" "$expected" "$result"
fi
# Test: filename_to_issue_ref
echo "--- Test: filename_to_issue_ref ---"
result=$(filename_to_issue_ref "github.com-shoko-kugetsu-14.json")
expected="github.com/shoko/kugetsu#14"
if [ "$result" = "$expected" ]; then
pass "filename_to_issue_ref"
else
fail "filename_to_issue_ref" "$expected" "$result"
fi
# Test: get_repo_url with org having hyphen
echo "--- Test: get_repo_url with hyphenated org ---"
result=$(get_repo_url "github.com/my-org/my-repo#1")
expected="https://github.com/my-org/my-repo.git"
if [ "$result" = "$expected" ]; then
pass "get_repo_url with hyphenated org"
else
fail "get_repo_url with hyphenated org" "$expected" "$result"
fi
# Test: get_repo_url with repo having dots
echo "--- Test: get_repo_url with dotted repo ---"
result=$(get_repo_url "github.com/shoko/kugetsu.utils#5")
expected="https://github.com/shoko/kugetsu.utils.git"
if [ "$result" = "$expected" ]; then
pass "get_repo_url with dotted repo"
else
fail "get_repo_url with dotted repo" "$expected" "$result"
fi
# Test: get_repo_url with underscore in username
echo "--- Test: get_repo_url with underscore in user ---"
result=$(get_repo_url "github.com/my_user/my_repo#10")
expected="https://github.com/my_user/my_repo.git"
if [ "$result" = "$expected" ]; then
pass "get_repo_url with underscore in user"
else
fail "get_repo_url with underscore in user" "$expected" "$result"
fi
# Test: get_repo_url with instance not in GIT_SERVERS (fallback)
echo "--- Test: get_repo_url with unknown instance ---"
result=$(get_repo_url "unknown.example.com/owner/repo#1")
expected="https://unknown.example.com/owner/repo.git"
if [ "$result" = "$expected" ]; then
pass "get_repo_url with unknown instance"
else
fail "get_repo_url with unknown instance" "$expected" "$result"
fi
echo ""
echo "=== Test Results ==="
echo "Passed: $PASS"
echo "Failed: $FAIL"
if [ $FAIL -eq 0 ]; then
echo "All tests passed!"
exit 0
else
echo "Some tests failed!"
exit 1
fi

View File

@@ -7,8 +7,6 @@
set -euo pipefail set -euo pipefail
KUGETSU="./skills/kugetsu/scripts/kugetsu" 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_ISSUE_REF="github.com/shoko/kugetsu#14"
TEST_DISCUSS_REF="github.com/shoko/kugetsu#-discuss" TEST_DISCUSS_REF="github.com/shoko/kugetsu#-discuss"
TEST_BASE_SESSION_ID="ses_test_base_123" TEST_BASE_SESSION_ID="ses_test_base_123"
@@ -20,28 +18,28 @@ PASS=0
FAIL=0 FAIL=0
cleanup() { cleanup() {
rm -rf "$TEST_KUGETSU_DIR" 2>/dev/null || true rm -rf ~/.kugetsu/sessions/* ~/.kugetsu/worktrees/* ~/.kugetsu/index.json 2>/dev/null || true
} }
setup_mock_base() { setup_mock_base() {
mkdir -p "$TEST_KUGETSU_DIR/sessions" "$TEST_KUGETSU_DIR/worktrees" mkdir -p ~/.kugetsu/sessions ~/.kugetsu/worktrees
cat > "$TEST_KUGETSU_DIR/index.json" << EOF cat > ~/.kugetsu/index.json << EOF
{ {
"base": "$TEST_BASE_SESSION_ID", "base": "$TEST_BASE_SESSION_ID",
"pm_agent": "$TEST_PM_AGENT_SESSION_ID", "pm_agent": "$TEST_PM_AGENT_SESSION_ID",
"issues": {} "issues": {}
} }
EOF EOF
cat > "$TEST_KUGETSU_DIR/sessions/$TEST_BASE_SESSION_FILE" << EOF cat > ~/.kugetsu/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"} {"type": "base", "opencode_session_id": "$TEST_BASE_SESSION_ID", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"}
EOF EOF
cat > "$TEST_KUGETSU_DIR/sessions/$TEST_PM_AGENT_SESSION_FILE" << EOF cat > ~/.kugetsu/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"} {"type": "pm_agent", "opencode_session_id": "$TEST_PM_AGENT_SESSION_ID", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"}
EOF EOF
} }
setup_mock_forked() { setup_mock_forked() {
cat > "$TEST_KUGETSU_DIR/index.json" << EOF cat > ~/.kugetsu/index.json << EOF
{ {
"base": "$TEST_BASE_SESSION_ID", "base": "$TEST_BASE_SESSION_ID",
"pm_agent": "$TEST_PM_AGENT_SESSION_ID", "pm_agent": "$TEST_PM_AGENT_SESSION_ID",
@@ -50,7 +48,7 @@ setup_mock_forked() {
} }
} }
EOF EOF
cat > "$TEST_KUGETSU_DIR/sessions/$TEST_FORKED_SESSION_FILE" << EOF cat > ~/.kugetsu/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"} {"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 EOF
} }
@@ -114,16 +112,16 @@ echo ""
# Test 3b: start fails without pm-agent # Test 3b: start fails without pm-agent
echo "--- Test: start without pm-agent session ---" echo "--- Test: start without pm-agent session ---"
rm -f $TEST_KUGETSU_DIR/index.json $TEST_KUGETSU_DIR/sessions/* rm -f ~/.kugetsu/index.json ~/.kugetsu/sessions/*
mkdir -p $TEST_KUGETSU_DIR/sessions mkdir -p ~/.kugetsu/sessions
cat > $TEST_KUGETSU_DIR/index.json << EOF cat > ~/.kugetsu/index.json << EOF
{ {
"base": "$TEST_BASE_SESSION_ID", "base": "$TEST_BASE_SESSION_ID",
"pm_agent": null, "pm_agent": null,
"issues": {} "issues": {}
} }
EOF EOF
cat > $TEST_KUGETSU_DIR/sessions/$TEST_BASE_SESSION_FILE << EOF cat > ~/.kugetsu/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"} {"type": "base", "opencode_session_id": "$TEST_BASE_SESSION_ID", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"}
EOF EOF
OUTPUT=$($KUGETSU start github.com/shoko/kugetsu#14 "test" 2>&1 || true) OUTPUT=$($KUGETSU start github.com/shoko/kugetsu#14 "test" 2>&1 || true)
@@ -178,7 +176,7 @@ echo ""
# Test 6c: index.json has pm_agent field # Test 6c: index.json has pm_agent field
echo "--- Test: index.json has pm_agent field ---" echo "--- Test: index.json has pm_agent field ---"
if grep -q '"pm_agent"' $TEST_KUGETSU_DIR/index.json; then if grep -q '"pm_agent"' ~/.kugetsu/index.json; then
pass "index.json has pm_agent field" pass "index.json has pm_agent field"
else else
fail "index.json missing pm_agent field" fail "index.json missing pm_agent field"
@@ -229,12 +227,12 @@ echo ""
echo "--- Test: destroy --pm-agent -y ---" echo "--- Test: destroy --pm-agent -y ---"
setup_mock_base setup_mock_base
OUTPUT=$($KUGETSU destroy --pm-agent -y 2>&1 || true) OUTPUT=$($KUGETSU destroy --pm-agent -y 2>&1 || true)
if [ -f $TEST_KUGETSU_DIR/sessions/$TEST_PM_AGENT_SESSION_FILE ]; then if [ -f ~/.kugetsu/sessions/$TEST_PM_AGENT_SESSION_FILE ]; then
fail "destroy --pm-agent -y removes pm-agent file" fail "destroy --pm-agent -y removes pm-agent file"
else else
pass "destroy --pm-agent -y removes pm-agent file" pass "destroy --pm-agent -y removes pm-agent file"
fi fi
if grep -q '"pm_agent": null' $TEST_KUGETSU_DIR/index.json; then if grep -q '"pm_agent": null' ~/.kugetsu/index.json; then
pass "destroy --pm-agent -y sets pm_agent to null in index" pass "destroy --pm-agent -y sets pm_agent to null in index"
else else
fail "destroy --pm-agent -y should set pm_agent to null" fail "destroy --pm-agent -y should set pm_agent to null"
@@ -245,7 +243,7 @@ echo ""
echo "--- Test: destroy --base -y ---" echo "--- Test: destroy --base -y ---"
setup_mock_base setup_mock_base
OUTPUT=$($KUGETSU destroy --base -y 2>&1 || true) OUTPUT=$($KUGETSU destroy --base -y 2>&1 || true)
if [ -f $TEST_KUGETSU_DIR/sessions/$TEST_BASE_SESSION_FILE ]; then if [ -f ~/.kugetsu/sessions/$TEST_BASE_SESSION_FILE ]; then
fail "destroy --base -y removes base file" fail "destroy --base -y removes base file"
else else
pass "destroy --base -y removes base file" pass "destroy --base -y removes base file"
@@ -294,7 +292,7 @@ echo ""
# Test 15: worktree path in session file # Test 15: worktree path in session file
echo "--- Test: worktree_path in session file ---" echo "--- Test: worktree_path in session file ---"
if grep -q "worktree_path" $TEST_KUGETSU_DIR/sessions/$TEST_FORKED_SESSION_FILE; then if grep -q "worktree_path" ~/.kugetsu/sessions/$TEST_FORKED_SESSION_FILE; then
pass "session file contains worktree_path" pass "session file contains worktree_path"
else else
fail "session file missing worktree_path" fail "session file missing worktree_path"
@@ -305,7 +303,7 @@ echo ""
echo "--- Test: prune with orphaned worktree ---" echo "--- Test: prune with orphaned worktree ---"
cleanup cleanup
setup_mock_base setup_mock_base
mkdir -p $TEST_KUGETSU_DIR/worktrees/orphaned-worktree mkdir -p ~/.kugetsu/worktrees/orphaned-worktree
OUTPUT=$($KUGETSU prune 2>&1 || true) OUTPUT=$($KUGETSU prune 2>&1 || true)
if echo "$OUTPUT" | grep -q "orphaned worktree"; then if echo "$OUTPUT" | grep -q "orphaned worktree"; then
pass "prune detects orphaned worktree" pass "prune detects orphaned worktree"
@@ -317,7 +315,7 @@ echo ""
# Test 17: prune --force removes orphaned worktrees # Test 17: prune --force removes orphaned worktrees
echo "--- Test: prune --force removes orphaned worktrees ---" echo "--- Test: prune --force removes orphaned worktrees ---"
OUTPUT=$($KUGETSU prune --force 2>&1 || true) OUTPUT=$($KUGETSU prune --force 2>&1 || true)
if [ -d $TEST_KUGETSU_DIR/worktrees/orphaned-worktree ]; then if [ -d ~/.kugetsu/worktrees/orphaned-worktree ]; then
fail "prune --force should remove orphaned worktree" fail "prune --force should remove orphaned worktree"
else else
pass "prune --force removes orphaned worktree" pass "prune --force removes orphaned worktree"
@@ -334,10 +332,10 @@ echo ""
echo "--- Test: destroy removes worktree ---" echo "--- Test: destroy removes worktree ---"
cleanup cleanup
setup_mock_forked setup_mock_forked
# remove_worktree_for_issue derives path from issue ref: $TEST_KUGETSU_DIR/worktrees/github.com-shoko-kugetsu-14 # remove_worktree_for_issue derives path from issue ref: ~/.kugetsu/worktrees/github.com-shoko-kugetsu-14
mkdir -p $TEST_KUGETSU_DIR/worktrees/github.com-shoko-kugetsu-14 mkdir -p ~/.kugetsu/worktrees/github.com-shoko-kugetsu-14
OUTPUT=$($KUGETSU destroy github.com/shoko/kugetsu#14 -y 2>&1 || true) OUTPUT=$($KUGETSU destroy github.com/shoko/kugetsu#14 -y 2>&1 || true)
if [ -d $TEST_KUGETSU_DIR/worktrees/github.com-shoko-kugetsu-14 ]; then if [ -d ~/.kugetsu/worktrees/github.com-shoko-kugetsu-14 ]; then
fail "destroy should remove worktree" fail "destroy should remove worktree"
else else
pass "destroy removes worktree" pass "destroy removes worktree"
@@ -347,7 +345,7 @@ echo ""
# Test 20: session file properly formatted for v2.2 # Test 20: session file properly formatted for v2.2
echo "--- Test: session file format v2.2 ---" echo "--- Test: session file format v2.2 ---"
setup_mock_forked setup_mock_forked
SESSION_CONTENT=$(cat $TEST_KUGETSU_DIR/sessions/$TEST_FORKED_SESSION_FILE) SESSION_CONTENT=$(cat ~/.kugetsu/sessions/$TEST_FORKED_SESSION_FILE)
if echo "$SESSION_CONTENT" | grep -q '"type": "forked"' && \ if echo "$SESSION_CONTENT" | grep -q '"type": "forked"' && \
echo "$SESSION_CONTENT" | grep -q '"worktree_path"'; then echo "$SESSION_CONTENT" | grep -q '"worktree_path"'; then
pass "session file has v2.2 format" pass "session file has v2.2 format"
@@ -369,8 +367,8 @@ echo ""
# Test 22: status when base missing # Test 22: status when base missing
echo "--- Test: status (base missing) ---" echo "--- Test: status (base missing) ---"
mkdir -p $TEST_KUGETSU_DIR/sessions mkdir -p ~/.kugetsu/sessions
cat > $TEST_KUGETSU_DIR/index.json << EOF cat > ~/.kugetsu/index.json << EOF
{ {
"base": null, "base": null,
"pm_agent": "$TEST_PM_AGENT_SESSION_ID", "pm_agent": "$TEST_PM_AGENT_SESSION_ID",
@@ -387,7 +385,7 @@ echo ""
# Test 23: status when pm-agent missing # Test 23: status when pm-agent missing
echo "--- Test: status (pm-agent missing) ---" echo "--- Test: status (pm-agent missing) ---"
cat > $TEST_KUGETSU_DIR/index.json << EOF cat > ~/.kugetsu/index.json << EOF
{ {
"base": "$TEST_BASE_SESSION_ID", "base": "$TEST_BASE_SESSION_ID",
"pm_agent": null, "pm_agent": null,
@@ -404,7 +402,7 @@ echo ""
# Test 24: status when pm-agent is "None" (Python None output) # Test 24: status when pm-agent is "None" (Python None output)
echo "--- Test: status (pm-agent is Python None) ---" echo "--- Test: status (pm-agent is Python None) ---"
cat > $TEST_KUGETSU_DIR/index.json << EOF cat > ~/.kugetsu/index.json << EOF
{ {
"base": "$TEST_BASE_SESSION_ID", "base": "$TEST_BASE_SESSION_ID",
"pm_agent": "None", "pm_agent": "None",
@@ -447,8 +445,8 @@ echo ""
# Test 27: delegate when pm-agent missing # Test 27: delegate when pm-agent missing
echo "--- Test: delegate (pm-agent missing) ---" echo "--- Test: delegate (pm-agent missing) ---"
cleanup cleanup
mkdir -p $TEST_KUGETSU_DIR/sessions $TEST_KUGETSU_DIR/worktrees mkdir -p ~/.kugetsu/sessions ~/.kugetsu/worktrees
cat > $TEST_KUGETSU_DIR/index.json << EOF cat > ~/.kugetsu/index.json << EOF
{ {
"base": "$TEST_BASE_SESSION_ID", "base": "$TEST_BASE_SESSION_ID",
"pm_agent": null, "pm_agent": null,
@@ -510,7 +508,7 @@ echo ""
# Test 32: delegate is fire-and-forget (returns immediately) # Test 32: delegate is fire-and-forget (returns immediately)
echo "--- Test: delegate is fire-and-forget ---" echo "--- Test: delegate is fire-and-forget ---"
setup_mock_base setup_mock_base
mkdir -p $TEST_KUGETSU_DIR/logs mkdir -p ~/.kugetsu/logs
START=$(date +%s) START=$(date +%s)
OUTPUT=$($KUGETSU delegate "test fire-and-forget" 2>&1 || true) OUTPUT=$($KUGETSU delegate "test fire-and-forget" 2>&1 || true)
END=$(date +%s) END=$(date +%s)
@@ -529,10 +527,10 @@ echo ""
# Test 33: delegate creates log file # Test 33: delegate creates log file
echo "--- Test: delegate creates log file ---" echo "--- Test: delegate creates log file ---"
setup_mock_base setup_mock_base
LOG_COUNT_BEFORE=$(ls $TEST_KUGETSU_DIR/logs/*.log 2>/dev/null | wc -l) LOG_COUNT_BEFORE=$(ls ~/.kugetsu/logs/*.log 2>/dev/null | wc -l)
$KUGETSU delegate "test log file" 2>&1 || true $KUGETSU delegate "test log file" 2>&1 || true
sleep 1 sleep 1
LOG_COUNT_AFTER=$(ls $TEST_KUGETSU_DIR/logs/*.log 2>/dev/null | wc -l) LOG_COUNT_AFTER=$(ls ~/.kugetsu/logs/*.log 2>/dev/null | wc -l)
if [ $LOG_COUNT_AFTER -gt $LOG_COUNT_BEFORE ]; then if [ $LOG_COUNT_AFTER -gt $LOG_COUNT_BEFORE ]; then
pass "delegate creates log file" pass "delegate creates log file"
else else
@@ -560,10 +558,10 @@ echo ""
# Test E2: env set creates file # Test E2: env set creates file
echo "--- Test: env set creates env file ---" echo "--- Test: env set creates env file ---"
mkdir -p $TEST_KUGETSU_DIR/env mkdir -p ~/.kugetsu/env
rm -f $TEST_KUGETSU_DIR/env/pm-agent.env rm -f ~/.kugetsu/env/pm-agent.env
$KUGETSU env set TEST_VAR "test_value" pm-agent 2>&1 || true $KUGETSU env set TEST_VAR "test_value" pm-agent 2>&1 || true
if [ -f $TEST_KUGETSU_DIR/env/pm-agent.env ]; then if [ -f ~/.kugetsu/env/pm-agent.env ]; then
pass "env set creates pm-agent.env file" pass "env set creates pm-agent.env file"
else else
fail "env set did not create pm-agent.env" fail "env set did not create pm-agent.env"
@@ -572,7 +570,7 @@ echo ""
# Test E3: env show masks sensitive values # Test E3: env show masks sensitive values
echo "--- Test: env show masks sensitive values ---" echo "--- Test: env show masks sensitive values ---"
cat > $TEST_KUGETSU_DIR/env/pm-agent.env << 'ENVEOF' cat > ~/.kugetsu/env/pm-agent.env << 'ENVEOF'
export GITEA_TOKEN="secret_token_123" export GITEA_TOKEN="secret_token_123"
export MY_VAR="visible_value" export MY_VAR="visible_value"
ENVEOF ENVEOF
@@ -586,14 +584,14 @@ echo ""
# Test E4: Variables exported to child processes via set -a # Test E4: Variables exported to child processes via set -a
echo "--- Test: set -a exports variables to children ---" echo "--- Test: set -a exports variables to children ---"
mkdir -p $TEST_KUGETSU_DIR/env mkdir -p ~/.kugetsu/env
cat > $TEST_KUGETSU_DIR/env/test.env << 'ENVEOF' cat > ~/.kugetsu/env/test.env << 'ENVEOF'
export EXPORT_TEST="exported_value" export EXPORT_TEST="exported_value"
SIMPLE_TEST="not_exported" SIMPLE_TEST="not_exported"
ENVEOF ENVEOF
# Simulate what cmd_delegate does # Simulate what cmd_delegate does
ENV_FILE="$TEST_KUGETSU_DIR/env/test.env" ENV_FILE="~/.kugetsu/env/test.env"
env_sh="set -a; source '$ENV_FILE'; set +a; " env_sh="set -a; source '$ENV_FILE'; set +a; "
result=$(bash -c "${env_sh}bash -c 'echo \$EXPORT_TEST'") result=$(bash -c "${env_sh}bash -c 'echo \$EXPORT_TEST'")
@@ -606,11 +604,11 @@ echo ""
# Test E5: pm-agent.env takes precedence # Test E5: pm-agent.env takes precedence
echo "--- Test: pm-agent.env takes precedence over default ---" echo "--- Test: pm-agent.env takes precedence over default ---"
mkdir -p $TEST_KUGETSU_DIR/env mkdir -p ~/.kugetsu/env
cat > $TEST_KUGETSU_DIR/env/default.env << 'ENVEOF' cat > ~/.kugetsu/env/default.env << 'ENVEOF'
export GITEA_TOKEN="default_token" export GITEA_TOKEN="default_token"
ENVEOF ENVEOF
cat > $TEST_KUGETSU_DIR/env/pm-agent.env << 'ENVEOF' cat > ~/.kugetsu/env/pm-agent.env << 'ENVEOF'
export GITEA_TOKEN="pm_agent_token" export GITEA_TOKEN="pm_agent_token"
ENVEOF ENVEOF
@@ -646,7 +644,7 @@ fi
echo "" echo ""
# Cleanup env files # Cleanup env files
rm -rf $TEST_KUGETSU_DIR/env 2>/dev/null || true rm -rf ~/.kugetsu/env 2>/dev/null || true
# Test E7: fix_session_permissions function exists # Test E7: fix_session_permissions function exists
echo "--- Test: fix_session_permissions function exists ---" echo "--- Test: fix_session_permissions function exists ---"
@@ -738,7 +736,7 @@ PASS=0
FAIL=0 FAIL=0
test_cleanup() { test_cleanup() {
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 rm -rf ~/.kugetsu/sessions/* ~/.kugetsu/worktrees/* ~/.kugetsu/index.json ~/.kugetsu/logs/* ~/.kugetsu/.agent_count ~/.kugetsu/.agent_lock 2>/dev/null || true
} }
pass() { pass() {
@@ -752,25 +750,25 @@ fail() {
} }
setup_mock_sessions() { setup_mock_sessions() {
mkdir -p $TEST_KUGETSU_DIR/sessions $TEST_KUGETSU_DIR/worktrees $TEST_KUGETSU_DIR/logs mkdir -p ~/.kugetsu/sessions ~/.kugetsu/worktrees ~/.kugetsu/logs
cat > $TEST_KUGETSU_DIR/index.json << INDEX cat > ~/.kugetsu/index.json << INDEX
{ {
"base": "ses_test_base_123", "base": "ses_test_base_123",
"pm_agent": "ses_test_pm_456", "pm_agent": "ses_test_pm_456",
"issues": {} "issues": {}
} }
INDEX INDEX
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": "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"}' > $TEST_KUGETSU_DIR/sessions/pm-agent.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
} }
# Test C1: Agent count file is initialized to 0 # Test C1: Agent count file is initialized to 0
echo "--- Test: agent count file initialized ---" echo "--- Test: agent count file initialized ---"
test_cleanup test_cleanup
mkdir -p $TEST_KUGETSU_DIR/sessions $TEST_KUGETSU_DIR/worktrees mkdir -p ~/.kugetsu/sessions ~/.kugetsu/worktrees
$KUGETSU list > /dev/null 2>&1 || true $KUGETSU list > /dev/null 2>&1 || true
if [ -f $TEST_KUGETSU_DIR/.agent_count ]; then if [ -f ~/.kugetsu/.agent_count ]; then
COUNT=$(cat $TEST_KUGETSU_DIR/.agent_count) COUNT=$(cat ~/.kugetsu/.agent_count)
if [ "$COUNT" = "0" ]; then if [ "$COUNT" = "0" ]; then
pass "agent count file initialized to 0" pass "agent count file initialized to 0"
else else
@@ -797,10 +795,10 @@ test_cleanup
setup_mock_sessions setup_mock_sessions
# Initialize count to 0 # Initialize count to 0
echo 0 > $TEST_KUGETSU_DIR/.agent_count echo 0 > ~/.kugetsu/.agent_count
# Verify initial state # Verify initial state
INITIAL=$(cat $TEST_KUGETSU_DIR/.agent_count) INITIAL=$(cat ~/.kugetsu/.agent_count)
if [ "$INITIAL" = "0" ]; then if [ "$INITIAL" = "0" ]; then
pass "agent count starts at 0" pass "agent count starts at 0"
else else
@@ -811,7 +809,7 @@ fi
$KUGETSU list > /dev/null 2>&1 $KUGETSU list > /dev/null 2>&1
# Verify count is still 0 (no slot leak) # Verify count is still 0 (no slot leak)
AFTER=$(cat $TEST_KUGETSU_DIR/.agent_count) AFTER=$(cat ~/.kugetsu/.agent_count)
if [ "$AFTER" = "0" ]; then if [ "$AFTER" = "0" ]; then
pass "agent count stays 0 after list (no leak)" pass "agent count stays 0 after list (no leak)"
else else