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,35 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'
|
|
2
|
+
import { homedir } from 'os'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
|
|
5
|
+
export const CONFIG_DIR = join(homedir(), '.shellmates')
|
|
6
|
+
export const CONFIG_PATH = join(CONFIG_DIR, 'config.json')
|
|
7
|
+
export const INBOX_DIR = join(CONFIG_DIR, 'inbox')
|
|
8
|
+
|
|
9
|
+
const DEFAULTS = {
|
|
10
|
+
permission_mode: 'default',
|
|
11
|
+
default_agent: 'gemini',
|
|
12
|
+
orchestrator: 'claude',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function readConfig() {
|
|
16
|
+
if (!existsSync(CONFIG_PATH)) return { ...DEFAULTS }
|
|
17
|
+
try {
|
|
18
|
+
const raw = readFileSync(CONFIG_PATH, 'utf8')
|
|
19
|
+
return { ...DEFAULTS, ...JSON.parse(raw) }
|
|
20
|
+
} catch {
|
|
21
|
+
return { ...DEFAULTS }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function writeConfig(config) {
|
|
26
|
+
mkdirSync(CONFIG_DIR, { recursive: true })
|
|
27
|
+
// Strip internal _docs key before writing user-facing config
|
|
28
|
+
const { _docs, ...clean } = config
|
|
29
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(clean, null, 2) + '\n')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function ensureDirs() {
|
|
33
|
+
mkdirSync(CONFIG_DIR, { recursive: true })
|
|
34
|
+
mkdirSync(INBOX_DIR, { recursive: true })
|
|
35
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
// Pixel grid definitions — 1 = filled block, 0 = empty
|
|
4
|
+
// 3 pixels wide × 5 pixels tall per letter (compact, fits 80-char terminals)
|
|
5
|
+
// Each pixel renders as '██' (2 chars), gap between letters = ' ' (1 empty pixel)
|
|
6
|
+
|
|
7
|
+
const LETTERS = {
|
|
8
|
+
S: [
|
|
9
|
+
[1,1,1],
|
|
10
|
+
[1,0,0],
|
|
11
|
+
[1,1,1],
|
|
12
|
+
[0,0,1],
|
|
13
|
+
[1,1,1],
|
|
14
|
+
],
|
|
15
|
+
H: [
|
|
16
|
+
[1,0,1],
|
|
17
|
+
[1,0,1],
|
|
18
|
+
[1,1,1],
|
|
19
|
+
[1,0,1],
|
|
20
|
+
[1,0,1],
|
|
21
|
+
],
|
|
22
|
+
E: [
|
|
23
|
+
[1,1,1],
|
|
24
|
+
[1,0,0],
|
|
25
|
+
[1,1,0],
|
|
26
|
+
[1,0,0],
|
|
27
|
+
[1,1,1],
|
|
28
|
+
],
|
|
29
|
+
L: [
|
|
30
|
+
[1,0,0],
|
|
31
|
+
[1,0,0],
|
|
32
|
+
[1,0,0],
|
|
33
|
+
[1,0,0],
|
|
34
|
+
[1,1,1],
|
|
35
|
+
],
|
|
36
|
+
M: [
|
|
37
|
+
[1,0,1],
|
|
38
|
+
[1,1,1],
|
|
39
|
+
[1,0,1],
|
|
40
|
+
[1,0,1],
|
|
41
|
+
[1,0,1],
|
|
42
|
+
],
|
|
43
|
+
A: [
|
|
44
|
+
[0,1,0],
|
|
45
|
+
[1,0,1],
|
|
46
|
+
[1,1,1],
|
|
47
|
+
[1,0,1],
|
|
48
|
+
[1,0,1],
|
|
49
|
+
],
|
|
50
|
+
T: [
|
|
51
|
+
[1,1,1],
|
|
52
|
+
[0,1,0],
|
|
53
|
+
[0,1,0],
|
|
54
|
+
[0,1,0],
|
|
55
|
+
[0,1,0],
|
|
56
|
+
],
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// SHELLMATES = S H E L L M A T E S
|
|
60
|
+
const WORD = ['S','H','E','L','L','M','A','T','E','S']
|
|
61
|
+
|
|
62
|
+
const FILLED = chalk.hex('#4FC3F7')('██')
|
|
63
|
+
const EMPTY = ' '
|
|
64
|
+
|
|
65
|
+
export function printLogo(version) {
|
|
66
|
+
const numRows = 5
|
|
67
|
+
const rows = Array.from({ length: numRows }, (_, rowIdx) => {
|
|
68
|
+
return WORD.map(ch => {
|
|
69
|
+
const grid = LETTERS[ch]
|
|
70
|
+
return grid[rowIdx].map(p => p ? FILLED : EMPTY).join('')
|
|
71
|
+
}).join(EMPTY) // 1-pixel gap between letters
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const pad = ' '
|
|
75
|
+
console.log('')
|
|
76
|
+
for (const row of rows) {
|
|
77
|
+
console.log(pad + row)
|
|
78
|
+
}
|
|
79
|
+
console.log('')
|
|
80
|
+
if (version) {
|
|
81
|
+
console.log(pad + chalk.dim(`v${version} · tmux multi-agent orchestration`))
|
|
82
|
+
}
|
|
83
|
+
console.log('')
|
|
84
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { execSync, spawnSync } from 'child_process'
|
|
2
|
+
import { readdirSync, existsSync } from 'fs'
|
|
3
|
+
|
|
4
|
+
export function tmuxAvailable() {
|
|
5
|
+
try {
|
|
6
|
+
execSync('which tmux', { stdio: 'ignore' })
|
|
7
|
+
return true
|
|
8
|
+
} catch {
|
|
9
|
+
return false
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function listSessions() {
|
|
14
|
+
try {
|
|
15
|
+
const out = execSync('tmux list-sessions -F "#{session_name}"', { encoding: 'utf8' })
|
|
16
|
+
return out.trim().split('\n').filter(Boolean)
|
|
17
|
+
} catch {
|
|
18
|
+
return []
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function sessionExists(name) {
|
|
23
|
+
return listSessions().includes(name)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function listInboxFiles(inboxDir) {
|
|
27
|
+
if (!existsSync(inboxDir)) return []
|
|
28
|
+
return readdirSync(inboxDir).filter(f => f.endsWith('.txt'))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function killSession(name) {
|
|
32
|
+
try {
|
|
33
|
+
execSync(`tmux kill-session -t ${name}`, { stdio: 'ignore' })
|
|
34
|
+
return true
|
|
35
|
+
} catch {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function runScript(scriptPath, args = [], { inherit = true } = {}) {
|
|
41
|
+
const result = spawnSync('bash', [scriptPath, ...args], {
|
|
42
|
+
stdio: inherit ? 'inherit' : 'pipe',
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
})
|
|
45
|
+
return result
|
|
46
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shellmates",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Seamless tmux multi-agent orchestration for Claude, Gemini, and Codex",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"tmux",
|
|
7
|
+
"ai",
|
|
8
|
+
"agents",
|
|
9
|
+
"claude",
|
|
10
|
+
"gemini",
|
|
11
|
+
"codex",
|
|
12
|
+
"orchestration"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/rs07-git/shellmates",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/rs07-git/shellmates.git"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"bin": {
|
|
22
|
+
"shellmates": "bin/shellmates.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"bin/",
|
|
26
|
+
"lib/",
|
|
27
|
+
"scripts/",
|
|
28
|
+
"templates/"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"commander": "^12.1.0",
|
|
35
|
+
"inquirer": "^9.3.7",
|
|
36
|
+
"chalk": "^5.3.0",
|
|
37
|
+
"ora": "^8.1.1"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# dispatch.sh — Reliably send a task to a sub-agent pane
|
|
3
|
+
#
|
|
4
|
+
# Fixes addressed:
|
|
5
|
+
# 1. Startup timing: waits for agent prompt to be ready before sending
|
|
6
|
+
# 2. Permission prompts: enables auto-edit mode (Shift+Tab) before dispatching
|
|
7
|
+
# 3. Token efficiency: prepends agent communication protocol header to every task
|
|
8
|
+
# 4. Completion notification: starts background watcher + writes to inbox file
|
|
9
|
+
# 5. Session visibility: shows or opens a live view of the worker after dispatch
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# bash scripts/dispatch.sh --pane %46 --task-file /tmp/task.txt
|
|
13
|
+
# bash scripts/dispatch.sh --pane %46 --task "one-liner task"
|
|
14
|
+
# bash scripts/dispatch.sh --pane orchestra:0.0 --task-file /tmp/task.txt --no-ping
|
|
15
|
+
#
|
|
16
|
+
# Options:
|
|
17
|
+
# --pane Target pane (stable %ID or positional session:0.0)
|
|
18
|
+
# --task-file Path to task file (preferred for multi-step tasks)
|
|
19
|
+
# --task Inline task (for simple one-liners)
|
|
20
|
+
# --job-id Job ID for inbox result file (default: auto-generated)
|
|
21
|
+
# --ping-back Pane to notify when done (default: caller's $TMUX_PANE)
|
|
22
|
+
# --task-name Short label for status display
|
|
23
|
+
# --no-ping Skip ping-back / inbox watcher (fire-and-forget)
|
|
24
|
+
# --no-view Don't show/open the session viewer after dispatch
|
|
25
|
+
# --no-header Skip prepending the agent communication protocol header
|
|
26
|
+
|
|
27
|
+
set -euo pipefail
|
|
28
|
+
|
|
29
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
30
|
+
HEADER_FILE="${SCRIPT_DIR}/../templates/task-header.txt"
|
|
31
|
+
INBOX_DIR="${HOME}/.shellmates/inbox"
|
|
32
|
+
PROJECT_TASKS_DIR="" # set to project-relative path once we know the pane's cwd
|
|
33
|
+
|
|
34
|
+
PANE=""
|
|
35
|
+
TASK_FILE=""
|
|
36
|
+
TASK_INLINE=""
|
|
37
|
+
JOB_ID=""
|
|
38
|
+
PING_BACK_PANE=""
|
|
39
|
+
TASK_NAME=""
|
|
40
|
+
NO_PING=false
|
|
41
|
+
NO_VIEW=false
|
|
42
|
+
NO_HEADER=false
|
|
43
|
+
|
|
44
|
+
while [[ $# -gt 0 ]]; do
|
|
45
|
+
case "$1" in
|
|
46
|
+
--pane) PANE="$2"; shift 2 ;;
|
|
47
|
+
--task-file) TASK_FILE="$2"; shift 2 ;;
|
|
48
|
+
--task) TASK_INLINE="$2"; shift 2 ;;
|
|
49
|
+
--job-id) JOB_ID="$2"; shift 2 ;;
|
|
50
|
+
--ping-back) PING_BACK_PANE="$2"; shift 2 ;;
|
|
51
|
+
--task-name) TASK_NAME="$2"; shift 2 ;;
|
|
52
|
+
--no-ping) NO_PING=true; shift ;;
|
|
53
|
+
--no-view) NO_VIEW=true; shift ;;
|
|
54
|
+
--no-header) NO_HEADER=false; shift ;; # reserved
|
|
55
|
+
-h|--help)
|
|
56
|
+
sed -n '2,20p' "$0" | sed 's/^# //' | sed 's/^#//'
|
|
57
|
+
exit 0 ;;
|
|
58
|
+
*) echo "ERROR: Unknown option: $1"; exit 1 ;;
|
|
59
|
+
esac
|
|
60
|
+
done
|
|
61
|
+
|
|
62
|
+
# ── Validate ──────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
if [[ -z "$PANE" ]]; then
|
|
65
|
+
echo "ERROR: --pane is required"
|
|
66
|
+
exit 1
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
if [[ -z "$TASK_FILE" && -z "$TASK_INLINE" ]]; then
|
|
70
|
+
echo "ERROR: --task-file or --task is required"
|
|
71
|
+
exit 1
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# ── Resolve ping-back pane ────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
if [[ -z "$PING_BACK_PANE" && "$NO_PING" == "false" ]]; then
|
|
77
|
+
if [[ -n "${TMUX_PANE:-}" ]]; then
|
|
78
|
+
PING_BACK_PANE="$TMUX_PANE"
|
|
79
|
+
elif [[ -n "${TMUX:-}" ]]; then
|
|
80
|
+
PING_BACK_PANE=$(tmux display-message -p '#{pane_id}' 2>/dev/null || echo "")
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
if [[ -z "$PING_BACK_PANE" ]]; then
|
|
84
|
+
echo "NOTE: Not inside tmux — ping-back via tmux disabled."
|
|
85
|
+
echo " Inbox watcher will still write to ~/.shellmates/inbox/"
|
|
86
|
+
echo " Pass --ping-back PANE_ID to enable active notification."
|
|
87
|
+
fi
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# ── Preflight: verify pane is running an agent ────────────────────────────────
|
|
91
|
+
|
|
92
|
+
PANE_CMD=$(tmux display-message -p -t "$PANE" '#{pane_current_command}' 2>/dev/null || echo "unknown")
|
|
93
|
+
SHELL_CMDS=("bash" "zsh" "sh" "fish")
|
|
94
|
+
|
|
95
|
+
for SHELL_CMD in "${SHELL_CMDS[@]}"; do
|
|
96
|
+
if [[ "$PANE_CMD" == "$SHELL_CMD" ]]; then
|
|
97
|
+
echo "ERROR: Pane $PANE is running $PANE_CMD — no agent detected."
|
|
98
|
+
echo " Start the agent first: tmux send-keys -t $PANE 'gemini' Enter"
|
|
99
|
+
exit 1
|
|
100
|
+
fi
|
|
101
|
+
done
|
|
102
|
+
|
|
103
|
+
echo "Target pane: $PANE (process: $PANE_CMD)"
|
|
104
|
+
|
|
105
|
+
# Resolve the pane's working directory (used to place task files inside the project)
|
|
106
|
+
PANE_CWD=$(tmux display-message -p -t "$PANE" '#{pane_current_path}' 2>/dev/null || echo "/tmp")
|
|
107
|
+
PROJECT_TASKS_DIR="${PANE_CWD}/.shellmates/tasks"
|
|
108
|
+
|
|
109
|
+
# ── Wait for agent prompt and dismiss startup banner ──────────────────────────
|
|
110
|
+
# Gemini shows a startup banner AFTER the initial prompt appears.
|
|
111
|
+
# If we send the task immediately after seeing "Type your message", the banner
|
|
112
|
+
# appears and steals the Enter keystroke — task never submits.
|
|
113
|
+
# Fix: explicitly detect and dismiss the banner before dispatching.
|
|
114
|
+
|
|
115
|
+
wait_for_prompt() {
|
|
116
|
+
local pane="$1"
|
|
117
|
+
local max_wait=20
|
|
118
|
+
local elapsed=0
|
|
119
|
+
echo -n "Waiting for agent prompt..."
|
|
120
|
+
while [[ $elapsed -lt $max_wait ]]; do
|
|
121
|
+
local output
|
|
122
|
+
output=$(tmux capture-pane -t "$pane" -p 2>/dev/null)
|
|
123
|
+
if echo "$output" | grep -q "Type your message"; then
|
|
124
|
+
echo " ready."
|
|
125
|
+
return 0
|
|
126
|
+
fi
|
|
127
|
+
sleep 1
|
|
128
|
+
elapsed=$((elapsed + 1))
|
|
129
|
+
done
|
|
130
|
+
echo " (timeout — proceeding anyway)"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
dismiss_startup_banner() {
|
|
134
|
+
local pane="$1"
|
|
135
|
+
local output
|
|
136
|
+
output=$(tmux capture-pane -t "$pane" -p 2>/dev/null)
|
|
137
|
+
# Gemini shows an announcement banner with "What's Changing" or similar
|
|
138
|
+
if echo "$output" | grep -qE "What's Changing|We're making changes|Read more:.*goo\.gle"; then
|
|
139
|
+
echo "Dismissing startup banner..."
|
|
140
|
+
tmux send-keys -t "$pane" "" Enter
|
|
141
|
+
sleep 1
|
|
142
|
+
# Wait for prompt to reappear after dismiss
|
|
143
|
+
wait_for_prompt "$pane"
|
|
144
|
+
fi
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Verify the pane's input bar contains the expected string.
|
|
148
|
+
# Returns 0 if found, 1 if not.
|
|
149
|
+
input_bar_contains() {
|
|
150
|
+
local pane="$1"
|
|
151
|
+
local expected="$2"
|
|
152
|
+
tmux capture-pane -t "$pane" -p 2>/dev/null | grep -qF "$expected"
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Ensure a string is in the input bar before submitting. If the banner
|
|
156
|
+
# cleared it (by consuming @filepath or the prior Enter), re-type it.
|
|
157
|
+
ensure_input_ready() {
|
|
158
|
+
local pane="$1"
|
|
159
|
+
local text="$2"
|
|
160
|
+
local max_retries=3
|
|
161
|
+
local attempt=0
|
|
162
|
+
|
|
163
|
+
while [[ $attempt -lt $max_retries ]]; do
|
|
164
|
+
if input_bar_contains "$pane" "$text"; then
|
|
165
|
+
echo "Input verified: $(basename "$text" 2>/dev/null || echo "$text")"
|
|
166
|
+
return 0
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
attempt=$((attempt + 1))
|
|
170
|
+
echo "Input bar missing expected text (attempt $attempt) — re-typing..."
|
|
171
|
+
# Clear whatever might be half-typed and retype
|
|
172
|
+
tmux send-keys -t "$pane" "C-c" 2>/dev/null || true
|
|
173
|
+
sleep 0.2
|
|
174
|
+
wait_for_prompt "$pane"
|
|
175
|
+
dismiss_startup_banner "$pane"
|
|
176
|
+
tmux send-keys -t "$pane" "$text"
|
|
177
|
+
sleep 0.4
|
|
178
|
+
done
|
|
179
|
+
|
|
180
|
+
echo "WARNING: Could not verify input bar after $max_retries attempts — submitting anyway"
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
wait_for_prompt "$PANE"
|
|
184
|
+
dismiss_startup_banner "$PANE"
|
|
185
|
+
|
|
186
|
+
# ── Enable auto-edit mode if not already in bypass mode ──────────────────────
|
|
187
|
+
# If the agent was NOT started with --yolo/--full-auto (i.e. permission_mode=default),
|
|
188
|
+
# send Shift+Tab to at least enable auto-edit for this session (approves file tools).
|
|
189
|
+
# If bypass mode is active, the agent already handles this at startup — skip it.
|
|
190
|
+
|
|
191
|
+
CONFIG_FILE="${HOME}/.shellmates/config.json"
|
|
192
|
+
PERMISSION_MODE=$(python3 -c "
|
|
193
|
+
import json, os
|
|
194
|
+
cfg = '${CONFIG_FILE}'
|
|
195
|
+
if os.path.exists(cfg):
|
|
196
|
+
d = json.load(open(cfg))
|
|
197
|
+
print(d.get('permission_mode', 'default'))
|
|
198
|
+
else:
|
|
199
|
+
print('default')
|
|
200
|
+
" 2>/dev/null || echo "default")
|
|
201
|
+
|
|
202
|
+
if [[ "$PERMISSION_MODE" != "bypass" ]]; then
|
|
203
|
+
if [[ "$PANE_CMD" == "node" || "$PANE_CMD" == "gemini" ]]; then
|
|
204
|
+
# Gemini: Shift+Tab toggles auto-edit (approves file read/write tools)
|
|
205
|
+
tmux send-keys -t "$PANE" "BTab"
|
|
206
|
+
sleep 0.3
|
|
207
|
+
fi
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
# ── Build final task file ─────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
# Resolve inline task to file
|
|
213
|
+
TEMP_TASK_FILE=""
|
|
214
|
+
if [[ -n "$TASK_INLINE" ]]; then
|
|
215
|
+
TEMP_TASK_FILE="/tmp/.shellmates-task-$$.txt"
|
|
216
|
+
echo "$TASK_INLINE" > "$TEMP_TASK_FILE"
|
|
217
|
+
TASK_FILE="$TEMP_TASK_FILE"
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
# Auto-generate job ID
|
|
221
|
+
if [[ -z "$JOB_ID" ]]; then
|
|
222
|
+
JOB_ID="job-$$-$(date +%s)"
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
# Auto-generate task name
|
|
226
|
+
if [[ -z "$TASK_NAME" ]]; then
|
|
227
|
+
TASK_NAME=$(grep -m1 '.' "$TASK_FILE" | sed 's/^#* *//' | cut -c1-60)
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
# Build the final task: header + original task + completion footer
|
|
231
|
+
# Place the file INSIDE the project dir so agents don't need a separate
|
|
232
|
+
# permission prompt to read it (Gemini --yolo still prompts for /tmp reads).
|
|
233
|
+
mkdir -p "$PROJECT_TASKS_DIR" 2>/dev/null || true
|
|
234
|
+
if [[ -d "$PROJECT_TASKS_DIR" ]]; then
|
|
235
|
+
FINAL_TASK_FILE="${PROJECT_TASKS_DIR}/.dispatch-$$.txt"
|
|
236
|
+
else
|
|
237
|
+
FINAL_TASK_FILE="/tmp/.shellmates-dispatch-$$.txt"
|
|
238
|
+
fi
|
|
239
|
+
mkdir -p "$INBOX_DIR"
|
|
240
|
+
|
|
241
|
+
{
|
|
242
|
+
# Token efficiency protocol header
|
|
243
|
+
if [[ -f "$HEADER_FILE" ]]; then
|
|
244
|
+
cat "$HEADER_FILE"
|
|
245
|
+
echo ""
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
# Original task content
|
|
249
|
+
cat "$TASK_FILE"
|
|
250
|
+
|
|
251
|
+
# Completion footer: write result to inbox file
|
|
252
|
+
cat << FOOTER
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
When your task is complete, write your result to this file:
|
|
256
|
+
${INBOX_DIR}/${JOB_ID}.txt
|
|
257
|
+
|
|
258
|
+
Use this exact format (concise — no prose):
|
|
259
|
+
\`\`\`
|
|
260
|
+
AGENT: $(echo "$PANE_CMD" | tr '[:upper:]' '[:lower:]')
|
|
261
|
+
JOB: ${JOB_ID}
|
|
262
|
+
STATUS: complete
|
|
263
|
+
CHANGED: <comma-separated file paths, or none>
|
|
264
|
+
RESULT: <≤5 line summary of what was done>
|
|
265
|
+
\`\`\`
|
|
266
|
+
|
|
267
|
+
Write the file with:
|
|
268
|
+
mkdir -p "${INBOX_DIR}" && cat > "${INBOX_DIR}/${JOB_ID}.txt" << 'EOF'
|
|
269
|
+
AGENT: gemini
|
|
270
|
+
JOB: ${JOB_ID}
|
|
271
|
+
STATUS: complete
|
|
272
|
+
CHANGED: <files>
|
|
273
|
+
RESULT: <summary>
|
|
274
|
+
EOF
|
|
275
|
+
|
|
276
|
+
Then output: PHASE_COMPLETE: ${TASK_NAME}
|
|
277
|
+
FOOTER
|
|
278
|
+
} > "$FINAL_TASK_FILE"
|
|
279
|
+
|
|
280
|
+
# Cleanup inline temp file
|
|
281
|
+
[[ -n "$TEMP_TASK_FILE" ]] && rm -f "$TEMP_TASK_FILE"
|
|
282
|
+
|
|
283
|
+
# ── Dispatch ──────────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
if [[ "$PANE_CMD" == "node" || "$PANE_CMD" == "gemini" ]]; then
|
|
286
|
+
echo "Dispatching via @filepath (Gemini CLI)..."
|
|
287
|
+
# Type the filepath but don't submit yet — Gemini may re-show the startup
|
|
288
|
+
# banner between the @filepath and the Enter. We:
|
|
289
|
+
# 1. Type the @filepath (no Enter)
|
|
290
|
+
# 2. Dismiss any banner that appeared
|
|
291
|
+
# 3. Verify the input bar still contains the filepath (re-type if not)
|
|
292
|
+
# 4. Submit
|
|
293
|
+
tmux send-keys -t "$PANE" "@${FINAL_TASK_FILE}"
|
|
294
|
+
sleep 0.5
|
|
295
|
+
dismiss_startup_banner "$PANE"
|
|
296
|
+
ensure_input_ready "$PANE" "@${FINAL_TASK_FILE}"
|
|
297
|
+
tmux send-keys -t "$PANE" "" Enter
|
|
298
|
+
else
|
|
299
|
+
echo "Dispatching via direct send..."
|
|
300
|
+
tmux send-keys -t "$PANE" "$(cat "$FINAL_TASK_FILE")"
|
|
301
|
+
tmux send-keys -t "$PANE" "" Enter
|
|
302
|
+
fi
|
|
303
|
+
|
|
304
|
+
echo "Task dispatched: '${TASK_NAME}'"
|
|
305
|
+
echo "Job ID: ${JOB_ID}"
|
|
306
|
+
echo "Result will appear in: ${INBOX_DIR}/${JOB_ID}.txt"
|
|
307
|
+
|
|
308
|
+
# ── Start background watcher ──────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
if [[ "$NO_PING" == "false" ]]; then
|
|
311
|
+
WATCH_ARGS="$JOB_ID"
|
|
312
|
+
[[ -n "$PING_BACK_PANE" ]] && WATCH_ARGS="$WATCH_ARGS $PING_BACK_PANE"
|
|
313
|
+
|
|
314
|
+
bash "$SCRIPT_DIR/watch-inbox.sh" $WATCH_ARGS &
|
|
315
|
+
WATCHER_PID=$!
|
|
316
|
+
echo "Background watcher started (PID: $WATCHER_PID)"
|
|
317
|
+
fi
|
|
318
|
+
|
|
319
|
+
# ── Show session view ─────────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
if [[ "$NO_VIEW" == "false" ]]; then
|
|
322
|
+
SESSION=$(tmux display-message -p -t "$PANE" '#S' 2>/dev/null || echo "")
|
|
323
|
+
if [[ -n "$SESSION" ]]; then
|
|
324
|
+
echo ""
|
|
325
|
+
bash "$SCRIPT_DIR/view-session.sh" "$SESSION" "$PANE"
|
|
326
|
+
fi
|
|
327
|
+
fi
|
|
328
|
+
|
|
329
|
+
echo ""
|
|
330
|
+
echo "Task file: $FINAL_TASK_FILE"
|
|
331
|
+
echo "Monitor: tmux capture-pane -t $PANE -p | tail -20"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# launch-full-team.sh — 4-pane multi-agent session
|
|
3
|
+
#
|
|
4
|
+
# Layout:
|
|
5
|
+
# ┌──────────────┬──────────────┐
|
|
6
|
+
# │ gemini-1 │ claude │
|
|
7
|
+
# │ (worker A) │ (orchestrat) │
|
|
8
|
+
# ├──────────────┼──────────────┤
|
|
9
|
+
# │ gemini-2 │ codex │
|
|
10
|
+
# │ (worker B) │ (executor) │
|
|
11
|
+
# └──────────────┴──────────────┘
|
|
12
|
+
#
|
|
13
|
+
# Pane targets:
|
|
14
|
+
# full:0.0 — Gemini CLI worker A
|
|
15
|
+
# full:0.1 — Claude Code orchestrator
|
|
16
|
+
# full:0.2 — Gemini CLI worker B
|
|
17
|
+
# full:0.3 — Codex CLI executor
|
|
18
|
+
#
|
|
19
|
+
# Usage:
|
|
20
|
+
# ./scripts/launch-full-team.sh [--session name] [--dir path]
|
|
21
|
+
|
|
22
|
+
set -euo pipefail
|
|
23
|
+
|
|
24
|
+
SESSION="full"
|
|
25
|
+
PROJECT_DIR="${PWD}"
|
|
26
|
+
|
|
27
|
+
while [[ $# -gt 0 ]]; do
|
|
28
|
+
case "$1" in
|
|
29
|
+
--session) SESSION="$2"; shift 2 ;;
|
|
30
|
+
--dir) PROJECT_DIR="$2"; shift 2 ;;
|
|
31
|
+
-h|--help)
|
|
32
|
+
echo "Usage: $0 [--session name] [--dir path]"
|
|
33
|
+
exit 0 ;;
|
|
34
|
+
*) echo "Unknown option: $1"; exit 1 ;;
|
|
35
|
+
esac
|
|
36
|
+
done
|
|
37
|
+
|
|
38
|
+
if tmux has-session -t "$SESSION" 2>/dev/null; then
|
|
39
|
+
echo "Session '$SESSION' already exists. Attaching..."
|
|
40
|
+
tmux attach-session -t "$SESSION"
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
echo "Creating 4-pane session '$SESSION'..."
|
|
45
|
+
|
|
46
|
+
# Create and tile into 4 panes
|
|
47
|
+
tmux new-session -d -s "$SESSION" -c "$PROJECT_DIR" # pane 0
|
|
48
|
+
tmux split-window -h -t "$SESSION:0" -c "$PROJECT_DIR" # pane 1 (right)
|
|
49
|
+
tmux split-window -v -t "$SESSION:0.0" -c "$PROJECT_DIR" # pane 2 (bottom-left)
|
|
50
|
+
tmux split-window -v -t "$SESSION:0.1" -c "$PROJECT_DIR" # pane 3 (bottom-right)
|
|
51
|
+
|
|
52
|
+
# Label panes
|
|
53
|
+
tmux select-pane -t "$SESSION:0.0" -T "gemini-A"
|
|
54
|
+
tmux select-pane -t "$SESSION:0.1" -T "orchestrator (claude)"
|
|
55
|
+
tmux select-pane -t "$SESSION:0.2" -T "gemini-B"
|
|
56
|
+
tmux select-pane -t "$SESSION:0.3" -T "codex"
|
|
57
|
+
|
|
58
|
+
# Start sub-agents
|
|
59
|
+
tmux send-keys -t "$SESSION:0.0" "gemini" Enter
|
|
60
|
+
tmux send-keys -t "$SESSION:0.2" "gemini" Enter
|
|
61
|
+
tmux send-keys -t "$SESSION:0.3" "codex" Enter
|
|
62
|
+
|
|
63
|
+
# Start orchestrator last
|
|
64
|
+
sleep 1
|
|
65
|
+
tmux send-keys -t "$SESSION:0.1" "claude" Enter
|
|
66
|
+
|
|
67
|
+
tmux select-pane -t "$SESSION:0.1"
|
|
68
|
+
|
|
69
|
+
echo ""
|
|
70
|
+
echo "4-pane session ready."
|
|
71
|
+
echo " Pane 0.0 — Gemini worker A"
|
|
72
|
+
echo " Pane 0.1 — Claude orchestrator"
|
|
73
|
+
echo " Pane 0.2 — Gemini worker B"
|
|
74
|
+
echo " Pane 0.3 — Codex executor"
|
|
75
|
+
echo ""
|
|
76
|
+
|
|
77
|
+
tmux attach-session -t "$SESSION"
|