initial: canary multi-agent skills with tmux isolation support

- lib.sh: TMUX_SERVER_NAME env var, _tmux helper, shim externalized
  to TMPDIR with recursive guard, resolve_tmux_server helper for
  YAML-driven server routing
- multi-agent-create: --tmux-server opt-in flag, YAML tmux_server
  field for orphan prevention
- multi-agent-delete/resume/status/agent-sessions-monitor: use
  resolve_tmux_server to auto-route to correct isolated server
- SKILL.md × 4: documented isolation server workflow
- Verified by claude review (R1+re-run) + agy R2 patches
  (orphan prevention + shim location fix)
This commit is contained in:
2026-06-19 13:32:36 +00:00
commit 8a3abff2d6
13 changed files with 2184 additions and 0 deletions
+213
View File
@@ -0,0 +1,213 @@
---
name: multi-agent-create
description: "Create a new agent session (claude, antigravity/agy) in a dedicated tmux session for context-preserving long-running work. Always creates a tmux session — never backgrounds with nohup/disown. Writes the new session to ~/PuKi/lab/agent_sessions/agent-sessions.yaml. Use when you want to start a fresh agent (no prior UUID) for a new project workspace."
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, session]
related_skills: [multi-agent-resume, multi-agent-delete, agent-sessions-monitor, claude-code]
prereq_skills: [claude-code]
---
# Multi-Agent Create — Start a Fresh Agent in a tmux Session
> **Companion skills**: `multi-agent-resume` (resume an existing UUID), `multi-agent-delete` (terminate), `agent-sessions-monitor` (live status).
> **Single source of truth**: `~/PuKi/lab/agent_sessions/agent-sessions.yaml` (this skill writes to it; never read it ad-hoc — go through this skill).
## What this skill does
Spawn a new agent (`claude` or `agy`/antigravity-cli) in a **dedicated tmux session** for context-preserving long-running work. The tmux session is the *container*; the agent's session ID is *data* inside the container. **This skill creates the container + starts the agent — but does not resume an old conversation** (use `multi-agent-resume` for that).
For all agents: the tmux session name is produced by **`lib.sh::derive_session_name`** — the single source of truth shared by create/resume/delete/status/monitor (P0-A). The rule (verbatim from the function):
> slug = the **two trailing path components** of the absolute workspace, `_`→`-`, lowercased, joined with `-`; name = `<slug>-creator-<agent>`.
So `/home/godopu16/PuKi/lab/landing_page/refer_landing_page` + `claude``landing-page-refer-landing-page-creator-claude`. The workspace basename (`refer_landing_page`) **is** included; the hand-written historical entry that dropped it (`lab-landing-page-creator-claude`) was the bug, not the convention.
## Pre-flight checks
Before doing anything, verify the environment:
```bash
# 1) tmux available and isolated server status
command -v tmux || { echo "ERROR: tmux not installed"; exit 1; }
echo "Tmux server name: ${TMUX_SERVER_NAME:-default}"
# 2) claude / agy available
command -v claude # required for --agent claude
command -v agy # required for --agent agy
# 3) claude auth (if --agent claude)
claude auth status 2>&1 | python3 -c "import json,sys; d=json.load(sys.stdin); assert d.get('loggedIn'), 'claude not logged in'"
# 4) target workspace exists
test -d "$WORKSPACE" || { echo "ERROR: workspace $WORKSPACE not a directory"; exit 1; }
```
If any check fails → `kanban_block(reason="...")` (worker path) or report to user (interactive path). Do not proceed with a half-broken setup.
## Standard names
- **tmux session name**: `derive_session_name <workspace> <agent>` (lib.sh)
- `<workspace-slug>` = `basename $(dirname $WORKSPACE)` `-` `basename $WORKSPACE` (lowercase, `_``-`)
- examples: `landing-page-refer-landing-page-creator-claude`, `paper-pdf2md-creator-agy`
- never re-derive this by hand — source lib.sh and call the function
- **wrapper script** (claude only): `~/.local/bin/<workspace-slug>-creator-claude`
- contents: tmux new-session with `claude` inside, auto-handles trust/bypass dialogs
- see `~/PuKi/lab/landing_page/refer_landing_page/agent_sessions.md` for the canonical wrapper template
## Tmux Server Isolation (격리 서버)
When running multiple agent sessions alongside other workflows (e.g., cmux, Kanban workers, manual tmux sessions), sharing the default tmux server can lead to session name conflicts, monitoring clutter, and accidental destruction of user sessions via global commands.
To prevent this, you can run this skill inside an **isolated tmux server** using the `TMUX_SERVER_NAME` environment variable or the `--tmux-server <name>` flag (opt-in).
### How to use
1. **Via Environment Variable**:
```bash
export TMUX_SERVER_NAME=multi-agent-canary
# All subsequent commands (create, status, delete, etc.) will run in the isolated 'multi-agent-canary' tmux server.
```
2. **Via Option Flag**:
```bash
bash scripts/create_session.sh --workspace /path/to/project --agent claude --tmux-server multi-agent-canary
```
### Recommended Alias
You can set an alias in your shell to easily query sessions on the isolated server:
```bash
alias tmc='tmux -L multi-agent-canary'
tmc ls # Lists only your multi-agent sessions
```
### Safety Rules (Pitfall 29 Summary)
- Never use global server termination commands like `tmux kill-server` or `tmux kill-session -a` as they will destroy all sessions on that server (including your own workspace sessions if they share the server).
- By using an isolated server via `TMUX_SERVER_NAME`, your agent sessions are completely separated from your default user workspace, ensuring 0% interference.
## Workflow
```bash
WORKSPACE=/path/to/project
AGENT=claude # or agy
source ~/PuKi/lab/agent_sessions/skills/lib.sh
SESSION_NAME="$(derive_session_name "$WORKSPACE" "$AGENT")"
# 1. If session already alive, fail fast
tmux has-session -t "$SESSION_NAME" 2>/dev/null && {
echo "ERROR: tmux session '$SESSION_NAME' already exists. Use multi-agent-resume to attach or multi-agent-delete first."
exit 1
}
# 2. Spawn the tmux session with the agent inside
case "$AGENT" in
claude)
# Use the wrapper if it exists, else inline tmux new-session
if [ -x "$HOME/.local/bin/$SESSION_NAME" ]; then
nohup "$HOME/.local/bin/$SESSION_NAME" >/dev/null 2>&1 &
else
tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "claude"
fi
;;
agy)
tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "agy --dangerously-skip-permissions"
;;
*) echo "ERROR: --agent must be claude or agy, got: $AGENT"; exit 2 ;;
esac
# 3. Wait for agent TUI to be ready (varies: claude ~5s, agy ~3s)
sleep 6
# 4. Capture pane metadata
PANE_PID=$(tmux list-panes -t "$SESSION_NAME" -F '#{pane_pid}')
PANE_CWD=$(tmux list-panes -t "$SESSION_NAME" -F '#{pane_current_path}')
PANE_CMD=$(tmux list-panes -t "$SESSION_NAME" -F '#{pane_current_command}')
TMUX_EPOCH=$(tmux list-sessions -F '#{session_created}' -t "$SESSION_NAME" 2>/dev/null | head -1)
```
## Registering the session in agent-sessions.yaml
After spawn, append a new `tmux_sessions[]` entry to `~/PuKi/lab/agent_sessions/agent-sessions.yaml`:
```yaml
- name: <SESSION_NAME>
status: running
tmux_session_created_at: 2026-06-17T...Z # ISO 8601 UTC
tmux_session_epoch: <TMUX_EPOCH>
tmux_server: <TMUX_SERVER_NAME> # Isolated server name (default: 'default')
pane:
index: 0
pid: <PANE_PID>
cmd: <AGENT> # 'claude' or 'agy'
cmd_full: <full command line, see table below>
cwd: <PANE_CWD>
tui: # only for claude
model: <from TUI status>
provider: <from TUI status>
plan: <from TUI status>
account: <from TUI status>
version: <from TUI status>
start_command: <the exact tmux new-session command used>
attach_command: "tmux attach -t <SESSION_NAME>"
kill_command: "tmux kill-session -t <SESSION_NAME>"
```
`cmd_full` per agent (this is the actual command line in the pane, not the resume command):
| agent | cmd_full |
|---|---|
| claude (interactive) | `claude` |
| agy (interactive) | `agy --dangerously-skip-permissions` |
Use the `agent-sessions-yaml-edit` script in `scripts/` to safely append (preserves comments + format):
```bash
bash ~/PuKi/lab/agent_sessions/skills/multi-agent-create/scripts/create_session.sh \
--workspace "$WORKSPACE" --agent "$AGENT" --session "$SESSION_NAME"
```
The script handles the YAML append, pane capture, and the `last_visible_status` placeholder.
## Pitfalls
- **Don't use `nohup`/`disown`/`setsid` for the agent itself** — those background the agent outside tmux. The whole point of this skill is *the tmux session is the supervisor*. `nohup` is OK only for *launching the wrapper* (which itself creates the tmux session via `tmux new-session -d`).
- **Don't trust `--session-id <uuid>` flags blindly** — claude/agy may not accept a fixed session id on first spawn. The session id is *assigned* on first user message; you can read it back from `~/.claude/projects/.../session.jsonl` headers or `~/.gemini/.../cache/last_conversations.json` AFTER the first message.
- **Wrapper script MUST NOT be created via `hermes profile alias`** — that command writes a `hermes -p <profile>` wrapper that destroys the tmux behavior. Create wrappers manually (see `lab-landing-page-creator-claude` template).
- **Always use the workspace-relative path** in tmux `cwd` — relative paths break when tmux respawns in a different shell context.
- **The first `claude` message generates the session id** — `multi-agent-create` only sets up the *container*. If you need a known session id for later resume, send a placeholder message (e.g. "init") and read it back, then call `multi-agent-resume` later.
## Verification
After spawn + YAML append:
```bash
# 1. tmux session is alive
tmux has-session -t "$SESSION_NAME" && echo OK || echo MISSING
# 2. pane has the expected cmd + cwd
tmux list-panes -t "$SESSION_NAME" -F 'cmd=#{pane_current_command} cwd=#{pane_current_path}'
# 3. agent-sessions.yaml has the new entry
python3 -c "
import yaml
d = yaml.safe_load(open('$HOME/PuKi/lab/agent_sessions/agent-sessions.yaml'))
names = [s['name'] for s in d['tmux_sessions']]
assert '$SESSION_NAME' in names, 'session not registered'
print('OK:', names)
"
# 4. Optional: send a probe via tmux send-keys and capture-pane
tmux send-keys -t "$SESSION_NAME" "" Enter
sleep 2
tmux capture-pane -t "$SESSION_NAME" -p -S -20
```
## When NOT to use this skill
- **Resuming an old conversation** → `multi-agent-resume`
- **Killing an existing session** → `multi-agent-delete`
- **Just attaching to an existing session** → `tmux attach -t <name>` (no skill needed)
- **One-shot print mode (claude -p "...")** → no tmux needed; use `claude-code` skill's print mode
+253
View File
@@ -0,0 +1,253 @@
#!/usr/bin/env bash
# create_session.sh — multi-agent-create 의 부속 스크립트
# Usage:
# bash create_session.sh --workspace <path> --agent <claude|agy> [--session <name>] [--wrapper]
#
# 동작:
# 1) preflight: tmux/claude/agy 가용성, workspace 존재
# 2) tmux 세션 이름 결정 (--session 없으면 자동)
# 3) tmux 세션 시작 (claude 는 wrapper 우선, agy 는 인라인)
# 4) pane 메타 캡처 (pid, cmd, cwd)
# 5) agent-sessions.yaml 에 tmux_sessions[] 엔트리 append
# 6) 검증 출력
#
# Exit codes:
# 0 = success
# 1 = preflight failure
# 2 = invalid args
# 3 = tmux session already exists (use multi-agent-resume or delete first)
# 4 = agent-sessions.yaml append failure
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh"
usage() {
cat <<EOF
Usage: $0 --workspace <path> --agent <claude|agy> [options]
Options:
--workspace PATH project directory (required)
--agent AGENT claude | agy (required)
--session NAME tmux session name (default: derived from workspace)
--wrapper force use of ~/.local/bin/<session> wrapper even if not present
--dry-run print commands without executing
--tmux-server NAME specify isolated tmux server name
-h, --help this help
EOF
}
WORKSPACE=""
AGENT=""
SESSION_NAME=""
USE_WRAPPER=0
DRY_RUN=0
TMUX_SERVER_OPT=""
while [ $# -gt 0 ]; do
case "$1" in
--workspace) WORKSPACE="$2"; shift 2 ;;
--agent) AGENT="$2"; shift 2 ;;
--session) SESSION_NAME="$2"; shift 2 ;;
--wrapper) USE_WRAPPER=1; shift ;;
--dry-run) DRY_RUN=1; shift ;;
--tmux-server) TMUX_SERVER_OPT="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "ERROR: unknown arg: $1" >&2; usage; exit 2 ;;
esac
done
if [ -n "$TMUX_SERVER_OPT" ]; then
export TMUX_SERVER_NAME="$TMUX_SERVER_OPT"
fi
# Preflight
[ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; usage; exit 2; }
[ -n "$AGENT" ] || { echo "ERROR: --agent required" >&2; usage; exit 2; }
[ -d "$WORKSPACE" ] || { echo "ERROR: workspace $WORKSPACE not a directory" >&2; exit 1; }
command -v tmux >/dev/null || { echo "ERROR: tmux not installed" >&2; exit 1; }
command -v "$AGENT" >/dev/null || { echo "ERROR: $AGENT CLI not in PATH" >&2; exit 1; }
# Auth Check (OAuth check for agy, loggedIn check for claude)
if [ "$AGENT" = "claude" ]; then
if ! claude auth status 2>/dev/null | grep -q '"loggedIn":\s*true'; then
echo "ERROR: claude not logged in. Run 'claude auth login' first." >&2
exit 1
fi
elif [ "$AGENT" = "agy" ]; then
if ! agy models >/dev/null 2>&1; then
echo "ERROR: agy is not authenticated. Please log in first." >&2
exit 1
fi
fi
# 세션 이름 — lib.sh::derive_session_name 이 단일 소스 (P0-A)
if [ -z "$SESSION_NAME" ]; then
SESSION_NAME="$(derive_session_name "$WORKSPACE" "$AGENT")"
fi
# 이미 살아있으면 실패
if _tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
echo "ERROR: tmux session '$SESSION_NAME' already exists. Use multi-agent-resume to attach, or multi-agent-delete first." >&2
exit 3
fi
# tmux 세션 띄우기
WRAPPER="$HOME/.local/bin/$SESSION_NAME"
spawn() {
case "$AGENT" in
claude)
if [ -x "$WRAPPER" ] || [ "$USE_WRAPPER" = "1" ]; then
nohup "$WRAPPER" >/dev/null 2>&1 &
disown
else
_tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "claude"
fi
;;
agy)
_tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "agy --dangerously-skip-permissions"
;;
*) echo "ERROR: --agent must be claude or agy, got: $AGENT" >&2; exit 2 ;;
esac
}
if [ "$DRY_RUN" = "1" ]; then
echo "[dry-run] would spawn: tmux session '$SESSION_NAME' in $WORKSPACE (agent=$AGENT)"
exit 0
fi
spawn
# TUI 준비 대기
sleep 6
# pane 메타 캡처
PANE_PID=$(_tmux list-panes -t "$SESSION_NAME" -F '#{pane_pid}' 2>/dev/null || echo "")
PANE_CWD=$(_tmux list-panes -t "$SESSION_NAME" -F '#{pane_current_path}' 2>/dev/null || echo "$WORKSPACE")
PANE_CMD=$(_tmux list-panes -t "$SESSION_NAME" -F '#{pane_current_command}' 2>/dev/null || echo "$AGENT")
TMUX_EPOCH=$(date +%s)
NOW_ISO=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
# cmd_full 결정
case "$AGENT" in
claude) CMD_FULL='claude' ;;
agy) CMD_FULL='agy --dangerously-skip-permissions' ;;
esac
# 시작 명령
local_tmux="tmux"
if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then
local_tmux="tmux -L $TMUX_SERVER_NAME"
fi
case "$AGENT" in
claude)
if [ -x "$WRAPPER" ]; then
START_CMD="$WRAPPER # ~/.local/bin 의 래퍼"
else
START_CMD="$local_tmux new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"claude\""
fi
;;
agy)
START_CMD="$local_tmux new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\""
;;
esac
# agent-sessions.yaml 에 append
if [ ! -f "$AGENT_SESSIONS_YAML" ]; then
echo "ERROR: $AGENT_SESSIONS_YAML not found. Run init first." >&2
exit 4
fi
# atomic_dump_yaml: flock + temp+rename + .bak + schema validate (P0-B).
# 모든 값은 환경변수로 전달 — heredoc interpolation 없음 (P1-B).
# 자식 pid 는 bash 에서 pgrep 으로 미리 구함 (P2: 도구명 필터).
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
atomic_dump_yaml "$AGENT_SESSIONS_YAML" \
SESSION_NAME="$SESSION_NAME" AGENT="$AGENT" NOW_ISO="$NOW_ISO" \
TMUX_EPOCH="$TMUX_EPOCH" PANE_PID="$PANE_PID" PANE_CWD="$PANE_CWD" \
CMD_FULL="$CMD_FULL" START_CMD="$START_CMD" CHILD_PID="$CHILD_PID" \
TMUX_SERVER_NAME="${TMUX_SERVER_NAME:-default}" <<'PYEOF'
name = os.environ['SESSION_NAME']
agent = os.environ['AGENT']
pid = os.environ.get('PANE_PID', '')
epoch = os.environ.get('TMUX_EPOCH', '')
server_name = os.environ.get('TMUX_SERVER_NAME', 'default')
server_opt = f"-L {server_name} " if server_name and server_name != 'default' else ""
sessions = d.setdefault('tmux_sessions', [])
# P0-D: 같은 이름 엔트리가 status=running 이면만 거부. terminated/archived 는
# 재사용 가능 — 낡은 엔트리를 제거하고 새로 append (create -> delete -> create).
running_same = [s for s in sessions if s.get('name') == name and s.get('status') == 'running']
if running_same:
print(f"ERROR: {name} already running in agent-sessions.yaml", flush=True)
raise SystemExit(4)
sessions[:] = [s for s in sessions if s.get('name') != name]
entry = {
'name': name,
'status': 'running',
'tmux_session_created_at': os.environ['NOW_ISO'],
'tmux_session_epoch': int(epoch) if epoch.isdigit() else 0,
'tmux_server': server_name,
'pane': {
'index': 0,
'pid': int(pid) if pid.isdigit() else 0,
'cmd': agent,
'cmd_full': os.environ['CMD_FULL'],
'cwd': os.environ['PANE_CWD'],
},
'start_command': os.environ['START_CMD'],
'attach_command': f'tmux {server_opt}attach -t {name}',
'kill_command': f'tmux {server_opt}kill-session -t {name}',
}
if agent == 'claude':
entry['tui'] = {
'model': '(unknown — capture after first message)',
'provider': 'anthropic',
'plan': '(unknown)',
'account': '(unknown — read from claude auth status)',
'version': '(unknown — read from TUI)',
}
entry['claude_session_id_own'] = None
entry['last_visible_status'] = "TUI started; awaiting first user message"
elif agent == 'agy':
cp = os.environ.get('CHILD_PID', '0')
entry['child_pid'] = int(cp) if cp.isdigit() else 0
entry['agy_conversation_id_own'] = None
entry['mcp_attachments'] = [
{
'name': 'stitch',
'transport': 'mcp-remote',
'endpoint': 'https://stitch.googleapis.com/mcp'
}
]
entry['last_visible_status'] = "TUI started; awaiting first user message"
sessions.append(entry)
snap = d.setdefault('snapshot', {})
snap['taken_at'] = os.environ['NOW_ISO']
snap['cwd'] = os.environ['PANE_CWD']
print(f"appended: {name}", flush=True)
PYEOF
echo
echo "=== created ==="
echo "tmux session: $SESSION_NAME (pane pid $PANE_PID, cmd $PANE_CMD, cwd $PANE_CWD)"
echo "agent-sessions.yaml updated"
echo
if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then
echo "Attach: tmux -L $TMUX_SERVER_NAME attach -t $SESSION_NAME"
else
echo "Attach: tmux attach -t $SESSION_NAME"
fi
echo "Delete: use multi-agent-delete skill"
echo "Resume: use multi-agent-resume skill (after first message creates a session id)"