Compare commits

..

1 Commits

Author SHA1 Message Date
shokollm
b306c96289 fix: move msg file inside worktree to avoid external_directory permission error 2026-04-07 03:13:31 +00:00
11 changed files with 281 additions and 795 deletions

1
.gitignore vendored
View File

@@ -4,4 +4,3 @@ results/
*/results/ */results/
*.pyc *.pyc
.kugetsu/

View File

@@ -1,18 +0,0 @@
repos:
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.9.0.6
hooks:
- id: shellcheck
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.8
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/commitizen-tools/commitizen
rev: v3.2.0
hooks:
- id: commitizen
stages: [commit-msg]

View File

@@ -16,38 +16,6 @@
- Test changes before submitting - Test changes before submitting
- See [VERSIONING.md](VERSIONING.md) for backport compatibility rules - See [VERSIONING.md](VERSIONING.md) for backport compatibility rules
## Pre-commit Hooks
This repository uses [pre-commit](https://pre-commit.com/) for linting and commit message enforcement.
### Setup
```bash
pip install pre-commit
pre-commit install
```
### Hooks
- **shellcheck** — Lints bash scripts
- **ruff** — Lints and formats Python
- **commitizen** — Enforces [Conventional Commits](https://www.conventionalcommits.org/) format
### Commit Message Format
Use Conventional Commits format:
```
type(scope): message
# Examples
fix(session): handle missing session gracefully
feat(pm): add queue daemon for task delegation
docs: update contributing guide
```
Types: `fix`, `feat`, `docs`, `refactor`, `chore`, `test`
## Branches ## Branches
### Primary Branches ### Primary Branches

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

@@ -17,129 +17,77 @@ usage() {
kugetsu - OpenCode Session Manager (Issue-Driven) kugetsu - OpenCode Session Manager (Issue-Driven)
Usage: Usage:
kugetsu <command> [subcommand] [options] kugetsu init [--force] Initialize base + pm-agent sessions (requires TTY)
kugetsu start <issue-ref> <message> [--debug] Start task for issue (forks base session)
Commands: kugetsu continue <issue-ref> [message] [--debug] Continue existing task for issue
init [--force] Initialize base + pm-agent sessions (requires TTY) kugetsu delegate <message> Send message to PM agent (fire-and-forget)
start <issue-ref> <message> [--debug] Start task for issue (forks base session) kugetsu logs [n] Show recent delegation logs (default: 10)
continue <issue-ref> [message] [--debug] Continue existing task for issue kugetsu status Check kugetsu initialization status
delegate <message> Send message to PM agent (fire-and-forget) kugetsu doctor [--fix] Diagnose and fix kugetsu issues
logs [n] Show recent delegation logs (default: 10) kugetsu notify [list|clear] Show or clear notifications
status Check kugetsu initialization status kugetsu list List all tracked sessions
doctor [--fix] Diagnose and fix kugetsu issues kugetsu prune [--force] Remove orphaned sessions (keeps base + pm-agent)
notify [list|clear] Show or clear notifications kugetsu destroy <issue-ref> [-y] Delete session for issue
list List all tracked sessions kugetsu destroy --pm-agent [-y] Delete pm-agent session (not recommended)
prune [--force] Remove orphaned sessions kugetsu destroy --base [-y] Delete base session
destroy <target> [-y] Delete session (issue, pm-agent, or base) kugetsu set-pr <issue-ref> <pr-url> Set PR URL for session (for PR tracking)
set-pr <issue-ref> <pr-url> Set PR URL for session kugetsu context <issue-ref> Show context for issue
context <issue-ref> Show context for issue kugetsu queue [list|stats|clear] Show queue status or statistics
queue [subcommand] Queue management kugetsu queue enqueue <issue-ref> <message> Enqueue a task (normally via delegate)
queue-daemon [subcommand] Queue daemon management kugetsu queue-daemon [start|stop|restart|status|logs] Manage queue daemon
env [subcommand] Environment variable management kugetsu env [get|set|list] Manage agent environment variables
server [subcommand] Git server configuration kugetsu server [list|add|remove|default|get] Manage git server configurations
help [command] Show help for a command kugetsu help Show this help
Use 'kugetsu <command> help' for subcommand help.
Example: kugetsu queue help, kugetsu queue-daemon help
Issue Ref Format: Issue Ref Format:
instance/user/repo#number instance/user/repo#number
Example: github.com/shoko/kugetsu#14 Example: github.com/shoko/kugetsu#14
EOF
}
usage_queue() { Commands:
cat << 'EOF' init Create base + pm-agent sessions via TUI. Requires terminal access.
kugetsu queue - Queue management Use --force to reinitialize if sessions exist.
start Fork new session from base for specific issue.
Requires pm-agent to be running (created by init).
continue Continue work on existing issue session.
delegate Send message to PM agent for task coordination.
Fire-and-forget: returns immediately, runs in background.
Use 'kugetsu logs' to check output.
logs Show recent delegation logs.
Default: 10 most recent. Use 'kugetsu logs 20' for more.
status Check if kugetsu is initialized and PM agent is active.
doctor Diagnose kugetsu issues. Use --fix to attempt repairs.
notify Show or clear notifications from PM agent.
Use 'kugetsu notify list' to see unread notifications.
list Show all sessions (base + pm-agent + forked issues).
prune Remove sessions not in index (orphaned from opencode).
Use --force to skip confirmation.
destroy Delete specific issue, pm-agent, or base session.
Usage: Options:
kugetsu queue [subcommand] --debug Show real-time debug output and capture to debug.log
Subcommands: PM Context:
list [--limit=N] [--format=json] Show pending tasks (default: 10 items, text format) kugetsu reads ~/.kugetsu/pm-agent.md (if exists) and injects it
stats Show queue statistics into the PM agent session at init time. This allows customizing PM
clear Clear all queue items behavior without recreating the session.
enqueue <issue-ref> <message> Enqueue a task
help Show this help
Options for list: Notifications:
--limit=N Number of items to return (default: 10) PM Agent writes task completion notifications to ~/.kugetsu/notifications.json
--format=json Output in JSON format with stats Use 'kugetsu notify list' to see unread notifications.
Examples: Examples:
kugetsu queue list kugetsu init
kugetsu queue list --limit=50 kugetsu status
kugetsu queue list --format=json kugetsu delegate "work on issue #5"
kugetsu queue list --limit=20 --format=json kugetsu logs
kugetsu queue stats kugetsu logs 20
kugetsu queue clear kugetsu doctor
kugetsu queue enqueue github.com/shoko/kugetsu#14 "fix bug" kugetsu doctor --fix
EOF kugetsu notify list
} kugetsu notify clear
kugetsu start github.com/shoko/kugetsu#14 "fix bug"
usage_queue_daemon() { kugetsu continue github.com/shoko/kugetsu#14 "add tests"
cat << 'EOF' kugetsu list
kugetsu queue-daemon - Queue daemon management
Usage:
kugetsu queue-daemon [subcommand]
Subcommands:
start Start the queue daemon
stop Stop the queue daemon
restart Restart the queue daemon
status Check daemon status
logs Show recent daemon logs
help Show this help
Examples:
kugetsu queue-daemon start
kugetsu queue-daemon status
kugetsu queue-daemon logs
EOF
}
usage_env() {
cat << 'EOF'
kugetsu env - Environment variable management
Usage:
kugetsu env [subcommand]
Subcommands:
list List all environment variables
get <key> Get a specific variable
set <key> <value> Set a variable
rm <key> Remove a variable
help Show this help
Examples:
kugetsu env list
kugetsu env get GITEA_TOKEN
kugetsu env set CUSTOM_VAR "value"
kugetsu env rm CUSTOM_VAR
EOF
}
usage_server() {
cat << 'EOF'
kugetsu server - Git server configuration
Usage:
kugetsu server [subcommand]
Subcommands:
list List all configured servers (default)
add <name> <url> Add a new server
remove <name> Remove a server
default [<name>] Get or set default server
get [<name>] Get server URL
help Show this help
Examples:
kugetsu server list
kugetsu server add github.com https://github.com
kugetsu server default github.com
EOF EOF
} }
@@ -362,25 +310,19 @@ PYEOF
get_pending_tasks() { get_pending_tasks() {
local limit="${1:-10}" local limit="${1:-10}"
local format="${2:-text}"
if [ ! -d "$QUEUE_ITEMS_DIR" ]; then if [ ! -d "$QUEUE_ITEMS_DIR" ]; then
if [ "$format" = "json" ]; then echo "[]"
echo '{"items": [], "total": 0, "pending": 0}'
else
echo "No pending tasks in queue."
fi
return return
fi fi
python3 << PYEOF python3 -c "
import json import json
import os import os
import sys import sys
queue_dir = os.environ.get('QUEUE_ITEMS_DIR', '') queue_dir = os.environ.get('QUEUE_ITEMS_DIR', '')
limit = int(sys.argv[1]) if len(sys.argv) > 1 else 10 limit = int(sys.argv[1]) if len(sys.argv) > 1 else 10
format_type = sys.argv[2] if len(sys.argv) > 2 else 'text'
items = [] items = []
if os.path.isdir(queue_dir): if os.path.isdir(queue_dir):
@@ -392,27 +334,13 @@ if os.path.isdir(queue_dir):
data = json.load(f) data = json.load(f)
if data.get('state') == 'pending': if data.get('state') == 'pending':
items.append(data) items.append(data)
if len(items) >= limit:
break
except: except:
pass pass
items.sort(key=lambda x: x.get('pending_since', '')) print(json.dumps(items))
" "$limit"
if format_type == 'json':
output = {
"items": items[:limit],
"total": len(items),
"pending": len(items)
}
print(json.dumps(output))
else:
if not items:
print("No pending tasks in queue.")
else:
print("Pending tasks:")
for t in items[:limit]:
msg = t.get('message', '')[:50]
print(f" {t.get('id')}: {t.get('issue_ref')} - {msg}...")
PYEOF
} }
get_queue_stats() { get_queue_stats() {
@@ -937,34 +865,18 @@ find_sessions_by_issue_number() {
} }
cmd_queue() { cmd_queue() {
local action="${1:-}" local action="${1:-list}"
shift
case "$action" in case "$action" in
""|help|--help|-h)
usage_queue
;;
help|--help|-h)
usage_queue
;;
list) list)
local limit="10" local pending_tasks=$(get_pending_tasks 10)
local format="text" if [ "$pending_tasks" = "[]" ]; then
echo "No pending tasks in queue."
while [ $# -gt 0 ]; do else
case "$1" in echo "Pending tasks:"
--limit=*) echo "$pending_tasks" | python3 -c "import sys, json; [print(f\" {t.get('id')}: {t.get('issue_ref')} - {t.get('message', '')[:50]}...\") for t in json.load(sys.stdin)]"
limit="${1#*=}" fi
;;
--format=json)
format="json"
;;
*)
;;
esac
shift
done
get_pending_tasks "$limit" "$format"
;; ;;
stats) stats)
local stats=$(get_queue_stats) local stats=$(get_queue_stats)
@@ -986,8 +898,8 @@ cmd_queue() {
echo "Queue cleared." echo "Queue cleared."
;; ;;
enqueue) enqueue)
local issue_ref="${2:-}" local issue_ref="${1:-}"
local message="${3:-}" local message="${2:-}"
if [ -z "$issue_ref" ] || [ -z "$message" ]; then if [ -z "$issue_ref" ] || [ -z "$message" ]; then
echo "Usage: kugetsu queue enqueue <issue-ref> <message>" >&2 echo "Usage: kugetsu queue enqueue <issue-ref> <message>" >&2
exit 1 exit 1
@@ -998,20 +910,16 @@ cmd_queue() {
check_task_timeouts check_task_timeouts
;; ;;
*) *)
echo "Unknown queue subcommand: $action" >&2 echo "Usage: kugetsu queue [list|stats|clear|enqueue]" >&2
usage_queue
exit 1 exit 1
;; ;;
esac esac
} }
cmd_queue_daemon() { cmd_queue_daemon() {
local action="${1:-}" local action="${1:-status}"
case "$action" in case "$action" in
""|help|--help|-h)
usage_queue_daemon
;;
start) start)
if [ -f "$QUEUE_DAEMON_PID_FILE" ]; then if [ -f "$QUEUE_DAEMON_PID_FILE" ]; then
local old_pid=$(cat "$QUEUE_DAEMON_PID_FILE") local old_pid=$(cat "$QUEUE_DAEMON_PID_FILE")
@@ -1070,8 +978,7 @@ cmd_queue_daemon() {
fi fi
;; ;;
*) *)
echo "Unknown queue-daemon subcommand: $action" >&2 echo "Usage: kugetsu queue-daemon [start|stop|restart|status|logs]" >&2
usage_queue_daemon
exit 1 exit 1
;; ;;
esac esac
@@ -1152,12 +1059,10 @@ set_debug_mode() {
} }
cmd_env() { cmd_env() {
local action="${1:-}" local action="${1:-list}"
shift
case "$action" in case "$action" in
""|help|--help|-h)
usage_env
;;
list) list)
echo "Agent environment variables:" echo "Agent environment variables:"
if [ -d "$ENV_DIR" ]; then if [ -d "$ENV_DIR" ]; then
@@ -1175,7 +1080,7 @@ cmd_env() {
fi fi
;; ;;
get) get)
local key="${2:-}" local key="${1:-}"
if [ -z "$key" ]; then if [ -z "$key" ]; then
echo "Usage: kugetsu env get <key>" >&2 echo "Usage: kugetsu env get <key>" >&2
exit 1 exit 1
@@ -1190,8 +1095,8 @@ cmd_env() {
fi fi
;; ;;
set) set)
local key="${2:-}" local key="${1:-}"
local value="${3:-}" local value="${2:-}"
if [ -z "$key" ] || [ -z "$value" ]; then if [ -z "$key" ] || [ -z "$value" ]; then
echo "Usage: kugetsu env set <key> <value>" >&2 echo "Usage: kugetsu env set <key> <value>" >&2
exit 1 exit 1
@@ -1200,8 +1105,8 @@ cmd_env() {
echo "${key}=${value}" >> "$ENV_DIR/default.env" echo "${key}=${value}" >> "$ENV_DIR/default.env"
echo "Set $key in $ENV_DIR/default.env" echo "Set $key in $ENV_DIR/default.env"
;; ;;
rm|remove) rm)
local key="${2:-}" local key="${1:-}"
if [ -z "$key" ]; then if [ -z "$key" ]; then
echo "Usage: kugetsu env rm <key>" >&2 echo "Usage: kugetsu env rm <key>" >&2
exit 1 exit 1
@@ -1212,8 +1117,7 @@ cmd_env() {
fi fi
;; ;;
*) *)
echo "Unknown env subcommand: $action" >&2 echo "Usage: kugetsu env [list|get|set|rm]" >&2
usage_env
exit 1 exit 1
;; ;;
esac esac
@@ -1223,10 +1127,7 @@ cmd_server() {
local action="${1:-}" local action="${1:-}"
case "$action" in case "$action" in
""|help|--help|-h) ""|"list")
usage_server
;;
"list")
if [ -z "${GIT_SERVERS+x}" ]; then if [ -z "${GIT_SERVERS+x}" ]; then
echo "No git servers configured" echo "No git servers configured"
return return
@@ -1303,8 +1204,14 @@ cmd_server() {
fi fi
;; ;;
*) *)
echo "Unknown server subcommand: $action" >&2 echo "Usage: kugetsu server <list|add|remove|default|get>" >&2
usage_server echo "" >&2
echo "Commands:" >&2
echo " list List all configured git servers" >&2
echo " add <name> <url> Add a new git server" >&2
echo " remove <name> Remove a git server" >&2
echo " default [<name>] Get or set default server" >&2
echo " get [<name>] Get URL for a server (default: current default)" >&2
exit 1 exit 1
;; ;;
esac esac
@@ -1414,29 +1321,7 @@ main() {
case "$command" in case "$command" in
help|--help|-h) help|--help|-h)
local subcommand="${1:-}" usage
case "$subcommand" in
queue|"")
usage_queue
;;
queue-daemon)
usage_queue_daemon
;;
env)
usage_env
;;
server)
usage_server
;;
"")
usage
;;
*)
echo "Help not available for '$subcommand'" >&2
usage
exit 1
;;
esac
;; ;;
init) init)
cmd_init "$@" cmd_init "$@"

View File

@@ -26,9 +26,6 @@ QUEUE_DAEMON_INTERVAL_MINUTES="${QUEUE_DAEMON_INTERVAL_MINUTES:-5}"
QUEUE_CLEANUP_AGE_DAYS="${QUEUE_CLEANUP_AGE_DAYS:-7}" QUEUE_CLEANUP_AGE_DAYS="${QUEUE_CLEANUP_AGE_DAYS:-7}"
TASK_TIMEOUT_HOURS="${TASK_TIMEOUT_HOURS:-1}" TASK_TIMEOUT_HOURS="${TASK_TIMEOUT_HOURS:-1}"
NETWORK_RETRY_ATTEMPTS="${NETWORK_RETRY_ATTEMPTS:-3}"
NETWORK_RETRY_DELAY_SECONDS="${NETWORK_RETRY_DELAY_SECONDS:-5}"
# Load user config overrides (~/.kugetsu/config) # Load user config overrides (~/.kugetsu/config)
if [ -f "$KUGETSU_DIR/config" ]; then if [ -f "$KUGETSU_DIR/config" ]; then
source "$KUGETSU_DIR/config" source "$KUGETSU_DIR/config"
@@ -67,47 +64,3 @@ load_agent_env() {
set +a 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[@]}"
}
retry_with_backoff() {
local max_attempts="${1:-$NETWORK_RETRY_ATTEMPTS}"
local delay_seconds="${2:-$NETWORK_RETRY_DELAY_SECONDS}"
local command="$3"
local remaining_attempts=$max_attempts
while [ $remaining_attempts -gt 0 ]; do
if eval "$command"; then
return 0
fi
remaining_attempts=$((remaining_attempts - 1))
if [ $remaining_attempts -gt 0 ]; then
log "warn" "retry_with_backoff" "Command failed, $remaining_attempts retries remaining. Waiting ${delay_seconds}s..."
sleep "$delay_seconds"
delay_seconds=$((delay_seconds * 2))
fi
done
log "error" "retry_with_backoff" "Command failed after $max_attempts attempts"
return 1
}

View File

@@ -139,77 +139,6 @@ validate_issue_ref() {
fi fi
} }
read_json_file() {
local file_path="$1"
if [ -f "$file_path" ]; then
cat "$file_path"
else
echo "{}"
fi
}
write_json_file() {
local file_path="$1"
local json_content="$2"
local temp_file="$file_path.tmp.$$"
printf '%s' "$json_content" > "$temp_file"
if ! python3 -c "import json; json.load(open('$temp_file'))" 2>/dev/null; then
echo "Error: write_json_file would create malformed JSON: $file_path" >&2
rm -f "$temp_file"
return 1
fi
mv "$temp_file" "$file_path"
}
get_json_value() {
local file_path="$1"
local key="$2"
local default="${3:-}"
if [ ! -f "$file_path" ]; then
echo "$default"
return
fi
python3 -c "import json; print(json.load(open('$file_path')).get('$key', '$default'))" 2>/dev/null || echo "$default"
}
set_json_value() {
local file_path="$1"
local key="$2"
local value="$3"
if [ ! -f "$file_path" ]; then
printf '{"%s": "%s"}\n' "$key" "$value" > "$file_path"
return
fi
python3 << PYEOF
import json
import sys
file_path = "$file_path"
key = "$key"
value = "$value"
try:
with open(file_path, 'r') as f:
data = json.load(f)
except:
data = {}
data[key] = value
with open(file_path, 'w') as f:
json.dump(data, f, indent=2)
print(f"Set $key = $value in $file_path")
PYEOF
}
update_session_pr_url() { update_session_pr_url() {
local issue_ref="$1" local issue_ref="$1"
local pr_url="$2" local pr_url="$2"

View File

@@ -1,40 +1,6 @@
#!/bin/bash #!/bin/bash
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/kugetsu-config.sh"
log() {
local level="${1:-}"
local component="${2:-}"
local message="${3:-}"
local timestamp
timestamp=$(date -Iseconds)
case "$level" in
info|warn|error|debug) ;;
*)
echo "Error: log level must be info|warn|error|debug" >&2
return 1
;;
esac
if [ -z "$message" ]; then
message="$component"
component="${level}"
level="info"
fi
local masked
masked=$(mask_sensitive_vars "$message")
echo "[$timestamp] $level $component $masked"
}
log_debug() { log "debug" "$1" "${2:-}"; }
log_info() { log "info" "$1" "${2:-}"; }
log_warn() { log "warn" "$1" "${2:-}"; }
log_error() { log "error" "$1" "${2:-}"; }
cmd_logs() { cmd_logs() {
local count="${1:-10}" local count="${1:-10}"

View File

@@ -34,8 +34,6 @@ release_lock() {
check_task_completion() { check_task_completion() {
local item="$1" local item="$1"
local queue_id=$(basename "$item" .json) local queue_id=$(basename "$item" .json)
local item_data=$(read_json_file "$item")
local state=$(python3 -c "import json; print(json.load(open('$item')).get('state', ''))" 2>/dev/null) local state=$(python3 -c "import json; print(json.load(open('$item')).get('state', ''))" 2>/dev/null)
[ "$state" = "notified" ] || return 0 [ "$state" = "notified" ] || return 0
@@ -43,31 +41,6 @@ check_task_completion() {
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) local pid=$(python3 -c "import json; print(json.load(open('$item')).get('pid', ''))" 2>/dev/null)
local notified_at=$(python3 -c "import json; print(json.load(open('$item')).get('notified_at', ''))" 2>/dev/null)
local timed_out=false
if [ -n "$notified_at" ]; then
local notified_epoch=$(date -d "$notified_at" +%s 2>/dev/null || echo "0")
local now_epoch=$(date +%s)
local hours_elapsed=$(( (now_epoch - notified_epoch) / 3600 ))
if [ "$hours_elapsed" -ge "${TASK_TIMEOUT_HOURS:-1}" ]; then
timed_out=true
log_warn "queue-daemon" "Task $queue_id ($issue_ref) timed out after ${hours_elapsed}h"
fi
fi
if [ "$timed_out" = true ]; then
if [ -n "$pid" ] && [ "$pid" != "None" ]; then
kill "$pid" 2>/dev/null || true
fi
if [ -n "$session_id" ]; then
opencode session stop "$session_id" 2>/dev/null || true
fi
update_queue_item_state "$queue_id" "error"
log_error "queue-daemon" "Task $queue_id ($issue_ref) marked error — timeout after ${hours_elapsed}h"
release_lock "$issue_ref"
return
fi
if [ -n "$pid" ] && [ "$pid" != "None" ]; then if [ -n "$pid" ] && [ "$pid" != "None" ]; then
if ! kill -0 "$pid" 2>/dev/null; then if ! kill -0 "$pid" 2>/dev/null; then
@@ -136,15 +109,28 @@ process_task() {
source "$SCRIPT_DIR/kugetsu-session.sh" source "$SCRIPT_DIR/kugetsu-session.sh"
log_file="$LOGS_DIR/delegate-$(date +%s).log" if worktree_exists "$issue_ref" "$WORKTREES_DIR" || [ -f "$SESSIONS_DIR/$(issue_ref_to_filename "$issue_ref").json" ]; then
if cmd_continue "$issue_ref" "$message" >> "$log_file" 2>&1; then log_file="$LOGS_DIR/delegate-$(date +%s).log"
sleep 1 if cmd_continue "$issue_ref" "$message" >> "$log_file" 2>&1; then
local session_id=$(get_session_id_for_issue "$issue_ref") sleep 1
update_queue_item_state "$queue_id" "notified" "$session_id" "" local session_id=$(get_session_id_for_issue "$issue_ref")
echo "Task $queue_id continued for $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 else
update_queue_item_state "$queue_id" "error" log_file="$LOGS_DIR/delegate-$(date +%s).log"
echo "Task $queue_id ($issue_ref) failed to continue" 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 fi
release_lock "$issue_ref" release_lock "$issue_ref"

View File

@@ -13,8 +13,7 @@ count_active_dev_sessions() {
if [ -d "$SESSIONS_DIR" ]; then if [ -d "$SESSIONS_DIR" ]; then
for session_file in "$SESSIONS_DIR"/*.json; do for session_file in "$SESSIONS_DIR"/*.json; do
if [ -f "$session_file" ]; then if [ -f "$session_file" ]; then
local filename local filename=$(basename "$session_file")
filename=$(basename "$session_file")
if [ "$filename" != "base.json" ] && [ "$filename" != "pm-agent.json" ]; then if [ "$filename" != "base.json" ] && [ "$filename" != "pm-agent.json" ]; then
count=$((count + 1)) count=$((count + 1))
fi fi
@@ -229,6 +228,7 @@ cmd_delegate() {
local msg_file="$LOGS_DIR/msg-$new_session.txt" local msg_file="$LOGS_DIR/msg-$new_session.txt"
printf '%s' "$message" > "$msg_file" printf '%s' "$message" > "$msg_file"
nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '@$msg_file' --session '$new_session'" >> "$log_file" 2>&1 & nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '@$msg_file' --session '$new_session'" >> "$log_file" 2>&1 &
rm -f "$msg_file"
echo "Delegated to new session (logged to $(basename "$log_file"))" echo "Delegated to new session (logged to $(basename "$log_file"))"
} }
@@ -240,55 +240,23 @@ create_session() {
return 1 return 1
fi fi
local before_file local before_json=$(opencode session list --format=json 2>/dev/null)
before_file="$KUGETSU_DIR/sessions/before$$.json" 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 "|")
local after_file
after_file="$KUGETSU_DIR/sessions/after$$.json"
opencode session list --format=json > "$before_file" 2>/dev/null || printf '{}' > "$before_file" opencode run --fork --session "$base_session" "new session" >/dev/null 2>&1
local fork_success=false
local attempt=0
local max_attempts="${NETWORK_RETRY_ATTEMPTS:-3}"
while [ $attempt -lt $max_attempts ] && [ "$fork_success" = false ]; do
attempt=$((attempt + 1))
if opencode run --fork --session "$base_session" "new session" >/dev/null 2>&1; then
fork_success=true
elif [ $attempt -lt $max_attempts ]; then
log "warn" "create_session" "Fork attempt $attempt failed, retrying..."
sleep "$((attempt * 2))"
fi
done
if [ "$fork_success" = false ]; then
log "error" "create_session" "Failed to fork session after $max_attempts attempts"
rm -f "$before_file" "$after_file"
return 1
fi
sleep 1 sleep 1
opencode session list --format=json > "$after_file" 2>/dev/null || printf '{}' > "$after_file" 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 local new_session_id=""
new_session_id=$(python3 << PYEOF while IFS= read -r sess; do
import json if [[ -n "$sess" ]] && [[ ! "$before_set" =~ \|${sess}\| ]]; then
new_session_id="$sess"
with open("$before_file", 'r') as f: break
before = json.load(f) fi
with open("$after_file", 'r') as f: done <<< "$after_sessions"
after = json.load(f)
before_ids = set(s['id'] for s in before)
for s in after:
if s['id'] not in before_ids:
print(s['id'])
break
PYEOF
)
rm -f "$before_file" "$after_file"
echo "$new_session_id" echo "$new_session_id"
} }
@@ -303,37 +271,12 @@ build_dev_agent_message() {
local number=$(echo "$issue_ref" | grep -oE '#[0-9]+$' | tr -d '#') local number=$(echo "$issue_ref" | grep -oE '#[0-9]+$' | tr -d '#')
local worktree_path=$(issue_ref_to_worktree_path "$issue_ref") local worktree_path=$(issue_ref_to_worktree_path "$issue_ref")
local conflict_check="" local base_message="You are assigned to work on $issue_ref.
local review_notes=""
local delegator_header=""
local delegator_footer=""
if [ -n "$user_message" ]; then
conflict_check=" - CRITICAL: Check if PR has merge conflicts before asking for review:
- Use: curl -s \"https://$instance/api/v1/repos/$owner/$repo/pulls/$number\" -H \"Authorization: Bearer \$GITEA_TOKEN\"
- If \"mergeable\": false, there ARE conflicts - you MUST resolve them FIRST
- To resolve: cd to worktree, git fetch origin, git rebase origin/main, resolve conflicts, git rebase --continue, git push --force-with-lease
- Only after resolving conflicts (mergeable: true) can you ask for review"
delegator_header="IMPORTANT: Follow the workflow below as your guideline, but prioritize the delegator's message.
Workflow:" Workflow:
delegator_footer="
Delegator's message:
$user_message"
else
review_notes=" - IMPORTANT: After listing reviews, READ the review comments and incorporate feedback
- Check for review state: \"APPROVED\" means ready to merge, \"COMMENT\" means feedback to address"
delegator_header="Workflow:"
fi
cat <<EOF
You are assigned to work on $issue_ref.
$delegator_header
1. Read the issue at $instance/$owner/$repo/issues/$number AND all comments on that issue 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 2. Check if a PR already exists for this issue
- If PR exists and is open, review it and learn from it - If PR exists and is open, review it and learn from it
$conflict_check
- If PR makes sense to continue, work on it instead - 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 - 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 3. Read README.md (if exists) to understand the general concept of this repository
@@ -343,66 +286,43 @@ $conflict_check
6. If anything is unclear, post a comment on the issue asking for clarification before implementing 6. If anything is unclear, post a comment on the issue asking for clarification before implementing
7. Implement the solution 7. Implement the solution
8. Create a branch named fix/issue-$number and implement the fix 8. Create a branch named fix/issue-$number and implement the fix
9. Create a PR when the implementation is complete using: tea pr create --repo $owner/$repo --title "Your PR title" --body "PR description" 9. Create a PR when the implementation is complete
- Make sure you are logged in with: tea login add --name gitea --token \$GITEA_TOKEN --url https://$instance
- If tea is not available, use: curl -X POST "https://$instance/api/v1/repos/$owner/$repo/pulls" -H "Authorization: Bearer \$GITEA_TOKEN" -H "Content-Type: application/json" -d '{"title":"PR Title","head":"branch-name","base":"main","body":"PR description"}'
Tools for PR interaction: Work directory: $worktree_path"
- Post issue/PR comment: curl -X POST "https://$instance/api/v1/repos/$owner/$repo/issues/$number/comments" -H "Authorization: Bearer \$GITEA_TOKEN" -H "Content-Type: application/json" -d '{"body":"Your comment"}'
- List PR comments: curl -s "https://$instance/api/v1/repos/$owner/$repo/issues/$number/comments" -H "Authorization: Bearer \$GITEA_TOKEN"
- List PR reviews: curl -s "https://$instance/api/v1/repos/$owner/$repo/pulls/$number/reviews" -H "Authorization: Bearer \$GITEA_TOKEN"
$review_notes
- Merge PR (only with approval): tea pr merge --repo $owner/$repo $number --style merge
- MERGING requires approval first! Check for: approval in reviews, OR "lgtm"/"approved" in comments
- If no approval, ask reviewer to approve first before merging
$delegator_footer
Work directory: $worktree_path if [ -n "$user_message" ]; then
EOF echo "$base_message
Additional instructions from delegator:
$user_message"
else
echo "$base_message"
fi
} }
ensure_worktree() { cmd_start() {
local issue_ref="$1" local issue_ref="${1:-}"
local message="${2:-}"
if worktree_exists "$issue_ref" "$WORKTREES_DIR"; then if [ -z "$issue_ref" ]; then
log "info" "ensure_worktree" "Worktree already exists for $issue_ref" echo "Error: issue ref is required" >&2
echo "existed" echo "Usage: kugetsu start <issue-ref> [message]" >&2
return 0 exit 1
fi fi
validate_issue_ref "$issue_ref"
local base_session_id=$(get_base_session_id) local base_session_id=$(get_base_session_id)
if [ -z "$base_session_id" ] || [ "$base_session_id" = "null" ]; then if [ -z "$base_session_id" ] || [ "$base_session_id" = "null" ]; then
log "error" "ensure_worktree" "Base session not found for $issue_ref" echo "Error: Base session not found. Run 'kugetsu init' first." >&2
echo "error" exit 1
return 1
fi fi
local active_count=$(count_active_dev_sessions)
if [ "$active_count" -ge "${MAX_CONCURRENT_AGENTS:-3}" ]; then
log "error" "ensure_worktree" "Max concurrent agents reached for $issue_ref"
echo "error"
return 1
fi
if create_worktree "$issue_ref" "$WORKTREES_DIR" 2>&1 | tee >(cat >&2); then
log "info" "ensure_worktree" "Created worktree for $issue_ref"
echo "created"
return 0
else
log "error" "ensure_worktree" "Failed to create worktree for $issue_ref"
echo "error"
return 1
fi
}
ensure_session() {
local issue_ref="$1"
local session_file=$(issue_ref_to_filename "$issue_ref") local session_file=$(issue_ref_to_filename "$issue_ref")
local session_path="$SESSIONS_DIR/$session_file" local session_path="$SESSIONS_DIR/$session_file"
local worktree_exists=false local worktree_exists=false
if worktree_exists "$issue_ref" "$WORKTREES_DIR"; then
if worktree_exists "$issue_ref"; then
worktree_exists=true worktree_exists=true
fi fi
@@ -411,41 +331,39 @@ ensure_session() {
session_exists=true session_exists=true
fi fi
if [ "$worktree_exists" = true ] && [ "$session_exists" = true ]; then if $worktree_exists && $session_exists; then
log "info" "ensure_session" "Session already exists for $issue_ref" echo "Issue '$issue_ref' already has a worktree and session." >&2
echo "continued" echo "Use 'kugetsu continue $issue_ref' to continue work." >&2
return 0 exit 1
fi fi
if [ "$worktree_exists" = false ] && [ "$session_exists" = true ]; then if $worktree_exists && ! $session_exists; then
log "warn" "ensure_session" "Session exists but worktree is missing. Removing stale session..." echo "Warning: Worktree exists but session is missing. Removing worktree to recreate both..." >&2
remove_worktree_for_issue "$issue_ref"
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" rm -f "$session_path"
remove_issue_from_index "$issue_ref" remove_issue_from_index "$issue_ref"
session_exists=false session_exists=false
fi fi
if [ "$worktree_exists" = false ]; then local active_count=$(count_active_dev_sessions)
local wt_status=$(ensure_worktree "$issue_ref") if [ "$active_count" -ge "${MAX_CONCURRENT_AGENTS:-3}" ]; then
if [ "$wt_status" != "created" ] && [ "$wt_status" != "existed" ]; then echo "Error: Max concurrent agents (${MAX_CONCURRENT_AGENTS:-3}) reached. Use 'kugetsu continue' or wait for an agent to finish." >&2
log "error" "ensure_session" "Failed to ensure worktree for $issue_ref" exit 1
echo "error"
return 1
fi
fi fi
local base_session_id=$(get_base_session_id) create_worktree "$issue_ref" "$WORKTREES_DIR"
if [ -z "$base_session_id" ] || [ "$base_session_id" = "null" ]; then
log "error" "ensure_session" "Base session not found for $issue_ref"
echo "error"
return 1
fi
local new_session_id=$(create_session "$base_session_id") local new_session_id=$(create_session "$base_session_id")
if [ -z "$new_session_id" ]; then if [ -z "$new_session_id" ]; then
log "error" "ensure_session" "Could not create session for $issue_ref" echo "Error: Could not create session" >&2
echo "error" remove_worktree_for_issue "$issue_ref"
return 1 exit 1
fi fi
local worktree_path=$(issue_ref_to_worktree_path "$issue_ref") local worktree_path=$(issue_ref_to_worktree_path "$issue_ref")
@@ -455,91 +373,83 @@ ensure_session() {
add_issue_to_index "$issue_ref" "$session_file" add_issue_to_index "$issue_ref" "$session_file"
log "info" "ensure_session" "Created session for $issue_ref: $new_session_id" local dev_message=$(build_dev_agent_message "$issue_ref" "$message")
echo "created"
return 0
}
fork_agent() {
local session_id="$1"
local worktree_path="$2"
local message="$3"
if [ -z "$worktree_path" ] || [ ! -d "$worktree_path" ]; then
log "error" "fork_agent" "Invalid worktree path: $worktree_path"
echo "error"
return 1
fi
load_agent_env "dev" load_agent_env "dev"
cd "$worktree_path" cd "$worktree_path"
local sanitized_id=$(echo "$session_id" | sed 's/[^a-zA-Z0-9_-]/_/g') local sanitized_id=$(echo "$new_session_id" | sed 's/[^a-zA-Z0-9_-]/_/g')
mkdir -p "$worktree_path/.kugetsu" local msg_file="$worktree_path/.kugetsu-msg.txt"
if [ ! -f "$worktree_path/.gitignore" ] || ! grep -q "^.kugetsu/" "$worktree_path/.gitignore"; then printf '%s' "$dev_message" > "$msg_file"
echo ".kugetsu/" >> "$worktree_path/.gitignore" 2>/dev/null || true nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '@$msg_file' --session '$new_session_id'" >> "$LOGS_DIR/dev-$sanitized_id.log" 2>&1 &
fi rm -f "$msg_file"
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 '$session_id'" >> "$LOGS_DIR/dev-$sanitized_id.log" 2>&1 &
log "info" "fork_agent" "Forked agent for session $session_id in $worktree_path" echo "Session started for '$issue_ref': $new_session_id"
echo "forked" echo "Worktree: $worktree_path"
return 0
}
cmd_start() {
cmd_continue "$@"
} }
cmd_continue() { cmd_continue() {
local issue_ref="${1:-}" local session_name=""
local message="${2:-}" local message=""
local args=("$@")
if [ -z "$issue_ref" ]; then args=$(set_debug_mode "${args[@]}")
for arg in $args; do
if [ -z "$session_name" ]; then
session_name="$arg"
else
message="$arg"
fi
done
if [ -z "$session_name" ]; then
echo "Error: issue ref is required" >&2 echo "Error: issue ref is required" >&2
echo "Usage: kugetsu continue <issue-ref> [message]" >&2 echo "Usage: kugetsu continue <issue-ref> [message]" >&2
exit 1 exit 1
fi fi
validate_issue_ref "$issue_ref" validate_issue_ref "$session_name"
local session_file=$(get_session_for_issue "$session_name")
if [ -z "$session_file" ] || [ "$session_file" = "null" ]; then
echo "Error: No session found for '$session_name'" >&2
echo "Use 'kugetsu start $session_name' to create a new session." >&2
exit 1
fi
local session_path="$SESSIONS_DIR/$session_file"
if [ ! -f "$session_path" ]; then
echo "Error: Session file not found: $session_path" >&2
exit 1
fi
load_agent_env "dev"
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 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
echo "Warning: Worktree is missing for '$session_name'. Recovering..." >&2
rm -f "$session_path"
remove_issue_from_index "$session_name"
echo "Calling cmd_start to create new session and worktree..." >&2
cmd_start "$session_name" "$message"
return $?
fi
if [ -z "$message" ]; then if [ -z "$message" ]; then
message=$(build_dev_agent_message "$issue_ref" "") message=$(build_dev_agent_message "$issue_ref" "")
else
message=$(build_dev_agent_message "$issue_ref" "$message")
fi fi
local worktree_status=$(ensure_worktree "$issue_ref") cd "$worktree_path"
if [ "$worktree_status" = "error" ]; then local sanitized_id=$(echo "$opencode_session_id" | sed 's/[^a-zA-Z0-9_-]/_/g')
echo "Error: Failed to ensure worktree for '$issue_ref'" >&2 local msg_file="$worktree_path/.kugetsu-msg.txt"
exit 1 printf '%s' "$message" > "$msg_file"
fi nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '@$msg_file' --session '$opencode_session_id'" >> "$LOGS_DIR/dev-$sanitized_id.log" 2>&1 &
rm -f "$msg_file"
local session_status=$(ensure_session "$issue_ref")
if [ "$session_status" = "error" ]; then
echo "Error: Failed to ensure session for '$issue_ref'" >&2
exit 1
fi
kugetsu_context_dump "$issue_ref" "$message" "$(issue_ref_to_branch_name "$issue_ref")"
local session_file=$(issue_ref_to_filename "$issue_ref")
local session_path="$SESSIONS_DIR/$session_file"
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 fork_status=$(fork_agent "$opencode_session_id" "$worktree_path" "$message")
if [ "$fork_status" = "error" ]; then
echo "Error: Failed to fork agent for '$issue_ref'" >&2
exit 1
fi
log "info" "cmd_continue" "Result for $issue_ref: worktree=$worktree_status session=$session_status fork=$fork_status"
echo "Session continued for '$issue_ref': $opencode_session_id"
echo "Worktree: $worktree_path"
echo "${worktree_status}-${session_status}-${fork_status}"
} }
cmd_list() { cmd_list() {

View File

@@ -76,8 +76,7 @@ create_worktree() {
exit 1 exit 1
fi fi
local worktree_parent_dir local worktree_parent_dir=$(dirname "$worktree_path")
worktree_parent_dir=$(dirname "$worktree_path")
mkdir -p "$worktree_parent_dir" mkdir -p "$worktree_parent_dir"
if worktree_exists "$issue_ref" "$parent_dir"; then if worktree_exists "$issue_ref" "$parent_dir"; then
@@ -86,36 +85,15 @@ create_worktree() {
fi fi
echo "Creating worktree at '$worktree_path'..." echo "Creating worktree at '$worktree_path'..."
git clone "$repo_url" "$worktree_path" 2>/dev/null || {
local clone_success=false echo "Error: Failed to clone repository" >&2
local attempt=0
local max_attempts="${NETWORK_RETRY_ATTEMPTS:-3}"
while [ $attempt -lt $max_attempts ] && [ "$clone_success" = false ]; do
attempt=$((attempt + 1))
if [ $attempt -gt 1 ]; then
echo "Clone attempt $attempt of $max_attempts..."
sleep "$((attempt * 2))"
fi
if git clone "$repo_url" "$worktree_path" 2>/dev/null; then
clone_success=true
fi
done
if [ "$clone_success" = false ]; then
echo "Error: Failed to clone repository after $max_attempts attempts" >&2
exit 1 exit 1
fi }
echo "Creating branch '$branch_name'..." echo "Creating branch '$branch_name'..."
if git -C "$worktree_path" checkout -b "$branch_name" origin/main 2>/dev/null; then (cd "$worktree_path" && git checkout -b "$branch_name" origin/main 2>/dev/null || git checkout -b "$branch_name" main 2>/dev/null) || {
:
elif git -C "$worktree_path" checkout -b "$branch_name" main 2>/dev/null; then
:
else
echo "Warning: Could not checkout branch (may need to run from within worktree after session)" >&2 echo "Warning: Could not checkout branch (may need to run from within worktree after session)" >&2
fi }
echo "Worktree created at: $worktree_path" echo "Worktree created at: $worktree_path"
} }
@@ -167,17 +145,15 @@ check_pr_status() {
token="${GITEA_TOKEN:-}" token="${GITEA_TOKEN:-}"
fi fi
local response_file="$KUGETSU_DIR/.pr_status_response_$$.json" local response
if [ -n "$token" ]; then if [ -n "$token" ]; then
curl -s -H "Authorization: token $token" "$api_url" > "$response_file" 2>/dev/null || printf '{}' > "$response_file" response=$(curl -s -H "Authorization: token $token" "$api_url" 2>/dev/null || echo "{}")
else else
curl -s "$api_url" > "$response_file" 2>/dev/null || printf '{}' > "$response_file" response=$(curl -s "$api_url" 2>/dev/null || echo "{}")
fi fi
local state=$(python3 -c "import json; print(json.load(open('$response_file')).get('state', 'unknown'))" 2>/dev/null || echo "unknown") local state=$(echo "$response" | python3 -c "import json, sys; d=json.load(sys.stdin); print(d.get('state', 'unknown'))" 2>/dev/null || echo "unknown")
local merged=$(python3 -c "import json; print('true' if json.load(open('$response_file')).get('merged', False) else 'false')" 2>/dev/null || echo "false") local merged=$(echo "$response" | python3 -c "import json, sys; d=json.load(sys.stdin); print('true' if d.get('merged', False) else 'false')" 2>/dev/null || echo "false")
rm -f "$response_file"
if [ "$merged" = "true" ]; then if [ "$merged" = "true" ]; then
echo "merged" echo "merged"