diff --git a/skills/kugetsu/pm/SKILL.md b/skills/kugetsu/pm/SKILL.md index 5e8c907..4af53a9 100644 --- a/skills/kugetsu/pm/SKILL.md +++ b/skills/kugetsu/pm/SKILL.md @@ -2,6 +2,36 @@ You are a PM (Project Manager) for software development. Your role is COORDINATOR. You break down requests, delegate work, monitor progress, and report results. You NEVER write code. Not even small fixes. Not even one-liners. Not even documentation. If asked to write code: delegate it using `kugetsu start`. +## Verbosity Control + +You have three verbosity modes. The DEFAULT is **total** (silent mode): + +### total (DEFAULT - RECOMMENDED) +- Work silently in background +- ONLY post final summary/results when done +- Do NOT post every action, glob, read, or edit +- Use logs for intermediate steps +- Post notification only on completion + +### verbose (current/legacy) +- Post every glob, read, edit as it happens +- Very noisy - floods notifications +- Use only for debugging + +### hybrid +- Post on errors only +- Quiet on success +- Only interrupt if something goes wrong + +## Configuration + +Set via KUGETSU_VERBOSITY environment variable (default: total): +``` +KUGETSU_VERBOSITY=total # silent, results only +KUGETSU_VERBOSITY=verbose # noisy, all actions +KUGETSU_VERBOSITY=hybrid # errors only +``` + ## Critical: How to Delegate Use `kugetsu start` to create dev agent sessions: @@ -21,14 +51,35 @@ You are the PM. Your job is to coordinate, not to code. - You break down complex requests into delegate-able tasks - You monitor progress and keep stakeholders informed +## Queue-Based Delegation (Phase 2) + +You read tasks from the queue instead of waiting for direct commands. Priority order: +1. dev_followups (highest) - Dev completed work, follow-up needed +2. user_interrupts - User requested something +3. background (lowest) - Passive discovery tasks + +### Queue Commands +``` +~/.kugetsu/scripts/dequeue # Get next task (highest priority) +~/.kugetsu/scripts/queue-list # See pending tasks +~/.kugetsu/scripts/enqueue # Add to queue +``` + +### Polling Loop +The PM poll loop continuously polls the queue and assigns work: +``` +~/.kugetsu/scripts/pm-poll-loop # Start daemon +``` + ## Delegation is Your Default Behavior When a request comes in: -1. **Understand** - What needs to be built? What's the repo and issue? -2. **Delegate** - Use `kugetsu start ` to create a dev agent task -3. **Monitor** - Watch for PR creation and review -4. **Report** - Post final results to the issue +1. **Check Queue** - Use `dequeue` to get next task (respects priority) +2. **Understand** - What needs to be built? What's the repo and issue? +3. **Delegate** - Use `kugetsu start ` to create a dev agent task +4. **Monitor** - Watch for PR creation and review +5. **Report** - Post final results to the issue ## Few-Shot Examples @@ -55,4 +106,4 @@ This is not just a rule - it is your identity. The code you coordinate is built --- -*PM Agent v3 - Coordinators coordinate, we do not code. We delegate with `kugetsu start`.* \ No newline at end of file +*PM Agent v4 - Coordinators coordinate, we do not code. Verbosity: total (silent mode, results only).* \ No newline at end of file diff --git a/skills/kugetsu/scripts/dequeue b/skills/kugetsu/scripts/dequeue new file mode 100755 index 0000000..494bebf --- /dev/null +++ b/skills/kugetsu/scripts/dequeue @@ -0,0 +1,50 @@ +#!/bin/bash +# dequeue - Remove and return next task from queue +# Usage: dequeue [tier] +# If tier not specified, dequeues from highest priority (dev_followups > user_interrupts > background) + +set -euo pipefail + +QUEUE_FILE="$HOME/.kugetsu/queue.json" +TIER="${1:-}" + +python3 << EOF +import json +import os +import sys + +queue_file = os.path.expanduser("$QUEUE_FILE") +preferred_tier = "$TIER" if "$TIER" else None + +try: + with open(queue_file, 'r') as f: + queue = json.load(f) +except: + print("Queue empty") + sys.exit(0) + +tiers = ["dev_followups", "user_interrupts", "background"] +if preferred_tier: + if preferred_tier not in tiers: + print(f"Error: Invalid tier '{preferred_tier}'", file=sys.stderr) + sys.exit(1) + tiers = [preferred_tier] + +task = None +dequeued_tier = None + +for tier in tiers: + if queue.get(tier) and len(queue[tier]) > 0: + task = queue[tier].pop(0) + dequeued_tier = tier + break + +if task is None: + print("Queue empty") + sys.exit(0) + +with open(queue_file, 'w') as f: + json.dump(queue, f, indent=2) + +print(f"{dequeued_tier}|{task['id']}|{task['message']}") +EOF \ No newline at end of file diff --git a/skills/kugetsu/scripts/enqueue b/skills/kugetsu/scripts/enqueue new file mode 100755 index 0000000..f91b0e4 --- /dev/null +++ b/skills/kugetsu/scripts/enqueue @@ -0,0 +1,55 @@ +#!/bin/bash +# enqueue - Add task to queue +# Usage: enqueue +# Tier: dev_followups | user_interrupts | background + +set -euo pipefail + +QUEUE_FILE="$HOME/.kugetsu/queue.json" +TIER="${1:-}" +MESSAGE="${2:-}" + +if [ -z "$TIER" ] || [ -z "$MESSAGE" ]; then + echo "Usage: enqueue " >&2 + echo " tier: dev_followups | user_interrupts | background" >&2 + exit 1 +fi + +if [[ ! "$TIER" =~ ^(dev_followups|user_interrupts|background)$ ]]; then + echo "Error: Invalid tier '$TIER'" >&2 + echo "Valid tiers: dev_followups, user_interrupts, background" >&2 + exit 1 +fi + +ID="qe-$(date +%s)-$$" + +python3 << EOF +import json +import os +import sys +from datetime import datetime + +queue_file = os.path.expanduser("$QUEUE_FILE") +tier = "$TIER" +message = "$MESSAGE" +task_id = "$ID" + +task = { + "id": task_id, + "message": message, + "created": datetime.now().isoformat() +} + +try: + with open(queue_file, 'r') as f: + queue = json.load(f) +except: + queue = {"dev_followups": [], "user_interrupts": [], "background": []} + +queue[tier].append(task) + +with open(queue_file, 'w') as f: + json.dump(queue, f, indent=2) + +print(f"Enqueued: [{tier}] {message} (id: {task_id})") +EOF \ No newline at end of file diff --git a/skills/kugetsu/scripts/kugetsu b/skills/kugetsu/scripts/kugetsu index e071bd5..fafcefb 100755 --- a/skills/kugetsu/scripts/kugetsu +++ b/skills/kugetsu/scripts/kugetsu @@ -558,7 +558,7 @@ cmd_delegate() { echo "Error: Max concurrent agents ($MAX_CONCURRENT_AGENTS) reached. Try again later." >&2 exit 1 fi - nohup bash -c "source /home/shoko/.local/bin/kugetsu; opencode run --continue --session '$pm_session' '$message' >> '$log_file' 2>&1; release_agent_slot" > /dev/null 2>&1 & + nohup sh -c "opencode run --continue --session '$pm_session' '$message' >> '$log_file' 2>&1; ~/.kugetsu/release-slot.sh" > /dev/null 2>&1 & disown echo "Delegated to PM agent (logged to $(basename "$log_file"))" } diff --git a/skills/kugetsu/scripts/pm-poll-loop b/skills/kugetsu/scripts/pm-poll-loop new file mode 100755 index 0000000..c976b21 --- /dev/null +++ b/skills/kugetsu/scripts/pm-poll-loop @@ -0,0 +1,72 @@ +#!/bin/bash +# pm-poll-loop - Continuous PM polling daemon +# Continuously polls queue and assigns work to dev agents + +set -euo pipefail + +QUEUE_FILE="$HOME/.kugetsu/queue.json" +LOCK_FILE="$HOME/.kugetsu/.pm-poll.lock" +PID_FILE="$HOME/.kugetsu/.pm-poll.pid" +POLL_INTERVAL="${POLL_INTERVAL:-600}" # 10 minutes default +VERBOSITY="${KUGETSU_VERBOSITY:-total}" + +log() { + if [ "$VERBOSITY" = "verbose" ]; then + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2 + fi +} + +acquire_lock() { + local my_pid=$$ + if [ -f "$PID_FILE" ]; then + local old_pid=$(cat "$PID_FILE" 2>/dev/null) + if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then + echo "Error: PM poll loop already running (PID: $old_pid)" >&2 + exit 1 + fi + fi + echo "$my_pid" > "$PID_FILE" + log "PM poll loop started (PID: $my_pid)" +} + +release_lock() { + rm -f "$PID_FILE" + log "PM poll loop stopped" +} + +cleanup() { + release_lock + exit 0 +} + +trap cleanup EXIT INT TERM + +acquire_lock + +while true; do + # Try to dequeue from highest priority tier + result=$(~/.kugetsu/scripts/dequeue 2>/dev/null || true) + + if [ -n "$result" ] && [ "$result" != "Queue empty" ]; then + tier=$(echo "$result" | cut -d'|' -f1) + task_id=$(echo "$result" | cut -d'|' -f2) + message=$(echo "$result" | cut -d'|' -f3-) + + log "Dequeued: [$tier] $message" + + # Extract issue ref if present, otherwise use generic + if [[ "$message" =~ (github\.com/[^/]+/[^/]+#[0-9]+) ]]; then + issue_ref="${BASH_REMATCH[1]}" + kugetsu start "$issue_ref" "$message" + else + # Use a generic issue if none specified + echo "Warning: No issue ref in message, skipping: $message" >&2 + fi + + log "Assigned task: $task_id" + else + log "Queue empty, waiting ${POLL_INTERVAL}s..." + fi + + sleep "$POLL_INTERVAL" +done \ No newline at end of file diff --git a/skills/kugetsu/scripts/queue-list b/skills/kugetsu/scripts/queue-list new file mode 100755 index 0000000..43add95 --- /dev/null +++ b/skills/kugetsu/scripts/queue-list @@ -0,0 +1,45 @@ +#!/bin/bash +# queue-list - List pending tasks in queue +# Usage: queue-list [tier] + +set -euo pipefail + +QUEUE_FILE="$HOME/.kugetsu/queue.json" +TIER="${1:-}" + +python3 << EOF +import json +import os +import sys + +queue_file = os.path.expanduser("$QUEUE_FILE") +tier_filter = "$TIER" if "$TIER" else None + +try: + with open(queue_file, 'r') as f: + queue = json.load(f) +except: + queue = {"dev_followups": [], "user_interrupts": [], "background": []} + +tiers = ["dev_followups", "user_interrupts", "background"] + +for tier in tiers: + if tier_filter and tier_filter != tier: + continue + + tasks = queue.get(tier, []) + count = len(tasks) + print(f"\n{tier} ({count}):") + + if count == 0: + print(" (empty)") + else: + for task in tasks: + msg = task.get('message', '')[:60] + created = task.get('created', '')[:19] + print(f" [{task['id']}] {msg}") + print(f" created: {created}") + +total = sum(len(queue.get(t, [])) for t in tiers) +print(f"\nTotal queued: {total}") +EOF \ No newline at end of file