Compare commits

..

1 Commits

Author SHA1 Message Date
shokollm
214a31e4bd feat: queue architecture Phase 1 & 2 + PM verbosity control
Phase 1 - Queue Infrastructure (#49):
- ~/.kugetsu/queue.json - Queue storage with 3 tiers
- ~/.kugetsu/scripts/enqueue - Add task to queue
- ~/.kugetsu/scripts/dequeue - Remove and return next task (priority order)
- ~/.kugetsu/scripts/queue-list - List pending tasks

Phase 2 - PM Polling Loop (#49):
- ~/.kugetsu/scripts/pm-poll-loop - Continuous polling daemon
- Priority: dev_followups > user_interrupts > background
- Configurable poll interval (default 600s)

PM Verbosity Control (#46):
- 3 modes: total (default), verbose, hybrid
- Config via KUGETSU_VERBOSITY env var
- PM v4

Other fixes:
- kugetsu: fixed nohup command (use sh -c instead of bash -c)
2026-03-31 15:09:20 +00:00
8 changed files with 363 additions and 197 deletions

View File

@@ -1,123 +0,0 @@
# Agent Concurrency Benchmark
**Date:** 2026-04-01
**Hardware:** 8GB RAM, 16 CPU cores
## Test Results
| Limit (PM+Dev) | Status | Rejection Test | Notes |
|----------------|--------|---------------|-------|
| 1 | ✓ Works | 1 dev rejected (PM=1, at limit) | Too strict for normal use |
| 3 | ✓ Works | 4th dev rejected (PM + 3 devs = 4, at limit) | Recommended |
| 5 | ✓ Works | 6th dev rejected (PM + 5 devs = 6, at limit) | Works, monitor memory |
## Architecture
OpenCode is a **cloud client** - agents run on OpenCode's server (MiniMax), not locally.
```
┌─────────────────┐ ┌─────────────────┐
│ Local Host │ │ OpenCode │
│ │ HTTPS │ Server │
│ kugetsu CLI │◄───────►│ (MiniMax) │
│ worktrees/ │ API │ Agents run │
│ sessions/ │ Key │ here │
│ opencode.db │ │ │
└─────────────────┘ └─────────────────┘
~4MB per agent Server-side
(worktree only) memory (unknown)
```
## Memory Analysis
### Local Memory (Measurable)
| Component | Memory | Notes |
|-----------|--------|-------|
| Per worktree | ~600KB | Git repository clone |
| Sessions dir | ~28KB | JSON metadata |
| opencode.db | ~93MB | Local cache (148 sessions, 10K+ messages) |
| **Total 5 agents** | **~4MB** | Worktrees only, negligible |
**Conclusion:** Local RAM does NOT limit agent count. A 1GB or 2GB system can run MAX=10 agents.
### Server Memory (Not Measurable)
- OpenCode server runs on MiniMax's infrastructure
- No local process to measure RSS/memory
- Agent computation happens server-side
- Memory limit determined by OpenCode service, not local hardware
### Local Bottleneck
The only local constraint is `MAX_CONCURRENT_AGENTS` limit, which:
- Counts session files (PM + dev agents)
- Enforced in kugetsu before spawning
- Prevents resource overload on OpenCode server
## Behavior
With MAX_CONCURRENT_AGENTS=N:
- PM agent counts toward the limit (along with all dev agents)
- At limit: NEW sessions are REJECTED
- Existing sessions can ALWAYS be continued (--continue doesn't count toward limit)
- PM is still accessible when at limit (user can wait or cancel tasks)
## Configuration
Default limit is set to **5 concurrent agents** in `skills/kugetsu/scripts/kugetsu`:
```bash
MAX_CONCURRENT_AGENTS="${MAX_CONCURRENT_AGENTS:-5}"
```
The limit can be overridden via environment variable:
```bash
MAX_CONCURRENT_AGENTS=3 kugetsu start <issue> <message>
```
## Implementation
Session counting approach (vs broken slot mechanism):
```bash
# Count all session files except base.json
count_active_dev_sessions() {
local count=0
if [ -d "$SESSIONS_DIR" ]; then
for session_file in "$SESSIONS_DIR"/*.json; do
if [ -f "$session_file" ]; then
local filename=$(basename "$session_file")
if [ "$filename" != "base.json" ]; then
count=$((count + 1))
fi
fi
done
fi
echo "$count"
}
```
## Session Files
```
~/.kugetsu/sessions/
base.json - base session (NOT counted)
pm-agent.json - PM agent (COUNTED)
github.com-user-repo#1.json - dev agent (COUNTED)
github.com-user-repo#2.json - dev agent (COUNTED)
```
## Recommendations
- **1 agent:** Too strict - just PM + 0 dev agents
- **3 agents:** Recommended - PM + 2 dev agents, leaves room for PM to coordinate
- **5 agents:** Works - PM + 4 dev agents, monitor OpenCode service limits
- **More than 5:** Not tested - depends on OpenCode server capacity
## Session Cleanup
Sessions persist until explicitly destroyed:
- `kugetsu destroy <issue-ref>` - destroy specific session
- `kugetsu destroy --pm-agent -y` - destroy PM agent
- PM should destroy sessions after PR merged (on natural breakpoints)

View File

@@ -2,53 +2,44 @@ 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`. 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`.
## Write Permissions: Strict Boundary ## Verbosity Control
PM has EXPLICIT write boundaries. You can ONLY write to two specific locations. You have three verbosity modes. The DEFAULT is **total** (silent mode):
### PM can ONLY write to: ### total (DEFAULT - RECOMMENDED)
- `~/.kugetsu/queue.json` - Queue state - Work silently in background
- `~/.kugetsu/logs/*` - Your logs - 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
### PM can NEVER write to (read-only): ### verbose (current/legacy)
- `~/.kugetsu/` - Everything else in this directory is read-only - Post every glob, read, edit as it happens
- `repositories/*` - All repository code - Very noisy - floods notifications
- `skills/*` - All skill files, including PM skill files - Use only for debugging
- **ANY directory outside `~/.kugetsu/`**
- Any `.md` files, config files, scripts, or code
### If Asked to Write Outside ~/.kugetsu/: ### hybrid
You MUST delegate to a dev agent: - Post on errors only
- Quiet on success
- Only interrupt if something goes wrong
## Configuration
Set via KUGETSU_VERBOSITY environment variable (default: total):
``` ```
kugetsu start <domain>/<user>/<repo>#<issue> <task description> KUGETSU_VERBOSITY=total # silent, results only
KUGETSU_VERBOSITY=verbose # noisy, all actions
KUGETSU_VERBOSITY=hybrid # errors only
``` ```
Where:
- `<domain>` = git server (e.g., `github.com`, `gitlab.com`, `git.fbrns.co`)
- `<user>` = git username (from `git config user.name`)
- `<repo>` = repository name (from `git remote -v`)
- `<issue>` = issue number to address
### New Kugetsu Scripts:
Do NOT write new kugetsu scripts yourself (even for internal use). Delegate to a dev agent via the normal workflow:
1. Create an issue describing the needed script
2. Delegate: `kugetsu start <domain>/<user>/<repo>#<issue> Create new kugetsu script`
3. After PR is merged, you may test the new script
**Example violations (DO NOT DO THESE):**
- "Update SKILL.md" → DELEGATE, don't edit it yourself
- "Fix the bug in login.js" → DELEGATE, don't write to repositories/
- "Add a new script for queue management" → DELEGATE via issue/PR workflow
## Critical: How to Delegate ## Critical: How to Delegate
Use `kugetsu start` to create dev agent sessions: Use `kugetsu start` to create dev agent sessions:
``` ```
kugetsu start <domain>/<user>/<repo>#<issue> <task description> kugetsu start github.com/user/repo#123 <task description>
``` ```
**Domain/User/Repo**: Pull from `git remote -v` and `git config user.name` to make this agnostic to any git server.
**NOT `kugetsu delegate`** - that routes back to the PM (you). Use `kugetsu start` to create a NEW dev agent. **NOT `kugetsu delegate`** - that routes back to the PM (you). Use `kugetsu start` to create a NEW dev agent.
## Your Identity ## Your Identity
@@ -60,31 +51,52 @@ You are the PM. Your job is to coordinate, not to code.
- You break down complex requests into delegate-able tasks - You break down complex requests into delegate-able tasks
- You monitor progress and keep stakeholders informed - 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 <tier> <msg> # 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 ## Delegation is Your Default Behavior
When a request comes in: When a request comes in:
1. **Understand** - What needs to be built? What's the repo and issue? 1. **Check Queue** - Use `dequeue` to get next task (respects priority)
2. **Delegate** - Use `kugetsu start <issue-ref> <task>` to create a dev agent task 2. **Understand** - What needs to be built? What's the repo and issue?
3. **Monitor** - Watch for PR creation and review 3. **Delegate** - Use `kugetsu start <issue-ref> <task>` to create a dev agent task
4. **Report** - Post final results to the issue 4. **Monitor** - Watch for PR creation and review
5. **Report** - Post final results to the issue
## Few-Shot Examples ## Few-Shot Examples
**User:** "Fix the bug in login.js" **User:** "Fix the bug in login.js"
**You:** `kugetsu start <domain>/<user>/<repo>#123 Investigate and fix the login bug in login.js` **You:** `kugetsu start github.com/user/repo#123 Investigate and fix the login bug in login.js`
**User:** "Add tests for the API" **User:** "Add tests for the API"
**You:** `kugetsu start <domain>/<user>/<repo>#124 Write tests for the API module` **You:** `kugetsu start github.com/user/repo#124 Write tests for the API module`
**User:** "Can you write a quick script to parse this JSON?" **User:** "Can you write a quick script to parse this JSON?"
**You:** `kugetsu start <domain>/<user>/<repo>#125 Create a script to parse the JSON file` **You:** `kugetsu start github.com/user/repo#125 Create a script to parse the JSON file`
**User:** "Update the README with installation instructions" **User:** "Update the README with installation instructions"
**You:** `kugetsu start <domain>/<user>/<repo>#126 Update README with installation instructions` **You:** `kugetsu start github.com/user/repo#126 Update README with installation instructions`
**User:** "Create a file at /tmp/test.txt" **User:** "Create a file at /tmp/test.txt"
**You:** `kugetsu start <domain>/<user>/<repo>#127 Create a file at /tmp/test.txt` **You:** `kugetsu start github.com/user/repo#127 Create a file at /tmp/test.txt`
Notice: In every example, the correct response is to DELEGATE using `kugetsu start`, not to do it yourself. Notice: In every example, the correct response is to DELEGATE using `kugetsu start`, not to do it yourself.
@@ -94,4 +106,4 @@ This is not just a rule - it is your identity. The code you coordinate is built
--- ---
*PM Agent v4 - Coordinators coordinate, we do not code. Strict write boundary: ONLY ~/.kugetsu/.* *PM Agent v4 - Coordinators coordinate, we do not code. Verbosity: total (silent mode, results only).*

50
skills/kugetsu/scripts/dequeue Executable file
View File

@@ -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

55
skills/kugetsu/scripts/enqueue Executable file
View File

@@ -0,0 +1,55 @@
#!/bin/bash
# enqueue - Add task to queue
# Usage: enqueue <tier> <message>
# 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 <tier> <message>" >&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

View File

@@ -9,20 +9,54 @@ INDEX_FILE="$KUGETSU_DIR/index.json"
NOTIFICATIONS_FILE="$KUGETSU_DIR/notifications.json" NOTIFICATIONS_FILE="$KUGETSU_DIR/notifications.json"
LOGS_DIR="$KUGETSU_DIR/logs" LOGS_DIR="$KUGETSU_DIR/logs"
MAX_CONCURRENT_AGENTS="${MAX_CONCURRENT_AGENTS:-3}" MAX_CONCURRENT_AGENTS="${MAX_CONCURRENT_AGENTS:-3}"
AGENT_COUNT_FILE="$KUGETSU_DIR/.agent_count"
AGENT_LOCK_FILE="$KUGETSU_DIR/.agent_lock"
count_active_dev_sessions() { acquire_agent_slot() {
local count=0 local timeout="${1:-300}"
if [ -d "$SESSIONS_DIR" ]; then local waited=0
for session_file in "$SESSIONS_DIR"/*.json; do (
if [ -f "$session_file" ]; then flock -w 1 200 || { echo "Error: Could not acquire lock" >&2; exit 1; }
local filename=$(basename "$session_file") local count
if [ "$filename" != "base.json" ]; then count=$(cat "$AGENT_COUNT_FILE" 2>/dev/null || echo 0)
count=$((count + 1)) if [ "$count" -lt "$MAX_CONCURRENT_AGENTS" ]; then
fi echo $((count + 1)) > "$AGENT_COUNT_FILE"
fi exit 0
done fi
exit 1
) 200>"$AGENT_LOCK_FILE"
local result=$?
if [ $result -ne 0 ]; then
local count
count=$(cat "$AGENT_COUNT_FILE" 2>/dev/null || echo 0)
if [ $waited -ge $timeout ]; then
echo "Error: Timeout waiting for agent slot (max: $MAX_CONCURRENT_AGENTS, current: $count)" >&2
fi
return 1
fi fi
echo "$count" return 0
}
release_agent_slot() {
(
flock -w 1 200 || true
local count
count=$(cat "$AGENT_COUNT_FILE" 2>/dev/null || echo 0)
if [ "$count" -gt 0 ]; then
echo $((count - 1)) > "$AGENT_COUNT_FILE"
fi
) 200>"$AGENT_LOCK_FILE"
}
run_with_limit() {
local log_file="$1"
shift
local cmd=("$@")
(
"${cmd[@]}" >> "$log_file" 2>&1
release_agent_slot
) &
disown
} }
usage() { usage() {
@@ -99,6 +133,8 @@ EOF
ensure_dirs() { ensure_dirs() {
mkdir -p "$SESSIONS_DIR" mkdir -p "$SESSIONS_DIR"
[ -f "$AGENT_COUNT_FILE" ] || echo 0 > "$AGENT_COUNT_FILE"
} }
ensure_worktree_dir() { ensure_worktree_dir() {
@@ -172,7 +208,7 @@ create_worktree() {
fi fi
echo "Creating worktree at '$worktree_path'..." echo "Creating worktree at '$worktree_path'..."
git clone "$repo_url" "$worktree_path" 2>/dev/null || { git clone --bare "$repo_url" "$worktree_path" 2>/dev/null || {
echo "Error: Failed to clone repository" >&2 echo "Error: Failed to clone repository" >&2
exit 1 exit 1
} }
@@ -518,7 +554,11 @@ cmd_delegate() {
mkdir -p "$LOGS_DIR" mkdir -p "$LOGS_DIR"
local log_file="$LOGS_DIR/delegate-$(date +%s).log" local log_file="$LOGS_DIR/delegate-$(date +%s).log"
nohup sh -c "opencode run '$message' --continue --session '$pm_session' >> '$log_file' 2>&1" > /dev/null 2>&1 & if ! acquire_agent_slot; then
echo "Error: Max concurrent agents ($MAX_CONCURRENT_AGENTS) reached. Try again later." >&2
exit 1
fi
nohup sh -c "opencode run --continue --session '$pm_session' '$message' >> '$log_file' 2>&1; ~/.kugetsu/release-slot.sh" > /dev/null 2>&1 &
disown disown
echo "Delegated to PM agent (logged to $(basename "$log_file"))" echo "Delegated to PM agent (logged to $(basename "$log_file"))"
} }
@@ -614,9 +654,9 @@ cmd_doctor() {
local pm_context=$(kugetsu_get_pm_context) local pm_context=$(kugetsu_get_pm_context)
if [ -n "$pm_context" ]; then if [ -n "$pm_context" ]; then
opencode run "You are a PM (Project Manager) agent. Your role is to coordinate task delegation and review PRs. $pm_context" --fork --session "$base" 2>&1 || true 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 else
opencode run "You are a PM (Project Manager) agent. Your role is to coordinate task delegation and review PRs. Wait for instructions." --fork --session "$base" 2>&1 || true 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 fi
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)
@@ -734,7 +774,7 @@ cmd_init() {
pm_prompt="You are a PM (Project Manager) agent. Your role is to coordinate task delegation and review PRs. $pm_context" pm_prompt="You are a PM (Project Manager) agent. Your role is to coordinate task delegation and review PRs. $pm_context"
fi fi
opencode run "$pm_prompt" --fork --session "$new_session_id" 2>&1 || true 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=""
@@ -812,21 +852,19 @@ cmd_start() {
local before_set="${before_sessions//$'\n'/|}" local before_set="${before_sessions//$'\n'/|}"
echo "Forking session for '$issue_ref'..." echo "Forking session for '$issue_ref'..."
if ! acquire_agent_slot; then
# Session-counting: count actual dev sessions, reject if at limit echo "Error: Max concurrent agents ($MAX_CONCURRENT_AGENTS) reached. Try again later." >&2
local active_count=$(count_active_dev_sessions)
if [ "$active_count" -ge "$MAX_CONCURRENT_AGENTS" ]; then
echo "Error: Max concurrent agents ($MAX_CONCURRENT_AGENTS) reached" >&2
echo "Active sessions: $active_count" >&2
remove_worktree_for_issue "$issue_ref" remove_worktree_for_issue "$issue_ref"
exit 1 exit 1
fi fi
trap release_agent_slot EXIT
if [ "$DEBUG_MODE" = true ]; then if [ "$DEBUG_MODE" = true ]; then
opencode run "$message" --fork --session "$base_session_id" --dir "$worktree_path" 2>&1 | tee "$SESSIONS_DIR/$session_file.debug.log" & opencode run --fork --session "$base_session_id" "$message" --workdir "$worktree_path" 2>&1 | tee "$SESSIONS_DIR/$session_file.debug.log"
else else
opencode run "$message" --fork --session "$base_session_id" --dir "$worktree_path" 2>&1 & opencode run --fork --session "$base_session_id" "$message" --workdir "$worktree_path" 2>&1
fi fi
release_agent_slot
trap - EXIT
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_session_id="" local new_session_id=""
@@ -895,21 +933,27 @@ cmd_continue() {
local worktree_path=$(python3 -c "import json; print(json.load(open('$session_path')).get('worktree_path', ''))" 2>/dev/null || echo "") local worktree_path=$(python3 -c "import json; print(json.load(open('$session_path')).get('worktree_path', ''))" 2>/dev/null || echo "")
echo "Continuing session for '$session_name'..." echo "Continuing session for '$session_name'..."
# Note: --continue always allowed (existing sessions don't count toward limit) if ! acquire_agent_slot; then
echo "Error: Max concurrent agents ($MAX_CONCURRENT_AGENTS) reached. Try again later." >&2
exit 1
fi
trap release_agent_slot EXIT
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
opencode run "$message" --continue --session "$opencode_session_id" --dir "$worktree_path" 2>&1 | tee "$session_path.debug.log" & opencode run --continue --session "$opencode_session_id" "$message" --workdir "$worktree_path" 2>&1 | tee "$session_path.debug.log"
else else
opencode run "$message" --continue --session "$opencode_session_id" --dir "$worktree_path" 2>&1 & opencode run --continue --session "$opencode_session_id" "$message" --workdir "$worktree_path"
fi fi
else else
if [ "$DEBUG_MODE" = true ]; then if [ "$DEBUG_MODE" = true ]; then
opencode run "$message" --continue --session "$opencode_session_id" 2>&1 | tee "$session_path.debug.log" & opencode run --continue --session "$opencode_session_id" "$message" 2>&1 | tee "$session_path.debug.log"
else else
opencode run "$message" --continue --session "$opencode_session_id" 2>&1 & opencode run --continue --session "$opencode_session_id" "$message"
fi fi
fi fi
release_agent_slot
trap - EXIT
} }
cmd_list() { cmd_list() {

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,11 @@
#!/bin/bash
KUGETSU_DIR="${KUGETSU_DIR:-$HOME/.kugetsu}"
AGENT_COUNT_FILE="$KUGETSU_DIR/.agent_count"
AGENT_LOCK_FILE="$KUGETSU_DIR/.agent_lock"
(
flock -w 1 200 || true
count=$(cat "$AGENT_COUNT_FILE" 2>/dev/null || echo 0)
if [ "$count" -gt 0 ]; then
echo $((count - 1)) > "$AGENT_COUNT_FILE"
fi
) 200>"$AGENT_LOCK_FILE"