refactor(skills): rename multi-agent-* + agent-sessions-monitor + delegate-job to tmux-agent-orchestrate-*

Renamed 6 skills directories to tmux-agent-orchestrate-* prefix:
- multi-agent-create → tmux-agent-orchestrate-create
- multi-agent-resume → tmux-agent-orchestrate-resume
- multi-agent-delete → tmux-agent-orchestrate-delete
- multi-agent-status → tmux-agent-orchestrate-status
- agent-sessions-monitor → tmux-agent-orchestrate-monitor
- delegate-job → tmux-agent-orchestrate-delegate-job

Updated:
- skills/lib.sh internal paths (delegate_submit_job etc.)
- skills/tmux-agent-orchestrate-status/scripts/status.sh (monitor path)
- skills/tmux-agent-orchestrate-monitor/scripts/reconcile.sh
- .gitignore (HTML ignore patterns)
- 6 SKILL.md frontmatter (name, related_skills, prereq_skills) and body
- All script headers and Korean comments

Notes:
- tmux session naming convention unchanged (<slug>-creator-<agent>) — workspace identifier based, kept for backward compatibility
- Existing 2 sessions in -L multi-agent-canary untouched
- YAML delegate_job_id / agent-session (tmux:canary-...) preserved for log history compatibility

Verified on isolated server -L agy-rename-test (kill-server after).
This commit is contained in:
2026-06-19 23:27:27 +00:00
parent 4fa276f3c5
commit e9fc763d31
25 changed files with 146 additions and 146 deletions
@@ -0,0 +1,131 @@
---
name: tmux-agent-orchestrate-resume
description: "Resume an existing agent (claude, antigravity/agy) conversation by UUID into a tmux session. Reads ~/PuKi/lab/agent_sessions/agent-sessions.yaml for the saved session/conversation id, spawns (or reuses) a tmux session of the matching name, and runs `claude -r <id>` or `agy --conversation <id>` inside. Use when you want to reattach to a previous session's context, or revive a session whose tmux died but the agent's conversation is still on disk."
version: 1.0.0
author: godopu
license: MIT
platforms: [linux, macos]
environments: [terminal, tmux]
metadata:
hermes:
tags: [agent, tmux, claude, antigravity, agy, multi-agent, context, resume, session-id]
related_skills: [tmux-agent-orchestrate-create, tmux-agent-orchestrate-delete, tmux-agent-orchestrate-monitor, claude-code]
prereq_skills: [tmux-agent-orchestrate-create]
---
# Multi-Agent Resume — Reattach to a Saved Conversation
> **Companion skills**: `tmux-agent-orchestrate-create` (start a fresh agent), `tmux-agent-orchestrate-delete` (terminate), `tmux-agent-orchestrate-monitor` (live status).
> **Tmux Isolation**: `TMUX_SERVER_NAME` env var를 create에서 설정한 경우, 동일 서버에서 동작합니다. 자세한 격리 패턴은 [tmux-agent-orchestrate-create/SKILL.md](file:///home/godopu16/PuKi/laa/canary_projects/advanced_multi_agent/skills/tmux-agent-orchestrate-create/SKILL.md) 참조.
> **Single source of truth**: `~/PuKi/lab/agent_sessions/agent-sessions.yaml`.
## What this skill does
**Container + data reconstruction**: spawn a tmux session (the container), then run the agent inside with a specific session id (the data) so the previous conversation's context is restored.
Three cases this skill handles:
1. **tmux is dead, conversation lives**`agent-sessions.yaml` has the UUID. The JSONL/db is on disk. Re-spawn the tmux session + run `claude -r <id>` / `agy --conversation <id>`.
2. **tmux is alive but empty** — You started a session with `tmux-agent-orchestrate-create` but haven't sent a message yet (so no session id was assigned). The user can either send their first message (and the id is auto-assigned), or you can read the *workspace's* most recent conversation from `~/.gemini/antigravity-cli/cache/last_conversations.json` for agy, or the latest `*.jsonl` in `~/.claude/projects/<workspace-key>/` for claude.
3. **tmux is alive AND the agent inside is already running** — Just attach. No re-spawn needed.
## UUID resolution order
`agent-sessions.yaml` is the *primary* source. The skill reads in this order:
1. **`agent-sessions.yaml``agent_identities.<agent>.session_id` (claude) / `conversation_id` (agy)** — explicit saved value
2. **`agent-sessions.yaml``agent_identities.<agent>.session_jsonl` (claude) / `conversation_db` (agy)** — the on-disk artifact
3. **Fallback: scan disk for the workspace's most recent conversation**
- claude: `ls -t ~/.claude/projects/<workspace-key>/*.jsonl | head -1` and parse the `sessionId` from the first line
- agy: `jq -r '."<workspace>"' ~/.gemini/antigravity-cli/cache/last_conversations.json`
If all three are empty → the workspace has no conversation yet. Fall back to `tmux-agent-orchestrate-create`.
## Workflow
```bash
WORKSPACE=/path/to/project
AGENT=claude # or agy
SESSION_NAME=<workspace>-creator-<agent> # same convention as tmux-agent-orchestrate-create
# 1. Resolve the session id
UUID=$(bash ~/PuKi/lab/agent_sessions/skills/tmux-agent-orchestrate-resume/scripts/resolve_session_id.sh \
--workspace "$WORKSPACE" --agent "$AGENT")
if [ -z "$UUID" ]; then
echo "No saved session for $WORKSPACE ($AGENT). Use tmux-agent-orchestrate-create first."
exit 1
fi
# Resolve the isolated tmux server name
source ~/PuKi/lab/agent_sessions/skills/lib.sh
export TMUX_SERVER_NAME="$(resolve_tmux_server "$SESSION_NAME")"
# 2. If tmux is alive, attach. Done.
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
echo "tmux '$SESSION_NAME' already running. Attaching..."
exec tmux attach -t "$SESSION_NAME"
fi
# 3. Spawn new tmux session + run agent with the saved id
case "$AGENT" in
claude)
tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" \
"claude --dangerously-skip-permissions -r $UUID"
# auto-handle trust / bypass dialogs
sleep 5
tmux send-keys -t "$SESSION_NAME" Enter 2>/dev/null || true
sleep 3
tmux send-keys -t "$SESSION_NAME" Down 2>/dev/null || true
sleep 0.3
tmux send-keys -t "$SESSION_NAME" Enter 2>/dev/null || true
;;
agy)
tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" \
"agy --dangerously-skip-permissions --conversation $UUID"
;;
esac
# 4. Update agent-sessions.yaml: status running, last_visible_status
# (Also automatically publishes a `progress --detail "resumed"` event to the tmux-agent-orchestrate-delegate-job registry if a delegate_job_id exists)
bash ~/PuKi/lab/agent_sessions/skills/tmux-agent-orchestrate-resume/scripts/update_yaml_resumed.sh \
--session "$SESSION_NAME" --uuid "$UUID"
# 5. Attach
tmux attach -t "$SESSION_NAME"
```
## Pitfalls
- **`claude -r` requires the SAME project directory** — if the workspace path differs from when the session was created, claude will create a new project dir key (`-home-...-different-name`) and put the resume in a different location. Always `-c` (cd to workspace) before running.
- **agy's `--conversation` flag name varies by version** — older versions used `--resume` or `-r`. Check `agy --help | grep -E "conversation|resume"` and use the right flag. v1.0.x: `--conversation`.
- **The first message after resume might re-trigger TUI dialogs** — if the original session was created with `--dangerously-skip-permissions`, those flags are NOT persisted; you must re-apply them on resume. The script above re-passes them.
- **Don't resume if the session is brand new and empty** — `tmux-agent-orchestrate-create` already set up an empty container; sending a probe message ("init") is the right way to materialize a session id, NOT `claude -r` with a placeholder.
- **`agy --conversation <id>` will fail if the conversation was deleted from disk** — check `~/.gemini/antigravity-cli/conversations/<uuid>.db` exists before attempting resume. If missing, the conversation is gone; you need a fresh session via `tmux-agent-orchestrate-create`.
## Verification
```bash
# 1. tmux alive with the right cmd
tmux list-panes -t "$SESSION_NAME" -F 'cmd=#{pane_current_command} cwd=#{pane_current_path}'
# 2. agent-sessions.yaml updated
python3 -c "
import yaml
d = yaml.safe_load(open('$HOME/PuKi/lab/agent_sessions/agent-sessions.yaml'))
s = [s for s in d['tmux_sessions'] if s['name'] == '$SESSION_NAME'][0]
print(f' status: {s[\"status\"]}')
print(f' pane.cmd_full: {s[\"pane\"][\"cmd_full\"]}')
"
# 3. TUI shows resumed conversation (capture-pane to verify)
sleep 5
tmux capture-pane -t "$SESSION_NAME" -p -S -30
# look for the previous message at top of the buffer (claude) or last_visible_status set (agy)
```
## When NOT to use this skill
- **No saved session yet** → `tmux-agent-orchestrate-create`
- **Killing an existing session** → `tmux-agent-orchestrate-delete`
- **Just attaching** → `tmux attach -t <name>` (no skill needed)
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# resolve_session_id.sh — tmux-agent-orchestrate-resume 의 부속 스크립트
# Usage:
# bash resolve_session_id.sh --workspace <path> --agent <claude|agy>
# 출력: stdout 으로 UUID 한 줄 (없으면 빈 줄 + exit 0)
#
# P0-C: 전역 agent_identities 를 즉시 반환하지 않는다. lib.sh::find_workspace_uuid
# 가 워크스페이스 격리된 해결 경로(per-row own id -> 디스크 스캔 -> cwd 일치하는
# cache)만 사용. 다른 워크스페이스의 UUID 를 절대 반환하지 않음.
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh"
usage() {
cat <<EOF
Usage: $0 --workspace <path> --agent <claude|agy>
Outputs the resolved UUID on stdout (empty if not found).
EOF
}
WORKSPACE=""
AGENT=""
while [ $# -gt 0 ]; do
case "$1" in
--workspace) WORKSPACE="$2"; shift 2 ;;
--agent) AGENT="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "ERROR: unknown arg: $1" >&2; exit 2 ;;
esac
done
[ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; exit 2; }
[ -n "$AGENT" ] || { echo "ERROR: --agent required" >&2; exit 2; }
case "$AGENT" in
claude|agy) ;;
*) echo "ERROR: --agent must be claude or agy" >&2; exit 2 ;;
esac
find_workspace_uuid "$WORKSPACE" "$AGENT"
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# update_yaml_resumed.sh — tmux-agent-orchestrate-resume 의 부속 스크립트
# Resume 한 세션의 agent-sessions.yaml 엔트리를 status=running + resume 메타로 갱신.
# resume UUID 를 per-row own id (claude_session_id_own / agy_conversation_id_own)
# 에 박는다 — agent_identities 전역은 더 이상 primary 아님 (cache 로 강등, P0-C/단계 e).
#
# Usage: bash update_yaml_resumed.sh --session <name> --uuid <id> [--agent claude|agy]
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh"
usage() {
cat <<EOF
Usage: $0 --session <name> --uuid <id> [--agent claude|agy]
EOF
}
SESSION_NAME=""
UUID=""
AGENT=""
while [ $# -gt 0 ]; do
case "$1" in
--session) SESSION_NAME="$2"; shift 2 ;;
--uuid) UUID="$2"; shift 2 ;;
--agent) AGENT="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "ERROR: unknown arg: $1" >&2; exit 2 ;;
esac
done
[ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; exit 2; }
[ -n "$UUID" ] || { echo "ERROR: --uuid required" >&2; exit 2; }
[ -f "$AGENT_SESSIONS_YAML" ] || { echo "ERROR: $AGENT_SESSIONS_YAML not found" >&2; exit 1; }
export TMUX_SERVER_NAME="$(resolve_tmux_server "$SESSION_NAME")"
# --agent 미지정 시 이름 suffix 로 fallback (P1-F: 가능하면 --agent 명시)
if [ -z "$AGENT" ]; then
case "$SESSION_NAME" in
*-creator-claude) AGENT=claude ;;
*-creator-agy) AGENT=agy ;;
*) echo "ERROR: cannot infer agent from '$SESSION_NAME'; pass --agent" >&2; exit 2 ;;
esac
fi
NOW_ISO=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
# 새 tmux pane pid / 자식 pid 를 bash 에서 캡처 (env 로 전달, P1-B)
PANE_PID=$(tmux list-panes -t "$SESSION_NAME" -F '#{pane_pid}' 2>/dev/null | head -1 || true)
PANE_PID="${PANE_PID:-}"
CHILD_PID=0
if [ "$AGENT" = "agy" ] && [ -n "$PANE_PID" ]; then
CHILD_PID=$(pgrep -P "$PANE_PID" -x agy 2>/dev/null | head -1 || true)
CHILD_PID="${CHILD_PID:-0}"
fi
DELEGATE_JOB_ID=$(env_python "$AGENT_SESSIONS_YAML" SESSION_NAME="$SESSION_NAME" <<'PYEOF'
import os, yaml
name = os.environ['SESSION_NAME']
with open(os.environ['YAML_PATH']) as f:
d = yaml.safe_load(f) or {}
for s in d.get('tmux_sessions', []):
if s.get('name') == name:
print(s.get('delegate_job_id', '') or '')
raise SystemExit(0)
raise SystemExit(0)
PYEOF
)
atomic_dump_yaml "$AGENT_SESSIONS_YAML" \
SESSION_NAME="$SESSION_NAME" UUID="$UUID" AGENT="$AGENT" NOW_ISO="$NOW_ISO" \
PANE_PID="$PANE_PID" CHILD_PID="$CHILD_PID" <<'PYEOF'
name = os.environ['SESSION_NAME']
uuid = os.environ['UUID']
agent = os.environ['AGENT']
now = os.environ['NOW_ISO']
pane_pid = os.environ.get('PANE_PID', '')
target = None
for s in d.get('tmux_sessions', []):
if s.get('name') == name:
target = s
break
if target is None:
print(f"ERROR: session not in YAML: {name}", flush=True)
raise SystemExit(1)
target['status'] = 'running'
target.pop('terminated_at', None)
target.pop('terminated_at_epoch', None)
target.pop('termination_mode', None)
target.pop('archived_at', None)
target['last_visible_status'] = f'resumed conversation {uuid} at {now}'
target.setdefault('pane', {})
if pane_pid.isdigit():
target['pane']['pid'] = int(pane_pid)
if agent == 'claude':
target['pane']['cmd'] = 'claude'
target['pane']['cmd_full'] = f'claude --dangerously-skip-permissions -r {uuid}'
target['claude_session_id_own'] = uuid
elif agent == 'agy':
target['pane']['cmd'] = 'agy'
target['pane']['cmd_full'] = f'agy --dangerously-skip-permissions --conversation {uuid}'
target['agy_conversation_id_own'] = uuid
cp = os.environ.get('CHILD_PID', '0')
if cp.isdigit() and int(cp) > 0:
target['child_pid'] = int(cp)
snap = d.setdefault('snapshot', {})
snap['taken_at'] = now
snap.pop('terminated_at', None)
snap.pop('terminated_at_epoch', None)
print(f"updated: {name} status=running (resume id -> per-row own id)", flush=True)
PYEOF
delegate_publish_event "$DELEGATE_JOB_ID" progress "resumed"