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,168 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# status.sh — Show all shellmates sessions with their current state
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# ./scripts/status.sh # Show all sessions
|
|
6
|
+
# ./scripts/status.sh --json # Machine-readable JSON output
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
MANIFEST_FILE="${HOME}/.shellmates/sessions.json"
|
|
11
|
+
JSON_MODE=false
|
|
12
|
+
|
|
13
|
+
while [[ $# -gt 0 ]]; do
|
|
14
|
+
case "$1" in
|
|
15
|
+
--json) JSON_MODE=true; shift ;;
|
|
16
|
+
-h|--help) echo "Usage: $0 [--json]"; exit 0 ;;
|
|
17
|
+
*) echo "Unknown option: $1"; exit 1 ;;
|
|
18
|
+
esac
|
|
19
|
+
done
|
|
20
|
+
|
|
21
|
+
# Collect all tmux session names
|
|
22
|
+
ALL_TMUX_SESSIONS=$(tmux list-sessions -F '#{session_name}' 2>/dev/null || true)
|
|
23
|
+
|
|
24
|
+
if [[ -z "$ALL_TMUX_SESSIONS" ]]; then
|
|
25
|
+
echo "No tmux sessions running."
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Build and print the session status table using Python
|
|
30
|
+
python3 - <<PYEOF
|
|
31
|
+
import json
|
|
32
|
+
import os
|
|
33
|
+
import subprocess
|
|
34
|
+
import datetime
|
|
35
|
+
|
|
36
|
+
manifest_file = "$MANIFEST_FILE"
|
|
37
|
+
json_mode = $( [[ "$JSON_MODE" == "true" ]] && echo "True" || echo "False" )
|
|
38
|
+
|
|
39
|
+
# Load manifest
|
|
40
|
+
manifest_sessions = {}
|
|
41
|
+
if os.path.exists(manifest_file):
|
|
42
|
+
try:
|
|
43
|
+
with open(manifest_file) as f:
|
|
44
|
+
data = json.load(f)
|
|
45
|
+
for s in data.get("sessions", []):
|
|
46
|
+
manifest_sessions[s["name"]] = s
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
# Get all live tmux sessions
|
|
51
|
+
try:
|
|
52
|
+
raw = subprocess.check_output(
|
|
53
|
+
["tmux", "list-sessions", "-F", "#{session_name}|#{session_created}"],
|
|
54
|
+
stderr=subprocess.DEVNULL, text=True
|
|
55
|
+
).strip()
|
|
56
|
+
tmux_sessions = {}
|
|
57
|
+
for line in raw.splitlines():
|
|
58
|
+
parts = line.split("|", 1)
|
|
59
|
+
name = parts[0]
|
|
60
|
+
created_ts = int(parts[1]) if len(parts) > 1 else 0
|
|
61
|
+
tmux_sessions[name] = created_ts
|
|
62
|
+
except Exception:
|
|
63
|
+
tmux_sessions = {}
|
|
64
|
+
|
|
65
|
+
# Combine: manifest entries + any live sessions not in manifest
|
|
66
|
+
all_names = list(manifest_sessions.keys())
|
|
67
|
+
for name in tmux_sessions:
|
|
68
|
+
if name not in all_names:
|
|
69
|
+
all_names.append(name)
|
|
70
|
+
|
|
71
|
+
if not all_names:
|
|
72
|
+
print("No shellmates sessions found.")
|
|
73
|
+
exit()
|
|
74
|
+
|
|
75
|
+
def get_pane_cmd(pane_id):
|
|
76
|
+
"""Check what process is running in a specific pane (by stable pane ID like %12)."""
|
|
77
|
+
try:
|
|
78
|
+
result = subprocess.check_output(
|
|
79
|
+
["tmux", "display-message", "-p", "-t", pane_id, "#{pane_current_command}"],
|
|
80
|
+
stderr=subprocess.DEVNULL, text=True
|
|
81
|
+
).strip()
|
|
82
|
+
return result
|
|
83
|
+
except Exception:
|
|
84
|
+
return "?"
|
|
85
|
+
|
|
86
|
+
def age_str(ts):
|
|
87
|
+
if ts == 0:
|
|
88
|
+
return "unknown"
|
|
89
|
+
delta = datetime.datetime.now() - datetime.datetime.fromtimestamp(ts)
|
|
90
|
+
days = delta.days
|
|
91
|
+
hours = delta.seconds // 3600
|
|
92
|
+
minutes = (delta.seconds % 3600) // 60
|
|
93
|
+
if days > 0:
|
|
94
|
+
return f"{days}d ago"
|
|
95
|
+
elif hours > 0:
|
|
96
|
+
return f"{hours}h ago"
|
|
97
|
+
else:
|
|
98
|
+
return f"{minutes}m ago"
|
|
99
|
+
|
|
100
|
+
def infer_status(entry, is_alive):
|
|
101
|
+
"""Return a human-readable status for the session."""
|
|
102
|
+
if not is_alive:
|
|
103
|
+
return "dead (tmux gone)"
|
|
104
|
+
panes = entry.get("panes", {})
|
|
105
|
+
if not panes:
|
|
106
|
+
return "alive (no pane info)"
|
|
107
|
+
statuses = []
|
|
108
|
+
shell_cmds = {"bash", "zsh", "sh", "fish", "?"}
|
|
109
|
+
for role, pane_id in panes.items():
|
|
110
|
+
cmd = get_pane_cmd(pane_id)
|
|
111
|
+
if cmd in shell_cmds:
|
|
112
|
+
statuses.append(f"{role} idle")
|
|
113
|
+
else:
|
|
114
|
+
statuses.append(f"{role} active ({cmd})")
|
|
115
|
+
return ", ".join(statuses)
|
|
116
|
+
|
|
117
|
+
rows = []
|
|
118
|
+
for idx, name in enumerate(all_names, 1):
|
|
119
|
+
is_alive = name in tmux_sessions
|
|
120
|
+
entry = manifest_sessions.get(name, {})
|
|
121
|
+
created_ts = tmux_sessions.get(name, 0)
|
|
122
|
+
|
|
123
|
+
purpose = entry.get("purpose", "(no manifest entry)")
|
|
124
|
+
project = entry.get("project_dir", "—")
|
|
125
|
+
project_short = project.replace(os.environ.get("HOME", ""), "~")
|
|
126
|
+
agents = ", ".join(entry.get("agents", ["?"])) if entry else "?"
|
|
127
|
+
status = infer_status(entry, is_alive) if entry else ("alive" if is_alive else "dead")
|
|
128
|
+
age = age_str(created_ts)
|
|
129
|
+
|
|
130
|
+
rows.append({
|
|
131
|
+
"idx": idx,
|
|
132
|
+
"name": name,
|
|
133
|
+
"purpose": purpose,
|
|
134
|
+
"project": project_short,
|
|
135
|
+
"agents": agents,
|
|
136
|
+
"status": status,
|
|
137
|
+
"age": age,
|
|
138
|
+
"is_alive": is_alive,
|
|
139
|
+
"has_manifest": bool(entry)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
if json_mode:
|
|
143
|
+
print(json.dumps(rows, indent=2))
|
|
144
|
+
else:
|
|
145
|
+
print("Shellmates sessions:\n")
|
|
146
|
+
fmt = " {idx:<3} {name:<16} {purpose:<28} {project:<30} {agents:<8} {age:<10} {status}"
|
|
147
|
+
header = fmt.format(idx="#", name="name", purpose="purpose", project="project",
|
|
148
|
+
agents="agents", age="age", status="status")
|
|
149
|
+
print(header)
|
|
150
|
+
print(" " + "─" * (len(header) - 2))
|
|
151
|
+
for r in rows:
|
|
152
|
+
marker = "" if r["is_alive"] else " [dead]"
|
|
153
|
+
manifest_note = "" if r["has_manifest"] else " *"
|
|
154
|
+
print(fmt.format(
|
|
155
|
+
idx=r["idx"],
|
|
156
|
+
name=r["name"] + marker,
|
|
157
|
+
purpose=r["purpose"] + manifest_note,
|
|
158
|
+
project=r["project"],
|
|
159
|
+
agents=r["agents"],
|
|
160
|
+
age=r["age"],
|
|
161
|
+
status=r["status"]
|
|
162
|
+
))
|
|
163
|
+
|
|
164
|
+
print("")
|
|
165
|
+
if any(not r["has_manifest"] for r in rows):
|
|
166
|
+
print(" * Session not tracked by shellmates (started outside launch.sh)")
|
|
167
|
+
print(" To close sessions: bash scripts/teardown.sh")
|
|
168
|
+
PYEOF
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# teardown.sh — Close shellmates sessions safely
|
|
3
|
+
#
|
|
4
|
+
# Shows all active sessions with context, then lets you choose which to close.
|
|
5
|
+
# Never kills sessions automatically — always confirms first.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# ./scripts/teardown.sh # Interactive: choose which sessions to close
|
|
9
|
+
# ./scripts/teardown.sh --all # Close all shellmates sessions (skips per-session prompt)
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
MANIFEST_FILE="${HOME}/.shellmates/sessions.json"
|
|
14
|
+
KILL_ALL=false
|
|
15
|
+
|
|
16
|
+
while [[ $# -gt 0 ]]; do
|
|
17
|
+
case "$1" in
|
|
18
|
+
--all) KILL_ALL=true; shift ;;
|
|
19
|
+
-h|--help)
|
|
20
|
+
echo "Usage: $0 [--all]"
|
|
21
|
+
echo " --all Close all sessions without per-session prompts"
|
|
22
|
+
exit 0 ;;
|
|
23
|
+
*) echo "Unknown option: $1"; exit 1 ;;
|
|
24
|
+
esac
|
|
25
|
+
done
|
|
26
|
+
|
|
27
|
+
# Check if tmux is running at all
|
|
28
|
+
if ! tmux list-sessions &>/dev/null 2>&1; then
|
|
29
|
+
echo "No tmux sessions running."
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Build session list via status.sh --json
|
|
34
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
35
|
+
SESSION_JSON=$(bash "$SCRIPT_DIR/status.sh" --json 2>/dev/null || echo "[]")
|
|
36
|
+
|
|
37
|
+
SESSION_COUNT=$(python3 -c "import json,sys; data=json.loads('$( echo "$SESSION_JSON" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))" )'); print(len(data))" 2>/dev/null || echo "0")
|
|
38
|
+
|
|
39
|
+
if [[ "$SESSION_COUNT" == "0" ]]; then
|
|
40
|
+
echo "No sessions found."
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Display sessions and collect kill list
|
|
45
|
+
python3 - <<PYEOF
|
|
46
|
+
import json, os, subprocess, sys
|
|
47
|
+
|
|
48
|
+
manifest_file = "$MANIFEST_FILE"
|
|
49
|
+
kill_all = $( [[ "$KILL_ALL" == "true" ]] && echo "True" || echo "False" )
|
|
50
|
+
|
|
51
|
+
# Re-read sessions
|
|
52
|
+
try:
|
|
53
|
+
raw = subprocess.check_output(
|
|
54
|
+
["tmux", "list-sessions", "-F", "#{session_name}|#{session_created}"],
|
|
55
|
+
stderr=subprocess.DEVNULL, text=True
|
|
56
|
+
).strip()
|
|
57
|
+
live_sessions = {line.split("|")[0] for line in raw.splitlines() if line}
|
|
58
|
+
except Exception:
|
|
59
|
+
live_sessions = set()
|
|
60
|
+
|
|
61
|
+
manifest_sessions = {}
|
|
62
|
+
if os.path.exists(manifest_file):
|
|
63
|
+
try:
|
|
64
|
+
with open(manifest_file) as f:
|
|
65
|
+
data = json.load(f)
|
|
66
|
+
for s in data.get("sessions", []):
|
|
67
|
+
manifest_sessions[s["name"]] = s
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
# Build list: manifest first, then untracked live sessions
|
|
72
|
+
all_names = list(manifest_sessions.keys())
|
|
73
|
+
for name in live_sessions:
|
|
74
|
+
if name not in all_names:
|
|
75
|
+
all_names.append(name)
|
|
76
|
+
|
|
77
|
+
if not all_names:
|
|
78
|
+
print("No sessions found.")
|
|
79
|
+
sys.exit(0)
|
|
80
|
+
|
|
81
|
+
import datetime
|
|
82
|
+
def age_str(ts):
|
|
83
|
+
if ts == 0: return "unknown"
|
|
84
|
+
delta = datetime.datetime.now() - datetime.datetime.fromtimestamp(ts)
|
|
85
|
+
d, h, m = delta.days, delta.seconds//3600, (delta.seconds%3600)//60
|
|
86
|
+
return f"{d}d" if d > 0 else (f"{h}h" if h > 0 else f"{m}m")
|
|
87
|
+
|
|
88
|
+
# Get tmux created times
|
|
89
|
+
try:
|
|
90
|
+
raw2 = subprocess.check_output(
|
|
91
|
+
["tmux", "list-sessions", "-F", "#{session_name}|#{session_created}"],
|
|
92
|
+
stderr=subprocess.DEVNULL, text=True
|
|
93
|
+
).strip()
|
|
94
|
+
created_map = {}
|
|
95
|
+
for line in raw2.splitlines():
|
|
96
|
+
parts = line.split("|")
|
|
97
|
+
if len(parts) == 2:
|
|
98
|
+
created_map[parts[0]] = int(parts[1])
|
|
99
|
+
except Exception:
|
|
100
|
+
created_map = {}
|
|
101
|
+
|
|
102
|
+
print("\nShellmates sessions:\n")
|
|
103
|
+
rows = []
|
|
104
|
+
for idx, name in enumerate(all_names, 1):
|
|
105
|
+
is_alive = name in live_sessions
|
|
106
|
+
entry = manifest_sessions.get(name, {})
|
|
107
|
+
purpose = entry.get("purpose", "(untracked)")
|
|
108
|
+
project = entry.get("project_dir", "—").replace(os.environ.get("HOME", ""), "~")
|
|
109
|
+
agents = ", ".join(entry.get("agents", ["?"])) if entry else "?"
|
|
110
|
+
ts = created_map.get(name, 0)
|
|
111
|
+
age = age_str(ts)
|
|
112
|
+
alive_str = "alive" if is_alive else "dead"
|
|
113
|
+
print(f" [{idx}] {name:<16} {purpose:<30} {project:<28} {agents:<8} {age:<6} {alive_str}")
|
|
114
|
+
rows.append({"idx": idx, "name": name, "is_alive": is_alive})
|
|
115
|
+
|
|
116
|
+
print("")
|
|
117
|
+
|
|
118
|
+
if kill_all:
|
|
119
|
+
to_kill = [r["name"] for r in rows if r["is_alive"]]
|
|
120
|
+
print(f"--all flag set. Closing {len(to_kill)} session(s): {', '.join(to_kill)}")
|
|
121
|
+
# Write kill list for bash to read
|
|
122
|
+
with open("/tmp/.shellmates_kill_list", "w") as f:
|
|
123
|
+
f.write("\n".join(to_kill))
|
|
124
|
+
sys.exit(0)
|
|
125
|
+
|
|
126
|
+
print("Which sessions would you like to close?")
|
|
127
|
+
print(" Enter numbers separated by spaces, 'all' to close all alive sessions,")
|
|
128
|
+
print(" or press Enter to cancel: ", end="", flush=True)
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
response = input().strip().lower()
|
|
132
|
+
except (EOFError, KeyboardInterrupt):
|
|
133
|
+
print("\nCancelled.")
|
|
134
|
+
sys.exit(0)
|
|
135
|
+
|
|
136
|
+
if not response:
|
|
137
|
+
print("Cancelled.")
|
|
138
|
+
sys.exit(0)
|
|
139
|
+
|
|
140
|
+
if response == "all":
|
|
141
|
+
to_kill = [r["name"] for r in rows if r["is_alive"]]
|
|
142
|
+
else:
|
|
143
|
+
to_kill = []
|
|
144
|
+
for part in response.split():
|
|
145
|
+
try:
|
|
146
|
+
idx = int(part)
|
|
147
|
+
match = next((r for r in rows if r["idx"] == idx), None)
|
|
148
|
+
if match:
|
|
149
|
+
if match["is_alive"]:
|
|
150
|
+
to_kill.append(match["name"])
|
|
151
|
+
else:
|
|
152
|
+
print(f" Session [{idx}] {match['name']} is already dead — skipping.")
|
|
153
|
+
else:
|
|
154
|
+
print(f" Unknown number: {part}")
|
|
155
|
+
except ValueError:
|
|
156
|
+
print(f" Skipping invalid input: {part}")
|
|
157
|
+
|
|
158
|
+
if not to_kill:
|
|
159
|
+
print("Nothing to close.")
|
|
160
|
+
sys.exit(0)
|
|
161
|
+
|
|
162
|
+
# Write kill list for bash to read
|
|
163
|
+
with open("/tmp/.shellmates_kill_list", "w") as f:
|
|
164
|
+
f.write("\n".join(to_kill))
|
|
165
|
+
|
|
166
|
+
print(f"\nClosing: {', '.join(to_kill)}")
|
|
167
|
+
PYEOF
|
|
168
|
+
|
|
169
|
+
# Read the kill list and execute
|
|
170
|
+
if [[ ! -f /tmp/.shellmates_kill_list ]]; then
|
|
171
|
+
exit 0
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
KILL_LIST=$(cat /tmp/.shellmates_kill_list)
|
|
175
|
+
rm -f /tmp/.shellmates_kill_list
|
|
176
|
+
|
|
177
|
+
if [[ -z "$KILL_LIST" ]]; then
|
|
178
|
+
exit 0
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
while IFS= read -r SESSION_NAME; do
|
|
182
|
+
[[ -z "$SESSION_NAME" ]] && continue
|
|
183
|
+
|
|
184
|
+
echo -n " Closing '$SESSION_NAME'... "
|
|
185
|
+
if tmux kill-session -t "$SESSION_NAME" 2>/dev/null; then
|
|
186
|
+
echo "done"
|
|
187
|
+
else
|
|
188
|
+
echo "already gone"
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
# Remove from manifest
|
|
192
|
+
if [[ -f "$MANIFEST_FILE" ]]; then
|
|
193
|
+
python3 - <<PYEOF2
|
|
194
|
+
import json, os
|
|
195
|
+
|
|
196
|
+
manifest_file = "$MANIFEST_FILE"
|
|
197
|
+
session_name = "$SESSION_NAME"
|
|
198
|
+
|
|
199
|
+
if os.path.exists(manifest_file):
|
|
200
|
+
with open(manifest_file) as f:
|
|
201
|
+
data = json.load(f)
|
|
202
|
+
data["sessions"] = [s for s in data.get("sessions", []) if s["name"] != session_name]
|
|
203
|
+
with open(manifest_file, "w") as f:
|
|
204
|
+
json.dump(data, f, indent=2)
|
|
205
|
+
PYEOF2
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
done <<< "$KILL_LIST"
|
|
209
|
+
|
|
210
|
+
echo ""
|
|
211
|
+
echo "Done. Run 'bash scripts/status.sh' to verify."
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# view-session.sh — Open a live view of a shellmates worker session
|
|
3
|
+
#
|
|
4
|
+
# Handles three cases automatically:
|
|
5
|
+
# 1. User is inside tmux → creates a new window in their session
|
|
6
|
+
# 2. User is on macOS, not in tmux → opens iTerm2 or Terminal.app
|
|
7
|
+
# 3. Fallback → prints the attach command prominently
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# bash scripts/view-session.sh SESSION_NAME [PANE_ID]
|
|
11
|
+
# bash scripts/view-session.sh --list
|
|
12
|
+
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
|
|
17
|
+
# List mode
|
|
18
|
+
if [[ "${1:-}" == "--list" ]]; then
|
|
19
|
+
bash "$SCRIPT_DIR/status.sh"
|
|
20
|
+
exit 0
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
SESSION="${1:-}"
|
|
24
|
+
PANE="${2:-}"
|
|
25
|
+
|
|
26
|
+
if [[ -z "$SESSION" ]]; then
|
|
27
|
+
echo "Usage: $0 SESSION_NAME"
|
|
28
|
+
echo " $0 --list # show all active sessions"
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
if ! tmux has-session -t "$SESSION" 2>/dev/null; then
|
|
33
|
+
echo "ERROR: Session '$SESSION' not found."
|
|
34
|
+
echo "Active sessions:"
|
|
35
|
+
tmux list-sessions -F " #{session_name}" 2>/dev/null || echo " (none)"
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# ── Case 1: Already inside tmux ───────────────────────────────────────────────
|
|
40
|
+
if [[ -n "${TMUX:-}" ]]; then
|
|
41
|
+
CURRENT_SESSION=$(tmux display-message -p '#S')
|
|
42
|
+
|
|
43
|
+
# If the worker is already in our session, just switch to it
|
|
44
|
+
if [[ "$SESSION" == "$CURRENT_SESSION" ]]; then
|
|
45
|
+
if [[ -n "$PANE" ]]; then
|
|
46
|
+
tmux select-pane -t "$PANE"
|
|
47
|
+
echo "Switched to pane $PANE in current session."
|
|
48
|
+
else
|
|
49
|
+
echo "Already in session '$SESSION' — use Ctrl+b [arrow] to navigate panes."
|
|
50
|
+
fi
|
|
51
|
+
exit 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Worker is in a different session — open it in a new window
|
|
55
|
+
tmux new-window -t "$CURRENT_SESSION" \; \
|
|
56
|
+
send-keys -t "$CURRENT_SESSION" "tmux attach -t $SESSION" Enter
|
|
57
|
+
echo "Opened '$SESSION' in a new tmux window."
|
|
58
|
+
echo "Navigate: Ctrl+b n (next window) / Ctrl+b p (prev window)"
|
|
59
|
+
exit 0
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# ── Case 2: macOS, not in tmux — try to open a terminal window ───────────────
|
|
63
|
+
if command -v osascript &>/dev/null; then
|
|
64
|
+
# Try iTerm2 first (common for developers)
|
|
65
|
+
if osascript -e 'tell application "iTerm2" to get version' &>/dev/null 2>&1; then
|
|
66
|
+
osascript << APPLESCRIPT
|
|
67
|
+
tell application "iTerm2"
|
|
68
|
+
activate
|
|
69
|
+
create window with default profile
|
|
70
|
+
tell current session of current window
|
|
71
|
+
write text "tmux attach -t $SESSION"
|
|
72
|
+
end tell
|
|
73
|
+
end tell
|
|
74
|
+
APPLESCRIPT
|
|
75
|
+
echo "Opened '$SESSION' in a new iTerm2 window."
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Try Terminal.app
|
|
80
|
+
if osascript -e 'tell application "Terminal" to get version' &>/dev/null 2>&1; then
|
|
81
|
+
osascript -e "tell application \"Terminal\" to do script \"tmux attach -t $SESSION\""
|
|
82
|
+
osascript -e 'tell application "Terminal" to activate'
|
|
83
|
+
echo "Opened '$SESSION' in a new Terminal.app window."
|
|
84
|
+
exit 0
|
|
85
|
+
fi
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# ── Case 3: Fallback — print prominently ─────────────────────────────────────
|
|
89
|
+
CMD="tmux attach -t $SESSION"
|
|
90
|
+
BORDER=$(printf '═%.0s' $(seq 1 $((${#CMD} + 6))))
|
|
91
|
+
|
|
92
|
+
echo ""
|
|
93
|
+
echo " ╔${BORDER}╗"
|
|
94
|
+
echo " ║ ${CMD} ║"
|
|
95
|
+
echo " ╚${BORDER}╝"
|
|
96
|
+
echo ""
|
|
97
|
+
echo " Run the command above in a new terminal to watch your agents work."
|
|
98
|
+
echo ""
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# watch-inbox.sh — Background watcher for shellmates completion files
|
|
3
|
+
#
|
|
4
|
+
# Watches ~/.shellmates/inbox/ for result files. When one appears,
|
|
5
|
+
# notifies the orchestrator (Claude) either via:
|
|
6
|
+
# - tmux send-keys (if orchestrator is in a tmux pane)
|
|
7
|
+
# - stdout (if orchestrator is polling or using asyncRewake hook)
|
|
8
|
+
#
|
|
9
|
+
# This is intended to run as a background process started by dispatch.sh.
|
|
10
|
+
# When a result arrives, it wakes Claude automatically — no manual polling.
|
|
11
|
+
#
|
|
12
|
+
# Usage:
|
|
13
|
+
# bash scripts/watch-inbox.sh JOB_ID [NOTIFY_PANE]
|
|
14
|
+
#
|
|
15
|
+
# JOB_ID Unique ID for this job (matches the result filename)
|
|
16
|
+
# NOTIFY_PANE tmux pane to notify (e.g. %47). If omitted, uses $TMUX_PANE.
|
|
17
|
+
#
|
|
18
|
+
# Exit codes (for asyncRewake hook integration):
|
|
19
|
+
# 0 — completed cleanly (no asyncRewake)
|
|
20
|
+
# 2 — result received (triggers asyncRewake if used as a hook)
|
|
21
|
+
|
|
22
|
+
set -euo pipefail
|
|
23
|
+
|
|
24
|
+
JOB_ID="${1:-}"
|
|
25
|
+
NOTIFY_PANE="${2:-${TMUX_PANE:-}}"
|
|
26
|
+
INBOX_DIR="${HOME}/.shellmates/inbox"
|
|
27
|
+
RESULT_FILE="${INBOX_DIR}/${JOB_ID}.txt"
|
|
28
|
+
TIMEOUT="${SHELLMATES_TIMEOUT:-300}" # 5 minutes default
|
|
29
|
+
INTERVAL=1
|
|
30
|
+
ELAPSED=0
|
|
31
|
+
|
|
32
|
+
if [[ -z "$JOB_ID" ]]; then
|
|
33
|
+
echo "Usage: $0 JOB_ID [NOTIFY_PANE]"
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
mkdir -p "$INBOX_DIR"
|
|
38
|
+
|
|
39
|
+
# Wait for the result file
|
|
40
|
+
while [[ ! -f "$RESULT_FILE" && $ELAPSED -lt $TIMEOUT ]]; do
|
|
41
|
+
sleep $INTERVAL
|
|
42
|
+
ELAPSED=$((ELAPSED + INTERVAL))
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
if [[ ! -f "$RESULT_FILE" ]]; then
|
|
46
|
+
MSG="SHELLMATES_TIMEOUT: job $JOB_ID timed out after ${TIMEOUT}s"
|
|
47
|
+
if [[ -n "$NOTIFY_PANE" ]]; then
|
|
48
|
+
tmux send-keys -t "$NOTIFY_PANE" "$MSG — AWAITING_INSTRUCTIONS" Enter 2>/dev/null || true
|
|
49
|
+
fi
|
|
50
|
+
echo "$MSG"
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Read the result
|
|
55
|
+
RESULT=$(cat "$RESULT_FILE")
|
|
56
|
+
SUMMARY=$(grep "^RESULT:" "$RESULT_FILE" -A 5 | tail -5 | tr '\n' ' ' | cut -c1-120)
|
|
57
|
+
|
|
58
|
+
MSG="AGENT_PING: job:${JOB_ID} status:complete ${SUMMARY} — AWAITING_INSTRUCTIONS"
|
|
59
|
+
|
|
60
|
+
# Notify the orchestrator
|
|
61
|
+
if [[ -n "$NOTIFY_PANE" ]]; then
|
|
62
|
+
# Active notification: type directly into orchestrator's pane
|
|
63
|
+
tmux send-keys -t "$NOTIFY_PANE" "$MSG" Enter 2>/dev/null && {
|
|
64
|
+
echo "Notified pane $NOTIFY_PANE"
|
|
65
|
+
exit 2 # asyncRewake signal
|
|
66
|
+
}
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Fallback: print to stdout (orchestrator polling or asyncRewake hook reads this)
|
|
70
|
+
echo "$MSG"
|
|
71
|
+
exit 2
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
developer_instructions = """
|
|
2
|
+
You are an implementation specialist.
|
|
3
|
+
Execute approved plan steps with the smallest defensible change set.
|
|
4
|
+
Keep commits scoped to the current task.
|
|
5
|
+
Provide verification evidence for each deliverable.
|
|
6
|
+
Signal PHASE_COMPLETE with a summary of what changed and what was verified.
|
|
7
|
+
"""
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
developer_instructions = """
|
|
2
|
+
You are a codebase mapper. Read-only — do not edit files.
|
|
3
|
+
Your job: given a task or file list, map dependencies, callers, and blast radius.
|
|
4
|
+
Return a structured summary of what files are involved, what they do, and what could be affected by changes.
|
|
5
|
+
"""
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
developer_instructions = """
|
|
2
|
+
You are a planning specialist.
|
|
3
|
+
Your job: turn a task description and codebase context into a structured, executable plan.
|
|
4
|
+
Output a markdown plan with: goal, file list, step-by-step tasks, and verification criteria.
|
|
5
|
+
Do NOT implement — only plan. Hand the plan back to the orchestrator.
|
|
6
|
+
"""
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
developer_instructions = """
|
|
2
|
+
You are a research specialist. Read only — do not edit files.
|
|
3
|
+
Your job: given a task, identify constraints, dependencies, and unknowns.
|
|
4
|
+
Check the relevant docs, existing code patterns, and potential integration risks.
|
|
5
|
+
Return a structured research report to the orchestrator.
|
|
6
|
+
"""
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
developer_instructions = """
|
|
2
|
+
You are a code reviewer. Read-only — do not edit files.
|
|
3
|
+
Review diffs and recent commits for correctness, security issues, regressions, and missing test coverage.
|
|
4
|
+
Rank findings by severity. Return a structured review to the orchestrator.
|
|
5
|
+
"""
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
developer_instructions = """
|
|
2
|
+
You are an independent verification specialist. Read-only — do not edit files.
|
|
3
|
+
Your job: given completed work, verify it against the original plan and UAT criteria.
|
|
4
|
+
Run tests, check outputs, and report pass/fail with evidence.
|
|
5
|
+
List any residual risks or gaps. Do not approve work that cannot be verified.
|
|
6
|
+
"""
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[features]
|
|
2
|
+
multi_agent = true
|
|
3
|
+
|
|
4
|
+
[agents]
|
|
5
|
+
max_threads = 8
|
|
6
|
+
max_depth = 1
|
|
7
|
+
job_max_runtime_seconds = 2700
|
|
8
|
+
|
|
9
|
+
# ── Built-in compatibility roles ──────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
[agents.default]
|
|
12
|
+
description = "General-purpose fallback agent for planning, research, and execution."
|
|
13
|
+
config_file = "agents/default.toml"
|
|
14
|
+
|
|
15
|
+
[agents.explorer]
|
|
16
|
+
description = "Read-only codebase mapper for scoping and dependency tracing before edits."
|
|
17
|
+
config_file = "agents/explorer.toml"
|
|
18
|
+
|
|
19
|
+
[agents.worker]
|
|
20
|
+
description = "Execution worker for targeted implementation tasks under a validated plan."
|
|
21
|
+
config_file = "agents/worker.toml"
|
|
22
|
+
|
|
23
|
+
[agents.reviewer]
|
|
24
|
+
description = "Read-only reviewer for regressions, risk, and missing verification evidence."
|
|
25
|
+
config_file = "agents/reviewer.toml"
|
|
26
|
+
|
|
27
|
+
# ── Specialized task roles ────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
[agents.planner]
|
|
30
|
+
description = "Planning specialist — produces structured PLAN.md artifacts from context and research."
|
|
31
|
+
config_file = "agents/planner.toml"
|
|
32
|
+
|
|
33
|
+
[agents.researcher]
|
|
34
|
+
description = "Research specialist — discovers constraints, docs, and integration unknowns before planning."
|
|
35
|
+
config_file = "agents/researcher.toml"
|
|
36
|
+
|
|
37
|
+
[agents.executor]
|
|
38
|
+
description = "Implementation specialist — executes approved plans with minimal blast radius."
|
|
39
|
+
config_file = "agents/executor.toml"
|
|
40
|
+
|
|
41
|
+
[agents.verifier]
|
|
42
|
+
description = "Independent verification specialist — runs tests, checks UAT criteria, reports residual risk."
|
|
43
|
+
config_file = "agents/verifier.toml"
|