cli-fleet 0.1.0__py3-none-any.whl
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.
- cli_fleet/__init__.py +9 -0
- cli_fleet/cli.py +132 -0
- cli_fleet/scripts/agents/brain-neuron.md +85 -0
- cli_fleet/scripts/agents/brain-pacemaker.md +92 -0
- cli_fleet/scripts/agents/hunter.md +27 -0
- cli_fleet/scripts/agents/researcher.md +18 -0
- cli_fleet/scripts/agents/reviewer.md +21 -0
- cli_fleet/scripts/captain.sh +160 -0
- cli_fleet/scripts/cleanup.sh +72 -0
- cli_fleet/scripts/examples/hunt-layerzero-brainstream.json +34 -0
- cli_fleet/scripts/examples/hunt-layerzero.json +30 -0
- cli_fleet/scripts/examples/openwindows-milestone2.json +34 -0
- cli_fleet/scripts/hooks/check-mailbox.sh +64 -0
- cli_fleet/scripts/hooks/consciousness-bridge.sh +64 -0
- cli_fleet/scripts/hooks/stream-stall-check.sh +27 -0
- cli_fleet/scripts/hooks/task-completed.sh +26 -0
- cli_fleet/scripts/hooks/teammate-idle.sh +49 -0
- cli_fleet/scripts/launch.sh +441 -0
- cli_fleet/scripts/lib/protocol.sh +250 -0
- cli_fleet/scripts/send.sh +24 -0
- cli_fleet/scripts/setup.sh +254 -0
- cli_fleet/scripts/status.sh +42 -0
- cli_fleet/scripts/templates/brain-stream-lead.md +76 -0
- cli_fleet/scripts/templates/team-lead.md +60 -0
- cli_fleet-0.1.0.dist-info/METADATA +67 -0
- cli_fleet-0.1.0.dist-info/RECORD +29 -0
- cli_fleet-0.1.0.dist-info/WHEEL +4 -0
- cli_fleet-0.1.0.dist-info/entry_points.txt +2 -0
- cli_fleet-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# FleetCode — launch.sh
|
|
3
|
+
# Creates N team leads, each in its own terminal window, fully interactive.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# ./launch.sh <config.json>
|
|
7
|
+
# ./launch.sh <config.json> --background # use claude -p instead of terminals
|
|
8
|
+
# ./launch.sh --example # print example config
|
|
9
|
+
#
|
|
10
|
+
# Run setup.sh first to enable agent teams and detect your terminal emulator.
|
|
11
|
+
#
|
|
12
|
+
# Each team gets:
|
|
13
|
+
# - Its own terminal window (gnome-terminal, xfce4-terminal, konsole, kitty, etc.)
|
|
14
|
+
# - A generated CLAUDE.md with cross-team protocol
|
|
15
|
+
# - A UserPromptSubmit hook that injects cross-team messages
|
|
16
|
+
# - An initial prompt sent to claude as first argument
|
|
17
|
+
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
21
|
+
META_DIR="${META_TEAM_DIR:-$HOME/.claude/meta-teams}"
|
|
22
|
+
|
|
23
|
+
print_example() {
|
|
24
|
+
cat <<'EXAMPLE'
|
|
25
|
+
{
|
|
26
|
+
"meta_team": "hunt-lz",
|
|
27
|
+
"project_dir": "/media/phantom-orchestrator/Elements1/Ubuntu/bounty-recon",
|
|
28
|
+
"teams": [
|
|
29
|
+
{
|
|
30
|
+
"name": "team-evm-v1",
|
|
31
|
+
"role": "Hunt V1 EVM contracts — UltraLightNodeV2, Endpoint V1, FPValidator ($15M cap)",
|
|
32
|
+
"teammates": 3,
|
|
33
|
+
"task": "Create an agent team with 3 teammates. Focus on V1 EVM contracts worth $15M each. Run hunt-auto evm on UltraLightNodeV2, Endpoint V1, and FPValidator. Chase every finding. Post bugs to the cross-team mailbox."
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "team-evm-v2",
|
|
37
|
+
"role": "Hunt V2 EVM contracts — EndpointV2, SendULN302, ReceiveULN302, DVN ($2M cap)",
|
|
38
|
+
"teammates": 3,
|
|
39
|
+
"task": "Create an agent team with 3 teammates. Focus on V2 EVM protocol contracts. Run hunt-auto evm on EndpointV2, SendULN302, ReceiveULN302, DVN. Focus on cross-contract interactions."
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "team-crosschain",
|
|
43
|
+
"role": "Hunt cross-chain exploit chains — EVM↔Solana, EVM↔TON, encoding mismatches ($2M-$15M)",
|
|
44
|
+
"teammates": 4,
|
|
45
|
+
"task": "Create an agent team with 4 teammates. Focus ONLY on cross-chain attack vectors: address encoding mismatches, nonce desync, executor gas model differences, composed message multi-hop. Use xchain-abi-check, xchain-fuzz, xchain-tracer."
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "team-non-evm",
|
|
49
|
+
"role": "Hunt Solana + TON + Aptos contracts ($2M cap)",
|
|
50
|
+
"teammates": 3,
|
|
51
|
+
"task": "Create an agent team with 3 teammates. One on Solana (DVN, Endpoint, ULN, OFT), one on TON (Controller, ULNManager, DVNProxy), one on Aptos (Endpoint). Run hunt-auto for each chain."
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
EXAMPLE
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if [[ "${1:-}" == "--example" ]]; then
|
|
59
|
+
print_example
|
|
60
|
+
exit 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
CONFIG="${1:?Usage: $0 <config.json> [--background] | $0 --example}"
|
|
64
|
+
[[ ! -f "$CONFIG" ]] && echo "Error: config file not found: $CONFIG" && exit 1
|
|
65
|
+
|
|
66
|
+
MODE="interactive"
|
|
67
|
+
[[ "${2:-}" == "--background" ]] && MODE="background"
|
|
68
|
+
|
|
69
|
+
# Parse config
|
|
70
|
+
META_TEAM=$(python3 -c "import json; print(json.load(open('$CONFIG'))['meta_team'])")
|
|
71
|
+
PROJECT_DIR=$(python3 -c "import json; print(json.load(open('$CONFIG'))['project_dir'])")
|
|
72
|
+
NUM_TEAMS=$(python3 -c "import json; print(len(json.load(open('$CONFIG'))['teams']))")
|
|
73
|
+
|
|
74
|
+
echo "=== Multi-Team Launcher ==="
|
|
75
|
+
echo "Meta-team: $META_TEAM"
|
|
76
|
+
echo "Project dir: $PROJECT_DIR"
|
|
77
|
+
echo "Teams: $NUM_TEAMS"
|
|
78
|
+
echo "Mode: $MODE"
|
|
79
|
+
echo ""
|
|
80
|
+
|
|
81
|
+
# Step 1: Initialize shared state
|
|
82
|
+
source "$SCRIPT_DIR/lib/protocol.sh"
|
|
83
|
+
SHARED_DIR=$(meta_init "$META_TEAM")
|
|
84
|
+
echo "Shared state: $SHARED_DIR"
|
|
85
|
+
mkdir -p "$SHARED_DIR/logs"
|
|
86
|
+
|
|
87
|
+
# Step 2: Build team roster for CLAUDE.md template
|
|
88
|
+
ROSTER=""
|
|
89
|
+
for i in $(seq 0 $((NUM_TEAMS - 1))); do
|
|
90
|
+
t_name=$(python3 -c "import json; print(json.load(open('$CONFIG'))['teams'][$i]['name'])")
|
|
91
|
+
t_role=$(python3 -c "import json; print(json.load(open('$CONFIG'))['teams'][$i]['role'])")
|
|
92
|
+
ROSTER+="- **${t_name}**: ${t_role}"$'\n'
|
|
93
|
+
done
|
|
94
|
+
|
|
95
|
+
# Step 3: Create per-team working directories and launch
|
|
96
|
+
PIDS=()
|
|
97
|
+
for i in $(seq 0 $((NUM_TEAMS - 1))); do
|
|
98
|
+
TEAM_NAME=$(python3 -c "import json; print(json.load(open('$CONFIG'))['teams'][$i]['name'])")
|
|
99
|
+
TEAM_ROLE=$(python3 -c "import json; print(json.load(open('$CONFIG'))['teams'][$i]['role'])")
|
|
100
|
+
TEAM_TASK=$(python3 -c "import json; print(json.load(open('$CONFIG'))['teams'][$i]['task'])")
|
|
101
|
+
TEAM_MATES=$(python3 -c "import json; print(json.load(open('$CONFIG'))['teams'][$i].get('teammates', 3))")
|
|
102
|
+
TEAM_MODE=$(python3 -c "import json; print(json.load(open('$CONFIG'))['teams'][$i].get('mode', 'standard'))")
|
|
103
|
+
|
|
104
|
+
echo ""
|
|
105
|
+
echo "--- Setting up $TEAM_NAME ---"
|
|
106
|
+
echo " Role: $TEAM_ROLE"
|
|
107
|
+
echo " Teammates: $TEAM_MATES"
|
|
108
|
+
echo " Mode: $TEAM_MODE"
|
|
109
|
+
|
|
110
|
+
# Create team workdir
|
|
111
|
+
TEAM_DIR="$SHARED_DIR/workdirs/$TEAM_NAME"
|
|
112
|
+
mkdir -p "$TEAM_DIR"
|
|
113
|
+
|
|
114
|
+
# Per-team consciousness file for brain-stream mode
|
|
115
|
+
CONSCIOUSNESS_FILE="$TEAM_DIR/consciousness.md"
|
|
116
|
+
|
|
117
|
+
# Select template based on mode
|
|
118
|
+
if [[ "$TEAM_MODE" == "brain-stream" ]]; then
|
|
119
|
+
TEMPLATE="$SCRIPT_DIR/templates/brain-stream-lead.md"
|
|
120
|
+
else
|
|
121
|
+
TEMPLATE="$SCRIPT_DIR/templates/team-lead.md"
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# Generate CLAUDE.md from template
|
|
125
|
+
TEAM_CLAUDE="$TEAM_DIR/CLAUDE.md"
|
|
126
|
+
sed \
|
|
127
|
+
-e "s|{{TEAM_NAME}}|$TEAM_NAME|g" \
|
|
128
|
+
-e "s|{{META_TEAM}}|$META_TEAM|g" \
|
|
129
|
+
-e "s|{{ROLE}}|$TEAM_ROLE|g" \
|
|
130
|
+
-e "s|{{META_DIR}}|$SHARED_DIR|g" \
|
|
131
|
+
-e "s|{{MULTI_TEAM_DIR}}|$SCRIPT_DIR|g" \
|
|
132
|
+
-e "s|{{TASK}}|$TEAM_TASK|g" \
|
|
133
|
+
-e "s|{{CONSCIOUSNESS_FILE}}|$CONSCIOUSNESS_FILE|g" \
|
|
134
|
+
"$TEMPLATE" > "$TEAM_CLAUDE"
|
|
135
|
+
|
|
136
|
+
# Replace roster (multiline — use python)
|
|
137
|
+
python3 -c "
|
|
138
|
+
content = open('$TEAM_CLAUDE').read()
|
|
139
|
+
content = content.replace('{{TEAM_ROSTER}}', '''$ROSTER''')
|
|
140
|
+
open('$TEAM_CLAUDE', 'w').write(content)
|
|
141
|
+
"
|
|
142
|
+
|
|
143
|
+
# Copy project CLAUDE.md so teams get project context
|
|
144
|
+
if [[ -f "$PROJECT_DIR/CLAUDE.md" ]]; then
|
|
145
|
+
cp "$PROJECT_DIR/CLAUDE.md" "$TEAM_DIR/PROJECT_CLAUDE.md"
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# Initialize workdir as a git repo so claude trusts it without prompting
|
|
149
|
+
(cd "$TEAM_DIR" && git init -q 2>/dev/null && git commit --allow-empty -m "init" -q 2>/dev/null) || true
|
|
150
|
+
|
|
151
|
+
# Create project-level settings.json with hooks based on mode
|
|
152
|
+
mkdir -p "$TEAM_DIR/.claude"
|
|
153
|
+
|
|
154
|
+
# Brain-stream mode gets the consciousness bridge + stall detection
|
|
155
|
+
# Standard mode gets the regular mailbox + task hooks
|
|
156
|
+
if [[ "$TEAM_MODE" == "brain-stream" ]]; then
|
|
157
|
+
cat > "$TEAM_DIR/.claude/settings.json" <<SETTINGS
|
|
158
|
+
{
|
|
159
|
+
"env": {
|
|
160
|
+
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1",
|
|
161
|
+
"META_TEAM_NAME": "$META_TEAM",
|
|
162
|
+
"TEAM_NAME": "$TEAM_NAME",
|
|
163
|
+
"META_TEAM_DIR": "$META_DIR",
|
|
164
|
+
"CONSCIOUSNESS_FILE": "$CONSCIOUSNESS_FILE"
|
|
165
|
+
},
|
|
166
|
+
"hooks": {
|
|
167
|
+
"UserPromptSubmit": [
|
|
168
|
+
{
|
|
169
|
+
"hooks": [
|
|
170
|
+
{
|
|
171
|
+
"type": "command",
|
|
172
|
+
"command": "bash $SCRIPT_DIR/hooks/check-mailbox.sh",
|
|
173
|
+
"timeout": 5000
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"hooks": [
|
|
179
|
+
{
|
|
180
|
+
"type": "command",
|
|
181
|
+
"command": "bash $SCRIPT_DIR/hooks/consciousness-bridge.sh",
|
|
182
|
+
"timeout": 5000
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
],
|
|
187
|
+
"TeammateIdle": [
|
|
188
|
+
{
|
|
189
|
+
"hooks": [
|
|
190
|
+
{
|
|
191
|
+
"type": "command",
|
|
192
|
+
"command": "bash $SCRIPT_DIR/hooks/stream-stall-check.sh"
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
SETTINGS
|
|
200
|
+
else
|
|
201
|
+
cat > "$TEAM_DIR/.claude/settings.json" <<STDSETTINGS
|
|
202
|
+
{
|
|
203
|
+
"env": {
|
|
204
|
+
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1",
|
|
205
|
+
"META_TEAM_NAME": "$META_TEAM",
|
|
206
|
+
"TEAM_NAME": "$TEAM_NAME",
|
|
207
|
+
"META_TEAM_DIR": "$META_DIR"
|
|
208
|
+
},
|
|
209
|
+
"hooks": {
|
|
210
|
+
"UserPromptSubmit": [
|
|
211
|
+
{
|
|
212
|
+
"hooks": [
|
|
213
|
+
{
|
|
214
|
+
"type": "command",
|
|
215
|
+
"command": "bash $SCRIPT_DIR/hooks/check-mailbox.sh",
|
|
216
|
+
"timeout": 5000
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
}
|
|
220
|
+
],
|
|
221
|
+
"TeammateIdle": [
|
|
222
|
+
{
|
|
223
|
+
"hooks": [
|
|
224
|
+
{
|
|
225
|
+
"type": "command",
|
|
226
|
+
"command": "bash $SCRIPT_DIR/hooks/teammate-idle.sh"
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
],
|
|
231
|
+
"TaskCompleted": [
|
|
232
|
+
{
|
|
233
|
+
"hooks": [
|
|
234
|
+
{
|
|
235
|
+
"type": "command",
|
|
236
|
+
"command": "bash $SCRIPT_DIR/hooks/task-completed.sh"
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
STDSETTINGS
|
|
244
|
+
fi
|
|
245
|
+
|
|
246
|
+
# Install subagent definitions so leads can spawn typed teammates
|
|
247
|
+
mkdir -p "$TEAM_DIR/.claude/agents"
|
|
248
|
+
for agent_def in "$SCRIPT_DIR"/agents/*.md; do
|
|
249
|
+
[[ -f "$agent_def" ]] && cp "$agent_def" "$TEAM_DIR/.claude/agents/"
|
|
250
|
+
done
|
|
251
|
+
|
|
252
|
+
# Fill in placeholders in the agent definitions
|
|
253
|
+
for agent_file in "$TEAM_DIR/.claude/agents/"*.md; do
|
|
254
|
+
[[ -f "$agent_file" ]] && sed -i \
|
|
255
|
+
-e "s|{{CONSCIOUSNESS_FILE}}|$CONSCIOUSNESS_FILE|g" \
|
|
256
|
+
-e "s|{{MULTI_TEAM_DIR}}|$SCRIPT_DIR|g" \
|
|
257
|
+
-e "s|{{META_TEAM}}|$META_TEAM|g" \
|
|
258
|
+
-e "s|{{TEAM_NAME}}|$TEAM_NAME|g" \
|
|
259
|
+
"$agent_file"
|
|
260
|
+
done
|
|
261
|
+
|
|
262
|
+
# Build the initial prompt based on mode
|
|
263
|
+
if [[ "$TEAM_MODE" == "brain-stream" ]]; then
|
|
264
|
+
INIT_PROMPT="$TEAM_TASK
|
|
265
|
+
|
|
266
|
+
THIS IS A BRAIN STREAM. You are the PACEMAKER.
|
|
267
|
+
1. Initialize the consciousness file at: $CONSCIOUSNESS_FILE
|
|
268
|
+
2. Spawn neurons using the brain-neuron agent type
|
|
269
|
+
3. DO NOT create tasks — neurons self-direct
|
|
270
|
+
4. Read your CLAUDE.md for full brain stream protocol
|
|
271
|
+
5. Bridge critical findings to the FleetCode mailbox for other brains
|
|
272
|
+
|
|
273
|
+
IMPORTANT CONTEXT:
|
|
274
|
+
- You are $TEAM_NAME in the $META_TEAM fleet"
|
|
275
|
+
else
|
|
276
|
+
INIT_PROMPT="$TEAM_TASK
|
|
277
|
+
|
|
278
|
+
IMPORTANT CONTEXT:
|
|
279
|
+
- You are $TEAM_NAME in the $META_TEAM multi-team operation
|
|
280
|
+
- Read your CLAUDE.md for cross-team mailbox protocol
|
|
281
|
+
- Other teams running in parallel: check $SHARED_DIR/registry.json
|
|
282
|
+
- Post all findings to the shared mailbox immediately
|
|
283
|
+
- Check mailbox for messages from other teams after each task"
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
# Write the prompt to a file so we can pass it cleanly (avoids shell escaping hell)
|
|
287
|
+
PROMPT_FILE="$SHARED_DIR/logs/${TEAM_NAME}.prompt"
|
|
288
|
+
echo "$INIT_PROMPT" > "$PROMPT_FILE"
|
|
289
|
+
|
|
290
|
+
LOG_FILE="$SHARED_DIR/logs/${TEAM_NAME}.log"
|
|
291
|
+
|
|
292
|
+
if [[ "$MODE" == "interactive" ]]; then
|
|
293
|
+
# Launch in a real terminal window — fully interactive claude session
|
|
294
|
+
echo " Opening terminal window for $TEAM_NAME..."
|
|
295
|
+
|
|
296
|
+
# Detect terminal emulator
|
|
297
|
+
TERMINAL=""
|
|
298
|
+
if [[ -f "$SCRIPT_DIR/.terminal" ]]; then
|
|
299
|
+
TERMINAL=$(cat "$SCRIPT_DIR/.terminal")
|
|
300
|
+
fi
|
|
301
|
+
# Fallback detection if setup.sh wasn't run
|
|
302
|
+
if [[ -z "$TERMINAL" ]]; then
|
|
303
|
+
for t in gnome-terminal xfce4-terminal konsole kitty alacritty wezterm xterm; do
|
|
304
|
+
if command -v "$t" &>/dev/null; then
|
|
305
|
+
TERMINAL="$t"
|
|
306
|
+
break
|
|
307
|
+
fi
|
|
308
|
+
done
|
|
309
|
+
fi
|
|
310
|
+
if [[ -z "$TERMINAL" ]]; then
|
|
311
|
+
echo " ERROR: No terminal emulator found. Run setup.sh or use --background"
|
|
312
|
+
exit 1
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
# Write a launcher script for this team
|
|
316
|
+
LAUNCHER="$SHARED_DIR/logs/${TEAM_NAME}-launcher.sh"
|
|
317
|
+
cat > "$LAUNCHER" <<LAUNCH
|
|
318
|
+
#!/usr/bin/env bash
|
|
319
|
+
export META_TEAM_NAME="$META_TEAM"
|
|
320
|
+
export TEAM_NAME="$TEAM_NAME"
|
|
321
|
+
export META_TEAM_DIR="$META_DIR"
|
|
322
|
+
export CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS="1"
|
|
323
|
+
cd "$PROJECT_DIR"
|
|
324
|
+
|
|
325
|
+
# Log PID for tracking
|
|
326
|
+
echo \$\$ > "$SHARED_DIR/logs/${TEAM_NAME}.pid"
|
|
327
|
+
|
|
328
|
+
# Source protocol for completion message
|
|
329
|
+
source "$SCRIPT_DIR/lib/protocol.sh"
|
|
330
|
+
meta_register_team "$META_TEAM" "$TEAM_NAME" "$TEAM_ROLE" \$\$ "$TEAM_DIR"
|
|
331
|
+
|
|
332
|
+
# Launch interactive claude from project dir (already trusted — no folder approval needed)
|
|
333
|
+
INIT_MSG=\$(cat "$PROMPT_FILE")
|
|
334
|
+
claude --dangerously-skip-permissions "\$INIT_MSG"
|
|
335
|
+
|
|
336
|
+
# On exit, notify other teams
|
|
337
|
+
meta_send "$META_TEAM" "$TEAM_NAME" "all" "status" "$TEAM_NAME session ended"
|
|
338
|
+
LAUNCH
|
|
339
|
+
chmod +x "$LAUNCHER"
|
|
340
|
+
|
|
341
|
+
# Open terminal with the launcher — supports multiple emulators
|
|
342
|
+
case "$TERMINAL" in
|
|
343
|
+
gnome-terminal)
|
|
344
|
+
gnome-terminal --title="$TEAM_NAME — $META_TEAM" \
|
|
345
|
+
--geometry=120x40 \
|
|
346
|
+
-- bash "$LAUNCHER" &
|
|
347
|
+
;;
|
|
348
|
+
xfce4-terminal)
|
|
349
|
+
xfce4-terminal --title="$TEAM_NAME — $META_TEAM" \
|
|
350
|
+
--geometry=120x40 \
|
|
351
|
+
-e "bash $LAUNCHER" &
|
|
352
|
+
;;
|
|
353
|
+
konsole)
|
|
354
|
+
konsole --title "$TEAM_NAME — $META_TEAM" \
|
|
355
|
+
-e bash "$LAUNCHER" &
|
|
356
|
+
;;
|
|
357
|
+
kitty)
|
|
358
|
+
kitty --title "$TEAM_NAME — $META_TEAM" \
|
|
359
|
+
bash "$LAUNCHER" &
|
|
360
|
+
;;
|
|
361
|
+
alacritty)
|
|
362
|
+
alacritty --title "$TEAM_NAME — $META_TEAM" \
|
|
363
|
+
-e bash "$LAUNCHER" &
|
|
364
|
+
;;
|
|
365
|
+
wezterm)
|
|
366
|
+
wezterm start --cwd "$TEAM_DIR" \
|
|
367
|
+
-- bash "$LAUNCHER" &
|
|
368
|
+
;;
|
|
369
|
+
xterm)
|
|
370
|
+
xterm -title "$TEAM_NAME — $META_TEAM" \
|
|
371
|
+
-geometry 120x40 \
|
|
372
|
+
-e bash "$LAUNCHER" &
|
|
373
|
+
;;
|
|
374
|
+
*)
|
|
375
|
+
echo " Unknown terminal: $TERMINAL — falling back to background mode"
|
|
376
|
+
MODE="background"
|
|
377
|
+
;;
|
|
378
|
+
esac
|
|
379
|
+
|
|
380
|
+
if [[ "$MODE" == "interactive" ]]; then
|
|
381
|
+
TERM_PID=$!
|
|
382
|
+
PIDS+=("$TERM_PID")
|
|
383
|
+
echo " Terminal ($TERMINAL) PID: $TERM_PID"
|
|
384
|
+
fi
|
|
385
|
+
fi
|
|
386
|
+
|
|
387
|
+
if [[ "$MODE" == "background" ]]; then
|
|
388
|
+
# Background mode — claude -p, no terminal window
|
|
389
|
+
echo " Launching background claude session (log: $LOG_FILE)..."
|
|
390
|
+
|
|
391
|
+
(
|
|
392
|
+
export META_TEAM_NAME="$META_TEAM"
|
|
393
|
+
export TEAM_NAME="$TEAM_NAME"
|
|
394
|
+
export META_TEAM_DIR="$META_DIR"
|
|
395
|
+
export CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS="1"
|
|
396
|
+
cd "$TEAM_DIR"
|
|
397
|
+
|
|
398
|
+
source "$SCRIPT_DIR/lib/protocol.sh"
|
|
399
|
+
meta_register_team "$META_TEAM" "$TEAM_NAME" "$TEAM_ROLE" $$ "$TEAM_DIR"
|
|
400
|
+
|
|
401
|
+
claude --dangerously-skip-permissions -p "$(cat "$PROMPT_FILE")" \
|
|
402
|
+
> "$LOG_FILE" 2>&1
|
|
403
|
+
|
|
404
|
+
meta_send "$META_TEAM" "$TEAM_NAME" "all" "status" "$TEAM_NAME completed its task"
|
|
405
|
+
) &
|
|
406
|
+
|
|
407
|
+
TEAM_PID=$!
|
|
408
|
+
PIDS+=("$TEAM_PID")
|
|
409
|
+
echo " PID: $TEAM_PID"
|
|
410
|
+
fi
|
|
411
|
+
done
|
|
412
|
+
|
|
413
|
+
echo ""
|
|
414
|
+
echo "=== All $NUM_TEAMS teams launched ($MODE mode) ==="
|
|
415
|
+
echo ""
|
|
416
|
+
echo "Monitor:"
|
|
417
|
+
echo " $SCRIPT_DIR/status.sh $META_TEAM"
|
|
418
|
+
echo ""
|
|
419
|
+
echo "Send message to a team:"
|
|
420
|
+
echo " $SCRIPT_DIR/send.sh $META_TEAM coordinator <team-name|all> <type> \"<content>\""
|
|
421
|
+
echo ""
|
|
422
|
+
if [[ "$MODE" == "background" ]]; then
|
|
423
|
+
echo "View logs:"
|
|
424
|
+
echo " tail -f $SHARED_DIR/logs/*.log"
|
|
425
|
+
echo ""
|
|
426
|
+
fi
|
|
427
|
+
echo "Cleanup:"
|
|
428
|
+
echo " $SCRIPT_DIR/cleanup.sh $META_TEAM"
|
|
429
|
+
echo ""
|
|
430
|
+
|
|
431
|
+
# Save PIDs for cleanup
|
|
432
|
+
printf '%s\n' "${PIDS[@]}" > "$SHARED_DIR/pids.txt"
|
|
433
|
+
|
|
434
|
+
echo "PIDs saved to $SHARED_DIR/pids.txt"
|
|
435
|
+
|
|
436
|
+
if [[ "$MODE" == "interactive" ]]; then
|
|
437
|
+
echo ""
|
|
438
|
+
echo "Each team is running in its own terminal window."
|
|
439
|
+
echo "You can type directly into any team's terminal."
|
|
440
|
+
echo "Teams communicate via: $SHARED_DIR/mailbox/"
|
|
441
|
+
fi
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Multi-team communication protocol — shared functions
|
|
3
|
+
# All teams use the same message format and directory layout.
|
|
4
|
+
|
|
5
|
+
META_DIR="${META_TEAM_DIR:-$HOME/.claude/meta-teams}"
|
|
6
|
+
|
|
7
|
+
# Initialize a meta-team's shared state directory
|
|
8
|
+
meta_init() {
|
|
9
|
+
local meta_name="$1"
|
|
10
|
+
local dir="$META_DIR/$meta_name"
|
|
11
|
+
mkdir -p "$dir"/{mailbox,findings,tasks,status}
|
|
12
|
+
|
|
13
|
+
# Registry: tracks all teams
|
|
14
|
+
if [[ ! -f "$dir/registry.json" ]]; then
|
|
15
|
+
echo '{"meta_team":"'"$meta_name"'","created":"'"$(date -Iseconds)"'","teams":[]}' | python3 -m json.tool > "$dir/registry.json"
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# Cross-team task list
|
|
19
|
+
if [[ ! -f "$dir/tasks.json" ]]; then
|
|
20
|
+
echo '{"tasks":[]}' > "$dir/tasks.json"
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
echo "$dir"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Register a team in the registry
|
|
27
|
+
meta_register_team() {
|
|
28
|
+
local meta_name="$1"
|
|
29
|
+
local team_name="$2"
|
|
30
|
+
local role="$3"
|
|
31
|
+
local pid="$4"
|
|
32
|
+
local workdir="$5"
|
|
33
|
+
local dir="$META_DIR/$meta_name"
|
|
34
|
+
|
|
35
|
+
python3 -c "
|
|
36
|
+
import json, sys
|
|
37
|
+
with open('$dir/registry.json') as f:
|
|
38
|
+
reg = json.load(f)
|
|
39
|
+
reg['teams'] = [t for t in reg['teams'] if t['name'] != '$team_name']
|
|
40
|
+
reg['teams'].append({
|
|
41
|
+
'name': '$team_name',
|
|
42
|
+
'role': '$role',
|
|
43
|
+
'pid': $pid,
|
|
44
|
+
'workdir': '$workdir',
|
|
45
|
+
'status': 'active',
|
|
46
|
+
'registered': '$(date -Iseconds)'
|
|
47
|
+
})
|
|
48
|
+
with open('$dir/registry.json', 'w') as f:
|
|
49
|
+
json.dump(reg, f, indent=2)
|
|
50
|
+
"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Send a message (file-based mailbox)
|
|
54
|
+
meta_send() {
|
|
55
|
+
local meta_name="$1"
|
|
56
|
+
local from="$2"
|
|
57
|
+
local to="$3" # team name or "all"
|
|
58
|
+
local msg_type="$4" # finding|task|question|status|directive
|
|
59
|
+
local content="$5"
|
|
60
|
+
local dir="$META_DIR/$meta_name"
|
|
61
|
+
local ts
|
|
62
|
+
ts=$(date +%s%N)
|
|
63
|
+
local msg_file="$dir/mailbox/${ts}-${from}-to-${to}.json"
|
|
64
|
+
|
|
65
|
+
python3 -c "
|
|
66
|
+
import json
|
|
67
|
+
msg = {
|
|
68
|
+
'id': '${ts}',
|
|
69
|
+
'from': '$from',
|
|
70
|
+
'to': '$to',
|
|
71
|
+
'type': '$msg_type',
|
|
72
|
+
'timestamp': '$(date -Iseconds)',
|
|
73
|
+
'content': $(python3 -c "import json; print(json.dumps('''$content'''))")
|
|
74
|
+
}
|
|
75
|
+
with open('$msg_file', 'w') as f:
|
|
76
|
+
json.dump(msg, f, indent=2)
|
|
77
|
+
"
|
|
78
|
+
echo "$msg_file"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Read unread messages for a team
|
|
82
|
+
meta_read_messages() {
|
|
83
|
+
local meta_name="$1"
|
|
84
|
+
local team_name="$2"
|
|
85
|
+
local dir="$META_DIR/$meta_name"
|
|
86
|
+
local cursor_file="$dir/status/${team_name}.cursor"
|
|
87
|
+
local last_read="0"
|
|
88
|
+
|
|
89
|
+
[[ -f "$cursor_file" ]] && last_read=$(cat "$cursor_file")
|
|
90
|
+
|
|
91
|
+
local new_msgs=()
|
|
92
|
+
for msg_file in "$dir/mailbox/"*.json; do
|
|
93
|
+
[[ ! -f "$msg_file" ]] && continue
|
|
94
|
+
local basename
|
|
95
|
+
basename=$(basename "$msg_file")
|
|
96
|
+
local msg_ts="${basename%%-*}"
|
|
97
|
+
|
|
98
|
+
# Skip already-read messages
|
|
99
|
+
(( msg_ts <= last_read )) && continue
|
|
100
|
+
|
|
101
|
+
# Check if message is for us or broadcast
|
|
102
|
+
local to
|
|
103
|
+
to=$(python3 -c "import json; print(json.load(open('$msg_file'))['to'])")
|
|
104
|
+
if [[ "$to" == "$team_name" || "$to" == "all" ]]; then
|
|
105
|
+
cat "$msg_file"
|
|
106
|
+
echo "---"
|
|
107
|
+
new_msgs+=("$msg_ts")
|
|
108
|
+
fi
|
|
109
|
+
done
|
|
110
|
+
|
|
111
|
+
# Update cursor to latest read
|
|
112
|
+
if [[ ${#new_msgs[@]} -gt 0 ]]; then
|
|
113
|
+
printf '%s\n' "${new_msgs[@]}" | sort -n | tail -1 > "$cursor_file"
|
|
114
|
+
fi
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Post a finding to shared findings directory
|
|
118
|
+
meta_post_finding() {
|
|
119
|
+
local meta_name="$1"
|
|
120
|
+
local team_name="$2"
|
|
121
|
+
local finding_id="$3"
|
|
122
|
+
local severity="$4"
|
|
123
|
+
local title="$5"
|
|
124
|
+
local details="$6"
|
|
125
|
+
local dir="$META_DIR/$meta_name"
|
|
126
|
+
local finding_file="$dir/findings/${team_name}-${finding_id}.json"
|
|
127
|
+
|
|
128
|
+
python3 -c "
|
|
129
|
+
import json
|
|
130
|
+
finding = {
|
|
131
|
+
'id': '$finding_id',
|
|
132
|
+
'team': '$team_name',
|
|
133
|
+
'severity': '$severity',
|
|
134
|
+
'title': $(python3 -c "import json; print(json.dumps('''$title'''))"),
|
|
135
|
+
'details': $(python3 -c "import json; print(json.dumps('''$details'''))"),
|
|
136
|
+
'timestamp': '$(date -Iseconds)',
|
|
137
|
+
'status': 'new'
|
|
138
|
+
}
|
|
139
|
+
with open('$finding_file', 'w') as f:
|
|
140
|
+
json.dump(finding, f, indent=2)
|
|
141
|
+
"
|
|
142
|
+
|
|
143
|
+
# Broadcast finding to all teams
|
|
144
|
+
meta_send "$meta_name" "$team_name" "all" "finding" "New finding: [$severity] $title — see $finding_file"
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Add/update a cross-team task
|
|
148
|
+
meta_add_task() {
|
|
149
|
+
local meta_name="$1"
|
|
150
|
+
local task_id="$2"
|
|
151
|
+
local title="$3"
|
|
152
|
+
local assigned_to="$4" # team name or empty
|
|
153
|
+
local depends_on="$5" # comma-separated task IDs or empty
|
|
154
|
+
local dir="$META_DIR/$meta_name"
|
|
155
|
+
|
|
156
|
+
python3 -c "
|
|
157
|
+
import json
|
|
158
|
+
with open('$dir/tasks.json') as f:
|
|
159
|
+
data = json.load(f)
|
|
160
|
+
data['tasks'] = [t for t in data['tasks'] if t['id'] != '$task_id']
|
|
161
|
+
deps = [d.strip() for d in '$depends_on'.split(',') if d.strip()]
|
|
162
|
+
data['tasks'].append({
|
|
163
|
+
'id': '$task_id',
|
|
164
|
+
'title': '''$title''',
|
|
165
|
+
'assigned_to': '$assigned_to' or None,
|
|
166
|
+
'status': 'pending',
|
|
167
|
+
'depends_on': deps,
|
|
168
|
+
'created': '$(date -Iseconds)',
|
|
169
|
+
'result': None
|
|
170
|
+
})
|
|
171
|
+
with open('$dir/tasks.json', 'w') as f:
|
|
172
|
+
json.dump(data, f, indent=2)
|
|
173
|
+
"
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# Update task status
|
|
177
|
+
meta_update_task() {
|
|
178
|
+
local meta_name="$1"
|
|
179
|
+
local task_id="$2"
|
|
180
|
+
local status="$3" # pending|in_progress|completed
|
|
181
|
+
local result="$4" # optional result text
|
|
182
|
+
local dir="$META_DIR/$meta_name"
|
|
183
|
+
|
|
184
|
+
python3 -c "
|
|
185
|
+
import json
|
|
186
|
+
with open('$dir/tasks.json') as f:
|
|
187
|
+
data = json.load(f)
|
|
188
|
+
for t in data['tasks']:
|
|
189
|
+
if t['id'] == '$task_id':
|
|
190
|
+
t['status'] = '$status'
|
|
191
|
+
if '''$result''':
|
|
192
|
+
t['result'] = '''$result'''
|
|
193
|
+
break
|
|
194
|
+
with open('$dir/tasks.json', 'w') as f:
|
|
195
|
+
json.dump(data, f, indent=2)
|
|
196
|
+
"
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# Get team status summary
|
|
200
|
+
meta_status() {
|
|
201
|
+
local meta_name="$1"
|
|
202
|
+
local dir="$META_DIR/$meta_name"
|
|
203
|
+
|
|
204
|
+
echo "=== META-TEAM: $meta_name ==="
|
|
205
|
+
echo ""
|
|
206
|
+
echo "--- Teams ---"
|
|
207
|
+
python3 -c "
|
|
208
|
+
import json
|
|
209
|
+
with open('$dir/registry.json') as f:
|
|
210
|
+
reg = json.load(f)
|
|
211
|
+
for t in reg['teams']:
|
|
212
|
+
pid_alive = '✓'
|
|
213
|
+
try:
|
|
214
|
+
import os
|
|
215
|
+
os.kill(t['pid'], 0)
|
|
216
|
+
except:
|
|
217
|
+
pid_alive = '✗'
|
|
218
|
+
print(f\" {t['name']:20s} [{t['status']:8s}] pid={t['pid']} {pid_alive} role: {t['role']}\")
|
|
219
|
+
"
|
|
220
|
+
echo ""
|
|
221
|
+
echo "--- Tasks ---"
|
|
222
|
+
python3 -c "
|
|
223
|
+
import json
|
|
224
|
+
with open('$dir/tasks.json') as f:
|
|
225
|
+
data = json.load(f)
|
|
226
|
+
for t in data['tasks']:
|
|
227
|
+
assigned = t.get('assigned_to') or 'unassigned'
|
|
228
|
+
print(f\" [{t['status']:12s}] {t['id']:15s} → {assigned:15s} | {t['title']}\")
|
|
229
|
+
"
|
|
230
|
+
echo ""
|
|
231
|
+
echo "--- Findings ---"
|
|
232
|
+
local count=0
|
|
233
|
+
for f in "$dir/findings/"*.json; do
|
|
234
|
+
[[ ! -f "$f" ]] && continue
|
|
235
|
+
python3 -c "
|
|
236
|
+
import json
|
|
237
|
+
with open('$f') as fh:
|
|
238
|
+
finding = json.load(fh)
|
|
239
|
+
print(f\" [{finding['severity']:8s}] {finding['team']:15s} | {finding['title']}\")
|
|
240
|
+
"
|
|
241
|
+
((count++))
|
|
242
|
+
done
|
|
243
|
+
[[ $count -eq 0 ]] && echo " (none yet)"
|
|
244
|
+
|
|
245
|
+
echo ""
|
|
246
|
+
echo "--- Mailbox ---"
|
|
247
|
+
local msg_count
|
|
248
|
+
msg_count=$(ls "$dir/mailbox/"*.json 2>/dev/null | wc -l)
|
|
249
|
+
echo " $msg_count messages total"
|
|
250
|
+
}
|