Compare commits

..

8 Commits

Author SHA1 Message Date
c4c3556247 Merge pull request 'fix(kugetsu): destroy --base and --pm-agent actually delete opencode sessions' (#113) from fix/destroy-removes-opencode-session into main 2026-04-02 15:30:48 +02:00
shokollm
4342347ac6 fix(kugetsu): destroy --base and --pm-agent actually delete opencode sessions
Previously destroy only removed local session files but didn't delete
the sessions from opencode's database. This caused init to reuse the
same session with old context.

Now destroy calls 'opencode session delete <id>' to properly remove
the session from opencode.
2026-04-02 13:28:47 +00:00
7888a34bd9 Merge pull request 'fix(kugetsu): warn if init run from non-empty directory' (#112) from fix/init-directory-warning into main 2026-04-02 15:21:30 +02:00
shokollm
e2a37cdbb9 fix(kugetsu): warn if init run from non-empty directory
Warn users if running kugetsu init from a directory with files or
git repository. This prevents project context from contaminating the
base session, which causes forked sessions to have unwanted context.
2026-04-02 13:19:49 +00:00
shokollm
6e9472b5e2 fix(kugetsu): detect session via DB query instead of opencode session list
opencode session list doesn't show sessions in ~/.kugetsu-worktrees/ directories.
This caused detection to fail even though sessions were being created.

Now we query the database directly for sessions matching the worktree path.
Also fixed database path in fix_session_permissions (was ~/.opencode/, should be ~/.local/share/opencode/).
2026-04-02 11:45:35 +00:00
shokollm
775f73348a fix(kugetsu): update forked session permissions after detection
Previously we only fixed base session permissions before forking.
But permissions are NOT inherited from parent to child.

Now we update the newly created session's permissions immediately
after detection, ensuring the forked session can access external
directories like ~/.kugetsu/worktrees/.
2026-04-02 11:15:27 +00:00
2e9081f4f5 Merge pull request 'fix(kugetsu): call fix_session_permissions before forking' (#109) from fix/prefork-permissions into main 2026-04-02 13:10:54 +02:00
shokollm
f7ac2f35fe fix(kugetsu): call fix_session_permissions before forking
- Call fix_session_permissions in cmd_start before forking to ensure
  base session has correct permissions for external_directory access
- Add debug logging to show forked session's directory and permissions
  after creation to help diagnose permission inheritance issues
2026-04-02 11:08:30 +00:00

View File

@@ -876,7 +876,7 @@ cmd_doctor() {
} }
fix_session_permissions() { fix_session_permissions() {
local opencode_db="${OPENCODE_DB:-$HOME/.opencode/opencode.db}" local opencode_db="${OPENCODE_DB:-$HOME/.local/share/opencode/opencode.db}"
if [ ! -f "$opencode_db" ]; then if [ ! -f "$opencode_db" ]; then
echo "[ERROR] opencode database not found: $opencode_db" echo "[ERROR] opencode database not found: $opencode_db"
@@ -1107,6 +1107,22 @@ EOF
exit 1 exit 1
fi fi
local cwd_files=$(ls -A "$PWD" 2>/dev/null | wc -l)
local cwd_git=$(git rev-parse --is-inside-work-tree 2>/dev/null || echo "false")
if [ "$cwd_files" -gt 0 ] || [ "$cwd_git" = "true" ]; then
echo "Warning: Current directory is not empty: $PWD" >&2
echo "This may cause project context to contaminate the base session." >&2
echo "Consider running kugetsu init from an empty directory." >&2
echo "" >&2
echo "Files in current directory: $cwd_files" >&2
if [ "$cwd_git" = "true" ]; then
echo "Git repository detected: $(git rev-parse --show-toplevel 2>/dev/null || echo 'unknown')" >&2
fi
echo "" >&2
echo "Press Ctrl+C to cancel or wait 5 seconds to continue anyway..." >&2
sleep 5
fi
echo "Starting TUI to create base session..." echo "Starting TUI to create base session..."
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
@@ -1226,12 +1242,6 @@ cmd_start() {
local session_file="$(issue_ref_to_filename "$issue_ref").json" local session_file="$(issue_ref_to_filename "$issue_ref").json"
# Get list of sessions before fork to compare against after
declare -a before_sessions=()
while IFS= read -r sess; do
before_sessions+=("$sess")
done < <(opencode session list 2>/dev/null | grep -oP '^ses_\w+')
echo "Forking session for '$issue_ref'..." echo "Forking session for '$issue_ref'..."
# Session-counting: count actual dev sessions, reject if at limit # Session-counting: count actual dev sessions, reject if at limit
@@ -1242,14 +1252,18 @@ cmd_start() {
remove_worktree_for_issue "$issue_ref" "$parent_dir" remove_worktree_for_issue "$issue_ref" "$parent_dir"
exit 1 exit 1
fi fi
local fork_log="$SESSIONS_DIR/$session_file.fork.log" local fork_log="$SESSIONS_DIR/$session_file.fork.log"
local opencode_db="${OPENCODE_DB:-$HOME/.local/share/opencode/opencode.db}" local opencode_db="${OPENCODE_DB:-$HOME/.local/share/opencode/opencode.db}"
> "$fork_log"
fix_session_permissions
if [ "$DEBUG_MODE" = true ]; then if [ "$DEBUG_MODE" = true ]; then
(cd "$worktree_path" && opencode run "$message" --fork --session "$base_session_id" 2>&1) | tee "$fork_log" & (cd "$worktree_path" && opencode run "$message" --fork --session "$base_session_id" --dir "$worktree_path" 2>&1) | tee "$fork_log" &
else else
(cd "$worktree_path" && opencode run "$message" --fork --session "$base_session_id" 2>&1) >> "$fork_log" & (cd "$worktree_path" && opencode run "$message" --fork --session "$base_session_id" --dir "$worktree_path" 2>&1) >> "$fork_log" &
fi fi
local fork_pid=$! local fork_pid=$!
@@ -1262,25 +1276,17 @@ cmd_start() {
while [ $attempt -le $max_attempts ]; do while [ $attempt -le $max_attempts ]; do
sleep 1 sleep 1
while IFS= read -r sess; do new_session_id=$(python3 -c "
[ "$sess" = "$base_session_id" ] && continue import sqlite3
[ "$sess" = "$pm_agent_session_id" ] && continue conn = sqlite3.connect('$opencode_db')
cursor = conn.cursor()
local existed_before=false cursor.execute(\"SELECT id FROM session WHERE directory = '$worktree_path' ORDER BY time_created DESC LIMIT 1\")
for before_sess in "${before_sessions[@]}"; do result = cursor.fetchone()
if [ "$sess" = "$before_sess" ]; then if result:
existed_before=true print(result[0])
break " 2>/dev/null || echo "")
fi
done
if [ "$existed_before" = false ]; then
new_session_id="$sess"
break
fi
done < <(opencode session list 2>/dev/null | grep -oP '^ses_\w+')
if [ -n "$new_session_id" ]; then if [ -n "$new_session_id" ] && [ "$new_session_id" != "$base_session_id" ] && [ "$new_session_id" != "$pm_agent_session_id" ]; then
break break
fi fi
@@ -1293,21 +1299,6 @@ cmd_start() {
done done
if [ -z "$new_session_id" ]; then if [ -z "$new_session_id" ]; then
if [ -f "$opencode_db" ]; then
local db_sessions=$(python3 -c "
import sqlite3
conn = sqlite3.connect('$opencode_db')
cursor = conn.cursor()
cursor.execute(\"SELECT id FROM session WHERE parent_id IS NOT NULL ORDER BY time_created DESC LIMIT 5\")
for row in cursor.fetchall():
print(row[0])
" 2>/dev/null || echo "")
if [ -n "$db_sessions" ]; then
echo "Recent forked sessions in DB:" >&2
echo "$db_sessions" >&2
fi
fi
echo "Error: Could not find newly created session after ${max_attempts}s" >&2 echo "Error: Could not find newly created session after ${max_attempts}s" >&2
if [ -n "$fork_log_output" ]; then if [ -n "$fork_log_output" ]; then
echo "Fork log output:" >&2 echo "Fork log output:" >&2
@@ -1317,6 +1308,31 @@ for row in cursor.fetchall():
exit 1 exit 1
fi fi
echo "Updating permissions for new session: $new_session_id"
python3 -c "
import sqlite3
conn = sqlite3.connect('$opencode_db')
cursor = conn.cursor()
PERMISSION_JSON = '[{\"permission\":\"question\",\"pattern\":\"*\",\"action\":\"deny\"},{\"permission\":\"plan_enter\",\"pattern\":\"*\",\"action\":\"deny\"},{\"permission\":\"plan_exit\",\"pattern\":\"*\",\"action\":\"deny\"},{\"permission\":\"external_directory\",\"pattern\":\"*\",\"action\":\"allow\"}]'
cursor.execute('UPDATE session SET permission = ? WHERE id = ?', (PERMISSION_JSON, '$new_session_id'))
conn.commit()
print('[OK] Session permissions updated')
"
if [ "$DEBUG_MODE" = true ]; then
echo "[DEBUG] Forked session permissions check:"
python3 -c "
import sqlite3
conn = sqlite3.connect('$opencode_db')
cursor = conn.cursor()
cursor.execute(\"SELECT id, directory, permission FROM session WHERE id = '$new_session_id'\")
for row in cursor.fetchall():
print(' ID:', row[0])
print(' Directory:', row[1])
print(' Permission:', row[2])
" 2>/dev/null || echo " (failed to query DB)"
fi
printf '{"type": "forked", "issue_ref": "%s", "opencode_session_id": "%s", "worktree_path": "%s", "created_at": "%s", "state": "idle"}\n' \ printf '{"type": "forked", "issue_ref": "%s", "opencode_session_id": "%s", "worktree_path": "%s", "created_at": "%s", "state": "idle"}\n' \
"$issue_ref" "$new_session_id" "$worktree_path" "$(date -Iseconds)" > "$SESSIONS_DIR/$session_file" "$issue_ref" "$new_session_id" "$worktree_path" "$(date -Iseconds)" > "$SESSIONS_DIR/$session_file"
@@ -1373,9 +1389,9 @@ cmd_continue() {
if [ -n "$worktree_path" ] && [ -d "$worktree_path" ]; then if [ -n "$worktree_path" ] && [ -d "$worktree_path" ]; then
echo "Using worktree: $worktree_path" echo "Using worktree: $worktree_path"
if [ "$DEBUG_MODE" = true ]; then if [ "$DEBUG_MODE" = true ]; then
(cd "$worktree_path" && opencode run "$message" --continue --session "$opencode_session_id" 2>&1) | tee "$session_path.debug.log" & opencode run "$message" --continue --session "$opencode_session_id" --dir "$worktree_path" 2>&1 | tee "$session_path.debug.log" &
else else
(cd "$worktree_path" && opencode run "$message" --continue --session "$opencode_session_id" 2>&1) & opencode run "$message" --continue --session "$opencode_session_id" --dir "$worktree_path" 2>&1 &
fi fi
else else
if [ "$DEBUG_MODE" = true ]; then if [ "$DEBUG_MODE" = true ]; then
@@ -1532,6 +1548,7 @@ cmd_destroy() {
if [ "$target" = "base" ]; then if [ "$target" = "base" ]; then
if [ "$force" = true ]; then if [ "$force" = true ]; then
local base_session_id=$(get_base_session_id)
rm -f "$SESSIONS_DIR/base.json" rm -f "$SESSIONS_DIR/base.json"
local pm_agent=$(get_pm_agent_session_id) local pm_agent=$(get_pm_agent_session_id)
if [ -n "$pm_agent" ] && [ "$pm_agent" != "null" ]; then if [ -n "$pm_agent" ] && [ "$pm_agent" != "null" ]; then
@@ -1540,6 +1557,10 @@ cmd_destroy() {
else else
echo '{"base": null, "pm_agent": null, "issues": {}}' > "$INDEX_FILE" echo '{"base": null, "pm_agent": null, "issues": {}}' > "$INDEX_FILE"
fi fi
if [ -n "$base_session_id" ] && [ "$base_session_id" != "null" ]; then
echo "Deleting opencode session: $base_session_id"
opencode session delete "$base_session_id" 2>/dev/null || echo "Warning: Could not delete session from opencode (may already be deleted)"
fi
echo "Base session destroyed" echo "Base session destroyed"
else else
echo "Error: destroying base session requires --base -y" >&2 echo "Error: destroying base session requires --base -y" >&2
@@ -1550,6 +1571,7 @@ cmd_destroy() {
if [ "$target" = "pm-agent" ]; then if [ "$target" = "pm-agent" ]; then
if [ "$force" = true ]; then if [ "$force" = true ]; then
local pm_session_id=$(get_pm_agent_session_id)
rm -f "$SESSIONS_DIR/pm-agent.json" rm -f "$SESSIONS_DIR/pm-agent.json"
local base=$(get_base_session_id) local base=$(get_base_session_id)
if [ -n "$base" ] && [ "$base" != "null" ]; then if [ -n "$base" ] && [ "$base" != "null" ]; then
@@ -1557,6 +1579,10 @@ cmd_destroy() {
else else
write_index "null" "null" "{}" write_index "null" "null" "{}"
fi fi
if [ -n "$pm_session_id" ] && [ "$pm_session_id" != "null" ]; then
echo "Deleting opencode session: $pm_session_id"
opencode session delete "$pm_session_id" 2>/dev/null || echo "Warning: Could not delete session from opencode (may already be deleted)"
fi
echo "PM agent session destroyed" echo "PM agent session destroyed"
else else
echo "Error: destroying pm-agent session requires --pm-agent -y" >&2 echo "Error: destroying pm-agent session requires --pm-agent -y" >&2