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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/bin/shellmates.js +112 -0
  4. package/lib/commands/config.js +88 -0
  5. package/lib/commands/init.js +56 -0
  6. package/lib/commands/install-hook.js +83 -0
  7. package/lib/commands/spawn.js +98 -0
  8. package/lib/commands/status.js +69 -0
  9. package/lib/utils/config.js +35 -0
  10. package/lib/utils/logo.js +84 -0
  11. package/lib/utils/tmux.js +46 -0
  12. package/package.json +39 -0
  13. package/scripts/dispatch.sh +331 -0
  14. package/scripts/launch-full-team.sh +77 -0
  15. package/scripts/launch.sh +183 -0
  16. package/scripts/monitor.sh +113 -0
  17. package/scripts/spawn-team.sh +302 -0
  18. package/scripts/status.sh +168 -0
  19. package/scripts/teardown.sh +211 -0
  20. package/scripts/view-session.sh +98 -0
  21. package/scripts/watch-inbox.sh +71 -0
  22. package/templates/.codex/agents/default.toml +5 -0
  23. package/templates/.codex/agents/executor.toml +7 -0
  24. package/templates/.codex/agents/explorer.toml +5 -0
  25. package/templates/.codex/agents/planner.toml +6 -0
  26. package/templates/.codex/agents/researcher.toml +6 -0
  27. package/templates/.codex/agents/reviewer.toml +5 -0
  28. package/templates/.codex/agents/verifier.toml +6 -0
  29. package/templates/.codex/agents/worker.toml +5 -0
  30. package/templates/.codex/config.toml +43 -0
  31. package/templates/AGENTS.md +109 -0
  32. package/templates/CLAUDE.md +50 -0
  33. package/templates/GEMINI.md +136 -0
  34. package/templates/config.json +10 -0
  35. package/templates/gitignore-additions.txt +2 -0
  36. package/templates/hooks/settings-addition.json +20 -0
  37. package/templates/hooks/shellmates-notify.sh +77 -0
  38. 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