-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcopilot-agent
More file actions
executable file
·215 lines (186 loc) · 7.88 KB
/
Copy pathcopilot-agent
File metadata and controls
executable file
·215 lines (186 loc) · 7.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
#!/usr/bin/env bash
# copilot-agent — launch or attach to a named Copilot CLI agent session
#
# Usage:
# copilot-agent <name> (or just: ca <name>)
#
# Behavior:
# - Resolves <name> (e.g., alpha) to a workspace directory:
# $WORKSPACE_BASE/agent-<name>
# (case-preserved; case-sensitive throughout)
# - If a tmux session named <name> exists, attaches to it.
# - Otherwise, unlocks the login keychain, creates a new tmux session,
# and launches Copilot CLI, attempting to resume an existing session
# of the same name (falling back to a fresh named session) with remote
# control enabled.
#
# Configuration (override via env or ~/.config/remote-agent-stack/config):
# WORKSPACE_BASE default: $HOME/Library/CloudStorage/Dropbox/copilot-workspace
# COPILOT_BIN default: copilot
# AGENT_DIR_PREFIX default: agent-
#
# A sample config file is created on first run.
set -euo pipefail
# ---- config ----------------------------------------------------------------
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/remote-agent-stack"
CONFIG_FILE="$CONFIG_DIR/config"
if [ -f "$CONFIG_FILE" ]; then
# shellcheck source=/dev/null
. "$CONFIG_FILE"
fi
: "${WORKSPACE_BASE:=$HOME/Library/CloudStorage/Dropbox/copilot-workspace}"
: "${COPILOT_BIN:=copilot}"
: "${AGENT_DIR_PREFIX:=agent-}"
# ---- args ------------------------------------------------------------------
usage() {
cat <<USAGE
Usage: $(basename "$0") <Name>
Launch or reattach to a named Copilot CLI agent session.
Example: $(basename "$0") alpha
Options:
-h, --help Show this help and exit.
Config:
~/.config/remote-agent-stack/config
Env: WORKSPACE_BASE, COPILOT_BIN, AGENT_DIR_PREFIX
USAGE
}
case "${1:-}" in
-h|--help) usage; exit 0 ;;
esac
if [ $# -ne 1 ] || [ -z "${1:-}" ] || [ "${1#-}" != "$1" ]; then
usage >&2
exit 2
fi
NAME="$1"
DIR="$WORKSPACE_BASE/${AGENT_DIR_PREFIX}${NAME}"
# ---- preflight -------------------------------------------------------------
if [ ! -d "$WORKSPACE_BASE" ]; then
echo "Workspace base not available: $WORKSPACE_BASE" >&2
echo "Set WORKSPACE_BASE in $CONFIG_FILE or via env." >&2
exit 1
fi
mkdir -p "$DIR"
if ! command -v tmux >/dev/null 2>&1; then
echo "tmux not found in PATH. Run the remote-agent-stack installer." >&2
exit 1
fi
if ! command -v "$COPILOT_BIN" >/dev/null 2>&1; then
echo "Copilot CLI ('$COPILOT_BIN') not found in PATH." >&2
exit 1
fi
# ---- mailbox poke resolver -------------------------------------------------
# Mailbox skill (dfrysinger/skills) lives in the plugin install path once
# installed; fall back to local-dev paths. Empty if not installed OR if
# MAILBOX_INTEGRATION is not set to "true" in config.
MAILBOX_POKE=""
if [ "${MAILBOX_INTEGRATION:-false}" = "true" ]; then
for _p in \
"$HOME/.copilot/installed-plugins/_direct/dfrysinger--skills/skills/mailbox/scripts/mailbox-poke.sh" \
"$HOME/code/skills/skills/mailbox/scripts/mailbox-poke.sh" \
"$HOME/.copilot/skills/mailbox/scripts/mailbox-poke.sh"; do
[ -x "$_p" ] && { MAILBOX_POKE="$_p"; break; }
done
fi
# ---- attach if session exists ---------------------------------------------
if tmux has-session -t "$NAME" 2>/dev/null; then
[ -n "$MAILBOX_POKE" ] && ( "$MAILBOX_POKE" "$NAME" ) &
exec tmux attach -t "$NAME"
fi
# ---- new session -----------------------------------------------------------
# Unlock the login keychain so credential helpers (git, gh, etc.) work in
# this SSH/tmux session. Quietly skipped if not on macOS.
if [ -f "$HOME/Library/Keychains/login.keychain-db" ] && command -v security >/dev/null 2>&1; then
security unlock-keychain "$HOME/Library/Keychains/login.keychain-db" || true
fi
# Build the Copilot CLI command.
#
# Session-ID strategy: we want one Copilot session per agent name, with
# no interactive picker. Copilot CLI accepts --session-id=<uuid> which
# means "resume by ID, or set the UUID for a new session". So we pick a
# UUID up front and Copilot does the right thing.
#
# UUID resolution, in priority order:
# 1. Scan ~/.copilot/session-state/*/workspace.yaml for an existing
# session whose `cwd:` equals $DIR (the wrapper's canonical
# agent-<NAME> workspace). Prefer sessions with summary_count > 0
# (i.e., real work has happened) over empty placeholder sessions;
# within each tier, pick the one with the most recent created_at.
#
# We key on cwd, NOT on the `name:` field: Copilot CLI's
# auto-summarizer rewrites `name:` in place whenever
# `user_named:` is false (e.g., after a `/clear`), so a session
# that originally was named "kilo" can end up named
# "Review Chat Transcript" — which would slip past a name match
# and leave us starting a brand-new empty session instead of
# resuming the real one.
#
# Within each tier (real and empty), latest `updated_at` wins
# — i.e., the session most recently active. `updated_at` is
# bumped by real activity inside Copilot CLI (user messages,
# tool calls, summarizer runs), not by `tmux attach` (which
# only reconnects the terminal without restarting the running
# copilot process). So `updated_at` reliably identifies the
# session the user was last working in, which is what `ca`
# should resume — including across `/new` (the new session
# promotes itself by being the one with fresh activity).
#
# We previously ranked real-tier sessions by `summary_count`
# (most-work-wins) and then by `created_at`. Both conflicted
# with real usage: most-work-wins lost to a fresh `/new`, and
# created_at lost to an older user-named session that was
# still being actively used after a `/new` experiment.
# `updated_at` handles both cases correctly.
#
# 2. Otherwise, fall back to a deterministic UUIDv5 derived from the
# name, so the first wrapper launch creates a stable UUID and
# subsequent launches resume it.
#
# `--name=<NAME>` is included so the session shows up under that name in
# the picker / `--resume=<NAME>` paths too (cosmetic, not required).
UUID=""
UUID_UPDATED=""
UUID_TIER="" # "real" (summary_count>0) wins over "empty" (summary_count==0)
SESSION_STATE_DIR="$HOME/.copilot/session-state"
if [ -d "$SESSION_STATE_DIR" ]; then
for ws in "$SESSION_STATE_DIR"/*/workspace.yaml; do
[ -r "$ws" ] || continue
this_cwd="$(awk -F': ' '/^cwd: /{sub(/[[:space:]]+$/, "", $2); print $2; exit}' "$ws")"
[ "$this_cwd" = "$DIR" ] || continue
this_id="$(awk -F': ' '/^id: /{sub(/[[:space:]]+$/, "", $2); print $2; exit}' "$ws")"
this_updated="$(awk -F': ' '/^updated_at: /{sub(/[[:space:]]+$/, "", $2); print $2; exit}' "$ws")"
this_count="$(awk -F': ' '/^summary_count: /{sub(/[[:space:]]+$/, "", $2); print $2; exit}' "$ws")"
if [ -z "$this_count" ] || [ "$this_count" = "0" ]; then
this_tier="empty"
else
this_tier="real"
fi
# Selection rules:
# 1. First match always wins (UUID empty).
# 2. real tier promotes over empty tier unconditionally.
# 3. Within each tier: latest updated_at wins (last-active signal).
should_update=false
if [ -z "$UUID" ]; then
should_update=true
elif [ "$UUID_TIER" = "empty" ] && [ "$this_tier" = "real" ]; then
should_update=true
elif [ "$UUID_TIER" = "real" ] && [ "$this_tier" = "empty" ]; then
should_update=false
elif [ "$this_updated" \> "$UUID_UPDATED" ]; then
should_update=true
fi
if [ "$should_update" = "true" ]; then
UUID="$this_id"
UUID_UPDATED="$this_updated"
UUID_TIER="$this_tier"
fi
done
fi
if [ -z "$UUID" ]; then
UUID="$(python3 -c "import uuid,sys; print(uuid.uuid5(uuid.NAMESPACE_URL, 'remote-agent-stack/'+sys.argv[1]))" "$NAME")"
fi
CMD="$COPILOT_BIN --session-id='$UUID' --name='$NAME' --remote"
if [ "${ALLOW_ALL:-false}" = "true" ]; then
CMD="$CMD --allow-all"
fi
[ -n "$MAILBOX_POKE" ] && ( "$MAILBOX_POKE" "$NAME" --wait ) &
exec tmux new-session -s "$NAME" -c "$DIR" \; send-keys "$CMD" Enter