Compare commits
20 Commits
214a31e4bd
...
4d3205de86
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d3205de86 | ||
| 6c23d4f5e9 | |||
| 21e4054634 | |||
|
|
e38cf6bc8b | ||
|
|
3cc2082a21 | ||
|
|
7ac4578369 | ||
|
|
7342a9a394 | ||
|
|
bd4e8587b4 | ||
|
|
bc60e644bf | ||
| 43aa1ac330 | |||
|
|
a95d1d556d | ||
| 79dc3ee3b9 | |||
|
|
6ad51f3c0b | ||
| 0d408e8fd8 | |||
|
|
d5866d4b0f | ||
|
|
71cab655fc | ||
|
|
cb0ada9e1c | ||
|
|
449dfaecc6 | ||
| d126cf0f00 | |||
|
|
251b22500c |
123
docs/agent-concurrency-benchmark.md
Normal file
123
docs/agent-concurrency-benchmark.md
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# 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)
|
||||||
@@ -2,44 +2,53 @@ 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`.
|
||||||
|
|
||||||
## Verbosity Control
|
## Write Permissions: Strict Boundary
|
||||||
|
|
||||||
You have three verbosity modes. The DEFAULT is **total** (silent mode):
|
PM has EXPLICIT write boundaries. You can ONLY write to two specific locations.
|
||||||
|
|
||||||
### total (DEFAULT - RECOMMENDED)
|
### PM can ONLY write to:
|
||||||
- Work silently in background
|
- `~/.kugetsu/queue.json` - Queue state
|
||||||
- ONLY post final summary/results when done
|
- `~/.kugetsu/logs/*` - Your logs
|
||||||
- Do NOT post every action, glob, read, or edit
|
|
||||||
- Use logs for intermediate steps
|
|
||||||
- Post notification only on completion
|
|
||||||
|
|
||||||
### verbose (current/legacy)
|
### PM can NEVER write to (read-only):
|
||||||
- Post every glob, read, edit as it happens
|
- `~/.kugetsu/` - Everything else in this directory is read-only
|
||||||
- Very noisy - floods notifications
|
- `repositories/*` - All repository code
|
||||||
- Use only for debugging
|
- `skills/*` - All skill files, including PM skill files
|
||||||
|
- **ANY directory outside `~/.kugetsu/`**
|
||||||
|
- Any `.md` files, config files, scripts, or code
|
||||||
|
|
||||||
### hybrid
|
### If Asked to Write Outside ~/.kugetsu/:
|
||||||
- Post on errors only
|
You MUST delegate to a dev agent:
|
||||||
- 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 start <domain>/<user>/<repo>#<issue> <task description>
|
||||||
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 github.com/user/repo#123 <task description>
|
kugetsu start <domain>/<user>/<repo>#<issue> <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
|
||||||
@@ -51,52 +60,31 @@ 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. **Check Queue** - Use `dequeue` to get next task (respects priority)
|
1. **Understand** - What needs to be built? What's the repo and issue?
|
||||||
2. **Understand** - What needs to be built? What's the repo and issue?
|
2. **Delegate** - Use `kugetsu start <issue-ref> <task>` to create a dev agent task
|
||||||
3. **Delegate** - Use `kugetsu start <issue-ref> <task>` to create a dev agent task
|
3. **Monitor** - Watch for PR creation and review
|
||||||
4. **Monitor** - Watch for PR creation and review
|
4. **Report** - Post final results to the issue
|
||||||
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 github.com/user/repo#123 Investigate and fix the login bug in login.js`
|
**You:** `kugetsu start <domain>/<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 github.com/user/repo#124 Write tests for the API module`
|
**You:** `kugetsu start <domain>/<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 github.com/user/repo#125 Create a script to parse the JSON file`
|
**You:** `kugetsu start <domain>/<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 github.com/user/repo#126 Update README with installation instructions`
|
**You:** `kugetsu start <domain>/<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 github.com/user/repo#127 Create a file at /tmp/test.txt`
|
**You:** `kugetsu start <domain>/<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.
|
||||||
|
|
||||||
@@ -106,4 +94,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. Verbosity: total (silent mode, results only).*
|
*PM Agent v4 - Coordinators coordinate, we do not code. Strict write boundary: ONLY ~/.kugetsu/.*
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -9,54 +9,20 @@ 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"
|
|
||||||
|
|
||||||
acquire_agent_slot() {
|
count_active_dev_sessions() {
|
||||||
local timeout="${1:-300}"
|
local count=0
|
||||||
local waited=0
|
if [ -d "$SESSIONS_DIR" ]; then
|
||||||
(
|
for session_file in "$SESSIONS_DIR"/*.json; do
|
||||||
flock -w 1 200 || { echo "Error: Could not acquire lock" >&2; exit 1; }
|
if [ -f "$session_file" ]; then
|
||||||
local count
|
local filename=$(basename "$session_file")
|
||||||
count=$(cat "$AGENT_COUNT_FILE" 2>/dev/null || echo 0)
|
if [ "$filename" != "base.json" ]; then
|
||||||
if [ "$count" -lt "$MAX_CONCURRENT_AGENTS" ]; then
|
count=$((count + 1))
|
||||||
echo $((count + 1)) > "$AGENT_COUNT_FILE"
|
fi
|
||||||
exit 0
|
fi
|
||||||
fi
|
done
|
||||||
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
|
||||||
return 0
|
echo "$count"
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
||||||
@@ -133,8 +99,6 @@ 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() {
|
||||||
@@ -208,7 +172,7 @@ create_worktree() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Creating worktree at '$worktree_path'..."
|
echo "Creating worktree at '$worktree_path'..."
|
||||||
git clone --bare "$repo_url" "$worktree_path" 2>/dev/null || {
|
git clone "$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
|
||||||
}
|
}
|
||||||
@@ -554,11 +518,7 @@ 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"
|
||||||
if ! acquire_agent_slot; then
|
nohup sh -c "opencode run '$message' --continue --session '$pm_session' >> '$log_file' 2>&1" > /dev/null 2>&1 &
|
||||||
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"))"
|
||||||
}
|
}
|
||||||
@@ -654,9 +614,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 --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
|
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
|
||||||
else
|
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
|
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
|
||||||
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)
|
||||||
@@ -774,7 +734,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 --fork --session "$new_session_id" "$pm_prompt" 2>&1 || true
|
opencode run "$pm_prompt" --fork --session "$new_session_id" 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=""
|
||||||
@@ -852,19 +812,21 @@ 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
|
|
||||||
echo "Error: Max concurrent agents ($MAX_CONCURRENT_AGENTS) reached. Try again later." >&2
|
# Session-counting: count actual dev sessions, reject if at limit
|
||||||
|
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 --fork --session "$base_session_id" "$message" --workdir "$worktree_path" 2>&1 | tee "$SESSIONS_DIR/$session_file.debug.log"
|
opencode run "$message" --fork --session "$base_session_id" --dir "$worktree_path" 2>&1 | tee "$SESSIONS_DIR/$session_file.debug.log" &
|
||||||
else
|
else
|
||||||
opencode run --fork --session "$base_session_id" "$message" --workdir "$worktree_path" 2>&1
|
opencode run "$message" --fork --session "$base_session_id" --dir "$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=""
|
||||||
@@ -933,27 +895,21 @@ 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'..."
|
||||||
if ! acquire_agent_slot; then
|
# Note: --continue always allowed (existing sessions don't count toward limit)
|
||||||
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 --continue --session "$opencode_session_id" "$message" --workdir "$worktree_path" 2>&1 | tee "$session_path.debug.log"
|
opencode run "$message" --continue --session "$opencode_session_id" --dir "$worktree_path" 2>&1 | tee "$session_path.debug.log" &
|
||||||
else
|
else
|
||||||
opencode run --continue --session "$opencode_session_id" "$message" --workdir "$worktree_path"
|
opencode run "$message" --continue --session "$opencode_session_id" --dir "$worktree_path" 2>&1 &
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
if [ "$DEBUG_MODE" = true ]; then
|
if [ "$DEBUG_MODE" = true ]; then
|
||||||
opencode run --continue --session "$opencode_session_id" "$message" 2>&1 | tee "$session_path.debug.log"
|
opencode run "$message" --continue --session "$opencode_session_id" 2>&1 | tee "$session_path.debug.log" &
|
||||||
else
|
else
|
||||||
opencode run --continue --session "$opencode_session_id" "$message"
|
opencode run "$message" --continue --session "$opencode_session_id" 2>&1 &
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
release_agent_slot
|
|
||||||
trap - EXIT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_list() {
|
cmd_list() {
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#!/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"
|
|
||||||
Reference in New Issue
Block a user