From 12ad4eb3b7b391e3c110fb696ccee67c19371123 Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:10:47 +0000 Subject: [PATCH] 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 --- skills/kugetsu/SKILL.md | 95 +++++++++++++++++-- skills/kugetsu/scripts/kugetsu | 161 ++++++++++++++++++++++++++++----- 2 files changed, 228 insertions(+), 28 deletions(-) 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")