diff --git a/skills/kugetsu/SKILL.md b/skills/kugetsu/SKILL.md
index 57b95b0..3354533 100644
--- a/skills/kugetsu/SKILL.md
+++ b/skills/kugetsu/SKILL.md
@@ -5,7 +5,7 @@ license: MIT
compatibility: Requires opencode CLI, bash, python3, and filesystem access.
metadata:
author: shoko
- version: "2.0"
+ version: "2.1"
---
# kugetsu - OpenCode Session Manager (Issue-Driven)
@@ -30,7 +30,8 @@ chmod +x ~/.local/bin/kugetsu
## Architecture
### Session Pattern
-- **Base Session**: Created once via TUI, used for forking
+- **Base Session**: Created once via TUI, used for forking dev agents
+- **PM Agent Session**: Created during init, persistent coordinator for task management
- **Forked Sessions**: One per issue, branched from base via `opencode run --fork --session `
### Directory Structure
@@ -38,14 +39,16 @@ chmod +x ~/.local/bin/kugetsu
~/.kugetsu/
├── sessions/
│ ├── base.json # Base session metadata
+│ ├── pm-agent.json # PM agent session metadata
│ └── github.com-shoko-kugetsu-14.json # Forked session per issue
-└── index.json # Maps issue refs to session files
+└── index.json # Maps session IDs and issue refs to session files
```
### Index File
```json
{
"base": "ses_abc123",
+ "pm_agent": "ses_pm_xyz789",
"issues": {
"github.com/shoko/kugetsu#14": "github.com-shoko-kugetsu-14.json"
}
@@ -55,8 +58,7 @@ chmod +x ~/.local/bin/kugetsu
### Session File
```json
{
- "type": "base|forked",
- "issue_ref": "github.com/shoko/kugetsu#14",
+ "type": "base|forked|pm_agent",
"opencode_session_id": "ses_xyz789",
"created_at": "2026-03-29T18:16:10+02:00",
"state": "idle"
@@ -76,11 +78,90 @@ Examples:
### kugetsu init [--force]
-Initialize base session via TUI:
+Initialize base + PM agent sessions via TUI:
```bash
kugetsu init
```
+- Requires a terminal (TTY) to spawn the opencode TUI
+- Creates base session and PM agent session
+- Stores both session IDs in `index.json`
+- Subsequent runs error unless `--force` is used
+
+### kugetsu start `` `` [--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
+- Requires PM agent to exist (created by init)
+- Stores mapping in `index.json`
+- Uses `opencode run --fork --session ""`
+
+### kugetsu continue `` `` [--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 ""`
+
+### kugetsu list
+
+List all tracked sessions:
+```bash
+kugetsu list
+```
+
+Output:
+```
+ISSUE_REF TYPE SESSION_ID CREATED
+─────────────────────────────────────────────────────────────────────────────────────────────────
+(base) base ses_abc123 N/A
+(pm-agent) pm_agent ses_pm_xyz789 2026-03-29T18:16:10+02:00
+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` and `pm-agent.json`
+- Useful after opencode session cleanup
+
+### kugetsu destroy `` [-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 --pm-agent [-y]
+
+Delete PM agent session (requires explicit `--pm-agent`):
+```bash
+kugetsu destroy --pm-agent -y
+```
+
+### kugetsu destroy --base [-y]
+
+Delete base session (requires explicit `--base`):
+```bash
+kugetsu destroy --base -y
+```
+
+**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`
@@ -153,6 +234,7 @@ kugetsu destroy --base -y
```bash
# First-time setup (requires TTY)
kugetsu init
+# Creates: base session + pm-agent session
# Start work on issue
kugetsu start github.com/shoko/kugetsu#14 "implement feature X"
@@ -182,6 +264,7 @@ This design solves the headless CLI limitation discovered in Issue #14:
The pattern:
- Base session created once via TUI (interactive)
+- PM agent session created during init (persistent coordinator)
- All subsequent work uses `--fork --session ` or `--continue --session `
## Recovery
diff --git a/skills/kugetsu/scripts/kugetsu b/skills/kugetsu/scripts/kugetsu
index b2fe8fe..87024b5 100755
--- a/skills/kugetsu/scripts/kugetsu
+++ b/skills/kugetsu/scripts/kugetsu
@@ -10,12 +10,13 @@ usage() {
kugetsu - OpenCode Session Manager (Issue-Driven)
Usage:
- kugetsu init [--force] Initialize base session (requires TTY)
+ kugetsu init [--force] Initialize base + pm-agent sessions (requires TTY)
kugetsu start [--debug] Start task for issue (forks base session)
kugetsu continue [message] [--debug] Continue existing task for issue
kugetsu list List all tracked sessions
- kugetsu prune [--force] Remove orphaned sessions (keeps base)
+ kugetsu prune [--force] Remove orphaned sessions (keeps base + pm-agent)
kugetsu destroy [-y] Delete session for issue
+ kugetsu destroy --pm-agent [-y] Delete pm-agent session
kugetsu destroy --base [-y] Delete base session
kugetsu help Show this help
@@ -24,14 +25,15 @@ Issue Ref Format:
Example: github.com/shoko/kugetsu#14
Commands:
- init Create base session via TUI. Requires terminal access.
- Use --force to reinitialize if base session exists.
+ init Create base + pm-agent sessions via TUI. Requires terminal access.
+ 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.
- list Show all sessions (base + forked issues).
+ 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 session or base session.
+ destroy Delete specific issue, pm-agent, or base session.
Options:
--debug Show real-time debug output and capture to debug.log
@@ -66,15 +68,16 @@ read_index() {
if [ -f "$INDEX_FILE" ]; then
cat "$INDEX_FILE"
else
- echo '{"base": null, "issues": {}}'
+ echo '{"base": null, "pm_agent": null, "issues": {}}'
fi
}
write_index() {
local base="$1"
- local issues_json="$2"
+ local pm_agent="$2"
+ local issues_json="$3"
local temp_file="$INDEX_FILE.tmp.$$"
- printf '{"base": %s, "issues": %s}\n' "$base" "$issues_json" > "$temp_file"
+ printf '{"base": %s, "pm_agent": %s, "issues": %s}\n' "$base" "$pm_agent" "$issues_json" > "$temp_file"
mv "$temp_file" "$INDEX_FILE"
}
@@ -83,6 +86,11 @@ get_base_session_id() {
echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('base') or '')"
}
+get_pm_agent_session_id() {
+ local index=$(read_index)
+ echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('pm_agent') or '')"
+}
+
get_session_for_issue() {
local issue_ref="$1"
local index=$(read_index)
@@ -91,8 +99,24 @@ get_session_for_issue() {
set_base_in_index() {
local base_session_id="$1"
+ local pm_agent=$(get_pm_agent_session_id)
local issues_json=$(read_index | python3 -c "import sys, json; d=json.load(sys.stdin); print(json.dumps(d['issues']))")
- write_index "\"$base_session_id\"" "$issues_json"
+ if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ]; then
+ write_index "\"$base_session_id\"" "null" "$issues_json"
+ else
+ write_index "\"$base_session_id\"" "\"$pm_agent\"" "$issues_json"
+ fi
+}
+
+set_pm_agent_in_index() {
+ local pm_agent_session_id="$1"
+ local base=$(get_base_session_id)
+ local issues_json=$(read_index | python3 -c "import sys, json; d=json.load(sys.stdin); print(json.dumps(d['issues']))")
+ if [ -z "$base" ] || [ "$base" = "null" ]; then
+ write_index "null" "\"$pm_agent_session_id\"" "$issues_json"
+ else
+ write_index "\"$base\"" "\"$pm_agent_session_id\"" "$issues_json"
+ fi
}
add_issue_to_index() {
@@ -100,12 +124,21 @@ add_issue_to_index() {
local session_file="$2"
local index=$(read_index)
local base=$(get_base_session_id)
+ local pm_agent=$(get_pm_agent_session_id)
local issues=$(echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); print(json.dumps(d['issues']))")
local new_issues=$(echo "$issues" | python3 -c "import sys, json; d=json.load(sys.stdin); d['$issue_ref']='$session_file'; print(json.dumps(d))")
- if [ "$base" = "null" ] || [ -z "$base" ]; then
- write_index "null" "$new_issues"
+ if [ -z "$base" ] || [ "$base" = "null" ]; then
+ if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ]; then
+ write_index "null" "null" "$new_issues"
+ else
+ write_index "null" "\"$pm_agent\"" "$new_issues"
+ fi
else
- write_index "\"$base\"" "$new_issues"
+ if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ]; then
+ write_index "\"$base\"" "null" "$new_issues"
+ else
+ write_index "\"$base\"" "\"$pm_agent\"" "$new_issues"
+ fi
fi
}
@@ -113,11 +146,20 @@ remove_issue_from_index() {
local issue_ref="$1"
local index=$(read_index)
local base=$(get_base_session_id)
+ local pm_agent=$(get_pm_agent_session_id)
local new_issues=$(echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); d['issues'].pop('$issue_ref', None); print(json.dumps(d['issues']))")
- if [ "$base" = "null" ] || [ -z "$base" ]; then
- write_index "null" "$new_issues"
+ if [ -z "$base" ] || [ "$base" = "null" ]; then
+ if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ]; then
+ write_index "null" "null" "$new_issues"
+ else
+ write_index "null" "\"$pm_agent\"" "$new_issues"
+ fi
else
- write_index "\"$base\"" "$new_issues"
+ if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ]; then
+ write_index "\"$base\"" "null" "$new_issues"
+ else
+ write_index "\"$base\"" "\"$pm_agent\"" "$new_issues"
+ fi
fi
}
@@ -172,9 +214,11 @@ cmd_init() {
ensure_dirs
local existing_base=$(get_base_session_id)
+ local existing_pm=$(get_pm_agent_session_id)
+
if [ -n "$existing_base" ] && [ "$existing_base" != "null" ]; then
if [ "$force" = true ]; then
- echo "Warning: Reinitializing base session (force mode)" >&2
+ echo "Warning: Reinitializing sessions (force mode)" >&2
else
echo "Error: Base session already exists: $existing_base" >&2
echo "Use --force to reinitialize" >&2
@@ -207,8 +251,40 @@ cmd_init() {
"$new_session_id" "$(date -Iseconds)" > "$SESSIONS_DIR/$session_file"
set_base_in_index "$new_session_id"
-
echo "Base session initialized: $new_session_id"
+
+ echo ""
+ echo "Creating PM agent session..."
+ sleep 1
+
+ local before_sessions=$(opencode session list 2>/dev/null | grep -oP '^ses_\w+' | sort)
+ local before_set="${before_sessions//$'\n'/|}"
+
+ opencode run --fork --session "$new_session_id" "You are a PM (Project Manager) agent. Your role is to coordinate task delegation and review PRs. Wait for instructions." 2>&1 || true
+
+ local after_sessions=$(opencode session list 2>/dev/null | grep -oP '^ses_\w+' | sort)
+ local new_pm_session_id=""
+ while IFS= read -r sess; do
+ if [[ ! "$before_set" =~ \|${sess}\| ]] && [[ "$sess" != "$new_session_id" ]]; then
+ new_pm_session_id="$sess"
+ break
+ fi
+ done <<< "$after_sessions"
+
+ if [ -z "$new_pm_session_id" ]; then
+ echo "Warning: Could not detect PM agent session ID. It may still have been created." >&2
+ else
+ local pm_session_file="pm-agent.json"
+ printf '{"type": "pm_agent", "opencode_session_id": "%s", "created_at": "%s", "state": "idle"}\n' \
+ "$new_pm_session_id" "$(date -Iseconds)" > "$SESSIONS_DIR/$pm_session_file"
+ set_pm_agent_in_index "$new_pm_session_id"
+ echo "PM agent session initialized: $new_pm_session_id"
+ fi
+
+ echo ""
+ echo "Initialization complete!"
+ echo "- Base session: $new_session_id"
+ echo "- PM agent: ${new_pm_session_id:-created by hermes}"
}
cmd_start() {
@@ -240,6 +316,12 @@ cmd_start() {
exit 1
fi
+ local pm_agent_session_id=$(get_pm_agent_session_id)
+ if [ -z "$pm_agent_session_id" ] || [ "$pm_agent_session_id" = "null" ]; then
+ echo "Error: No PM agent session. Run 'kugetsu init' first to create it." >&2
+ exit 1
+ fi
+
local existing_session=$(get_session_for_issue "$issue_ref")
if [ -n "$existing_session" ] && [ "$existing_session" != "null" ]; then
echo "Error: Session for '$issue_ref' already exists" >&2
@@ -348,13 +430,22 @@ cmd_list() {
printf "%-50s %-10s %-25s %s\n" "(base)" "base" "$base_session_id" "N/A"
fi
+ local pm_agent_session_id=$(get_pm_agent_session_id)
+ if [ -n "$pm_agent_session_id" ] && [ "$pm_agent_session_id" != "null" ]; then
+ local pm_created="N/A"
+ 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")
+ fi
+ printf "%-50s %-10s %-25s %s\n" "(pm-agent)" "pm_agent" "$pm_agent_session_id" "$pm_created"
+ fi
+
local index=$(read_index)
local issue_refs=$(echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); print('\n'.join(d['issues'].keys()))" 2>/dev/null || true)
for session_file in "$SESSIONS_DIR"/*.json; do
if [ -f "$session_file" ]; then
local filename=$(basename "$session_file" .json)
- if [ "$filename" = "base" ]; then
+ if [ "$filename" = "base" ] || [ "$filename" = "pm-agent" ]; then
continue
fi
@@ -382,7 +473,7 @@ cmd_prune() {
ensure_dirs
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'); print('\n'.join(sessions))" 2>/dev/null || echo "base.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")
local orphaned=()
for session_file in "$SESSIONS_DIR"/*.json; do
@@ -424,6 +515,9 @@ cmd_destroy() {
--base)
target="base"
;;
+ --pm-agent)
+ target="pm-agent"
+ ;;
-y|--yes)
force=true
;;
@@ -437,14 +531,20 @@ cmd_destroy() {
done
if [ -z "$target" ]; then
- echo "Error: destroy requires or --base" >&2
+ echo "Error: destroy requires , --base, or --pm-agent" >&2
exit 1
fi
if [ "$target" = "base" ]; then
if [ "$force" = true ]; then
rm -f "$SESSIONS_DIR/base.json"
- echo '{"base": null, "issues": {}}' > "$INDEX_FILE"
+ local pm_agent=$(get_pm_agent_session_id)
+ if [ -n "$pm_agent" ] && [ "$pm_agent" != "null" ]; then
+ rm -f "$SESSIONS_DIR/pm-agent.json"
+ echo '{"base": null, "pm_agent": null, "issues": {}}' > "$INDEX_FILE"
+ else
+ echo '{"base": null, "pm_agent": null, "issues": {}}' > "$INDEX_FILE"
+ fi
echo "Base session destroyed"
else
echo "Error: destroying base session requires --base -y" >&2
@@ -453,6 +553,23 @@ cmd_destroy() {
return
fi
+ if [ "$target" = "pm-agent" ]; then
+ if [ "$force" = true ]; then
+ rm -f "$SESSIONS_DIR/pm-agent.json"
+ local base=$(get_base_session_id)
+ if [ -n "$base" ] && [ "$base" != "null" ]; then
+ write_index "\"$base\"" "null" "{}"
+ else
+ write_index "null" "null" "{}"
+ fi
+ echo "PM agent session destroyed"
+ else
+ echo "Error: destroying pm-agent session requires --pm-agent -y" >&2
+ exit 1
+ fi
+ return
+ fi
+
validate_issue_ref "$target"
local session_file=$(get_session_for_issue "$target")