Compare commits

..

34 Commits

Author SHA1 Message Date
shokollm
7f7f8b1085 fix: multiple issues with queue daemon and agent forking
1. daemon: use $WORKTREES_DIR instead of $HOME/.kugetsu-worktrees
   - Worktrees are created in ~/.kugetsu/worktrees but daemon was checking ~/.kugetsu-worktrees
   - This caused cmd_start to be called when cmd_continue should have been

2. load_agent_env: add fallback to pm-agent.env
   - When dev.env and default.env don't exist, fallback to pm-agent.env
   - Ensures GITEA_TOKEN is available

3. Remove '&& disown' pattern that causes 'no such job' errors
   - nohup already makes process immune to SIGHUP
   - disown was causing errors because job context was lost
2026-04-06 09:14:37 +00:00
shokollm
6aa84a35b9 fix: syntax error in cmd_continue line 372 (issue #189)
Wrap nohup command in subshell to fix '&& disown' syntax error
2026-04-06 08:44:29 +00:00
6e2841bbda Merge pull request 'fix: cmd_start and cmd_continue now fork dev agent to work on task (issue #187)' (#188) from fix/issue-187-start-forks-agent into main 2026-04-06 10:13:42 +02:00
shokollm
99d09c7dda fix: cmd_start and cmd_continue now fork dev agent to work on task (issue #187)
- Added build_dev_agent_message() function to generate default workflow prompt
- cmd_start now forks the agent after creating session (fire-and-forget)
- cmd_continue now uses default message when empty and forks agent (fire-and-forget)
- Both commands log output to $LOGS_DIR/dev-$session_id.log
2026-04-06 08:05:42 +00:00
383f538438 Merge pull request 'fix: worktree created in wrong directory (issue #185)' (#186) from fix/issue-185-worktree-wrong-directory into main 2026-04-06 07:33:02 +02:00
shokollm
28b343f817 fix: worktree created in wrong directory (issue #185)
cmd_start was calling create_worktree without passing $WORKTREES_DIR,
causing worktrees to be created in $PWD (daemon's working directory)
instead of ~/.kugetsu-worktrees/
2026-04-06 05:18:31 +00:00
3a2095861f Merge pull request 'fix: destroy --base -y fails with 'target is required' (issue #183)' (#184) from fix/issue-183-destroy-base-requires-target into main 2026-04-06 06:16:30 +02:00
shokollm
836fde07fc fix: destroy --base -y fails with 'target is required' (issue #183)
Removed erroneous code that set target="" when target="--base".
This caused the early exit at 'if [ -z "$target" ]' to trigger
before reaching the actual --base handling at line 517.
2026-04-06 04:14:03 +00:00
4f2a04e0b4 Merge pull request 'fix: get_repo_url() strips user/org from path (issue #181)' (#182) from fix/issue-181-get-repo-url-strips-user-org into main 2026-04-06 06:07:46 +02:00
shokollm
c8bb0b36f4 fix: get_repo_url() strips user/org from path (issue #181)
The sed pattern 's/.*\///' on line 44 was removing everything up to the
LAST slash, but for issue refs like 'git.fbrns.co/shoko/kugetsu#158' it
should remove the instance prefix only (up to the FIRST slash).

Before: git.fbrns.co/shoko/kugetsu#158 -> kugetsu (WRONG)
After:  git.fbrns.co/shoko/kugetsu#158 -> shoko/kugetsu (CORRECT)

Also adds comprehensive test suite for git URL parsing functions:
- get_repo_url(), issue_ref_to_worktree_name(), issue_ref_to_branch_name()
- extract_issue_ref_from_message(), validate_issue_ref()
- issue_ref_to_filename(), filename_to_issue_ref()
2026-04-06 04:00:18 +00:00
937b7c69de Merge pull request 'fix: remove doubled .kugetsu-worktrees path segment in issue_ref_to_worktree_path' (#180) from fix/issue-179-worktree-path-doubled into main 2026-04-06 05:27:16 +02:00
shokollm
2051266809 fix: remove doubled .kugetsu-worktrees path segment in issue_ref_to_worktree_path
Fixes #179 - cmd_start fails due to incorrect worktree path being created
with .kugetsu-worktrees/.kugetsu-worktrees/ instead of just the worktree name.
2026-04-06 03:23:23 +00:00
80a3228be9 Merge pull request 'fix(kugetsu-session): extract_issue_ref_from_message fix URL parsing' (#178) from fix/issue-176-extract-issue-ref into main 2026-04-06 04:54:11 +02:00
shokollm
d68a63af41 fix(kugetsu-session): extract_issue_ref_from_message fix URL parsing
Fix issue where full URLs like #158
were incorrectly parsed to 'shoko/kugetsu/issues#158' instead of
'git.fbrns.co/shoko/kugetsu#158'.

The regex now correctly extracts instance, owner, repo, and number
using bash regex capture groups instead of fragile sed/cut pipeline.
2026-04-06 02:51:51 +00:00
56310755b8 Merge pull request 'fix: queue daemon crashes on every task (issue #174)' (#175) from fix/issue-174-queue-daemon-crashes into main 2026-04-06 04:16:12 +02:00
shokollm
fb33be3a64 fix: queue daemon crashes on every task - 3 bugs
Fix 3 bugs from issue #174 that caused silent failure loop:

1. kugetsu-log.sh: Fix json.loads with newlines
   - Previously, notifications JSON was embedded in a single-quoted Python
     string literal, but newlines in the JSON broke the Python parser.
   - Fix: Pass JSON via stdin to Python instead of embedding in string.

2. kugetsu-queue-daemon.sh: Create logs directory during init
   - The logs/ directory ($LOGS_DIR) was never created during kugetsu init.
   - Fix: Add mkdir -p for LOGS_DIR, WORKTREES_DIR, QUEUE_DIR, and
     QUEUE_ITEMS_DIR to ensure_dirs() and ensure_queue_dirs().

3. kugetsu: Fix parse_issue_ref_from_message URL parsing
   - The function used buggy grep/sed to parse URLs like
     #158
   - Fix: Use bash regex (=~) for reliable URL parsing with proper
     capture groups.

Additional improvements:
   - ensure_dirs() now creates all necessary directories instead of just
     SESSIONS_DIR
   - ensure_queue_dirs() now also creates QUEUE_DIR and LOGS_DIR
   - parse_issue_ref_from_message uses consistent bash regex approach
     for all URL patterns
2026-04-06 02:14:27 +00:00
1b19c9a92c Merge pull request 'fix: init script captures wrong session IDs when old sessions exist' (#173) from fix/issue-172-init-script-wrong-session-ids into main 2026-04-06 03:09:30 +02:00
shokollm
85a4239383 fix: init script captures wrong session IDs when old sessions exist
The init script used 'tail -1' to find newly created sessions, which
fails when old sessions exist because it picks an existing session
instead of the newly created one.

The fix captures session IDs before and after creating new sessions,
then diffs to identify newly created sessions.

Fixes #172
2026-04-06 01:08:09 +00:00
shokollm
91b51f62c0 docs: update changelog for v0.2.4 2026-04-06 00:49:00 +00:00
7234837284 Merge pull request 'fix(kugetsu): remove duplicate update_queue_item_state to use fixed version from kugetsu-index.sh' (#171) from fix/issue-170-duplicate-update-queue into main 2026-04-06 02:42:24 +02:00
shokollm
59f6a4883e fix(kugetsu): remove duplicate update_queue_item_state to use fixed version from kugetsu-index.sh
The main kugetsu script had its own update_queue_item_state() definition
with broken os.system() calls that overwrote the fixed version in kugetsu-index.sh.

Now kugetsu will use the fixed version from kugetsu-index.sh (which sources
kugetsu-log.sh) since that module is sourced first.

Fixes #170
2026-04-06 00:40:55 +00:00
9667c3e800 Merge pull request 'fix(kugetsu-index): call kugetsu_add_notification from bash instead of os.system()' (#169) from fix/issue-167-notification-bash into main 2026-04-06 02:35:36 +02:00
shokollm
796e1fe454 fix(kugetsu-index): call kugetsu_add_notification from bash instead of os.system()
os.system() spawns a new subprocess that cannot access bash functions.
Now calling kugetsu_add_notification directly from bash after Python updates JSON.

Fixes #167
2026-04-06 00:33:35 +00:00
84c59a3b64 Merge pull request 'fix(kugetsu): queue daemon improvements - locking, error handling, cmd_delegate enqueue' (#164) from fix/issue-156-queue-fixes into main 2026-04-06 02:06:36 +02:00
shokollm
dc9d4d7327 Merge origin/main into fix/issue-156-queue-fixes 2026-04-06 00:02:47 +00:00
shokollm
bdcb7a476c fix(queue-daemon): add locking, proper state updates, and error handling
- Add acquire_lock/release_lock to prevent daemon vs manual conflicts
- Check cmd_start/cmd_continue success before updating state to 'notified'
- Set state to 'error' if command fails
- Track actual session_id from session file after cmd_start completes
- Release lock when task completes (success or error)
- Use load_agent_env 'pm-agent' for GITEA_TOKEN

Fixes critical race conditions and failure handling in queue processing
2026-04-06 00:01:41 +00:00
77cf817568 Merge pull request 'fix(kugetsu): return proper JSON array from get_pending_tasks()' (#163) from fix/issue-155-queue-list-json into main 2026-04-06 01:45:09 +02:00
shokollm
0f6a30f01c fix(kugetsu): return proper JSON array from get_pending_tasks() 2026-04-05 23:43:52 +00:00
77b0963fa4 Merge pull request 'fix(queue-daemon): source pm-agent.env for GITEA_TOKEN instead of default.env' (#162) from fix/issue-160-gitea-token-from-pm-agent into main 2026-04-06 01:42:04 +02:00
shokollm
f39e39156a fix(queue-daemon): source pm-agent.env for GITEA_TOKEN instead of default.env 2026-04-05 23:38:07 +00:00
shokollm
5a0a54898b fix(kugetsu): kugetsu-session.sh needs to source required modules
When daemon sources kugetsu-session.sh to call cmd_start/cmd_continue,
it needs access to functions from kugetsu-config.sh, kugetsu-index.sh,
kugetsu-worktree.sh, and kugetsu-log.sh. Add sourcing at top of
kugetsu-session.sh.
2026-04-05 22:20:49 +00:00
shokollm
b1028a6556 fix(kugetsu): move queue functions to kugetsu-index.sh for daemon access
The daemon (kugetsu-queue-daemon.sh) sources kugetsu-index.sh but not the main kugetsu script.
Move update_queue_item_state and kugetsu_add_notification to kugetsu-index.sh
so the daemon can use these functions when processing tasks.
2026-04-05 22:17:59 +00:00
shokollm
270219873f fix(kugetsu): cmd_delegate should enqueue instead of calling cmd_start
When cmd_delegate detects an issue ref with number (e.g. git.fbrns.co/shoko/kugetsu#158),
it was calling cmd_start directly which tries to create worktree and clone.
This breaks the queue-based workflow where daemon should handle task execution.

Now cmd_delegate calls enqueue_task to add to queue, and daemon processes
tasks by calling cmd_start/cmd_continue as appropriate.
2026-04-05 22:05:18 +00:00
deb18f1e32 Merge pull request 'fix(kugetsu): queue daemon runs PM agent in correct worktree with proper token' (#157) from fix/issue-156 into main 2026-04-05 23:39:35 +02:00
9 changed files with 699 additions and 163 deletions

View File

@@ -6,6 +6,25 @@ 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

@@ -93,6 +93,10 @@ 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() {
@@ -257,7 +261,9 @@ 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() {
@@ -310,12 +316,31 @@ get_pending_tasks() {
return return
fi fi
find "$QUEUE_ITEMS_DIR" -name "*.json" -type f 2>/dev/null | while read -r file; do python3 -c "
local state=$(python3 -c "import json; print(json.load(open('$file')).get('state', ''))" 2>/dev/null || echo "") import json
if [ "$state" = "pending" ]; then import os
cat "$file" import sys
fi
done | head -"$limit" queue_dir = os.environ.get('QUEUE_ITEMS_DIR', '')
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() {
@@ -342,55 +367,6 @@ 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
@@ -878,6 +854,11 @@ 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=""
@@ -885,21 +866,20 @@ parse_issue_ref_from_message() {
local repo="" local repo=""
local issue_number="" local issue_number=""
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 if [[ "$message" =~ (https?://)?([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/([a-zA-Z0-9._-]+)/([a-zA-Z0-9._-]+)/(issues|pull)/([0-9]+) ]]; then
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/\/[^/]*\/[^/]*$//') gitserver="${BASH_REMATCH[2]}"
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) owner="${BASH_REMATCH[3]}"
owner=$(echo "$full_path" | cut -d'/' -f2) repo="${BASH_REMATCH[4]}"
repo=$(echo "$full_path" | cut -d'/' -f3) issue_number="${BASH_REMATCH[6]}"
issue_number=$(echo "$full_path" | grep -oE '[0-9]+$' | head -1) elif [[ "$message" =~ (https?://)?([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/([a-zA-Z0-9._-]+)/([a-zA-Z0-9._-]+)#([0-9]+) ]]; then
elif echo "$message" | grep -qE '[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+#[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) owner="${BASH_REMATCH[3]}"
owner=$(echo "$gitserver" | cut -d'/' -f2) repo="${BASH_REMATCH[4]}"
repo=$(echo "$gitserver" | cut -d'/' -f3) issue_number="${BASH_REMATCH[5]}"
issue_number=$(echo "$message" | grep -oE '#[0-9]+' | grep -oE '[0-9]+' | head -1) elif [[ "$message" =~ ([a-zA-Z0-9._-]+)/([a-zA-Z0-9._-]+)#([0-9]+) ]]; then
elif echo "$message" | grep -qE '[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+#([0-9]+)'; then owner="${BASH_REMATCH[1]}"
owner=$(echo "$message" | grep -oE '[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+#' | sed 's/#$//' | cut -d'/' -f1) repo="${BASH_REMATCH[2]}"
repo=$(echo "$message" | grep -oE '[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+#' | sed 's/#$//' | cut -d'/' -f2) issue_number="${BASH_REMATCH[3]}"
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}"

View File

@@ -53,5 +53,9 @@ 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
} }

View File

@@ -146,3 +146,98 @@ filename_to_issue_ref() {
local name="${filename%.json}" local name="${filename%.json}"
echo "$name" | sed 's-\([0-9]*\)$-#\1-' | sed 's/-/\//g' 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

@@ -43,15 +43,24 @@ kugetsu_add_notification() {
notifications=$(cat "$NOTIFICATIONS_FILE") notifications=$(cat "$NOTIFICATIONS_FILE")
fi fi
local new_notification=$(python3 -c "import json; print(json.dumps({ notifications=$(echo "$notifications" | python3 -c "
import json
import sys
notifications = json.load(sys.stdin)
new_notification = {
'type': '$notification_type', 'type': '$notification_type',
'message': '$message', 'message': '''$message'''.replace('\"', '\"'),
'issue_ref': '$issue_ref', 'issue_ref': '$issue_ref' if '$issue_ref' else None,
'timestamp': '$timestamp', 'timestamp': '$timestamp',
'read': False 'read': False
}))") }
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))") 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"
} }

View File

@@ -8,12 +8,29 @@ 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 GITEA_TOKEN from default.env load_agent_env "pm-agent"
if [ -f "$HOME/.kugetsu/env/default.env" ]; then
source "$HOME/.kugetsu/env/default.env" acquire_lock() {
fi 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 if a notified task has completed (forked session ended or has new commits)
check_task_completion() { check_task_completion() {
local item="$1" local item="$1"
local queue_id=$(basename "$item" .json) local queue_id=$(basename "$item" .json)
@@ -21,21 +38,16 @@ check_task_completion() {
[ "$state" = "notified" ] || return 0 [ "$state" = "notified" ] || return 0
# Use opencode_session_id (the forked session, not the parent pm_session)
local session_id=$(python3 -c "import json; print(json.load(open('$item')).get('opencode_session_id', ''))" 2>/dev/null) local 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 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 no session tracked, skip if [ -n "$pid" ] && [ "$pid" != "None" ]; then
[ -n "$session_id" ] || return 0 if ! kill -0 "$pid" 2>/dev/null; then
# Check if forked session still exists in opencode
if ! opencode session list 2>/dev/null | grep -q "$session_id"; then
# Forked session ended — check if work was done
local worktree_path=$(issue_ref_to_worktree_path "$issue_ref" "$HOME/.kugetsu-worktrees") local worktree_path=$(issue_ref_to_worktree_path "$issue_ref" "$HOME/.kugetsu-worktrees")
local has_commits=false local has_commits=false
if [ -d "$worktree_path" ] && [ -d "$worktree_path/.git" ]; then if [ -d "$worktree_path" ] && [ -d "$worktree_path/.git" ]; then
# Check if worktree has new commits beyond origin/main
if [ -n "$(git -C "$worktree_path" log --oneline origin/main..HEAD 2>/dev/null)" ]; then if [ -n "$(git -C "$worktree_path" log --oneline origin/main..HEAD 2>/dev/null)" ]; then
has_commits=true has_commits=true
fi fi
@@ -48,44 +60,94 @@ check_task_completion() {
update_queue_item_state "$queue_id" "error" update_queue_item_state "$queue_id" "error"
echo "Task $queue_id ($issue_ref) marked error — no commits found after session ended" echo "Task $queue_id ($issue_ref) marked error — no commits found after session ended"
fi 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" "$HOME/.kugetsu-worktrees")
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 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
# Check completion of notified tasks
if [ -d "$QUEUE_ITEMS_DIR" ]; then if [ -d "$QUEUE_ITEMS_DIR" ]; then
for item in "$QUEUE_ITEMS_DIR"/*.json; do for item in "$QUEUE_ITEMS_DIR"/*.json; do
[ -f "$item" ] || continue [ -f "$item" ] || continue
check_task_completion "$item" check_task_completion "$item"
done done
# Process pending tasks
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
queue_id=$(basename "$item" .json) process_task "$item"
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)
# Source session management and use cmd_start/cmd_continue
source "$SCRIPT_DIR/kugetsu-session.sh"
if worktree_exists "$issue_ref" "$HOME/.kugetsu-worktrees" || [ -f "$SESSIONS_DIR/$(issue_ref_to_filename "$issue_ref").json" ]; then
# Continue existing session
log_file="$LOGS_DIR/delegate-$(date +%s).log"
cmd_continue "$issue_ref" "$message" >> "$log_file" 2>&1 &
pid=$!
update_queue_item_state "$queue_id" "notified" "" "$pid"
echo "Task $queue_id continued for $issue_ref"
else
# Start new session
log_file="$LOGS_DIR/delegate-$(date +%s).log"
cmd_start "$issue_ref" "$message" >> "$log_file" 2>&1 &
pid=$!
update_queue_item_state "$queue_id" "notified" "" "$pid"
echo "Task $queue_id started for $issue_ref"
fi
fi fi
done done
fi fi

View File

@@ -1,6 +1,13 @@
#!/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
@@ -74,9 +81,20 @@ 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 session_ids=$(opencode session list 2>/dev/null | grep -E '^ses_' | awk '{print $1}' | tail -1) local after_sessions=$(opencode session list 2>/dev/null | grep -E '^ses_' | awk '{print $1}' || true)
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
@@ -88,9 +106,20 @@ 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
local pm_session_ids=$(opencode session list 2>/dev/null | grep -E '^ses_' | grep -v "$session_ids" | tail -1) after_sessions=$(opencode session list 2>/dev/null | grep -E '^ses_' | awk '{print $1}' || true)
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"
@@ -127,13 +156,11 @@ 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 url="${BASH_REMATCH[1]}" local instance="${BASH_REMATCH[2]}"
local path=$(echo "$url" | sed 's|https\?://||' | cut -d'/' -f2-) local owner="${BASH_REMATCH[3]}"
local instance=$(echo "$path" | cut -d'/' -f1) local repo="${BASH_REMATCH[4]}"
local owner=$(echo "$path" | cut -d'/' -f2) local num="${BASH_REMATCH[6]}"
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
@@ -153,10 +180,12 @@ 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
cmd_start "$issue_ref" "$message" # Enqueue for daemon to process via cmd_start/cmd_continue
enqueue_task "$issue_ref" "$message"
return return
fi fi
# No issue ref detected — delegate directly to PM agent (legacy path)
local pm_session=$(get_pm_agent_session_id) local pm_session=$(get_pm_agent_session_id)
if [ -z "$pm_session" ] || [ "$pm_session" = "null" ] || [ "$pm_session" = "None" ]; then if [ -z "$pm_session" ] || [ "$pm_session" = "null" ] || [ "$pm_session" = "None" ]; then
echo "Error: PM agent session not found. Run 'kugetsu init' first." >&2 echo "Error: PM agent session not found. Run 'kugetsu init' first." >&2
@@ -165,11 +194,50 @@ cmd_delegate() {
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"
nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '$message' --continue --session '$pm_session' >> '$log_file' 2>&1" > /dev/null 2>&1 & load_agent_env "pm-agent"
disown 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"))" echo "Delegated to PM agent (logged to $(basename "$log_file"))"
} }
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
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() {
local issue_ref="${1:-}" local issue_ref="${1:-}"
local message="${2:-}" local message="${2:-}"
@@ -217,7 +285,7 @@ cmd_start() {
local before_sessions=$(opencode session list 2>/dev/null | grep -oP '^ses_\w+' | sort) local before_sessions=$(opencode session list 2>/dev/null | grep -oP '^ses_\w+' | sort)
local before_set="|$before_sessions|" local before_set="|$before_sessions|"
create_worktree "$issue_ref" create_worktree "$issue_ref" "$WORKTREES_DIR"
local after_sessions=$(opencode session list 2>/dev/null | grep -oP '^ses_\w+' | sort) local after_sessions=$(opencode session list 2>/dev/null | grep -oP '^ses_\w+' | sort)
local new_session_id="" local new_session_id=""
@@ -241,6 +309,13 @@ 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"
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 "Session started for '$issue_ref': $new_session_id"
echo "Worktree: $worktree_path" echo "Worktree: $worktree_path"
} }
@@ -286,19 +361,17 @@ 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 "$message" ]; then
message=$(build_dev_agent_message "$issue_ref" "")
fi
if [ -n "$worktree_path" ] && [ -d "$worktree_path" ]; then if [ -n "$worktree_path" ] && [ -d "$worktree_path" ]; then
if [ -n "$message" ]; then cd "$worktree_path"
(cd "$worktree_path" && opencode run "$message" --continue --session "$opencode_session_id" "$@") 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 else
(cd "$worktree_path" && opencode --continue --session "$opencode_session_id" "$@") 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
else
if [ -n "$message" ]; then
opencode run "$message" --continue --session "$opencode_session_id" "$@"
else
opencode --continue --session "$opencode_session_id" "$@"
fi
fi fi
} }
@@ -450,10 +523,6 @@ cmd_destroy() {
local target="${1:-}" local target="${1:-}"
local force=false local force=false
if [ "$target" = "--base" ]; then
target=""
fi
if [ "$2" = "-y" ]; then 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/.kugetsu-worktrees/$worktree_name" echo "$parent_dir/$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

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