feat(kugetsu): auto-create pm-agent session during init

- kugetsu init now creates both base and pm-agent sessions
- kugetsu start checks for pm-agent existence, errors if missing
- Add kugetsu destroy --pm-agent command
- Update list to show pm-agent session
- Update prune to preserve pm-agent.json
- Update SKILL.md documentation to v2.1

Part of issue #19 Phase 3 implementation
This commit is contained in:
shokollm
2026-03-30 13:10:47 +00:00
parent 202d8ccfbb
commit 12ad4eb3b7
2 changed files with 228 additions and 28 deletions

View File

@@ -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 <issue-ref> <message> [--debug] Start task for issue (forks base session)
kugetsu continue <issue-ref> [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 <issue-ref> [-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 <issue-ref> or --base" >&2
echo "Error: destroy requires <issue-ref>, --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")