shellmates 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/bin/shellmates.js +112 -0
- package/lib/commands/config.js +88 -0
- package/lib/commands/init.js +56 -0
- package/lib/commands/install-hook.js +83 -0
- package/lib/commands/spawn.js +98 -0
- package/lib/commands/status.js +69 -0
- package/lib/utils/config.js +35 -0
- package/lib/utils/logo.js +84 -0
- package/lib/utils/tmux.js +46 -0
- package/package.json +39 -0
- package/scripts/dispatch.sh +331 -0
- package/scripts/launch-full-team.sh +77 -0
- package/scripts/launch.sh +183 -0
- package/scripts/monitor.sh +113 -0
- package/scripts/spawn-team.sh +302 -0
- package/scripts/status.sh +168 -0
- package/scripts/teardown.sh +211 -0
- package/scripts/view-session.sh +98 -0
- package/scripts/watch-inbox.sh +71 -0
- package/templates/.codex/agents/default.toml +5 -0
- package/templates/.codex/agents/executor.toml +7 -0
- package/templates/.codex/agents/explorer.toml +5 -0
- package/templates/.codex/agents/planner.toml +6 -0
- package/templates/.codex/agents/researcher.toml +6 -0
- package/templates/.codex/agents/reviewer.toml +5 -0
- package/templates/.codex/agents/verifier.toml +6 -0
- package/templates/.codex/agents/worker.toml +5 -0
- package/templates/.codex/config.toml +43 -0
- package/templates/AGENTS.md +109 -0
- package/templates/CLAUDE.md +50 -0
- package/templates/GEMINI.md +136 -0
- package/templates/config.json +10 -0
- package/templates/gitignore-additions.txt +2 -0
- package/templates/hooks/settings-addition.json +20 -0
- package/templates/hooks/shellmates-notify.sh +77 -0
- package/templates/task-header.txt +10 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# launch.sh — Start or restore the shellmates session
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# ./scripts/launch.sh # 2-pane: Gemini + Claude
|
|
6
|
+
# ./scripts/launch.sh --codex # 2-pane: Codex + Claude
|
|
7
|
+
# ./scripts/launch.sh --session myname # Custom session name (default: orchestra)
|
|
8
|
+
# ./scripts/launch.sh --purpose "phase 3 work" # Describe what this session is for
|
|
9
|
+
#
|
|
10
|
+
# Pane layout:
|
|
11
|
+
# 0.0 — Sub-agent (Gemini CLI or Codex CLI)
|
|
12
|
+
# 0.1 — Orchestrator (Claude Code)
|
|
13
|
+
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
SESSION="orchestra"
|
|
17
|
+
SUB_AGENT="gemini" # "gemini" or "codex"
|
|
18
|
+
PROJECT_DIR="${PWD}"
|
|
19
|
+
PURPOSE=""
|
|
20
|
+
MANIFEST_DIR="${HOME}/.shellmates"
|
|
21
|
+
MANIFEST_FILE="${MANIFEST_DIR}/sessions.json"
|
|
22
|
+
|
|
23
|
+
# Parse arguments
|
|
24
|
+
while [[ $# -gt 0 ]]; do
|
|
25
|
+
case "$1" in
|
|
26
|
+
--codex) SUB_AGENT="codex"; shift ;;
|
|
27
|
+
--session) SESSION="$2"; shift 2 ;;
|
|
28
|
+
--dir) PROJECT_DIR="$2"; shift 2 ;;
|
|
29
|
+
--purpose) PURPOSE="$2"; shift 2 ;;
|
|
30
|
+
-h|--help)
|
|
31
|
+
echo "Usage: $0 [--codex] [--session name] [--dir path] [--purpose \"description\"]"
|
|
32
|
+
exit 0 ;;
|
|
33
|
+
*) echo "Unknown option: $1"; exit 1 ;;
|
|
34
|
+
esac
|
|
35
|
+
done
|
|
36
|
+
|
|
37
|
+
# Default purpose if not provided
|
|
38
|
+
if [[ -z "$PURPOSE" ]]; then
|
|
39
|
+
PURPOSE="$(basename "$PROJECT_DIR") session"
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Check dependencies
|
|
43
|
+
check_dep() {
|
|
44
|
+
if ! command -v "$1" &>/dev/null; then
|
|
45
|
+
echo "ERROR: '$1' not found. Install it first."
|
|
46
|
+
echo " Claude Code: npm install -g @anthropic-ai/claude-code"
|
|
47
|
+
echo " Gemini CLI: npm install -g @google/gemini-cli"
|
|
48
|
+
echo " Codex CLI: npm install -g @openai/codex"
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
check_dep tmux
|
|
54
|
+
check_dep claude
|
|
55
|
+
|
|
56
|
+
if [[ "$SUB_AGENT" == "codex" ]]; then
|
|
57
|
+
check_dep codex
|
|
58
|
+
else
|
|
59
|
+
check_dep gemini
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# If session already exists, just attach
|
|
63
|
+
if tmux has-session -t "$SESSION" 2>/dev/null; then
|
|
64
|
+
echo "Session '$SESSION' already exists. Attaching..."
|
|
65
|
+
echo "(Use 'bash scripts/status.sh' to see all active sessions)"
|
|
66
|
+
tmux attach-session -t "$SESSION"
|
|
67
|
+
exit 0
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
echo "Creating tmux session '$SESSION'..."
|
|
71
|
+
echo " Sub-agent: $SUB_AGENT (pane 0.0)"
|
|
72
|
+
echo " Orchestrator: claude (pane 0.1)"
|
|
73
|
+
echo " Project dir: $PROJECT_DIR"
|
|
74
|
+
echo " Purpose: $PURPOSE"
|
|
75
|
+
echo ""
|
|
76
|
+
|
|
77
|
+
# Create session and split into two panes
|
|
78
|
+
tmux new-session -d -s "$SESSION" -c "$PROJECT_DIR"
|
|
79
|
+
tmux split-window -h -t "$SESSION:0" -c "$PROJECT_DIR"
|
|
80
|
+
tmux select-layout -t "$SESSION:0" even-horizontal
|
|
81
|
+
|
|
82
|
+
# Capture stable pane IDs (these survive pane reordering, unlike positional 0.0/0.1)
|
|
83
|
+
AGENT_PANE=$(tmux list-panes -t "$SESSION:0" -F '#{pane_id}' | sed -n '1p')
|
|
84
|
+
CLAUDE_PANE=$(tmux list-panes -t "$SESSION:0" -F '#{pane_id}' | sed -n '2p')
|
|
85
|
+
|
|
86
|
+
# Label panes for clarity
|
|
87
|
+
tmux select-pane -t "$AGENT_PANE" -T "sub-agent ($SUB_AGENT)"
|
|
88
|
+
tmux select-pane -t "$CLAUDE_PANE" -T "orchestrator (claude)"
|
|
89
|
+
|
|
90
|
+
# Enable pane border titles
|
|
91
|
+
tmux set-option -w -t "$SESSION:0" pane-border-status top 2>/dev/null || true
|
|
92
|
+
|
|
93
|
+
# Read permission mode from config
|
|
94
|
+
CONFIG_FILE="${HOME}/.shellmates/config.json"
|
|
95
|
+
PERMISSION_MODE=$(python3 -c "
|
|
96
|
+
import json, os
|
|
97
|
+
cfg = '${CONFIG_FILE}'
|
|
98
|
+
if os.path.exists(cfg):
|
|
99
|
+
d = json.load(open(cfg))
|
|
100
|
+
print(d.get('permission_mode', 'default'))
|
|
101
|
+
else:
|
|
102
|
+
print('default')
|
|
103
|
+
" 2>/dev/null || echo "default")
|
|
104
|
+
|
|
105
|
+
# Start sub-agent with appropriate permission flags
|
|
106
|
+
if [[ "$SUB_AGENT" == "codex" ]]; then
|
|
107
|
+
[[ "$PERMISSION_MODE" == "bypass" ]] && tmux send-keys -t "$AGENT_PANE" "codex --full-auto" Enter || tmux send-keys -t "$AGENT_PANE" "codex" Enter
|
|
108
|
+
else
|
|
109
|
+
[[ "$PERMISSION_MODE" == "bypass" ]] && tmux send-keys -t "$AGENT_PANE" "gemini --yolo" Enter || tmux send-keys -t "$AGENT_PANE" "gemini" Enter
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# Wait for agent shell to initialize, then verify it launched
|
|
113
|
+
sleep 2
|
|
114
|
+
AGENT_CMD=$(tmux display-message -p -t "$AGENT_PANE" '#{pane_current_command}' 2>/dev/null || echo "unknown")
|
|
115
|
+
if [[ "$AGENT_CMD" == "bash" || "$AGENT_CMD" == "zsh" || "$AGENT_CMD" == "sh" ]]; then
|
|
116
|
+
echo "WARNING: $SUB_AGENT may not have started (pane is still running $AGENT_CMD)."
|
|
117
|
+
echo " After attaching, check the left pane and run: $SUB_AGENT"
|
|
118
|
+
echo ""
|
|
119
|
+
else
|
|
120
|
+
echo " $SUB_AGENT started (pane $AGENT_PANE, process: $AGENT_CMD)"
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# Start Claude in right pane
|
|
124
|
+
tmux send-keys -t "$CLAUDE_PANE" "claude" Enter
|
|
125
|
+
sleep 2
|
|
126
|
+
CLAUDE_CMD=$(tmux display-message -p -t "$CLAUDE_PANE" '#{pane_current_command}' 2>/dev/null || echo "unknown")
|
|
127
|
+
if [[ "$CLAUDE_CMD" == "bash" || "$CLAUDE_CMD" == "zsh" || "$CLAUDE_CMD" == "sh" ]]; then
|
|
128
|
+
echo "WARNING: Claude Code may not have started (pane is still running $CLAUDE_CMD)."
|
|
129
|
+
echo " After attaching, check the right pane and run: claude"
|
|
130
|
+
echo ""
|
|
131
|
+
else
|
|
132
|
+
echo " Claude started (pane $CLAUDE_PANE, process: $CLAUDE_CMD)"
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
# Register session in the manifest
|
|
136
|
+
mkdir -p "$MANIFEST_DIR"
|
|
137
|
+
LAUNCHED_AT=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
|
138
|
+
|
|
139
|
+
python3 - <<PYEOF
|
|
140
|
+
import json, os
|
|
141
|
+
|
|
142
|
+
manifest_file = "$MANIFEST_FILE"
|
|
143
|
+
entry = {
|
|
144
|
+
"name": "$SESSION",
|
|
145
|
+
"purpose": "$PURPOSE",
|
|
146
|
+
"project_dir": "$PROJECT_DIR",
|
|
147
|
+
"agents": ["$SUB_AGENT"],
|
|
148
|
+
"launched_at": "$LAUNCHED_AT",
|
|
149
|
+
"panes": {
|
|
150
|
+
"$SUB_AGENT": "$AGENT_PANE",
|
|
151
|
+
"claude": "$CLAUDE_PANE"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if os.path.exists(manifest_file):
|
|
156
|
+
with open(manifest_file) as f:
|
|
157
|
+
data = json.load(f)
|
|
158
|
+
else:
|
|
159
|
+
data = {"sessions": []}
|
|
160
|
+
|
|
161
|
+
# Replace any existing entry with the same session name
|
|
162
|
+
data["sessions"] = [s for s in data["sessions"] if s["name"] != "$SESSION"]
|
|
163
|
+
data["sessions"].append(entry)
|
|
164
|
+
|
|
165
|
+
with open(manifest_file, "w") as f:
|
|
166
|
+
json.dump(data, f, indent=2)
|
|
167
|
+
PYEOF
|
|
168
|
+
|
|
169
|
+
echo ""
|
|
170
|
+
echo "Session registered."
|
|
171
|
+
echo ""
|
|
172
|
+
|
|
173
|
+
# Show or open the session view automatically
|
|
174
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
175
|
+
bash "$SCRIPT_DIR/view-session.sh" "$SESSION" "$CLAUDE_PANE"
|
|
176
|
+
|
|
177
|
+
echo ""
|
|
178
|
+
echo "Tips:"
|
|
179
|
+
echo " Switch panes: Ctrl+b then arrow keys"
|
|
180
|
+
echo " Detach: Ctrl+b then d"
|
|
181
|
+
echo " Re-attach: tmux attach -t $SESSION"
|
|
182
|
+
echo " All sessions: bash scripts/status.sh"
|
|
183
|
+
echo " Close session: bash scripts/teardown.sh"
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# monitor.sh — Background watcher for sub-agent panes
|
|
3
|
+
#
|
|
4
|
+
# Watches one or more tmux panes and reports:
|
|
5
|
+
# - PHASE_COMPLETE signals
|
|
6
|
+
# - AWAITING_INSTRUCTIONS signals
|
|
7
|
+
# - Error keywords (error, failed, crash, exception, traceback)
|
|
8
|
+
# - New git commits
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# ./scripts/monitor.sh # Watch orchestra:0.0 (default)
|
|
12
|
+
# ./scripts/monitor.sh orchestra:0.0 # Explicit pane target
|
|
13
|
+
# ./scripts/monitor.sh full:0.0 full:0.2 # Watch multiple panes
|
|
14
|
+
# ./scripts/monitor.sh --interval 10 full:0.0 # Custom poll interval (seconds)
|
|
15
|
+
#
|
|
16
|
+
# Run in background:
|
|
17
|
+
# ./scripts/monitor.sh > /tmp/orchestra-monitor.log 2>&1 &
|
|
18
|
+
# tail -f /tmp/orchestra-monitor.log
|
|
19
|
+
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
INTERVAL=15
|
|
23
|
+
TARGETS=()
|
|
24
|
+
PROJECT_DIR="${PWD}"
|
|
25
|
+
|
|
26
|
+
# Parse arguments
|
|
27
|
+
while [[ $# -gt 0 ]]; do
|
|
28
|
+
case "$1" in
|
|
29
|
+
--interval) INTERVAL="$2"; shift 2 ;;
|
|
30
|
+
--dir) PROJECT_DIR="$2"; shift 2 ;;
|
|
31
|
+
-h|--help)
|
|
32
|
+
echo "Usage: $0 [--interval N] [--dir path] [pane-target...]"
|
|
33
|
+
echo " Default pane: orchestra:0.0"
|
|
34
|
+
exit 0 ;;
|
|
35
|
+
*) TARGETS+=("$1"); shift ;;
|
|
36
|
+
esac
|
|
37
|
+
done
|
|
38
|
+
|
|
39
|
+
# Default target
|
|
40
|
+
if [[ ${#TARGETS[@]} -eq 0 ]]; then
|
|
41
|
+
TARGETS=("orchestra:0.0")
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
log() {
|
|
45
|
+
echo "[$(date '+%H:%M:%S')] $*"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
log "Monitoring ${#TARGETS[@]} pane(s): ${TARGETS[*]}"
|
|
49
|
+
log "Poll interval: ${INTERVAL}s | Project: ${PROJECT_DIR}"
|
|
50
|
+
log "Press Ctrl+C to stop."
|
|
51
|
+
echo ""
|
|
52
|
+
|
|
53
|
+
# Track state per pane
|
|
54
|
+
declare -A LAST_STATE
|
|
55
|
+
declare -A LAST_COMMIT
|
|
56
|
+
|
|
57
|
+
for TARGET in "${TARGETS[@]}"; do
|
|
58
|
+
LAST_STATE[$TARGET]=""
|
|
59
|
+
LAST_COMMIT[$TARGET]=$(git -C "$PROJECT_DIR" log --oneline -1 --format="%H" 2>/dev/null || echo "")
|
|
60
|
+
done
|
|
61
|
+
|
|
62
|
+
while true; do
|
|
63
|
+
for TARGET in "${TARGETS[@]}"; do
|
|
64
|
+
# Capture last N lines of pane
|
|
65
|
+
PANE_TAIL=$(tmux capture-pane -t "$TARGET" -p 2>/dev/null | tail -10) || {
|
|
66
|
+
log "[$TARGET] WARNING: could not capture pane — is the session running?"
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Detect PHASE_COMPLETE
|
|
71
|
+
if echo "$PANE_TAIL" | grep -q "PHASE_COMPLETE:"; then
|
|
72
|
+
SIGNAL=$(echo "$PANE_TAIL" | grep "PHASE_COMPLETE:" | tail -1)
|
|
73
|
+
if [[ "$SIGNAL" != "${LAST_STATE[$TARGET]:-}" ]]; then
|
|
74
|
+
log "[$TARGET] >>> $SIGNAL"
|
|
75
|
+
LAST_STATE[$TARGET]="$SIGNAL"
|
|
76
|
+
fi
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Detect AWAITING_INSTRUCTIONS
|
|
80
|
+
if echo "$PANE_TAIL" | grep -q "AWAITING_INSTRUCTIONS"; then
|
|
81
|
+
if [[ "${LAST_STATE[$TARGET]:-}" != "AWAITING" ]]; then
|
|
82
|
+
log "[$TARGET] >>> Sub-agent idle — AWAITING_INSTRUCTIONS"
|
|
83
|
+
LAST_STATE[$TARGET]="AWAITING"
|
|
84
|
+
fi
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# Detect shell prompt (agent returned to shell — likely done or crashed)
|
|
88
|
+
if echo "$PANE_TAIL" | grep -qE "\\\$ $|> $"; then
|
|
89
|
+
if [[ "${LAST_STATE[$TARGET]:-}" != "SHELL" ]]; then
|
|
90
|
+
log "[$TARGET] >>> Shell prompt detected — agent may have exited"
|
|
91
|
+
LAST_STATE[$TARGET]="SHELL"
|
|
92
|
+
fi
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# Detect errors
|
|
96
|
+
if echo "$PANE_TAIL" | grep -qiE "(^error|failed:|crash|exception|traceback|SyntaxError)"; then
|
|
97
|
+
log "[$TARGET] !!! POSSIBLE ERROR — last 15 lines:"
|
|
98
|
+
tmux capture-pane -t "$TARGET" -p 2>/dev/null | tail -15 | sed "s/^/ [$TARGET] /"
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# Detect new git commits
|
|
102
|
+
CURRENT_COMMIT=$(git -C "$PROJECT_DIR" log --oneline -1 --format="%H" 2>/dev/null || echo "")
|
|
103
|
+
if [[ -n "$CURRENT_COMMIT" && "$CURRENT_COMMIT" != "${LAST_COMMIT[$TARGET]:-}" && -n "${LAST_COMMIT[$TARGET]:-}" ]]; then
|
|
104
|
+
COMMIT_MSG=$(git -C "$PROJECT_DIR" log --oneline -1 2>/dev/null)
|
|
105
|
+
log "[$TARGET] >>> NEW COMMIT: $COMMIT_MSG"
|
|
106
|
+
LAST_COMMIT[$TARGET]="$CURRENT_COMMIT"
|
|
107
|
+
elif [[ -z "${LAST_COMMIT[$TARGET]:-}" ]]; then
|
|
108
|
+
LAST_COMMIT[$TARGET]="$CURRENT_COMMIT"
|
|
109
|
+
fi
|
|
110
|
+
done
|
|
111
|
+
|
|
112
|
+
sleep "$INTERVAL"
|
|
113
|
+
done
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# spawn-team.sh — Spawn an agent team and delegate a task in one command
|
|
3
|
+
#
|
|
4
|
+
# This is the frictionless interface to shellmates.
|
|
5
|
+
# Tell it what you want done — it handles session creation, agent startup,
|
|
6
|
+
# task dispatch, and ping-back. No tmux knowledge required.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# bash scripts/spawn-team.sh --task "check GSD status" --project ~/Projects
|
|
10
|
+
# bash scripts/spawn-team.sh --task-file /tmp/my-task.txt --agent codex
|
|
11
|
+
# bash scripts/spawn-team.sh --task "run phase 3" --workers 2
|
|
12
|
+
#
|
|
13
|
+
# The agent will notify your current pane when done via AGENT_PING.
|
|
14
|
+
# You don't need to poll — just wait for the ping to appear.
|
|
15
|
+
#
|
|
16
|
+
# Options:
|
|
17
|
+
# --task Inline task description
|
|
18
|
+
# --task-file Path to detailed task file (preferred for multi-step tasks)
|
|
19
|
+
# --agent Agent type: gemini (default) or codex
|
|
20
|
+
# --workers Number of parallel agents (1-2, default: 1)
|
|
21
|
+
# --project Project directory (default: current working directory)
|
|
22
|
+
# --session Session name (default: auto-generated from timestamp)
|
|
23
|
+
# --purpose Short label shown in status.sh (default: first line of task)
|
|
24
|
+
# --ping-back Pane ID to notify when done (default: your current pane)
|
|
25
|
+
# --no-ping Don't send AGENT_PING (fire-and-forget mode)
|
|
26
|
+
# --attach Attach to the new session after launching (default: no)
|
|
27
|
+
# -h|--help Show this help
|
|
28
|
+
|
|
29
|
+
set -euo pipefail
|
|
30
|
+
|
|
31
|
+
TASK_INLINE=""
|
|
32
|
+
TASK_FILE=""
|
|
33
|
+
AGENT="gemini"
|
|
34
|
+
WORKERS=1
|
|
35
|
+
PROJECT_DIR="${PWD}"
|
|
36
|
+
SESSION=""
|
|
37
|
+
PURPOSE=""
|
|
38
|
+
PING_BACK_PANE=""
|
|
39
|
+
NO_PING=false
|
|
40
|
+
ATTACH=false
|
|
41
|
+
|
|
42
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
43
|
+
MANIFEST_DIR="${HOME}/.shellmates"
|
|
44
|
+
MANIFEST_FILE="${MANIFEST_DIR}/sessions.json"
|
|
45
|
+
|
|
46
|
+
while [[ $# -gt 0 ]]; do
|
|
47
|
+
case "$1" in
|
|
48
|
+
--task) TASK_INLINE="$2"; shift 2 ;;
|
|
49
|
+
--task-file) TASK_FILE="$2"; shift 2 ;;
|
|
50
|
+
--agent) AGENT="$2"; shift 2 ;;
|
|
51
|
+
--workers) WORKERS="$2"; shift 2 ;;
|
|
52
|
+
--project) PROJECT_DIR="$2"; shift 2 ;;
|
|
53
|
+
--session) SESSION="$2"; shift 2 ;;
|
|
54
|
+
--purpose) PURPOSE="$2"; shift 2 ;;
|
|
55
|
+
--ping-back) PING_BACK_PANE="$2"; shift 2 ;;
|
|
56
|
+
--no-ping) NO_PING=true; shift ;;
|
|
57
|
+
--no-view) ATTACH=false; shift ;;
|
|
58
|
+
--attach) ATTACH=true; shift ;;
|
|
59
|
+
-h|--help)
|
|
60
|
+
sed -n '2,25p' "$0" | sed 's/^# //' | sed 's/^#//'
|
|
61
|
+
exit 0 ;;
|
|
62
|
+
*) echo "ERROR: Unknown option: $1"; exit 1 ;;
|
|
63
|
+
esac
|
|
64
|
+
done
|
|
65
|
+
|
|
66
|
+
# Validate input
|
|
67
|
+
if [[ -z "$TASK_INLINE" && -z "$TASK_FILE" ]]; then
|
|
68
|
+
echo "ERROR: Provide --task or --task-file"
|
|
69
|
+
echo " Example: $0 --task \"check GSD status\" --project ~/Projects"
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
if [[ -n "$TASK_FILE" && ! -f "$TASK_FILE" ]]; then
|
|
74
|
+
echo "ERROR: Task file not found: $TASK_FILE"
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
if [[ "$WORKERS" -lt 1 || "$WORKERS" -gt 2 ]]; then
|
|
79
|
+
echo "ERROR: --workers must be 1 or 2"
|
|
80
|
+
exit 1
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# Resolve project dir
|
|
84
|
+
PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
|
|
85
|
+
|
|
86
|
+
# Auto-generate session name from timestamp if not provided
|
|
87
|
+
if [[ -z "$SESSION" ]]; then
|
|
88
|
+
SESSION="team-$(date +%H%M%S)"
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Auto-detect purpose from first line of task
|
|
92
|
+
if [[ -z "$PURPOSE" ]]; then
|
|
93
|
+
if [[ -n "$TASK_FILE" ]]; then
|
|
94
|
+
PURPOSE=$(grep -m1 '.' "$TASK_FILE" | sed 's/^#* *//' | cut -c1-50)
|
|
95
|
+
else
|
|
96
|
+
PURPOSE=$(echo "$TASK_INLINE" | head -1 | cut -c1-50)
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# Resolve ping-back pane
|
|
101
|
+
if [[ -z "$PING_BACK_PANE" && "$NO_PING" == "false" ]]; then
|
|
102
|
+
if [[ -n "${TMUX_PANE:-}" ]]; then
|
|
103
|
+
PING_BACK_PANE="$TMUX_PANE"
|
|
104
|
+
elif [[ -n "${TMUX:-}" ]]; then
|
|
105
|
+
PING_BACK_PANE=$(tmux display-message -p '#{pane_id}' 2>/dev/null || echo "")
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
if [[ -z "$PING_BACK_PANE" ]]; then
|
|
109
|
+
echo "NOTE: Not running inside tmux — ping-back disabled."
|
|
110
|
+
echo " You'll need to poll the agent pane manually to check progress."
|
|
111
|
+
echo " Or use --ping-back PANE_ID to specify where to send the completion notification."
|
|
112
|
+
NO_PING=true
|
|
113
|
+
fi
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Check for existing session
|
|
117
|
+
if tmux has-session -t "$SESSION" 2>/dev/null; then
|
|
118
|
+
echo "ERROR: Session '$SESSION' already exists."
|
|
119
|
+
echo " Use a different name: --session my-task-name"
|
|
120
|
+
echo " Or check existing: bash $SCRIPT_DIR/status.sh"
|
|
121
|
+
exit 1
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# Check agent is available
|
|
125
|
+
if ! command -v "$AGENT" &>/dev/null; then
|
|
126
|
+
echo "ERROR: '$AGENT' not found."
|
|
127
|
+
[[ "$AGENT" == "gemini" ]] && echo " Install: npm install -g @google/gemini-cli"
|
|
128
|
+
[[ "$AGENT" == "codex" ]] && echo " Install: npm install -g @openai/codex"
|
|
129
|
+
exit 1
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
echo "Spawning team: $SESSION"
|
|
133
|
+
echo " Agent: $AGENT × $WORKERS"
|
|
134
|
+
echo " Project: $PROJECT_DIR"
|
|
135
|
+
echo " Task: $PURPOSE"
|
|
136
|
+
[[ "$NO_PING" == "false" ]] && echo " Ping: $PING_BACK_PANE"
|
|
137
|
+
echo ""
|
|
138
|
+
|
|
139
|
+
# ── Create session ──────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
tmux new-session -d -s "$SESSION" -c "$PROJECT_DIR"
|
|
142
|
+
tmux set-option -w -t "$SESSION:0" pane-border-status top 2>/dev/null || true
|
|
143
|
+
|
|
144
|
+
# First worker pane (already created with new-session)
|
|
145
|
+
PANE_1=$(tmux list-panes -t "$SESSION:0" -F '#{pane_id}' | sed -n '1p')
|
|
146
|
+
tmux select-pane -t "$PANE_1" -T "worker-1 ($AGENT)"
|
|
147
|
+
|
|
148
|
+
PANE_2=""
|
|
149
|
+
if [[ "$WORKERS" -eq 2 ]]; then
|
|
150
|
+
tmux split-window -h -t "$SESSION:0" -c "$PROJECT_DIR"
|
|
151
|
+
tmux select-layout -t "$SESSION:0" even-horizontal
|
|
152
|
+
PANE_2=$(tmux list-panes -t "$SESSION:0" -F '#{pane_id}' | sed -n '2p')
|
|
153
|
+
tmux select-pane -t "$PANE_2" -T "worker-2 ($AGENT)"
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
# ── Start agents ─────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
CONFIG_FILE="${HOME}/.shellmates/config.json"
|
|
159
|
+
PERMISSION_MODE=$(python3 -c "
|
|
160
|
+
import json, os
|
|
161
|
+
cfg = '${CONFIG_FILE}'
|
|
162
|
+
if os.path.exists(cfg):
|
|
163
|
+
d = json.load(open(cfg))
|
|
164
|
+
print(d.get('permission_mode', 'default'))
|
|
165
|
+
else:
|
|
166
|
+
print('default')
|
|
167
|
+
" 2>/dev/null || echo "default")
|
|
168
|
+
|
|
169
|
+
agent_start_cmd() {
|
|
170
|
+
local agent="$1"
|
|
171
|
+
if [[ "$PERMISSION_MODE" == "bypass" ]]; then
|
|
172
|
+
case "$agent" in
|
|
173
|
+
gemini) echo "gemini --yolo" ;;
|
|
174
|
+
codex) echo "codex --full-auto" ;;
|
|
175
|
+
*) echo "$agent" ;;
|
|
176
|
+
esac
|
|
177
|
+
else
|
|
178
|
+
echo "$agent"
|
|
179
|
+
fi
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
start_agent() {
|
|
183
|
+
local pane="$1"
|
|
184
|
+
local label="$2"
|
|
185
|
+
local cmd
|
|
186
|
+
cmd=$(agent_start_cmd "$AGENT")
|
|
187
|
+
echo -n "Starting $cmd in $label..."
|
|
188
|
+
tmux send-keys -t "$pane" "$cmd" Enter
|
|
189
|
+
|
|
190
|
+
# Wait until the agent prompt is visible (not just process started)
|
|
191
|
+
local elapsed=0
|
|
192
|
+
while [[ $elapsed -lt 15 ]]; do
|
|
193
|
+
sleep 1
|
|
194
|
+
elapsed=$((elapsed + 1))
|
|
195
|
+
local cmd
|
|
196
|
+
cmd=$(tmux display-message -p -t "$pane" '#{pane_current_command}' 2>/dev/null || echo "unknown")
|
|
197
|
+
if [[ "$cmd" != "bash" && "$cmd" != "zsh" && "$cmd" != "sh" && "$cmd" != "fish" ]]; then
|
|
198
|
+
# Process is an agent — now wait for the prompt to appear
|
|
199
|
+
if tmux capture-pane -t "$pane" -p 2>/dev/null | grep -q "Type your message"; then
|
|
200
|
+
echo " ready ($cmd)"
|
|
201
|
+
return 0
|
|
202
|
+
fi
|
|
203
|
+
fi
|
|
204
|
+
done
|
|
205
|
+
|
|
206
|
+
# Timeout — show what we got
|
|
207
|
+
local cmd
|
|
208
|
+
cmd=$(tmux display-message -p -t "$pane" '#{pane_current_command}' 2>/dev/null || echo "unknown")
|
|
209
|
+
if [[ "$cmd" == "bash" || "$cmd" == "zsh" || "$cmd" == "sh" ]]; then
|
|
210
|
+
echo " WARNING: agent may not have started (still $cmd)"
|
|
211
|
+
else
|
|
212
|
+
echo " OK ($cmd — prompt not yet visible)"
|
|
213
|
+
fi
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
start_agent "$PANE_1" "worker-1"
|
|
217
|
+
|
|
218
|
+
if [[ "$WORKERS" -eq 2 ]]; then
|
|
219
|
+
start_agent "$PANE_2" "worker-2"
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
# ── Register in manifest ──────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
mkdir -p "$MANIFEST_DIR"
|
|
225
|
+
LAUNCHED_AT=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
|
226
|
+
ALL_PANES="\"worker-1\": \"$PANE_1\""
|
|
227
|
+
[[ -n "$PANE_2" ]] && ALL_PANES="$ALL_PANES, \"worker-2\": \"$PANE_2\""
|
|
228
|
+
|
|
229
|
+
python3 - <<PYEOF
|
|
230
|
+
import json, os
|
|
231
|
+
|
|
232
|
+
manifest_file = "$MANIFEST_FILE"
|
|
233
|
+
panes = {"worker-1": "$PANE_1"}
|
|
234
|
+
if "$PANE_2":
|
|
235
|
+
panes["worker-2"] = "$PANE_2"
|
|
236
|
+
|
|
237
|
+
entry = {
|
|
238
|
+
"name": "$SESSION",
|
|
239
|
+
"purpose": "$PURPOSE",
|
|
240
|
+
"project_dir": "$PROJECT_DIR",
|
|
241
|
+
"agents": ["$AGENT"] * $WORKERS,
|
|
242
|
+
"launched_at": "$LAUNCHED_AT",
|
|
243
|
+
"panes": panes
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if os.path.exists(manifest_file):
|
|
247
|
+
with open(manifest_file) as f:
|
|
248
|
+
data = json.load(f)
|
|
249
|
+
else:
|
|
250
|
+
data = {"sessions": []}
|
|
251
|
+
|
|
252
|
+
data["sessions"] = [s for s in data["sessions"] if s["name"] != "$SESSION"]
|
|
253
|
+
data["sessions"].append(entry)
|
|
254
|
+
|
|
255
|
+
with open(manifest_file, "w") as f:
|
|
256
|
+
json.dump(data, f, indent=2)
|
|
257
|
+
PYEOF
|
|
258
|
+
|
|
259
|
+
# ── Dispatch task ─────────────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
echo ""
|
|
262
|
+
echo "Dispatching task to worker-1..."
|
|
263
|
+
|
|
264
|
+
DISPATCH_ARGS="--pane $PANE_1 --no-view" # spawn-team handles view itself
|
|
265
|
+
|
|
266
|
+
if [[ -n "$TASK_FILE" ]]; then
|
|
267
|
+
DISPATCH_ARGS="$DISPATCH_ARGS --task-file $TASK_FILE"
|
|
268
|
+
else
|
|
269
|
+
INLINE_TASK_FILE="/tmp/.shellmates-spawn-$$.txt"
|
|
270
|
+
echo "$TASK_INLINE" > "$INLINE_TASK_FILE"
|
|
271
|
+
DISPATCH_ARGS="$DISPATCH_ARGS --task-file $INLINE_TASK_FILE"
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
if [[ "$NO_PING" == "true" ]]; then
|
|
275
|
+
DISPATCH_ARGS="$DISPATCH_ARGS --no-ping"
|
|
276
|
+
elif [[ -n "$PING_BACK_PANE" ]]; then
|
|
277
|
+
DISPATCH_ARGS="$DISPATCH_ARGS --ping-back $PING_BACK_PANE"
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
DISPATCH_ARGS="$DISPATCH_ARGS --task-name $SESSION"
|
|
281
|
+
# shellcheck disable=SC2086
|
|
282
|
+
bash "$SCRIPT_DIR/dispatch.sh" $DISPATCH_ARGS
|
|
283
|
+
|
|
284
|
+
# ── Summary ───────────────────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
echo ""
|
|
287
|
+
echo "Team spawned: $SESSION ($WORKERS worker)"
|
|
288
|
+
echo ""
|
|
289
|
+
|
|
290
|
+
# Open the session view automatically
|
|
291
|
+
bash "$SCRIPT_DIR/view-session.sh" "$SESSION" "$PANE_1"
|
|
292
|
+
|
|
293
|
+
if [[ "$NO_PING" == "false" && -n "$PING_BACK_PANE" ]]; then
|
|
294
|
+
echo "Agent will notify pane $PING_BACK_PANE when done."
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
echo "Kill when done: bash $SCRIPT_DIR/teardown.sh"
|
|
298
|
+
|
|
299
|
+
# Optionally attach (overrides view-session behaviour)
|
|
300
|
+
if [[ "$ATTACH" == "true" ]]; then
|
|
301
|
+
tmux attach-session -t "$SESSION"
|
|
302
|
+
fi
|