feat(kugetsu): add git worktree isolation per session
- Each issue session gets isolated git worktree to prevent workspace conflicts
- Worktree created on 'kugetsu start', removed on 'kugetsu destroy'
- Worktree path: ~/.kugetsu/worktrees/{sanitized-issue-ref}/
- Branch naming: fix/issue-{number} or fix/{identifier}
- Worktree always recreated on start (guaranteed clean state)
- 'kugetsu list' now shows worktree path
- 'kugetsu prune' also cleans orphaned worktrees
- 'kugetsu continue' runs opencode with --workdir pointing to worktree
- Update SKILL.md to v2.2 with worktree documentation
Part of issue #19 Phase 3 implementation
This commit is contained in:
@@ -5,12 +5,12 @@ license: MIT
|
|||||||
compatibility: Requires opencode CLI, bash, python3, and filesystem access.
|
compatibility: Requires opencode CLI, bash, python3, and filesystem access.
|
||||||
metadata:
|
metadata:
|
||||||
author: shoko
|
author: shoko
|
||||||
version: "2.1"
|
version: "2.2"
|
||||||
---
|
---
|
||||||
|
|
||||||
# kugetsu - OpenCode Session Manager (Issue-Driven)
|
# kugetsu - OpenCode Session Manager (Issue-Driven)
|
||||||
|
|
||||||
Manages opencode sessions with a base session + forked session pattern optimized for headless orchestration.
|
Manages opencode sessions with a base session + forked session pattern optimized for headless orchestration. Each issue gets an isolated git worktree to prevent workspace conflicts.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -34,6 +34,12 @@ chmod +x ~/.local/bin/kugetsu
|
|||||||
- **PM Agent Session**: Created during init, persistent coordinator for task management
|
- **PM Agent Session**: Created during init, persistent coordinator for task management
|
||||||
- **Forked Sessions**: One per issue, branched from base via `opencode run --fork --session <base>`
|
- **Forked Sessions**: One per issue, branched from base via `opencode run --fork --session <base>`
|
||||||
|
|
||||||
|
### Git Worktree Isolation
|
||||||
|
Each issue session gets its own git worktree to prevent conflicts:
|
||||||
|
- Isolated working directory (no file collisions)
|
||||||
|
- Isolated branch (no checkout conflicts)
|
||||||
|
- Shared `.git` objects (efficient storage)
|
||||||
|
|
||||||
### Directory Structure
|
### Directory Structure
|
||||||
```
|
```
|
||||||
~/.kugetsu/
|
~/.kugetsu/
|
||||||
@@ -41,6 +47,9 @@ chmod +x ~/.local/bin/kugetsu
|
|||||||
│ ├── base.json # Base session metadata
|
│ ├── base.json # Base session metadata
|
||||||
│ ├── pm-agent.json # PM agent session metadata
|
│ ├── pm-agent.json # PM agent session metadata
|
||||||
│ └── github.com-shoko-kugetsu-14.json # Forked session per issue
|
│ └── github.com-shoko-kugetsu-14.json # Forked session per issue
|
||||||
|
├── worktrees/
|
||||||
|
│ ├── github.com-shoko-kugetsu-14/ # Isolated workdir for issue #14
|
||||||
|
│ └── github.com-shoko-kugetsu-15/ # Isolated workdir for issue #15
|
||||||
└── index.json # Maps session IDs and issue refs to session files
|
└── index.json # Maps session IDs and issue refs to session files
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -58,8 +67,10 @@ chmod +x ~/.local/bin/kugetsu
|
|||||||
### Session File
|
### Session File
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "base|forked|pm_agent",
|
"type": "forked",
|
||||||
|
"issue_ref": "github.com/shoko/kugetsu#14",
|
||||||
"opencode_session_id": "ses_xyz789",
|
"opencode_session_id": "ses_xyz789",
|
||||||
|
"worktree_path": "/home/user/.kugetsu/worktrees/github.com-shoko-kugetsu-14",
|
||||||
"created_at": "2026-03-29T18:16:10+02:00",
|
"created_at": "2026-03-29T18:16:10+02:00",
|
||||||
"state": "idle"
|
"state": "idle"
|
||||||
}
|
}
|
||||||
@@ -67,12 +78,33 @@ chmod +x ~/.local/bin/kugetsu
|
|||||||
|
|
||||||
## Issue Ref Format
|
## Issue Ref Format
|
||||||
|
|
||||||
All issue references use the format: `instance/user/repo#number`
|
All issue references use the format: `instance/user/repo#identifier`
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
- `github.com/shoko/kugetsu#14`
|
- `github.com/shoko/kugetsu#14` (issue number)
|
||||||
- `gitlab.com/username/project#42`
|
- `github.com/shoko/kugetsu#-discuss` (discussion, no issue number yet)
|
||||||
- `codeberg.org/user/repo#100`
|
- `gitlab.com/username/project#42` (issue number)
|
||||||
|
|
||||||
|
## Worktree Behavior
|
||||||
|
|
||||||
|
### On `kugetsu start`
|
||||||
|
1. Derives worktree path from issue ref: `~/.kugetsu/worktrees/{sanitized-ref}/`
|
||||||
|
2. If worktree exists: removes and recreates (guaranteed clean state)
|
||||||
|
3. If worktree doesn't exist: creates fresh
|
||||||
|
4. Clones repo, creates branch `fix/issue-{id}`
|
||||||
|
5. Runs opencode with `--workdir` pointing to worktree
|
||||||
|
|
||||||
|
### On `kugetsu destroy`
|
||||||
|
1. Removes worktree via `git worktree remove`
|
||||||
|
2. Deletes session file and index entry
|
||||||
|
|
||||||
|
### Repo Configuration
|
||||||
|
If the repo URL cannot be derived from the issue ref, add to `~/.kugetsu/repos.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"github.com/shoko kugetsu#14": "https://custom.repo.url/owner/repo.git"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -93,12 +125,13 @@ kugetsu init
|
|||||||
Start task for an issue by forking from base session:
|
Start task for an issue by forking from base session:
|
||||||
```bash
|
```bash
|
||||||
kugetsu start github.com/shoko/kugetsu#14 "fix authentication bug"
|
kugetsu start github.com/shoko/kugetsu#14 "fix authentication bug"
|
||||||
|
kugetsu start github.com/shoko/kugetsu#-discuss "research auth options"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- Creates isolated git worktree for the issue
|
||||||
- Forks new session from base
|
- Forks new session from base
|
||||||
- Requires PM agent to exist (created by init)
|
- Requires PM agent to exist (created by init)
|
||||||
- Stores mapping in `index.json`
|
- Uses `opencode run --fork --session <base-session-id> "<message>" --workdir <worktree>`
|
||||||
- Uses `opencode run --fork --session <base-session-id> "<message>"`
|
|
||||||
|
|
||||||
### kugetsu continue `<issue-ref>` `<message>` [--debug]
|
### kugetsu continue `<issue-ref>` `<message>` [--debug]
|
||||||
|
|
||||||
@@ -108,7 +141,7 @@ kugetsu continue github.com/shoko/kugetsu#14 "add unit tests"
|
|||||||
```
|
```
|
||||||
|
|
||||||
- Looks up session file from index
|
- Looks up session file from index
|
||||||
- Uses `opencode run --continue --session <opencode-session-id> "<message>"`
|
- Uses `opencode run --continue --session <opencode-session-id> "<message>" --workdir <worktree>`
|
||||||
|
|
||||||
### kugetsu list
|
### kugetsu list
|
||||||
|
|
||||||
@@ -119,28 +152,28 @@ kugetsu list
|
|||||||
|
|
||||||
Output:
|
Output:
|
||||||
```
|
```
|
||||||
ISSUE_REF TYPE SESSION_ID CREATED
|
ISSUE_REF TYPE SESSION_ID WORKTREE
|
||||||
─────────────────────────────────────────────────────────────────────────────────────────────────
|
────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
(base) base ses_abc123 N/A
|
(base) base ses_abc123 N/A
|
||||||
(pm-agent) pm_agent ses_pm_xyz789 2026-03-29T18:16:10+02:00
|
(pm-agent) pm_agent ses_pm_xyz789 N/A
|
||||||
github.com/shoko/kugetsu#14 forked ses_xyz789 2026-03-29T18:16:10+02:00
|
github.com/shoko/kugetsu#14 forked ses_xyz789 /home/user/.kugetsu/worktrees/github.com-shoko-kugetsu-14
|
||||||
```
|
```
|
||||||
|
|
||||||
### kugetsu prune [--force]
|
### kugetsu prune [--force]
|
||||||
|
|
||||||
Remove orphaned sessions (files not in index):
|
Remove orphaned sessions and worktrees:
|
||||||
```bash
|
```bash
|
||||||
kugetsu prune # Shows what would be deleted
|
kugetsu prune # Shows what would be deleted
|
||||||
kugetsu prune --force # Deletes orphaned sessions
|
kugetsu prune --force # Deletes orphaned items
|
||||||
```
|
```
|
||||||
|
|
||||||
- Orphaned = session files in `sessions/` but not in `index.json`
|
- Orphaned = session files or worktrees not in index
|
||||||
- Always keeps `base.json` and `pm-agent.json`
|
- Always keeps `base.json` and `pm-agent.json`
|
||||||
- Useful after opencode session cleanup
|
- Useful after opencode session cleanup
|
||||||
|
|
||||||
### kugetsu destroy `<issue-ref>` [-y]
|
### kugetsu destroy `<issue-ref>` [-y]
|
||||||
|
|
||||||
Delete session for specific issue:
|
Delete session and worktree for specific issue:
|
||||||
```bash
|
```bash
|
||||||
kugetsu destroy github.com/shoko/kugetsu#14 # Prompts for confirmation
|
kugetsu destroy github.com/shoko/kugetsu#14 # Prompts for confirmation
|
||||||
kugetsu destroy github.com/shoko/kugetsu#14 -y # Skips confirmation
|
kugetsu destroy github.com/shoko/kugetsu#14 -y # Skips confirmation
|
||||||
@@ -162,73 +195,6 @@ kugetsu destroy --base -y
|
|||||||
|
|
||||||
**Note**: Destroying base also destroys PM agent since PM depends on base.
|
**Note**: Destroying base also destroys PM agent since PM depends on base.
|
||||||
|
|
||||||
- Requires a terminal (TTY) to spawn the opencode TUI
|
|
||||||
- Creates base session once; subsequent runs error unless `--force` is used
|
|
||||||
- Stores base session ID in `index.json`
|
|
||||||
|
|
||||||
### kugetsu start `<issue-ref>` `<message>` [--debug]
|
|
||||||
|
|
||||||
Start task for an issue by forking from base session:
|
|
||||||
```bash
|
|
||||||
kugetsu start github.com/shoko/kugetsu#14 "fix authentication bug"
|
|
||||||
```
|
|
||||||
|
|
||||||
- Forks new session from base
|
|
||||||
- Stores mapping in `index.json`
|
|
||||||
- Uses `opencode run --fork --session <base-session-id> "<message>"`
|
|
||||||
|
|
||||||
### kugetsu continue `<issue-ref>` `<message>` [--debug]
|
|
||||||
|
|
||||||
Continue work on an existing issue session:
|
|
||||||
```bash
|
|
||||||
kugetsu continue github.com/shoko/kugetsu#14 "add unit tests"
|
|
||||||
```
|
|
||||||
|
|
||||||
- Looks up session file from index
|
|
||||||
- Uses `opencode run --continue --session <opencode-session-id> "<message>"`
|
|
||||||
|
|
||||||
### kugetsu list
|
|
||||||
|
|
||||||
List all tracked sessions:
|
|
||||||
```bash
|
|
||||||
kugetsu list
|
|
||||||
```
|
|
||||||
|
|
||||||
Output:
|
|
||||||
```
|
|
||||||
ISSUE_REF TYPE SESSION_ID CREATED
|
|
||||||
──────────────────────────────────────────────────────────────────────────────────────────────────
|
|
||||||
(base) base ses_abc123 N/A
|
|
||||||
github.com/shoko/kugetsu#14 forked ses_xyz789 2026-03-29T18:16:10+02:00
|
|
||||||
```
|
|
||||||
|
|
||||||
### kugetsu prune [--force]
|
|
||||||
|
|
||||||
Remove orphaned sessions (files not in index):
|
|
||||||
```bash
|
|
||||||
kugetsu prune # Shows what would be deleted
|
|
||||||
kugetsu prune --force # Deletes orphaned sessions
|
|
||||||
```
|
|
||||||
|
|
||||||
- Orphaned = session files in `sessions/` but not in `index.json`
|
|
||||||
- Always keeps `base.json`
|
|
||||||
- Useful after opencode session cleanup
|
|
||||||
|
|
||||||
### kugetsu destroy `<issue-ref>` [-y]
|
|
||||||
|
|
||||||
Delete session for specific issue:
|
|
||||||
```bash
|
|
||||||
kugetsu destroy github.com/shoko/kugetsu#14 # Prompts for confirmation
|
|
||||||
kugetsu destroy github.com/shoko/kugetsu#14 -y # Skips confirmation
|
|
||||||
```
|
|
||||||
|
|
||||||
### kugetsu destroy --base [-y]
|
|
||||||
|
|
||||||
Delete base session (requires explicit `--base`):
|
|
||||||
```bash
|
|
||||||
kugetsu destroy --base -y
|
|
||||||
```
|
|
||||||
|
|
||||||
## Workflow Example
|
## Workflow Example
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -238,6 +204,7 @@ kugetsu init
|
|||||||
|
|
||||||
# Start work on issue
|
# Start work on issue
|
||||||
kugetsu start github.com/shoko/kugetsu#14 "implement feature X"
|
kugetsu start github.com/shoko/kugetsu#14 "implement feature X"
|
||||||
|
# Creates: worktree at ~/.kugetsu/worktrees/github.com-shoko-kugetsu-14/
|
||||||
|
|
||||||
# Continue later
|
# Continue later
|
||||||
kugetsu continue github.com/shoko/kugetsu#14 "add tests"
|
kugetsu continue github.com/shoko/kugetsu#14 "add tests"
|
||||||
@@ -248,10 +215,10 @@ kugetsu continue github.com/shoko/kugetsu#14 "fix failing test"
|
|||||||
# List all sessions
|
# List all sessions
|
||||||
kugetsu list
|
kugetsu list
|
||||||
|
|
||||||
# Clean up orphaned sessions
|
# Clean up orphaned items
|
||||||
kugetsu prune --force
|
kugetsu prune --force
|
||||||
|
|
||||||
# Delete session when done
|
# Delete session and worktree when done
|
||||||
kugetsu destroy github.com/shoko/kugetsu#14
|
kugetsu destroy github.com/shoko/kugetsu#14
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -266,13 +233,14 @@ The pattern:
|
|||||||
- Base session created once via TUI (interactive)
|
- Base session created once via TUI (interactive)
|
||||||
- PM agent session created during init (persistent coordinator)
|
- PM agent session created during init (persistent coordinator)
|
||||||
- All subsequent work uses `--fork --session <base>` or `--continue --session <forked>`
|
- All subsequent work uses `--fork --session <base>` or `--continue --session <forked>`
|
||||||
|
- Each session works in isolated git worktree
|
||||||
|
|
||||||
## Recovery
|
## Recovery
|
||||||
|
|
||||||
If opencode sessions become out of sync:
|
If opencode sessions become out of sync:
|
||||||
|
|
||||||
1. `kugetsu list` shows tracked sessions
|
1. `kugetsu list` shows tracked sessions
|
||||||
2. `kugetsu prune` removes orphaned files
|
2. `kugetsu prune` removes orphaned files and worktrees
|
||||||
3. For full reset: `kugetsu destroy --base -y && kugetsu init`
|
3. For full reset: `kugetsu destroy --base -y && kugetsu init`
|
||||||
|
|
||||||
## Remote Access via SSH (Optional)
|
## Remote Access via SSH (Optional)
|
||||||
@@ -362,4 +330,4 @@ opencode run --fork --session <base-session-id> "task"
|
|||||||
opencode run --continue --session <forked-session-id> "continue"
|
opencode run --continue --session <forked-session-id> "continue"
|
||||||
```
|
```
|
||||||
|
|
||||||
Tradeoff: No issue mapping, no index, manual session tracking.
|
Tradeoff: No issue mapping, no index, manual session tracking, no worktree isolation.
|
||||||
@@ -3,6 +3,8 @@ set -euo pipefail
|
|||||||
|
|
||||||
KUGETSU_DIR="${KUGETSU_DIR:-$HOME/.kugetsu}"
|
KUGETSU_DIR="${KUGETSU_DIR:-$HOME/.kugetsu}"
|
||||||
SESSIONS_DIR="$KUGETSU_DIR/sessions"
|
SESSIONS_DIR="$KUGETSU_DIR/sessions"
|
||||||
|
WORKTREES_DIR="$KUGETSU_DIR/worktrees"
|
||||||
|
REPOS_CONFIG="$KUGETSU_DIR/repos.json"
|
||||||
INDEX_FILE="$KUGETSU_DIR/index.json"
|
INDEX_FILE="$KUGETSU_DIR/index.json"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
@@ -53,6 +55,109 @@ ensure_dirs() {
|
|||||||
mkdir -p "$SESSIONS_DIR"
|
mkdir -p "$SESSIONS_DIR"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensure_worktree_dir() {
|
||||||
|
mkdir -p "$WORKTREES_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
issue_ref_to_worktree_name() {
|
||||||
|
local issue_ref="$1"
|
||||||
|
echo "$issue_ref" | sed 's/[\/:]/-/g' | sed 's/#/--/'
|
||||||
|
}
|
||||||
|
|
||||||
|
issue_ref_to_worktree_path() {
|
||||||
|
local issue_ref="$1"
|
||||||
|
local worktree_name=$(issue_ref_to_worktree_name "$issue_ref")
|
||||||
|
echo "$WORKTREES_DIR/$worktree_name"
|
||||||
|
}
|
||||||
|
|
||||||
|
issue_ref_to_branch_name() {
|
||||||
|
local issue_ref="$1"
|
||||||
|
local number_part=$(echo "$issue_ref" | grep -oE '#[0-9]+$' || echo "")
|
||||||
|
if [ -n "$number_part" ]; then
|
||||||
|
echo "fix/issue-${number_part#\#}"
|
||||||
|
else
|
||||||
|
local identifier=$(echo "$issue_ref" | grep -oE '#[^-]+$' || echo "")
|
||||||
|
if [ -n "$identifier" ]; then
|
||||||
|
local clean_id=$(echo "$identifier" | sed 's/^#//' | sed 's/-/_/g')
|
||||||
|
echo "fix/${clean_id}"
|
||||||
|
else
|
||||||
|
echo "fix/issue-temp"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
get_repo_url() {
|
||||||
|
local issue_ref="$1"
|
||||||
|
if [ -f "$REPOS_CONFIG" ]; then
|
||||||
|
local url=$(python3 -c "import json, sys; d=json.load(open('$REPOS_CONFIG')); print(d.get('$issue_ref', ''))" 2>/dev/null || echo "")
|
||||||
|
if [ -n "$url" ]; then
|
||||||
|
echo "$url"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
local instance=$(echo "$issue_ref" | cut -d'/' -f1 | cut -d'#' -f1)
|
||||||
|
local rest=$(echo "$issue_ref" | sed 's/.*\///' | sed 's/#.*//')
|
||||||
|
echo "https://${instance}/${rest}.git"
|
||||||
|
}
|
||||||
|
|
||||||
|
worktree_exists() {
|
||||||
|
local issue_ref="$1"
|
||||||
|
local worktree_path=$(issue_ref_to_worktree_path "$issue_ref")
|
||||||
|
[ -d "$worktree_path" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
create_worktree() {
|
||||||
|
local issue_ref="$1"
|
||||||
|
local worktree_path=$(issue_ref_to_worktree_path "$issue_ref")
|
||||||
|
local branch_name=$(issue_ref_to_branch_name "$issue_ref")
|
||||||
|
local repo_url=$(get_repo_url "$issue_ref")
|
||||||
|
|
||||||
|
if [ -z "$repo_url" ]; then
|
||||||
|
echo "Error: Cannot determine repo URL for '$issue_ref'" >&2
|
||||||
|
echo "Please add to $REPOS_CONFIG or ensure worktree exists" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ensure_worktree_dir
|
||||||
|
|
||||||
|
if worktree_exists "$issue_ref"; then
|
||||||
|
echo "Removing existing worktree at '$worktree_path'..."
|
||||||
|
git worktree remove "$worktree_path" 2>/dev/null || rm -rf "$worktree_path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Creating worktree at '$worktree_path'..."
|
||||||
|
git clone --bare "$repo_url" "$worktree_path" 2>/dev/null || {
|
||||||
|
echo "Error: Failed to clone repository" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Creating branch '$branch_name'..."
|
||||||
|
(cd "$worktree_path" && git checkout -b "$branch_name" origin/main 2>/dev/null || git checkout -b "$branch_name" main 2>/dev/null) || {
|
||||||
|
echo "Warning: Could not checkout branch (may need to run from within worktree after session)" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Worktree created at: $worktree_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_worktree_for_issue() {
|
||||||
|
local issue_ref="$1"
|
||||||
|
local worktree_path=$(issue_ref_to_worktree_path "$issue_ref")
|
||||||
|
|
||||||
|
if worktree_exists "$issue_ref"; then
|
||||||
|
echo "Removing worktree at '$worktree_path'..."
|
||||||
|
git worktree remove "$worktree_path" 2>/dev/null || rm -rf "$worktree_path"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
get_worktree_path_for_session() {
|
||||||
|
local session_file="$1"
|
||||||
|
if [ -f "$session_file" ]; then
|
||||||
|
python3 -c "import json; print(json.load(open('$session_file')).get('worktree_path', ''))" 2>/dev/null || echo ""
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
issue_ref_to_filename() {
|
issue_ref_to_filename() {
|
||||||
local issue_ref="$1"
|
local issue_ref="$1"
|
||||||
echo "$issue_ref" | sed 's/[\/:]/-/g' | sed 's/#/-/'
|
echo "$issue_ref" | sed 's/[\/:]/-/g' | sed 's/#/-/'
|
||||||
@@ -329,6 +434,9 @@ cmd_start() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
local worktree_path=$(issue_ref_to_worktree_path "$issue_ref")
|
||||||
|
create_worktree "$issue_ref"
|
||||||
|
|
||||||
local session_file="$(issue_ref_to_filename "$issue_ref").json"
|
local session_file="$(issue_ref_to_filename "$issue_ref").json"
|
||||||
|
|
||||||
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)
|
||||||
@@ -336,9 +444,9 @@ cmd_start() {
|
|||||||
|
|
||||||
echo "Forking session for '$issue_ref'..."
|
echo "Forking session for '$issue_ref'..."
|
||||||
if [ "$DEBUG_MODE" = true ]; then
|
if [ "$DEBUG_MODE" = true ]; then
|
||||||
opencode run --fork --session "$base_session_id" "$message" 2>&1 | tee "$SESSIONS_DIR/$session_file.debug.log"
|
opencode run --fork --session "$base_session_id" "$message" --workdir "$worktree_path" 2>&1 | tee "$SESSIONS_DIR/$session_file.debug.log"
|
||||||
else
|
else
|
||||||
opencode run --fork --session "$base_session_id" "$message" 2>&1
|
opencode run --fork --session "$base_session_id" "$message" --workdir "$worktree_path" 2>&1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
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)
|
||||||
@@ -352,15 +460,17 @@ cmd_start() {
|
|||||||
|
|
||||||
if [ -z "$new_session_id" ]; then
|
if [ -z "$new_session_id" ]; then
|
||||||
echo "Error: Could not find newly created session" >&2
|
echo "Error: Could not find newly created session" >&2
|
||||||
|
remove_worktree_for_issue "$issue_ref"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf '{"type": "forked", "issue_ref": "%s", "opencode_session_id": "%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" "$(date -Iseconds)" > "$SESSIONS_DIR/$session_file"
|
"$issue_ref" "$new_session_id" "$worktree_path" "$(date -Iseconds)" > "$SESSIONS_DIR/$session_file"
|
||||||
|
|
||||||
add_issue_to_index "$issue_ref" "$session_file"
|
add_issue_to_index "$issue_ref" "$session_file"
|
||||||
|
|
||||||
echo "Session started for '$issue_ref': $new_session_id"
|
echo "Session started for '$issue_ref': $new_session_id"
|
||||||
|
echo "Worktree: $worktree_path"
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_continue() {
|
cmd_continue() {
|
||||||
@@ -405,6 +515,7 @@ cmd_continue() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local opencode_session_id=$(python3 -c "import json; print(json.load(open('$session_path'))['opencode_session_id'])")
|
local opencode_session_id=$(python3 -c "import json; print(json.load(open('$session_path'))['opencode_session_id'])")
|
||||||
|
local worktree_path=$(python3 -c "import json; print(json.load(open('$session_path')).get('worktree_path', ''))" 2>/dev/null || echo "")
|
||||||
|
|
||||||
if ! check_opencode_session_exists "$opencode_session_id"; then
|
if ! check_opencode_session_exists "$opencode_session_id"; then
|
||||||
echo "Warning: Session may have expired in opencode" >&2
|
echo "Warning: Session may have expired in opencode" >&2
|
||||||
@@ -412,22 +523,31 @@ cmd_continue() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Continuing session for '$issue_ref'..."
|
echo "Continuing session for '$issue_ref'..."
|
||||||
|
if [ -n "$worktree_path" ] && [ -d "$worktree_path" ]; then
|
||||||
|
echo "Using worktree: $worktree_path"
|
||||||
|
if [ "$DEBUG_MODE" = true ]; then
|
||||||
|
opencode run --continue --session "$opencode_session_id" "$message" --workdir "$worktree_path" 2>&1 | tee "$session_path.debug.log"
|
||||||
|
else
|
||||||
|
opencode run --continue --session "$opencode_session_id" "$message" --workdir "$worktree_path"
|
||||||
|
fi
|
||||||
|
else
|
||||||
if [ "$DEBUG_MODE" = true ]; then
|
if [ "$DEBUG_MODE" = true ]; then
|
||||||
opencode run --continue --session "$opencode_session_id" "$message" 2>&1 | tee "$session_path.debug.log"
|
opencode run --continue --session "$opencode_session_id" "$message" 2>&1 | tee "$session_path.debug.log"
|
||||||
else
|
else
|
||||||
opencode run --continue --session "$opencode_session_id" "$message"
|
opencode run --continue --session "$opencode_session_id" "$message"
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_list() {
|
cmd_list() {
|
||||||
ensure_dirs
|
ensure_dirs
|
||||||
|
|
||||||
printf "%-50s %-10s %-25s %s\n" "ISSUE_REF" "TYPE" "SESSION_ID" "CREATED"
|
printf "%-50s %-10s %-25s %-40s\n" "ISSUE_REF" "TYPE" "SESSION_ID" "WORKTREE"
|
||||||
printf "%-50s %-10s %-25s %s\n" "─────────" "─────" "──────────" "───────"
|
printf "%-50s %-10s %-25s %-40s\n" "─────────" "─────" "──────────" "────────"
|
||||||
|
|
||||||
local base_session_id=$(get_base_session_id)
|
local base_session_id=$(get_base_session_id)
|
||||||
if [ -n "$base_session_id" ] && [ "$base_session_id" != "null" ]; then
|
if [ -n "$base_session_id" ] && [ "$base_session_id" != "null" ]; then
|
||||||
printf "%-50s %-10s %-25s %s\n" "(base)" "base" "$base_session_id" "N/A"
|
printf "%-50s %-10s %-25s %-40s\n" "(base)" "base" "$base_session_id" "N/A"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local pm_agent_session_id=$(get_pm_agent_session_id)
|
local pm_agent_session_id=$(get_pm_agent_session_id)
|
||||||
@@ -436,7 +556,7 @@ cmd_list() {
|
|||||||
if [ -f "$SESSIONS_DIR/pm-agent.json" ]; then
|
if [ -f "$SESSIONS_DIR/pm-agent.json" ]; then
|
||||||
pm_created=$(python3 -c "import json; print(json.load(open('$SESSIONS_DIR/pm-agent.json'))['created_at'])" 2>/dev/null || echo "N/A")
|
pm_created=$(python3 -c "import json; print(json.load(open('$SESSIONS_DIR/pm-agent.json'))['created_at'])" 2>/dev/null || echo "N/A")
|
||||||
fi
|
fi
|
||||||
printf "%-50s %-10s %-25s %s\n" "(pm-agent)" "pm_agent" "$pm_agent_session_id" "$pm_created"
|
printf "%-50s %-10s %-25s %-40s\n" "(pm-agent)" "pm_agent" "$pm_agent_session_id" "N/A"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local index=$(read_index)
|
local index=$(read_index)
|
||||||
@@ -452,8 +572,9 @@ cmd_list() {
|
|||||||
local issue_ref=$(python3 -c "import json; print(json.load(open('$session_file'))['issue_ref'])" 2>/dev/null || echo "$filename")
|
local issue_ref=$(python3 -c "import json; print(json.load(open('$session_file'))['issue_ref'])" 2>/dev/null || echo "$filename")
|
||||||
local sess_id=$(python3 -c "import json; print(json.load(open('$session_file'))['opencode_session_id'])" 2>/dev/null || echo "unknown")
|
local sess_id=$(python3 -c "import json; print(json.load(open('$session_file'))['opencode_session_id'])" 2>/dev/null || echo "unknown")
|
||||||
local created=$(python3 -c "import json; print(json.load(open('$session_file'))['created_at'])" 2>/dev/null || echo "unknown")
|
local created=$(python3 -c "import json; print(json.load(open('$session_file'))['created_at'])" 2>/dev/null || echo "unknown")
|
||||||
|
local worktree=$(python3 -c "import json; print(json.load(open('$session_file')).get('worktree_path', 'N/A'))" 2>/dev/null || echo "N/A")
|
||||||
|
|
||||||
printf "%-50s %-10s %-25s %s\n" "$issue_ref" "forked" "$sess_id" "$created"
|
printf "%-50s %-10s %-25s %-40s\n" "$issue_ref" "forked" "$sess_id" "$worktree"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
@@ -471,6 +592,7 @@ cmd_prune() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
ensure_dirs
|
ensure_dirs
|
||||||
|
ensure_worktree_dir
|
||||||
|
|
||||||
local index=$(read_index)
|
local index=$(read_index)
|
||||||
local index_session_files=$(echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); sessions=set(d['issues'].values()); sessions.add('base.json'); sessions.add('pm-agent.json'); print('\n'.join(sessions))" 2>/dev/null || echo -e "base.json\npm-agent.json")
|
local index_session_files=$(echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); sessions=set(d['issues'].values()); sessions.add('base.json'); sessions.add('pm-agent.json'); print('\n'.join(sessions))" 2>/dev/null || echo -e "base.json\npm-agent.json")
|
||||||
@@ -485,21 +607,47 @@ cmd_prune() {
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ ${#orphaned[@]} -eq 0 ]; then
|
local orphaned_worktrees=()
|
||||||
echo "No orphaned sessions found"
|
if [ -d "$WORKTREES_DIR" ]; then
|
||||||
|
for worktree_path in "$WORKTREES_DIR"/*; do
|
||||||
|
if [ -d "$worktree_path" ]; then
|
||||||
|
local worktree_name=$(basename "$worktree_path")
|
||||||
|
local session_name="${worktree_name}.json"
|
||||||
|
if ! echo "$index_session_files" | grep -q "^${session_name}$"; then
|
||||||
|
orphaned_worktrees+=("$worktree_path")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#orphaned[@]} -eq 0 ] && [ ${#orphaned_worktrees[@]} -eq 0 ]; then
|
||||||
|
echo "No orphaned sessions or worktrees found"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ ${#orphaned[@]} -gt 0 ]; then
|
||||||
echo "Found ${#orphaned[@]} orphaned session(s):"
|
echo "Found ${#orphaned[@]} orphaned session(s):"
|
||||||
for f in "${orphaned[@]}"; do
|
for f in "${orphaned[@]}"; do
|
||||||
echo " - $(basename "$f")"
|
echo " - $(basename "$f")"
|
||||||
done
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#orphaned_worktrees[@]} -gt 0 ]; then
|
||||||
|
echo "Found ${#orphaned_worktrees[@]} orphaned worktree(s):"
|
||||||
|
for wt in "${orphaned_worktrees[@]}"; do
|
||||||
|
echo " - $(basename "$wt")"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$force" = true ]; then
|
if [ "$force" = true ]; then
|
||||||
echo "Removing orphaned sessions (force mode)..."
|
echo "Removing orphaned items (force mode)..."
|
||||||
for f in "${orphaned[@]}"; do
|
for f in "${orphaned[@]}"; do
|
||||||
rm -f "$f"
|
rm -f "$f"
|
||||||
echo "Removed: $(basename "$f")"
|
echo "Removed session: $(basename "$f")"
|
||||||
|
done
|
||||||
|
for wt in "${orphaned_worktrees[@]}"; do
|
||||||
|
git worktree remove "$wt" 2>/dev/null || rm -rf "$wt"
|
||||||
|
echo "Removed worktree: $(basename "$wt")"
|
||||||
done
|
done
|
||||||
else
|
else
|
||||||
echo "Run with --force to remove"
|
echo "Run with --force to remove"
|
||||||
@@ -581,14 +729,16 @@ cmd_destroy() {
|
|||||||
local session_path="$SESSIONS_DIR/$session_file"
|
local session_path="$SESSIONS_DIR/$session_file"
|
||||||
|
|
||||||
if [ "$force" = true ]; then
|
if [ "$force" = true ]; then
|
||||||
|
remove_worktree_for_issue "$target"
|
||||||
rm -f "$session_path"
|
rm -f "$session_path"
|
||||||
remove_issue_from_index "$target"
|
remove_issue_from_index "$target"
|
||||||
echo "Session for '$target' destroyed"
|
echo "Session for '$target' destroyed"
|
||||||
else
|
else
|
||||||
echo "Delete session for '$target'? [y/N] "
|
echo "Delete session and worktree for '$target'? [y/N] "
|
||||||
local reply
|
local reply
|
||||||
read reply
|
read reply
|
||||||
if [ "$reply" = "y" ] || [ "$reply" = "Y" ]; then
|
if [ "$reply" = "y" ] || [ "$reply" = "Y" ]; then
|
||||||
|
remove_worktree_for_issue "$target"
|
||||||
rm -f "$session_path"
|
rm -f "$session_path"
|
||||||
remove_issue_from_index "$target"
|
remove_issue_from_index "$target"
|
||||||
echo "Session for '$target' destroyed"
|
echo "Session for '$target' destroyed"
|
||||||
|
|||||||
Reference in New Issue
Block a user