feat(phase3): Full Phase 3 implementation - Chat Agent, PM Agent, and Integration #32

Merged
shoko merged 14 commits from feat/issue-19-phase3 into main 2026-03-31 04:55:21 +02:00
11 changed files with 1157 additions and 53 deletions

View File

@@ -326,7 +326,7 @@ When a Coding Agent starts, it:
| Phase 1 | ✅ Complete | SSH + Tailscale remote access | | Phase 1 | ✅ Complete | SSH + Tailscale remote access |
| Phase 1b | ✅ Complete | Tailscale VPN setup | | Phase 1b | ✅ Complete | Tailscale VPN setup |
| Phase 2 | 📋 Planned | API Interface | | Phase 2 | 📋 Planned | API Interface |
| Phase 3 | 📋 Planned | Chat Integration (Telegram) | | Phase 3 | 🔄 In Progress | Chat Integration (Telegram) |
| Phase 4 | 📋 Planned | Web Dashboard | | Phase 4 | 📋 Planned | Web Dashboard |
### 6.2 Current Implementation ### 6.2 Current Implementation
@@ -344,7 +344,7 @@ When a Coding Agent starts, it:
| Parallel capacity | How many Coding Agents can run simultaneously on one machine? | Pending | | Parallel capacity | How many Coding Agents can run simultaneously on one machine? | Pending |
| Session management | Does kugetsu properly manage opencode sessions? | ✅ Working | | Session management | Does kugetsu properly manage opencode sessions? | ✅ Working |
| Remote access | Does SSH + Tailscale enable remote work? | ✅ Working | | Remote access | Does SSH + Tailscale enable remote work? | ✅ Working |
| Chat interface | Can Hermes bridge Telegram for mobile UX? | Planned (Phase 3) | | Chat interface | Can Hermes bridge Telegram for mobile UX? | Phase 3a Testing |
### 6.4 Success Criteria ### 6.4 Success Criteria

170
docs/kugetsu-chat-setup.md Normal file
View File

@@ -0,0 +1,170 @@
# Kugetsu Phase 3a Installation Guide
Guide for setting up the Kugetsu Chat Agent (Phase 3a) on a new host/container.
## Prerequisites
1. **Hermes Agent** installed and configured
2. **Telegram bot** created via @BotFather
3. **kugetsu CLI** installed
4. **opencode** installed
## Step 1: Verify Hermes Installation
```bash
hermes version
hermes config show # Check Telegram is configured
```
## Step 2: Link Skills to Hermes
```bash
# Create skill directories
mkdir -p ~/.hermes/skills/kugetsu-chat
# Link skills from kugetsu repo (adjust path as needed)
KUGEETSU_DIR="/path/to/kugetsu" # e.g., ~/repositories/kugetsu
ln -sf "$KUGEETSU_DIR/skills/kugetsu-chat" ~/.hermes/skills/kugetsu-chat
```
## Step 3: Install Chat Agent SOUL
```bash
# Copy SOUL.md to Hermes home (this defines the Chat Agent personality)
cp "$KUGEETSU_DIR/skills/kugetsu-chat/SOUL.md" ~/.hermes/SOUL-chat.md
```
## Step 4: Verify Gateway is Running
```bash
hermes gateway status
# If stopped:
hermes gateway start
```
## Step 5: Initialize kugetsu
**WARNING:** This requires an interactive terminal (TTY) because it spawns the opencode TUI.
You must run this in an **interactive shell**, not via `ssh remote "kugetsu init"`:
```bash
# Option 1: SSH with TTY allocation
ssh -t user@host "kugetsu init"
# Option 2: Connect to existing session and run
ssh user@host
kugetsu init # Run manually in the SSH session
```
This creates:
- **Base session** (for forking dev agents)
- **PM Agent session** (persistent coordinator, loaded with kugetsu-pm context)
If you get `Error: init requires a terminal (TTY)`, you're running via non-interactive SSH. Use `-t` flag or connect directly.
## Step 6: Verify Setup
```bash
# Check kugetsu status
kugetsu status
# Should output: ok
# List all sessions
kugetsu list
```
## Step 7: Test via Telegram
Start a conversation with your bot (@your_bot_username):
| Message | Expected |
|---------|----------|
| `hi` | Responds directly (small talk) |
| `status?` | Routes to PM Agent |
| `fix issue #5` | Routes to PM Agent |
## Troubleshooting
### kugetsu command not found
```bash
export PATH="$HOME/.local/bin:$PATH"
# Or add to ~/.bashrc
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
```
### Gateway not responding
```bash
hermes gateway restart
```
### PM agent issues
```bash
# Diagnose
kugetsu doctor
# Fix (if needed)
kugetsu doctor --fix
# Or reinitialize
kugetsu destroy --pm-agent -y
kugetsu init
```
## kugetsu Commands
| Command | Description |
|---------|-------------|
| `kugetsu init` | Initialize base + PM agent sessions |
| `kugetsu status` | Check if kugetsu is ready |
| `kugetsu delegate <msg>` | Send message to PM agent |
| `kugetsu doctor [--fix]` | Diagnose and fix issues |
| `kugetsu start <issue-ref> <msg>` | Start dev agent for issue |
| `kugetsu continue <issue-ref> <msg>` | Continue existing issue session |
| `kugetsu list` | List all tracked sessions |
| `kugetsu prune [--force]` | Clean up orphaned sessions |
## File Locations
| File | Location | Purpose |
|------|----------|---------|
| Chat Agent SOUL | `~/.hermes/SOUL-chat.md` | Personality |
| kugetsu-chat skill | `~/.hermes/skills/kugetsu-chat/` | Routing behavior |
| kugetsu | `~/.local/bin/kugetsu` | Main CLI |
~/.kugetsu/
├── sessions/
│ ├── base.json # Base opencode session
│ └── pm-agent.json # PM Agent opencode session
├── index.json # Session registry
└── pm-agent.md # PM context (optional, injected at init)
## Architecture Summary
```
~/.hermes/
├── SOUL-chat.md # Chat Agent personality
└── skills/
└── kugetsu-chat/ # Routing + delegation via kugetsu CLI
~/.kugetsu/
├── sessions/
│ ├── base.json # Base opencode session
│ └── pm-agent.json # PM Agent opencode session
├── index.json # Session registry
└── pm-agent.md # PM context (optional)
~/.local/bin/
└── kugetsu # Main CLI (handles delegation, status, doctor)
```
## PM Context (Optional)
To customize PM Agent behavior, create `~/.kugetsu/pm-agent.md` with additional context. This file is injected into the PM Agent session at init time.
## Security Notes
- Never commit `~/.kugetsu/` or SOUL files to version control
- Bot tokens should be in environment variables, not files
- PM agent session IDs are internal - don't expose to users

View File

@@ -1,6 +1,6 @@
# Kugetsu Chat Architecture (Phase 3) # Kugetsu Chat Architecture (Phase 3)
**Status:** Planned (Not Yet Implemented) **Status:** Phase 3a Implemented (Testing in Progress)
**Related Issue:** #19 **Related Issue:** #19
## Overview ## Overview
@@ -135,9 +135,9 @@ PM Agent
│ PM sends clarification request │ PM sends clarification request
Hermes (Chat Gateway) Hermes (Chat Gateway)
│ "Which project did you mean? github.com/user/project or git.fbrns.co/team/core?" │ "Which project did you mean? github.com/user/project or git.example.com/team/core?"
User (Telegram): "git.fbrns.co/team/core" User (Telegram): "git.example.com/team/core"
Hermes (Chat Gateway) Hermes (Chat Gateway)

View File

@@ -61,7 +61,7 @@ Your focus shifts from doing to overseeing — reviewing PRs, approving plans, m
```bash ```bash
# Clone repository # Clone repository
git clone https://git.fbrns.co/shoko/kugetsu.git git clone https://git.example.com/shoko/kugetsu.git
# Install kugetsu # Install kugetsu
bash kugetsu/skills/kugetsu/scripts/kugetsu-install.sh bash kugetsu/skills/kugetsu/scripts/kugetsu-install.sh

View File

@@ -0,0 +1,117 @@
---
name: kugetsu-chat
description: Kugetsu Chat Agent - REQUIRED skill for ALL non-casual Telegram messages. This skill MUST be loaded and used for task delegation.
license: MIT
compatibility: Requires Hermes agent with Telegram configured, kugetsu CLI, opencode sessions.
metadata:
author: shoko
version: "4.0"
---
# kugetsu-chat - REQUIRED SKILL FOR KUGETSU ROUTING
**THIS SKILL MUST BE USED for any message that is not casual conversation.**
## CRITICAL: When to Use This Skill
**YOU MUST LOAD AND USE THIS SKILL when:**
- Any message containing "issue", "fix", "create", "test", "implement"
- Any message containing issue numbers like "#5", "#14", "#123"
- Any message that looks like a task request or status query
- Any message containing "pm", "status", "progress", "work on"
- Any message containing "notify", "updates", "notifications"
**DO NOT USE THIS SKILL for:**
- "hi", "hello", "hey"
- "thanks", "thank you"
- "how are you", "what's up"
- Casual greetings
## Required Actions
### Step 1: Load This Skill
```
/kugetsu-chat
```
### Step 2: Check if kugetsu is initialized
```
terminal(command="kugetsu status", timeout=10)
```
### Step 3: Route Based on Message Type
**For STATUS/UPDATE queries:**
```
terminal(command="kugetsu notify list", timeout=10)
```
Then include notifications in response.
**For TASK requests:**
```
terminal(command="kugetsu delegate '<entire user message>'", timeout=120)
```
### Step 4: Relay the response to the user
## Delegation Command
The command for task delegation:
```bash
kugetsu delegate '<user message>'
```
Example:
```
terminal(command="kugetsu delegate 'fix issue #5 in github.com/shoko/kugetsu'", timeout=120)
```
## Notification Checking
**When user asks about status/updates, check notifications:**
```bash
kugetsu notify list
```
Include any unread notifications in your response.
## Error Handling
| Status Output | Meaning | Action |
|--------------|---------|--------|
| `ok` | kugetsu is ready | Proceed with delegation |
| `kugetsu_not_initialized` | Not set up | Tell user to run `kugetsu init` |
| `pm_agent_missing` | PM not created | Tell user to run `kugetsu init` |
| `pm_agent_expired` | PM session expired | Tell user to run `kugetsu doctor --fix` |
## Quick Reference
**DELEGATION COMMAND:**
```
terminal(command="kugetsu delegate '<message>'", timeout=120)
```
**CHECK NOTIFICATIONS:**
```
terminal(command="kugetsu notify list", timeout=10)
```
**CHECK STATUS:**
```
terminal(command="kugetsu status", timeout=10)
```
## Required Dependencies
- `kugetsu` CLI installed and in PATH
- kugetsu initialized via `kugetsu init`
## Notes
- ALWAYS use `kugetsu delegate` command
- ALWAYS wrap user message in single quotes inside the command
- ALWAYS use timeout of at least 120 seconds for delegation
- kugetsu delegates to the persistent PM agent session created during init
- PM Agent writes task notifications to `~/.kugetsu/notifications.json`

View File

@@ -0,0 +1,57 @@
# Kugetsu Chat Agent
You are the friendly, professional face of the Kugetsu agent team on Telegram.
## Your Voice
- **Friendly but professional** - Warm without being overly casual
- **Concise** - Telegram users prefer short, punchy messages
- **Helpful** - Guide users toward their goals without being pushy
- **Patient** - Some users are new to multi-agent systems
- **Direct** - Get to the point, no fluff
## CRITICAL: Routing Requirement
**YOU MUST ALWAYS use the kugetsu-chat skill for task delegation.**
For ANY message that is not casual conversation, you MUST:
1. First invoke: `/kugetsu-chat`
2. Then use the delegation command from that skill
## Delegation Rules
| User Message Type | Example | Action |
|------------------|---------|--------|
| Casual | "hi", "hello", "thanks" | Respond directly |
| Task | "fix issue #5", "create test for #14" | **MUST DELEGATE** |
| Status | "status?", "what's on #7?" | **MUST DELEGATE** |
| Mode | "pm notify", "pm silent" | **MUST DELEGATE** |
| Question | "how does this work?" | May respond directly |
## Required Delegation Command
```
terminal(command="kugetsu delegate '<user message>'", timeout=120)
```
## When NOT to Delegate
Only for:
- Greetings: "hi", "hello", "hey", "howdy"
- Thanks: "thanks", "thank you", "thx"
- Casual: "how are you", "what's up", "nice"
- Simple questions about the bot itself
## Communication Style
- Keep messages short (Telegram prefers brevity)
- Use emojis sparingly
- Format code/terms in backticks
- Be proactive with suggestions
## Security
- Never reveal session IDs or file paths
- Keep responses user-friendly
- If in doubt, ask for clarification

194
skills/kugetsu-chat/scripts/setup Executable file
View File

@@ -0,0 +1,194 @@
#!/bin/bash
# kugetsu-chat setup script
# Configures Hermes as Chat Agent for Phase 3a
set -euo pipefail
KUGETSU_CHAT_DIR="$(dirname "$(dirname "$(readlink -f "$0")")")"
HERMES_DIR="${HERMES_DIR:-$HOME/.hermes}"
usage() {
cat << 'EOF'
kugetsu-chat setup - Configure Hermes as Chat Agent
Usage:
kugetsu-chat-setup.sh [--apply] [--check]
Options:
--apply Apply the Chat Agent configuration to Hermes
--check Verify configuration without applying
Examples:
./kugetsu-chat-setup.sh --check # Check configuration
./kugetsu-chat-setup.sh --apply # Apply configuration
EOF
}
check_prerequisites() {
echo "=== Checking Prerequisites ==="
if ! command -v hermes &> /dev/null; then
echo "Error: Hermes is not installed or not in PATH"
exit 1
fi
echo "✓ Hermes is installed"
if ! command -v kugetsu &> /dev/null; then
echo "Error: kugetsu is not installed or not in PATH"
exit 1
fi
echo "✓ kugetsu is installed"
if [ ! -f "$HERMES_DIR/config.yaml" ]; then
echo "Error: Hermes config not found at $HERMES_DIR/config.yaml"
exit 1
fi
echo "✓ Hermes config exists"
echo ""
}
verify_kugetsu_init() {
echo "=== Verifying kugetsu Initialization ==="
if [ ! -f "$HOME/.kugetsu/index.json" ]; then
echo "Error: kugetsu not initialized. Run 'kugetsu init' first."
exit 1
fi
if ! grep -q '"pm_agent"' "$HOME/.kugetsu/index.json"; then
echo "Error: kugetsu index.json missing pm_agent field"
exit 1
fi
PM_AGENT=$(python3 -c "import json; print(json.load(open('$HOME/.kugetsu/index.json')).get('pm_agent', ''))" 2>/dev/null || echo "")
if [ -z "$PM_AGENT" ] || [ "$PM_AGENT" = "null" ]; then
echo "Error: PM agent session not initialized. Run 'kugetsu init' first."
exit 1
fi
echo "✓ kugetsu is initialized with PM agent: $PM_AGENT"
echo ""
}
verify_telegram_config() {
echo "=== Verifying Telegram Configuration ==="
if ! grep -q "TELEGRAM_HOME_CHANNEL" "$HERMES_DIR/config.yaml"; then
echo "Warning: TELEGRAM_HOME_CHANNEL not found in Hermes config"
echo " Telegram may not be configured. Run 'hermes gateway setup' to configure."
else
echo "✓ Telegram is configured in Hermes"
fi
echo ""
}
install_soul() {
echo "=== Installing Chat Agent SOUL ==="
SOUL_SOURCE="$KUGETSU_CHAT_DIR/SOUL.md"
SOUL_TARGET="$HERMES_DIR/SOUL-chat.md"
if [ ! -f "$SOUL_SOURCE" ]; then
echo "Error: SOUL.md not found at $SOUL_SOURCE"
exit 1
fi
cp "$SOUL_SOURCE" "$SOUL_TARGET"
echo "✓ Copied SOUL.md to $SOUL_TARGET"
echo ""
}
install_skill() {
echo "=== Installing kugetsu-chat Skill ==="
SKILL_SOURCE="$KUGETSU_CHAT_DIR"
SKILL_TARGET="$HERMES_DIR/skills/kugetsu-chat"
if [ -L "$SKILL_TARGET" ]; then
rm "$SKILL_TARGET"
elif [ -d "$SKILL_TARGET" ]; then
echo "Warning: $SKILL_TARGET already exists (not a symlink)"
fi
ln -sf "$SKILL_SOURCE" "$SKILL_TARGET"
echo "✓ Linked skill to $SKILL_TARGET"
echo ""
}
apply_config() {
echo "=== Applying Chat Agent Configuration ==="
check_prerequisites
verify_kugetsu_init
verify_telegram_config
install_soul
install_skill
echo "=== Configuration Complete ==="
echo ""
echo "Next steps:"
echo "1. Run 'hermes gateway' to start the Telegram gateway"
echo "2. Or run 'hermes' to use Chat Agent in CLI mode"
echo ""
echo "The Chat Agent will:"
echo "- Receive Telegram messages"
echo "- Handle small talk directly"
echo "- Route task requests to PM Agent"
echo "- Relay PM Agent responses back"
}
check_config() {
echo "=== Checking Chat Agent Configuration ==="
echo ""
check_prerequisites
verify_kugetsu_init
verify_telegram_config
SOUL_TARGET="$HERMES_DIR/SOUL-chat.md"
if [ -f "$SOUL_TARGET" ]; then
echo "✓ Chat Agent SOUL is installed"
else
echo "○ Chat Agent SOUL not installed (run with --apply)"
fi
SKILL_TARGET="$HERMES_DIR/skills/kugetsu-chat"
if [ -L "$SKILL_TARGET" ]; then
echo "✓ kugetsu-chat skill is linked"
else
echo "○ kugetsu-chat skill not linked (run with --apply)"
fi
echo ""
}
main() {
if [ $# -eq 0 ]; then
usage
exit 1
fi
case "$1" in
--apply)
apply_config
;;
--check)
check_config
;;
-h|--help)
usage
;;
*)
echo "Error: Unknown option '$1'"
usage
exit 1
;;
esac
}
main "$@"

View File

@@ -0,0 +1,79 @@
---
name: kugetsu-pm
description: PM (Project Manager) Agent role for kugetsu. Coordinates tasks and delegates to Dev Agents.
license: MIT
compatibility: Requires kugetsu CLI, opencode sessions, Gitea API access.
metadata:
author: shoko
version: "3.0"
---
# kugetsu-pm - PM Agent Role
PM Agent is a persistent opencode session that coordinates tasks and delegates to Dev Agents.
## Core Responsibilities
1. Receive task requests from Chat Agent
2. Create Dev Agent sessions via `kugetsu start`
3. Monitor Gitea for task completion
4. Write notifications to `~/.kugetsu/notifications.json`
5. Respond concisely (Telegram-friendly)
## Commands
### Delegate to PM
```bash
kugetsu delegate "<task>"
```
### Create Dev Agent
```bash
kugetsu start <issue-ref> "<task>"
```
### Continue Dev Agent
```bash
kugetsu continue <issue-ref> "<update>"
```
### Check Notifications
```bash
kugetsu notify list
```
## Notification Events
Write to `~/.kugetsu/notifications.json` on:
| Event | Action |
|-------|--------|
| Task assigned | Write: type=task_assigned |
| Task completed | Write: type=task_complete + Gitea comment |
| Task blocked | Write: type=task_blocked |
| Gitea unavailable | Write to notifications.json with note |
## Task Completion Detection
Check issue/PR for completion by querying:
- Issue comments for status updates
- PR commits (new commits = work in progress)
- PR merged/closed status
## Review Modes
When dev agent signals completion, choose:
- **Review immediately**: Check PR, merge if good
- **Ask dev**: Post "Ready for review?" comment, wait for confirmation
## Response Format
Keep responses short and action-oriented:
- "Created task for #5. Dev agent started."
- "#5 complete. PR #12 merged."
- "Blocked: Need clarification on #7."
## Context Injection
PM context is injected at session creation (init/start/continue).
No external skill loading needed.

View File

@@ -6,6 +6,7 @@ SESSIONS_DIR="$KUGETSU_DIR/sessions"
WORKTREES_DIR="$KUGETSU_DIR/worktrees" WORKTREES_DIR="$KUGETSU_DIR/worktrees"
REPOS_CONFIG="$KUGETSU_DIR/repos.json" REPOS_CONFIG="$KUGETSU_DIR/repos.json"
INDEX_FILE="$KUGETSU_DIR/index.json" INDEX_FILE="$KUGETSU_DIR/index.json"
NOTIFICATIONS_FILE="$KUGETSU_DIR/notifications.json"
usage() { usage() {
cat << 'EOF' cat << 'EOF'
@@ -15,10 +16,14 @@ Usage:
kugetsu init [--force] Initialize base + pm-agent sessions (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 start <issue-ref> <message> [--debug] Start task for issue (forks base session)
kugetsu continue <issue-ref> [message] [--debug] Continue existing task for issue kugetsu continue <issue-ref> [message] [--debug] Continue existing task for issue
kugetsu delegate <message> Send message to PM agent
kugetsu status Check kugetsu initialization status
kugetsu doctor [--fix] Diagnose and fix kugetsu issues
kugetsu notify [list|clear] Show or clear notifications
kugetsu list List all tracked sessions kugetsu list List all tracked sessions
kugetsu prune [--force] Remove orphaned sessions (keeps base + pm-agent) kugetsu prune [--force] Remove orphaned sessions (keeps base + pm-agent)
kugetsu destroy <issue-ref> [-y] Delete session for issue kugetsu destroy <issue-ref> [-y] Delete session for issue
kugetsu destroy --pm-agent [-y] Delete pm-agent session kugetsu destroy --pm-agent [-y] Delete pm-agent session (not recommended)
kugetsu destroy --base [-y] Delete base session kugetsu destroy --base [-y] Delete base session
kugetsu help Show this help kugetsu help Show this help
@@ -32,6 +37,12 @@ Commands:
start Fork new session from base for specific issue. start Fork new session from base for specific issue.
Requires pm-agent to be running (created by init). Requires pm-agent to be running (created by init).
continue Continue work on existing issue session. continue Continue work on existing issue session.
delegate Send message to PM agent for task coordination.
PM context is loaded once at init time.
status Check if kugetsu is initialized and PM agent is active.
doctor Diagnose kugetsu issues. Use --fix to attempt repairs.
notify Show or clear notifications from PM agent.
Use 'kugetsu notify list' to see unread notifications.
list Show all sessions (base + pm-agent + forked issues). list Show all sessions (base + pm-agent + forked issues).
prune Remove sessions not in index (orphaned from opencode). prune Remove sessions not in index (orphaned from opencode).
Use --force to skip confirmation. Use --force to skip confirmation.
@@ -40,14 +51,26 @@ Commands:
Options: Options:
--debug Show real-time debug output and capture to debug.log --debug Show real-time debug output and capture to debug.log
PM Context:
kugetsu reads ~/.kugetsu/pm-agent.md (if exists) and injects it
into the PM agent session at init time. This allows customizing PM
behavior without recreating the session.
Notifications:
PM Agent writes task completion notifications to ~/.kugetsu/notifications.json
Use 'kugetsu notify list' to see unread notifications.
Examples: Examples:
kugetsu init kugetsu init
kugetsu status
kugetsu delegate "work on issue #5"
kugetsu doctor
kugetsu doctor --fix
kugetsu notify list
kugetsu notify clear
kugetsu start github.com/shoko/kugetsu#14 "fix bug" kugetsu start github.com/shoko/kugetsu#14 "fix bug"
kugetsu continue github.com/shoko/kugetsu#14 "add tests" kugetsu continue github.com/shoko/kugetsu#14 "add tests"
kugetsu list kugetsu list
kugetsu prune
kugetsu prune --force
kugetsu destroy github.com/shoko/kugetsu#14
EOF EOF
} }
@@ -283,6 +306,329 @@ check_opencode_session_exists() {
opencode session list 2>/dev/null | grep -q "^$session_id" opencode session list 2>/dev/null | grep -q "^$session_id"
} }
kugetsu_get_pm_context() {
local user_pm_context="${KUGETSU_DIR}/pm-agent.md"
local skill_pm_context="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../pm/SKILL.md"
if [ -f "$user_pm_context" ]; then
cat "$user_pm_context"
elif [ -f "$skill_pm_context" ]; then
cat "$skill_pm_context"
else
echo ""
fi
}
kugetsu_add_notification() {
local type="$1"
local message="$2"
local issue_ref="${3:-}"
local gitea_url="${4:-}"
mkdir -p "$(dirname "$NOTIFICATIONS_FILE")"
local notification=$(python3 << PYEOF
import json
import os
from datetime import datetime
notification = {
"type": "$type",
"message": "$message",
"issue_ref": "$issue_ref" if "$issue_ref" else None,
"gitea_url": "$gitea_url" if "$gitea_url" else None,
"timestamp": datetime.now().isoformat(),
"read": False
}
file_path = os.path.expanduser("$NOTIFICATIONS_FILE")
notifications = []
if os.path.exists(file_path):
try:
with open(file_path, 'r') as f:
notifications = json.load(f)
except:
notifications = []
notifications.append(notification)
with open(file_path, 'w') as f:
json.dump(notifications, f, indent=2)
print("Notification added")
PYEOF
)
echo "$notification"
}
kugetsu_get_notifications() {
local limit="${1:-10}"
if [ ! -f "$NOTIFICATIONS_FILE" ]; then
echo "[]"
return
fi
python3 << PYEOF
import json
import os
from datetime import datetime
file_path = os.path.expanduser("$NOTIFICATIONS_FILE")
if not os.path.exists(file_path):
print("[]")
exit(0)
try:
with open(file_path, 'r') as f:
notifications = json.load(f)
unread = [n for n in notifications if not n.get("read", False)]
unread.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
for n in unread[:$limit]:
ts = n.get("timestamp", "unknown")
ntype = n.get("type", "info")
msg = n.get("message", "")
issue = n.get("issue_ref", "")
gitea = n.get("gitea_url", "")
print(f"[{ts}] {ntype}: {msg}")
if issue:
print(f" Issue: {issue}")
if gitea:
print(f" Link: {gitea}")
print()
if not unread:
print("No unread notifications.")
except Exception as e:
print(f"Error reading notifications: {e}")
PYEOF
}
kugetsu_clear_notifications() {
if [ ! -f "$NOTIFICATIONS_FILE" ]; then
return
fi
python3 << PYEOF
import json
import os
file_path = os.path.expanduser("$NOTIFICATIONS_FILE")
if not os.path.exists(file_path):
exit(0)
try:
with open(file_path, 'r') as f:
notifications = json.load(f)
for n in notifications:
n["read"] = True
with open(file_path, 'w') as f:
json.dump(notifications, f, indent=2)
print("Notifications marked as read")
except Exception as e:
print(f"Error: {e}")
PYEOF
}
cmd_notify() {
local action="${1:-}"
case "$action" in
""|"list"|"show")
kugetsu_get_notifications 10
;;
"clear")
kugetsu_clear_notifications
;;
*)
echo "Usage: kugetsu notify [list|clear]"
;;
esac
}
cmd_status() {
if [ ! -f "$INDEX_FILE" ]; then
echo "kugetsu_not_initialized"
return
fi
local base=$(get_base_session_id)
local pm_agent=$(get_pm_agent_session_id)
if [ -z "$base" ] || [ "$base" = "null" ]; then
echo "base_session_missing"
return
fi
if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ] || [ "$pm_agent" = "None" ]; then
echo "pm_agent_missing"
return
fi
if ! check_opencode_session_exists "$pm_agent"; then
echo "pm_agent_expired"
return
fi
echo "ok"
}
cmd_delegate() {
local message="${1:-}"
if [ -z "$message" ]; then
echo "Error: message is required" >&2
echo "Usage: kugetsu delegate <message>" >&2
exit 1
fi
local pm_session=$(get_pm_agent_session_id)
if [ -z "$pm_session" ] || [ "$pm_session" = "null" ] || [ "$pm_session" = "None" ]; then
echo "Error: PM agent session not found. Run 'kugetsu init' first." >&2
exit 1
fi
if ! check_opencode_session_exists "$pm_session"; then
echo "Error: PM agent session has expired. Run 'kugetsu init' again." >&2
exit 1
fi
opencode run --continue --session "$pm_session" "$message" 2>&1
}
cmd_doctor() {
local fix=false
while [ $# -gt 0 ]; do
case "$1" in
--fix)
fix=true
;;
*)
;;
esac
shift
done
echo "=== kugetsu doctor ==="
echo ""
local issues=0
if [ ! -f "$INDEX_FILE" ]; then
echo "[ISSUE] kugetsu not initialized (index.json missing)"
issues=$((issues + 1))
else
echo "[OK] kugetsu initialized"
local base=$(get_base_session_id)
if [ -z "$base" ] || [ "$base" = "null" ]; then
echo "[ISSUE] Base session missing"
issues=$((issues + 1))
else
echo "[OK] Base session: $base"
if check_opencode_session_exists "$base"; then
echo "[OK] Base session active"
else
echo "[ISSUE] Base session expired"
issues=$((issues + 1))
fi
fi
local pm_agent=$(get_pm_agent_session_id)
if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ] || [ "$pm_agent" = "None" ]; then
echo "[ISSUE] PM agent session missing"
issues=$((issues + 1))
else
echo "[OK] PM agent: $pm_agent"
if check_opencode_session_exists "$pm_agent"; then
echo "[OK] PM agent session active"
else
echo "[ISSUE] PM agent session expired"
issues=$((issues + 1))
fi
fi
local pm_context_file="${KUGETSU_DIR}/pm-agent.md"
if [ -f "$pm_context_file" ]; then
echo "[OK] PM context file exists"
else
echo "[INFO] PM context file not found (optional): $pm_context_file"
fi
fi
echo ""
if [ $issues -eq 0 ]; then
echo "No issues found."
else
echo "Found $issues issue(s)."
fi
if [ "$fix" = true ] && [ $issues -gt 0 ]; then
echo ""
echo "Running fixes..."
if [ ! -f "$INDEX_FILE" ]; then
echo "Cannot fix: not initialized. Run 'kugetsu init' first."
else
local pm_agent=$(get_pm_agent_session_id)
if [ -n "$pm_agent" ] && [ "$pm_agent" != "null" ] && [ "$pm_agent" != "None" ]; then
if ! check_opencode_session_exists "$pm_agent"; then
echo "[FIX] Recreating expired PM agent session..."
local base=$(get_base_session_id)
if [ -n "$base" ] && [ "$base" != "null" ]; then
rm -f "$SESSIONS_DIR/pm-agent.json"
local before_sessions=$(opencode session list 2>/dev/null | grep -oP '^ses_\w+' | sort)
local before_set="${before_sessions//$'\n'/|}"
local pm_context=$(kugetsu_get_pm_context)
if [ -n "$pm_context" ]; then
opencode run --fork --session "$base" "You are a PM (Project Manager) agent. Your role is to coordinate task delegation and review PRs. $pm_context" 2>&1 || true
else
opencode run --fork --session "$base" "You are a PM (Project Manager) agent. Your role is to coordinate task delegation and review PRs. Wait for instructions." 2>&1 || true
fi
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" != "$base" ]]; then
new_pm_session_id="$sess"
break
fi
done <<< "$after_sessions"
if [ -n "$new_pm_session_id" ]; then
printf '{"type": "pm_agent", "opencode_session_id": "%s", "created_at": "%s", "state": "idle"}\n' \
"$new_pm_session_id" "$(date -Iseconds)" > "$SESSIONS_DIR/pm-agent.json"
set_pm_agent_in_index "$new_pm_session_id"
echo "[FIX] PM agent recreated: $new_pm_session_id"
else
echo "[FIX] Warning: Could not detect new PM session ID"
fi
else
echo "[FIX] Cannot recreate PM agent: base session missing"
fi
else
echo "[FIX] PM agent is active, no fix needed"
fi
else
echo "[FIX] Cannot fix: PM agent not initialized. Run 'kugetsu init' first."
fi
fi
fi
}
DEBUG_MODE=false DEBUG_MODE=false
set_debug_mode() { set_debug_mode() {
@@ -365,7 +711,13 @@ cmd_init() {
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)
local before_set="${before_sessions//$'\n'/|}" 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 pm_context=$(kugetsu_get_pm_context)
local pm_prompt="You are a PM (Project Manager) agent. Your role is to coordinate task delegation and review PRs. Wait for instructions."
if [ -n "$pm_context" ]; then
pm_prompt="You are a PM (Project Manager) agent. Your role is to coordinate task delegation and review PRs. $pm_context"
fi
opencode run --fork --session "$new_session_id" "$pm_prompt" 2>&1 || true
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)
local new_pm_session_id="" local new_pm_session_id=""
@@ -474,22 +826,22 @@ cmd_start() {
} }
cmd_continue() { cmd_continue() {
local issue_ref="" local session_name=""
local message="" local message=""
local args=("$@") local args=("$@")
args=$(set_debug_mode "${args[@]}") args=$(set_debug_mode "${args[@]}")
for arg in $args; do for arg in $args; do
if [ -z "$issue_ref" ]; then if [ -z "$session_name" ]; then
issue_ref="$arg" session_name="$arg"
elif [ -z "$message" ]; then elif [ -z "$message" ]; then
message="$arg" message="$arg"
fi fi
done done
if [ -z "$issue_ref" ]; then if [ -z "$session_name" ]; then
echo "Error: continue requires <issue-ref>" >&2 echo "Error: continue requires <session-name>" >&2
exit 1 exit 1
fi fi
@@ -498,19 +850,17 @@ cmd_continue() {
exit 1 exit 1
fi fi
validate_issue_ref "$issue_ref" local session_file=$(get_session_for_issue "$session_name")
local session_file=$(get_session_for_issue "$issue_ref")
if [ -z "$session_file" ] || [ "$session_file" = "null" ]; then if [ -z "$session_file" ] || [ "$session_file" = "null" ]; then
echo "Error: No session found for '$issue_ref'" >&2 echo "Error: No session found for '$session_name'" >&2
echo "Use 'kugetsu start $issue_ref <message>' to create one" >&2 echo "Use 'kugetsu start <issue-ref> <message>' to create one" >&2
exit 1 exit 1
fi fi
local session_path="$SESSIONS_DIR/$session_file" local session_path="$SESSIONS_DIR/$session_file"
if [ ! -f "$session_path" ]; then if [ ! -f "$session_path" ]; then
echo "Error: Session file missing: $session_path" >&2 echo "Error: Session file missing: $session_path" >&2
echo "Run 'kugetsu start $issue_ref <message>' to recreate" >&2 echo "Run 'kugetsu start <issue-ref> <message>' to recreate" >&2
exit 1 exit 1
fi fi
@@ -522,7 +872,7 @@ cmd_continue() {
echo "Attempting to continue anyway..." >&2 echo "Attempting to continue anyway..." >&2
fi fi
echo "Continuing session for '$issue_ref'..." echo "Continuing session for '$session_name'..."
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
@@ -770,6 +1120,18 @@ main() {
continue) continue)
cmd_continue "$@" cmd_continue "$@"
;; ;;
delegate)
cmd_delegate "$@"
;;
status)
cmd_status
;;
doctor)
cmd_doctor "$@"
;;
notify)
cmd_notify "$@"
;;
list) list)
cmd_list "$@" cmd_list "$@"
;; ;;

View File

@@ -1,10 +1,13 @@
#!/bin/bash #!/bin/bash
# kugetsu installation script
# Installs kugetsu CLI and optionally sets up Phase 3a Chat Agent
set -euo pipefail set -euo pipefail
KUGETSU_DIR="${KUGETSU_DIR:-$HOME/.kugetsu}" KUGETSU_DIR="${KUGETSU_DIR:-$HOME/.kugetsu}"
BIN_DIR="$KUGETSU_DIR/bin" BIN_DIR="${BIN_DIR:-$HOME/.local/bin}"
echo "Installing kugetsu to $KUGETSU_DIR..." echo "Installing kugetsu..."
mkdir -p "$BIN_DIR" mkdir -p "$BIN_DIR"
@@ -13,24 +16,21 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cp "$SCRIPT_DIR/kugetsu" "$BIN_DIR/kugetsu" cp "$SCRIPT_DIR/kugetsu" "$BIN_DIR/kugetsu"
chmod +x "$BIN_DIR/kugetsu" chmod +x "$BIN_DIR/kugetsu"
echo "kugetsu installed at: $BIN_DIR/kugetsu"
add_to_shell() { add_to_shell() {
local rc_file="$1" local rc_file="$1"
local export_line="export PATH=\"\$HOME/.kugetsu/bin:\$PATH\"" local export_line="export PATH=\"\$HOME/.local/bin:\$PATH\""
if [ -f "$rc_file" ]; then if [ -f "$rc_file" ]; then
if grep -q "$export_line" "$rc_file" 2>/dev/null; then if grep -q "$export_line" "$rc_file" 2>/dev/null; then
echo "$rc_file already has kugetsu in PATH" echo "$rc_file already has .local/bin in PATH"
else else
echo "" >> "$rc_file" echo "" >> "$rc_file"
echo "# kugetsu - opencode session manager" >> "$rc_file" echo "# kugetsu and other tools" >> "$rc_file"
echo "$export_line" >> "$rc_file" echo "$export_line" >> "$rc_file"
echo "Added to $rc_file" echo "Added to $rc_file"
fi fi
else
echo "" >> "$rc_file"
echo "# kugetsu - opencode session manager" >> "$rc_file"
echo "$export_line" >> "$rc_file"
echo "Created $rc_file with kugetsu PATH"
fi fi
} }
@@ -39,29 +39,25 @@ add_to_shell "$HOME/.zshrc"
echo "" echo ""
echo "=== Verifying installation ===" echo "=== Verifying installation ==="
if [ ! -f "$BIN_DIR/kugetsu" ]; then "$BIN_DIR/kugetsu" help | head -10
echo "ERROR: kugetsu was not installed correctly."
exit 1
fi
echo "kugetsu installed at: $BIN_DIR/kugetsu"
echo "" echo ""
echo "Installation complete!" echo "Installation complete!"
echo "" echo ""
echo "Run this to start using kugetsu immediately:" echo "=== Phase 3a Chat Agent Setup (Optional) ==="
echo " export PATH=\"\$HOME/.kugetsu/bin:\$PATH\"" echo "To also install the Chat Agent skills for Phase 3a:"
echo "" echo ""
echo "Or start a new shell." echo " 1. Link skills to Hermes:"
echo " mkdir -p ~/.hermes/skills/kugetsu-chat"
echo " ln -sf /path/to/kugetsu/skills/kugetsu-chat ~/.hermes/skills/"
echo "" echo ""
echo "Usage:" echo " 2. Install Chat Agent SOUL:"
echo " kugetsu init Initialize base session (requires TTY)" echo " cp /path/to/kugetsu/skills/kugetsu-chat/SOUL.md ~/.hermes/SOUL-chat.md"
echo " kugetsu start <issue-ref> <message> Start task for issue"
echo " kugetsu continue <issue-ref> [msg] Continue existing task"
echo " kugetsu list List all sessions"
echo " kugetsu prune [--force] Remove orphaned sessions"
echo " kugetsu destroy <issue-ref> [-y] Delete session for issue"
echo " kugetsu destroy --base [-y] Delete base session"
echo " kugetsu help Show help"
echo "" echo ""
echo "Issue ref format: instance/user/repo#number" echo " 3. Initialize kugetsu (requires TTY):"
echo "Example: github.com/shoko/kugetsu#14" echo " kugetsu init"
echo ""
echo " 4. Verify setup:"
echo " kugetsu status"
echo ""
echo "See docs/phase3a-setup.md for full installation guide."

View File

@@ -354,6 +354,135 @@ else
fi fi
echo "" echo ""
# Test 21: status when not initialized
echo "--- Test: status (not initialized) ---"
cleanup
OUTPUT=$($KUGETSU status 2>&1 || true)
if [ "$OUTPUT" = "kugetsu_not_initialized" ]; then
pass "status returns kugetsu_not_initialized when no index.json"
else
fail "status not initialized: got '$OUTPUT', expected 'kugetsu_not_initialized'"
fi
echo ""
# Test 22: status when base missing
echo "--- Test: status (base missing) ---"
mkdir -p ~/.kugetsu/sessions
cat > ~/.kugetsu/index.json << EOF
{
"base": null,
"pm_agent": "$TEST_PM_AGENT_SESSION_ID",
"issues": {}
}
EOF
OUTPUT=$($KUGETSU status 2>&1 || true)
if [ "$OUTPUT" = "base_session_missing" ]; then
pass "status returns base_session_missing when base is null"
else
fail "status base missing: got '$OUTPUT', expected 'base_session_missing'"
fi
echo ""
# Test 23: status when pm-agent missing
echo "--- Test: status (pm-agent missing) ---"
cat > ~/.kugetsu/index.json << EOF
{
"base": "$TEST_BASE_SESSION_ID",
"pm_agent": null,
"issues": {}
}
EOF
OUTPUT=$($KUGETSU status 2>&1 || true)
if [ "$OUTPUT" = "pm_agent_missing" ]; then
pass "status returns pm_agent_missing when pm_agent is null"
else
fail "status pm_agent missing: got '$OUTPUT', expected 'pm_agent_missing'"
fi
echo ""
# Test 24: status when pm-agent is "None" (Python None output)
echo "--- Test: status (pm-agent is Python None) ---"
cat > ~/.kugetsu/index.json << EOF
{
"base": "$TEST_BASE_SESSION_ID",
"pm_agent": "None",
"issues": {}
}
EOF
OUTPUT=$($KUGETSU status 2>&1 || true)
if [ "$OUTPUT" = "pm_agent_missing" ]; then
pass "status returns pm_agent_missing when pm_agent is 'None'"
else
fail "status pm_agent 'None': got '$OUTPUT', expected 'pm_agent_missing'"
fi
echo ""
# Test 25: status when all good (pm-agent in json but session expired)
echo "--- Test: status (session expired) ---"
setup_mock_base
OUTPUT=$($KUGETSU status 2>&1 || true)
if [ "$OUTPUT" = "pm_agent_expired" ]; then
pass "status returns pm_agent_expired when session not in opencode"
else
fail "status session expired: got '$OUTPUT', expected 'pm_agent_expired'"
fi
echo ""
# Test 26: delegate without message
echo "--- Test: delegate (no message) ---"
cleanup
OUTPUT=$($KUGETSU delegate 2>&1 || true)
if echo "$OUTPUT" | grep -q "Error: message is required"; then
pass "delegate fails without message"
else
fail "delegate no message: got '$OUTPUT', expected error about message required"
fi
echo ""
# Test 27: delegate when pm-agent missing
echo "--- Test: delegate (pm-agent missing) ---"
setup_mock_base
OUTPUT=$($KUGETSU delegate "test" 2>&1 || true)
if echo "$OUTPUT" | grep -q "Error: PM agent session"; then
pass "delegate fails when PM agent not found"
else
fail "delegate pm-agent missing: got '$OUTPUT', expected error about PM agent"
fi
echo ""
# Test 28: doctor command works
echo "--- Test: doctor command ---"
cleanup
OUTPUT=$($KUGETSU doctor 2>&1 || true)
if echo "$OUTPUT" | grep -q "kugetsu doctor"; then
pass "doctor command works"
else
fail "doctor command: got '$OUTPUT', expected doctor output"
fi
echo ""
# Test 29: notify list when no file
echo "--- Test: notify list (no file) ---"
cleanup
OUTPUT=$($KUGETSU notify list 2>&1 || true)
if [ "$OUTPUT" = "[]" ]; then
pass "notify list returns empty array when file missing"
else
fail "notify list no file: got '$OUTPUT', expected '[]'"
fi
echo ""
# Test 30: notify clear when no file
echo "--- Test: notify clear (no file) ---"
cleanup
OUTPUT=$($KUGETSU notify clear 2>&1 || true)
if [ -z "$OUTPUT" ] || echo "$OUTPUT" | grep -q "marked as read"; then
pass "notify clear works when file missing (no-op)"
else
fail "notify clear: got '$OUTPUT', expected success or empty"
fi
echo ""
# Cleanup # Cleanup
cleanup cleanup