shellmates 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/bin/shellmates.js +112 -0
  4. package/lib/commands/config.js +88 -0
  5. package/lib/commands/init.js +56 -0
  6. package/lib/commands/install-hook.js +83 -0
  7. package/lib/commands/spawn.js +98 -0
  8. package/lib/commands/status.js +69 -0
  9. package/lib/utils/config.js +35 -0
  10. package/lib/utils/logo.js +84 -0
  11. package/lib/utils/tmux.js +46 -0
  12. package/package.json +39 -0
  13. package/scripts/dispatch.sh +331 -0
  14. package/scripts/launch-full-team.sh +77 -0
  15. package/scripts/launch.sh +183 -0
  16. package/scripts/monitor.sh +113 -0
  17. package/scripts/spawn-team.sh +302 -0
  18. package/scripts/status.sh +168 -0
  19. package/scripts/teardown.sh +211 -0
  20. package/scripts/view-session.sh +98 -0
  21. package/scripts/watch-inbox.sh +71 -0
  22. package/templates/.codex/agents/default.toml +5 -0
  23. package/templates/.codex/agents/executor.toml +7 -0
  24. package/templates/.codex/agents/explorer.toml +5 -0
  25. package/templates/.codex/agents/planner.toml +6 -0
  26. package/templates/.codex/agents/researcher.toml +6 -0
  27. package/templates/.codex/agents/reviewer.toml +5 -0
  28. package/templates/.codex/agents/verifier.toml +6 -0
  29. package/templates/.codex/agents/worker.toml +5 -0
  30. package/templates/.codex/config.toml +43 -0
  31. package/templates/AGENTS.md +109 -0
  32. package/templates/CLAUDE.md +50 -0
  33. package/templates/GEMINI.md +136 -0
  34. package/templates/config.json +10 -0
  35. package/templates/gitignore-additions.txt +2 -0
  36. package/templates/hooks/settings-addition.json +20 -0
  37. package/templates/hooks/shellmates-notify.sh +77 -0
  38. package/templates/task-header.txt +10 -0
@@ -0,0 +1,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,5 @@
1
+ developer_instructions = """
2
+ You are a general-purpose coding agent. Research the codebase before making changes.
3
+ Follow the project conventions in AGENTS.md and GEMINI.md.
4
+ Commit after each logical unit of work. Signal PHASE_COMPLETE when done.
5
+ """
@@ -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,5 @@
1
+ developer_instructions = """
2
+ You are a targeted execution worker.
3
+ Implement a specific, well-scoped task as described in the prompt.
4
+ Make the minimum necessary changes. Commit with a clear message. Return a summary.
5
+ """
@@ -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"