diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ae2f1e8..7af9dcb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,10 +2,10 @@ ## Workflow -1. Create a branch for your work: `git checkout -b fix/issue-N-name` or `git checkout -b docs/topic-name` +1. Create a branch for your work: `git checkout -b fix/issue-N-name` or `git checkout -b feat/issue-N-feature-name` 2. Make changes and commit with clear messages 3. Open a Pull Request for review -4. Do not merge directly to `master` for reviewable changes +4. Do not merge directly to `main` or `develop` for reviewable changes 5. After approval, squash and merge ## Guidelines @@ -14,10 +14,53 @@ - Keep PRs focused and reasonably sized - Document any non-obvious decisions - Test changes before submitting +- See [VERSIONING.md](VERSIONING.md) for backport compatibility rules ## Branches -- `master` — stable, reviewed content only +### Primary Branches + +- `main` — stable 0.1.x releases, production-ready code +- `develop` — experimental 0.2.x work, next major version + +### Feature Branches + - `fix/*` — bug fixes +- `feat/*` — new features - `docs/*` — documentation updates -- `research/*` — new research notes +- `refactor/*` — code refactoring (no behavior change) + +## Branch Model + +``` +main (0.1.x stable) + └── v0.1.0, v0.1.1, v0.1.2, ... + +develop (0.2.x experimental) + └── (next major version work) +``` + +### Which Branch to Target? + +| Change Type | Target Branch | Backport? | +|-------------|---------------|-----------| +| Bug fix | `main` | N/A | +| Documentation | `main` | N/A | +| New feature (backport-compatible) | `main` | Can cherry-pick to `develop` | +| Experimental feature | `develop` | No | +| Breaking change | `develop` | No | + +## Backport Compatibility + +Before merging, consider if your change is backport-compatible: + +- **YES**: Bug fixes, docs, adding new optional inputs +- **NO**: Changing behavior, changing defaults, removing features + +See [VERSIONING.md](VERSIONING.md) for full policy. + +## Release Process + +1. Bug fixes and docs → directly to `main` +2. New features → `develop` or feature branches → `develop` +3. When `develop` is stable enough → merge to `main` for release diff --git a/VERSIONING.md b/VERSIONING.md new file mode 100644 index 0000000..4eae7f5 --- /dev/null +++ b/VERSIONING.md @@ -0,0 +1,71 @@ +# Versioning Policy + +## Branch Strategy + +Kugetsu uses a dual-branch model: + +| Branch | Purpose | Version | Stability | +|--------|---------|---------|-----------| +| `main` | Stable releases | 0.1.x | Production-ready | +| `develop` | Experimental work | 0.2.x | Active development | + +### Branch Definitions + +- **`main`**: Contains the latest stable 0.1.x releases. All changes here should be production-ready and backport-compatible when possible. + +- **`develop`**: Contains work for the next major version (0.2.x). This branch may contain experimental features that could change or be removed. + +## Version Format + +Versions follow [Semantic Versioning](https://semver.org/): +``` +MAJOR.MINOR.PATCH +``` + +- **MAJOR**: Incompatible API/behavior changes +- **MINOR**: New functionality (backward-compatible) +- **PATCH**: Bug fixes (backward-compatible) + +## Backport Compatibility + +### Backport-Compatible Changes (0.1.x) +- Bug fixes +- Documentation updates +- Performance improvements +- Adding new inputs/options (must have sensible defaults) +- Changes that only affect 0.2.x-specific features + +### NOT Backport-Compatible +- Removing or renaming existing options +- Changing default values of existing options +- Changing behavior of existing commands +- Introducing breaking changes to the API/shell interface + +## Deprecation Policy + +When introducing breaking changes: + +1. **Deprecate in minor X**: Add warning messages, document the change +2. **Remove in major X+1**: The breaking change is removed in the next major version + +Example: +- Option `--old-flag` deprecated in v0.1.5 +- Option `--old-flag` removed in v1.0.0 (not v0.2.0) + +## What Constitutes a Version Bump + +| Change Type | Version Bump | +|-------------|--------------| +| Add new command/option | MINOR | +| Bug fix | PATCH | +| Change default value | MINOR (may warrant PATCH) | +| Add new required input | MAJOR | +| Remove deprecated feature | MAJOR | +| Change behavior of existing command | MINOR (needs deprecation first) | + +## Release Process + +1. Changes are developed on feature branches +2. PRs are opened against `main` for 0.1.x changes, or `develop` for 0.2.x +3. After review and approval, changes are squash-merged +4. Releases are tagged from `main` after significant changes diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md new file mode 100644 index 0000000..cb4dc74 --- /dev/null +++ b/docs/CHANGELOG.md @@ -0,0 +1,111 @@ +# Changelog + +All notable changes to kugetsu are documented here. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [Unreleased] + +## [v0.2.1] - 2026-04-03 + +### Fixed +- Prevent excess agent spawning with flock + sequential processing + +## [v0.2.0] - 2026-03-30 + +### Added +- Queue system with background daemon +- Agent timeout handling +- Context dump/load for session isolation +- PR tracking and safe destroy + +## [v0.1.13] - 2026-03-29 + +### Fixed +- Add missing closing parenthesis in process_queue Python extraction + +## [v0.1.12] - 2026-03-25 + +### Added +- Post-comment helper for PM agent + +## [v0.1.11] - 2026-03-20 + +### Fixed +- Wrap cmd_continue in subshell with cd for correct worktree dir + +## [v0.1.10] - 2026-03-15 + +### Fixed +- destroy --base now also deletes PM agent session + +## [v0.1.9] - 2026-03-10 + +### Added +- init creates base session in ~/.kugetsu-worktrees +- Adds context to forked sessions +- Clears logs on init + +## [v0.1.8] - 2026-03-05 + +### Fixed +- destroy --base and --pm-agent actually delete opencode sessions + +## [v0.1.7] - 2026-02-28 + +### Fixed +- Warn if init run from non-empty directory + +## [v0.1.6] - 2026-02-20 + +### Fixed +- Detect session via DB query instead of opencode session list + +## [v0.1.5] - 2026-02-15 + +### Fixed +- Update forked session permissions after detection + +## [v0.1.4] - 2026-02-10 + +### Fixed +- Call fix_session_permissions before forking + +## [v0.1.3] - 2026-02-05 + +### Fixed +- Session detection ordering bug and debugging + +## [v0.1.2] - 2026-01-28 + +### Fixed +- Improve session detection in cmd_start with retry logic and logging + +## [v0.1.1] - 2026-01-20 + +### Fixed +- Use cd + worktree inside parent dir instead of --dir flag + +## [v0.1.0] - 2026-01-15 + +### Added +- KUGETSU_VERBOSITY for PM agent output control +- Initial documented release + +[Unreleased]: https://git.fbrns.co/shoko/kugetsu/compare/v0.2.1...HEAD +[v0.2.1]: https://git.fbrns.co/shoko/kugetsu/compare/v0.2.0...v0.2.1 +[v0.2.0]: https://git.fbrns.co/shoko/kugetsu/compare/v0.1.13...v0.2.0 +[v0.1.13]: https://git.fbrns.co/shoko/kugetsu/compare/v0.1.12...v0.1.13 +[v0.1.12]: https://git.fbrns.co/shoko/kugetsu/compare/v0.1.11...v0.1.12 +[v0.1.11]: https://git.fbrns.co/shoko/kugetsu/compare/v0.1.10...v0.1.11 +[v0.1.10]: https://git.fbrns.co/shoko/kugetsu/compare/v0.1.9...v0.1.10 +[v0.1.9]: https://git.fbrns.co/shoko/kugetsu/compare/v0.1.8...v0.1.9 +[v0.1.8]: https://git.fbrns.co/shoko/kugetsu/compare/v0.1.7...v0.1.8 +[v0.1.7]: https://git.fbrns.co/shoko/kugetsu/compare/v0.1.6...v0.1.7 +[v0.1.6]: https://git.fbrns.co/shoko/kugetsu/compare/v0.1.5...v0.1.6 +[v0.1.5]: https://git.fbrns.co/shoko/kugetsu/compare/v0.1.4...v0.1.5 +[v0.1.4]: https://git.fbrns.co/shoko/kugetsu/compare/v0.1.3...v0.1.4 +[v0.1.3]: https://git.fbrns.co/shoko/kugetsu/compare/v0.1.2...v0.1.3 +[v0.1.2]: https://git.fbrns.co/shoko/kugetsu/compare/v0.1.1...v0.1.2 +[v0.1.1]: https://git.fbrns.co/shoko/kugetsu/compare/v0.1.0...v0.1.1 +[v0.1.0]: https://git.fbrns.co/shoko/kugetsu/releases/tag/v0.1.0 diff --git a/skills/kugetsu/scripts/kugetsu b/skills/kugetsu/scripts/kugetsu index b0794f1..3ba539f 100755 --- a/skills/kugetsu/scripts/kugetsu +++ b/skills/kugetsu/scripts/kugetsu @@ -447,6 +447,534 @@ check_task_timeouts() { done } +cleanup_old_queue_items() { + local days="${QUEUE_CLEANUP_AGE_DAYS:-7}" + + if [ ! -d "$QUEUE_ITEMS_DIR" ]; then + return + fi + + find "$QUEUE_ITEMS_DIR" -name "*.json" -type f -mtime "+$days" 2>/dev/null | while read -r file; do + local state=$(python3 -c "import json; print(json.load(open('$file')).get('state', ''))" 2>/dev/null || echo "") + if [ "$state" = "completed" ] || [ "$state" = "error" ]; then + rm -f "$file" + echo "Cleaned up: $(basename "$file")" + fi + done +} +update_session_pr_url() { + local issue_ref="$1" + local pr_url="$2" + + if [ -z "$issue_ref" ] || [ -z "$pr_url" ]; then + echo "Error: update_session_pr_url requires and " >&2 + return 1 + fi + + local session_file=$(get_session_for_issue "$issue_ref") + if [ -z "$session_file" ] || [ "$session_file" = "null" ]; then + echo "Error: No session found for '$issue_ref'" >&2 + return 1 + fi + + local session_path="$SESSIONS_DIR/$session_file" + + if [ ! -f "$session_path" ]; then + echo "Error: Session file not found: $session_path" >&2 + return 1 + fi + + python3 << PYEOF +import json + +session_path = "$session_path" +pr_url = "$pr_url" + +with open(session_path, 'r') as f: + session = json.load(f) + +session['pr_url'] = pr_url + +with open(session_path, 'w') as f: + json.dump(session, f, indent=2) + +print(f"Updated pr_url to: {pr_url}") +PYEOF +} + +read_index() { + if [ -f "$INDEX_FILE" ]; then + cat "$INDEX_FILE" + else + echo '{"base": null, "pm_agent": null, "issues": {}}' + fi +} + +write_index() { + local base="$1" + local pm_agent="$2" + local issues_json="$3" + local temp_file="$INDEX_FILE.tmp.$$" + printf '{"base": %s, "pm_agent": %s, "issues": %s}\n' "$base" "$pm_agent" "$issues_json" > "$temp_file" + mv "$temp_file" "$INDEX_FILE" +} + +get_base_session_id() { + local index=$(read_index) + echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('base') or '')" +} + +get_pm_agent_session_id() { + local index=$(read_index) + echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('pm_agent') or '')" +} + +get_session_for_issue() { + local issue_ref="$1" + local index=$(read_index) + echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d['issues'].get('$issue_ref') or '')" +} + +set_base_in_index() { + local base_session_id="$1" + local pm_agent=$(get_pm_agent_session_id) + local issues_json=$(read_index | python3 -c "import sys, json; d=json.load(sys.stdin); print(json.dumps(d['issues']))") + if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ]; then + write_index "\"$base_session_id\"" "null" "$issues_json" + else + write_index "\"$base_session_id\"" "\"$pm_agent\"" "$issues_json" + fi +} + +set_pm_agent_in_index() { + local pm_agent_session_id="$1" + local base=$(get_base_session_id) + local issues_json=$(read_index | python3 -c "import sys, json; d=json.load(sys.stdin); print(json.dumps(d['issues']))") + if [ -z "$base" ] || [ "$base" = "null" ]; then + write_index "null" "\"$pm_agent_session_id\"" "$issues_json" + else + write_index "\"$base\"" "\"$pm_agent_session_id\"" "$issues_json" + fi +} + +add_issue_to_index() { + local issue_ref="$1" + local session_file="$2" + local index=$(read_index) + local base=$(get_base_session_id) + local pm_agent=$(get_pm_agent_session_id) + local issues=$(echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); print(json.dumps(d['issues']))") + local new_issues=$(echo "$issues" | python3 -c "import sys, json; d=json.load(sys.stdin); d['$issue_ref']='$session_file'; print(json.dumps(d))") + if [ -z "$base" ] || [ "$base" = "null" ]; then + if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ]; then + write_index "null" "null" "$new_issues" + else + write_index "null" "\"$pm_agent\"" "$new_issues" + fi + else + if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ]; then + write_index "\"$base\"" "null" "$new_issues" + else + write_index "\"$base\"" "\"$pm_agent\"" "$new_issues" + fi + fi +} + +remove_issue_from_index() { + local issue_ref="$1" + local index=$(read_index) + local base=$(get_base_session_id) + local pm_agent=$(get_pm_agent_session_id) + local new_issues=$(echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); d['issues'].pop('$issue_ref', None); print(json.dumps(d['issues']))") + if [ -z "$base" ] || [ "$base" = "null" ]; then + if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ]; then + write_index "null" "null" "$new_issues" + else + write_index "null" "\"$pm_agent\"" "$new_issues" + fi + else + if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ]; then + write_index "\"$base\"" "null" "$new_issues" + else + write_index "\"$base\"" "\"$pm_agent\"" "$new_issues" + fi + fi +} + +validate_issue_ref() { + local issue_ref="$1" + if [[ ! "$issue_ref" =~ ^[^/]+/[^/]+/[^#]+#[0-9]+$ ]]; then + echo "Error: invalid issue ref format" >&2 + echo "Expected: instance/user/repo#number" >&2 + echo "Example: github.com/shoko/kugetsu#14" >&2 + exit 1 + fi +} + +check_opencode_session_exists() { + local session_id="$1" + opencode session list --format json 2>/dev/null | grep -q "\"$session_id\"" +} + +kugetsu_get_pm_context() { + local user_pm_context="${KUGETSU_DIR}/pm-agent.md" + local skill_pm_context="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../pm/SKILL.md" + + if [ -f "$user_pm_context" ]; then + cat "$user_pm_context" + elif [ -f "$skill_pm_context" ]; then + cat "$skill_pm_context" + else + echo "" + fi +} + +kugetsu_get_fork_context() { + local issue_ref="$1" + local context="" + + context="## IMPORTANT WORKING RULES + +1. You are working on issue: $issue_ref +2. If you encounter ANY error, blocker, or cannot complete the task: + - STOP immediately + - Log what happened and why you cannot proceed + - Do NOT switch to other work or try alternative approaches +3. Do NOT work on other issues or PRs unless explicitly asked +4. Environment variables are available in ~/.kugetsu/env/ + +" + + if [ -f "$REPOS_CONFIG" ]; then + context="${context} +## REPOSITORIES CONFIG +$(cat "$REPOS_CONFIG") + +" + fi + + if [ -f "$ENV_DIR/default.env" ]; then + context="${context} +## ENVIRONMENT (available at ~/.kugetsu/env/) +Environment file exists at: $ENV_DIR/default.env +Source it with: source ~/.kugetsu/env/default.env +" + fi + + echo "$context" +} + +kugetsu_add_notification() { + local type="$1" + local message="$2" + local issue_ref="${3:-}" + local gitea_url="${4:-}" + + mkdir -p "$(dirname "$NOTIFICATIONS_FILE")" + + local notification=$(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 +) + echo "$notification" +} + +kugetsu_get_notifications() { + local limit="${1:-10}" + + if [ ! -f "$NOTIFICATIONS_FILE" ]; then + echo "[]" + return + fi + + python3 << PYEOF +import json +import os +from datetime import datetime + +file_path = os.path.expanduser("$NOTIFICATIONS_FILE") + +if not os.path.exists(file_path): + print("[]") + exit(0) + +try: + with open(file_path, 'r') as f: + notifications = json.load(f) + + unread = [n for n in notifications if not n.get("read", False)] + unread.sort(key=lambda x: x.get("timestamp", ""), reverse=True) + + for n in unread[:$limit]: + ts = n.get("timestamp", "unknown") + ntype = n.get("type", "info") + msg = n.get("message", "") + issue = n.get("issue_ref", "") + gitea = n.get("gitea_url", "") + + print(f"[{ts}] {ntype}: {msg}") + if issue: + print(f" Issue: {issue}") + if gitea: + print(f" Link: {gitea}") + print() + + if not unread: + print("No unread notifications.") + +except Exception as e: + print(f"Error reading notifications: {e}") +PYEOF +} + +kugetsu_clear_notifications() { + if [ ! -f "$NOTIFICATIONS_FILE" ]; then + return + fi + + python3 << PYEOF +import json +import os + +file_path = os.path.expanduser("$NOTIFICATIONS_FILE") + +if not os.path.exists(file_path): + exit(0) + +try: + with open(file_path, 'r') as f: + notifications = json.load(f) + + for n in notifications: + n["read"] = True + + with open(file_path, 'w') as f: + json.dump(notifications, f, indent=2) + + print("Notifications marked as read") +except Exception as e: + print(f"Error: {e}") +PYEOF +} + +cmd_notify() { + local action="${1:-}" + + case "$action" in + ""|"list"|"show") + kugetsu_get_notifications 10 + ;; + "clear") + kugetsu_clear_notifications + ;; + *) + echo "Usage: kugetsu notify [list|clear]" + ;; + esac +} + +cmd_status() { + if [ ! -f "$INDEX_FILE" ]; then + echo "kugetsu_not_initialized" + return + fi + + local base=$(get_base_session_id) + local pm_agent=$(get_pm_agent_session_id) + + if [ -z "$base" ] || [ "$base" = "null" ]; then + echo "base_session_missing" + return + fi + + if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ] || [ "$pm_agent" = "None" ]; then + echo "pm_agent_missing" + return + fi + + echo "ok" +} + +get_verbosity_context() { + local verbosity="${KUGETSU_VERBOSITY:-default}" + local verbosity_file="$VERBOSITY_DIR/${verbosity}.md" + + if [ -f "$verbosity_file" ]; then + cat "$verbosity_file" + else + echo "## Verbosity: $verbosity" + fi +} + +init_verbosity_templates() { + mkdir -p "$VERBOSITY_DIR" + + if [ ! -f "$VERBOSITY_DIR/verbose.md" ]; then + cat > "$VERBOSITY_DIR/verbose.md" << 'EOF' +## Verbosity: Verbose + +You are operating in HIGH verbosity mode. Include ALL available context: +- Full command outputs and their results +- Detailed reasoning and thinking process +- All file changes with diffs when relevant +- Complete log excerpts +- Comprehensive status updates +- Ask clarifying questions when uncertain +EOF + fi + + if [ ! -f "$VERBOSITY_DIR/default.md" ]; then + cat > "$VERBOSITY_DIR/default.md" << 'EOF' +## Verbosity: Default + +You are operating in NORMAL verbosity mode. Provide balanced output: +- Standard command outputs and key results +- Moderate reasoning detail +- Important file changes summarized +- Regular status updates +EOF + fi + + if [ ! -f "$VERBOSITY_DIR/quiet.md" ]; then + cat > "$VERBOSITY_DIR/quiet.md" << 'EOF' +## Verbosity: Quiet + +You are operating in QUIET verbosity mode. Keep output minimal: +- Only essential information +- Brief status updates (1-2 sentences) +- Final decisions only +- Yes/No answers when appropriate +EOF + fi +} + +parse_issue_ref_from_message() { + local message="$1" + + local gitserver="" + local owner="" + local repo="" + 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 + 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/\/[^/]*\/[^/]*$//') + 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=$(echo "$full_path" | cut -d'/' -f2) + repo=$(echo "$full_path" | cut -d'/' -f3) + issue_number=$(echo "$full_path" | grep -oE '[0-9]+$' | head -1) + 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=$(echo "$message" | grep -oE '[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+' | head -1) + owner=$(echo "$gitserver" | cut -d'/' -f2) + repo=$(echo "$gitserver" | cut -d'/' -f3) + issue_number=$(echo "$message" | grep -oE '#[0-9]+' | grep -oE '[0-9]+' | head -1) + elif echo "$message" | grep -qE '[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+#([0-9]+)'; then + owner=$(echo "$message" | grep -oE '[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+#' | sed 's/#$//' | cut -d'/' -f1) + 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 + + echo "${gitserver}|${owner}|${repo}|${issue_number}" +} + +get_missing_info() { + local parsed="$1" + local gitserver=$(echo "$parsed" | cut -d'|' -f1) + local owner=$(echo "$parsed" | cut -d'|' -f2) + local repo=$(echo "$parsed" | cut -d'|' -f3) + local issue_number=$(echo "$parsed" | cut -d'|' -f4) + + local missing="" + [ -z "$gitserver" ] && missing="${missing}git server, " + [ -z "$owner" ] && missing="${missing}owner, " + [ -z "$repo" ] && missing="${missing}repository, " + [ -z "$issue_number" ] && missing="${missing}issue number, " + + echo "$missing" | sed 's/, $//' +} + +build_missing_info_context() { + local missing="$1" + if [ -n "$missing" ]; then + echo "" + echo "NOTE: This task delegation has no information about: ${missing}." + echo "We need them if user wants to work on a specific issue. Otherwise we don't need it." + fi +} + +find_worktrees_by_issue_number() { + local issue_number="$1" + local results="" + + if [ ! -d "$WORKTREES_DIR/.kugetsu-worktrees" ]; then + echo "" + return + fi + + for wt in "$WORKTREES_DIR/.kugetsu-worktrees"/*; do + if [ -d "$wt" ]; then + local wt_issue_number=$(echo "$wt" | grep -oE '#?[0-9]+$' | grep -oE '[0-9]+' | head -1) + if [ "$wt_issue_number" = "$issue_number" ]; then + results="${results}${wt}:worktree +" + fi + fi + done + + echo "$results" +} + +find_sessions_by_issue_number() { + local issue_number="$1" + local results="" + + if [ ! -d "$SESSIONS_DIR" ]; then + echo "" + return + fi + + for session_file in "$SESSIONS_DIR"/*.json; do + if [ -f "$session_file" ]; then + local session_issue_ref=$(basename "$session_file" .json | sed 's/_/\//g') + local session_issue_number=$(echo "$session_issue_ref" | grep -oE '#?[0-9]+$' | grep -oE '[0-9]+' | head -1) + if [ "$session_issue_number" = "$issue_number" ]; then + results="${results}${session_file}:session +" + fi + fi + done + + echo "$results" +} + +>>>>>>> origin/main cmd_queue() { local action="${1:-list}" shift