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.
@@ -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
+ }