diff --git a/skills/lib.sh b/skills/lib.sh index 9e13808..bf1d011 100644 --- a/skills/lib.sh +++ b/skills/lib.sh @@ -18,6 +18,11 @@ SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" WORKSPACE_ROOT="$(cd "$SKILL_DIR/.." && pwd)" AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$WORKSPACE_ROOT/.hermes/agent-sessions.yaml}" +# Workspace-relative defaults with environment overrides (Phase Z) +HOME_DIR="${HOME_DIR:-$WORKSPACE_ROOT}" +CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$HOME/.claude/projects}" +LOCAL_BIN="${LOCAL_BIN:-$HOME/.local/bin}" + # --------------------------------------------------------------------------- # Tmux Server Isolation support # --------------------------------------------------------------------------- @@ -159,7 +164,7 @@ derive_session_name() { # --------------------------------------------------------------------------- env_python() { local yaml_path="$1"; shift - local -a envs=("YAML_PATH=$yaml_path" "HOME_DIR=$HOME") + local -a envs=("YAML_PATH=$yaml_path" "HOME_DIR=$HOME_DIR" "CLAUDE_PROJECT_DIR=$CLAUDE_PROJECT_DIR" "LOCAL_BIN=$LOCAL_BIN") while [ $# -gt 0 ]; do case "$1" in *=*) envs+=("$1"); shift ;; @@ -186,7 +191,7 @@ env_python() { # --------------------------------------------------------------------------- atomic_dump_yaml() { local yaml_path="$1"; shift - local -a envs=("YAML_PATH=$yaml_path" "HOME_DIR=$HOME") + local -a envs=("YAML_PATH=$yaml_path" "HOME_DIR=$HOME_DIR" "CLAUDE_PROJECT_DIR=$CLAUDE_PROJECT_DIR" "LOCAL_BIN=$LOCAL_BIN") while [ $# -gt 0 ]; do case "$1" in *=*) envs+=("$1"); shift ;; @@ -283,6 +288,7 @@ ws = os.environ['WS_ABS'] agent = os.environ['AGENT'] home = os.environ['HOME_DIR'] yaml_path = os.environ['YAML_PATH'] +claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects") d = {} if os.path.exists(yaml_path): @@ -292,7 +298,7 @@ if os.path.exists(yaml_path): def jsonl_exists(uuid): key = ws.replace('/', '-').replace('_', '-') - return os.path.exists(f"{home}/.claude/projects/{key}/{uuid}.jsonl") + return os.path.exists(f"{claude_project_dir}/{key}/{uuid}.jsonl") def db_exists(uuid): @@ -323,7 +329,7 @@ for s in d.get('tmux_sessions', []): # 2) disk scan scoped to THIS workspace if agent == 'claude': key = ws.replace('/', '-').replace('_', '-') - proj = f"{home}/.claude/projects/{key}" + proj = f"{claude_project_dir}/{key}" if os.path.isdir(proj): for j in sorted(glob.glob(f"{proj}/*.jsonl"), key=os.path.getmtime, reverse=True): sid = None diff --git a/skills/tmux-agent-orchestrate-create/SKILL.md b/skills/tmux-agent-orchestrate-create/SKILL.md index 911864f..b193574 100644 --- a/skills/tmux-agent-orchestrate-create/SKILL.md +++ b/skills/tmux-agent-orchestrate-create/SKILL.md @@ -111,8 +111,10 @@ tmux has-session -t "$SESSION_NAME" 2>/dev/null && { 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 & + # Use the wrapper if it exists (LOCAL_BIN env var overrides default $HOME/.local/bin) + local_bin="${LOCAL_BIN:-$HOME/.local/bin}" + if [ -x "$local_bin/$SESSION_NAME" ]; then + nohup "$local_bin/$SESSION_NAME" >/dev/null 2>&1 & else tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "claude" fi diff --git a/skills/tmux-agent-orchestrate-create/scripts/create_session.sh b/skills/tmux-agent-orchestrate-create/scripts/create_session.sh index eef4fd5..d2a7261 100755 --- a/skills/tmux-agent-orchestrate-create/scripts/create_session.sh +++ b/skills/tmux-agent-orchestrate-create/scripts/create_session.sh @@ -95,7 +95,8 @@ if _tmux has-session -t "$SESSION_NAME" 2>/dev/null; then fi # tmux 세션 띄우기 -WRAPPER="$HOME/.local/bin/$SESSION_NAME" +LOCAL_BIN="${LOCAL_BIN:-$HOME/.local/bin}" +WRAPPER="$LOCAL_BIN/$SESSION_NAME" spawn() { case "$AGENT" in diff --git a/skills/tmux-agent-orchestrate-delete/SKILL.md b/skills/tmux-agent-orchestrate-delete/SKILL.md index fb8bf5b..cb1eb83 100644 --- a/skills/tmux-agent-orchestrate-delete/SKILL.md +++ b/skills/tmux-agent-orchestrate-delete/SKILL.md @@ -120,8 +120,8 @@ print(f'OK: terminated at {s[\"terminated_at\"]}') print(f' preserved: pane.pid={s[\"pane\"][\"pid\"]}, cmd={s[\"pane\"][\"cmd\"]}, cwd={s[\"pane\"][\"cwd\"]}') " -# 3. (if --purge-conversation) disk artifacts gone -[ -f "$HOME/.claude/projects//.jsonl" ] && echo "WARN: jsonl still exists" || echo "OK: jsonl purged" +# 3. (if --purge-conversation) disk artifacts gone (CLAUDE_PROJECT_DIR env var overrides default $HOME/.claude/projects) +[ -f "${CLAUDE_PROJECT_DIR:-$HOME/.claude/projects}//.jsonl" ] && echo "WARN: jsonl still exists" || echo "OK: jsonl purged" ``` ## When NOT to use this skill diff --git a/skills/tmux-agent-orchestrate-delete/scripts/delete_session.sh b/skills/tmux-agent-orchestrate-delete/scripts/delete_session.sh index 21554e0..ca9c2f9 100755 --- a/skills/tmux-agent-orchestrate-delete/scripts/delete_session.sh +++ b/skills/tmux-agent-orchestrate-delete/scripts/delete_session.sh @@ -162,7 +162,8 @@ if last_status: if purge and purge_uuid: if agent == 'claude': key = ws.replace('/', '-').replace('_', '-') - jsonl = f"{home}/.claude/projects/{key}/{purge_uuid}.jsonl" + claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects") + jsonl = f"{claude_project_dir}/{key}/{purge_uuid}.jsonl" if os.path.exists(jsonl): os.remove(jsonl) print(f"purged: {jsonl}", flush=True) diff --git a/skills/tmux-agent-orchestrate-monitor/SKILL.md b/skills/tmux-agent-orchestrate-monitor/SKILL.md index d2a0ec9..215778e 100644 --- a/skills/tmux-agent-orchestrate-monitor/SKILL.md +++ b/skills/tmux-agent-orchestrate-monitor/SKILL.md @@ -154,7 +154,7 @@ disk: ~/.claude/projects/.../87dc548e-...jsonl: missing - **Don't run the monitor without `--goal`** — without goal mode, a single turn will spawn, do one reconcile, and complete. Goal mode keeps the worker alive across many turns. - **The 30s poll is a default** — workers may override if they detect heavy churn. A workspace with 5+ agent sessions should bump to 60s to avoid noise. -- **`kanban_comment` rate limits** — Kanban may throttle if you comment too fast. Coalesce: only comment when the diff is *new* (not the same drift on every poll). The script tracks a state file at `~/.cache/tmux-agent-orchestrate-monitor/.state` for this. +- **`kanban_comment` rate limits** — Kanban may throttle if you comment too fast. Coalesce: only comment when the diff is *new* (not the same drift on every poll). The script tracks a state file at `.cache/tmux-agent-orchestrate-monitor/.state` in the workspace root for this (overridable via `AGENT_SESSIONS_STATE_DIR`). - **Don't fight the user's explicit action** — if `tmux-agent-orchestrate-delete` is mid-flight and the monitor sees the same session in two states within 5s, prefer the user's most recent action. The monitor should not auto-revert a fresh `terminated` to `running` because of a stale `tmux has-session` check. - **The monitor should never modify the conversation artifacts** (jsonl, db) — only the YAML. If you see a stale UUID, comment about it but don't delete the file. - **TUI capture-pane is expensive** — only capture when you need to update `last_visible_status`, not every poll. diff --git a/skills/tmux-agent-orchestrate-monitor/scripts/reconcile.sh b/skills/tmux-agent-orchestrate-monitor/scripts/reconcile.sh index f4e1f8e..b161bd9 100755 --- a/skills/tmux-agent-orchestrate-monitor/scripts/reconcile.sh +++ b/skills/tmux-agent-orchestrate-monitor/scripts/reconcile.sh @@ -16,7 +16,7 @@ set -euo pipefail source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh" -STATE_DIR="${AGENT_SESSIONS_STATE_DIR:-$HOME/.cache/tmux-agent-orchestrate-monitor}" +STATE_DIR="${AGENT_SESSIONS_STATE_DIR:-$WORKSPACE_ROOT/.cache/tmux-agent-orchestrate-monitor}" ONCE=0 EMIT_DIFF=0 @@ -54,7 +54,7 @@ if [ "$SUBSCRIBE" = "1" ]; then # The MQTT subscribe loop exits 3 to signal "broker unavailable → poll instead". set +e - YAML_PATH="$AGENT_SESSIONS_YAML" HOME_DIR="$HOME" \ + YAML_PATH="$AGENT_SESSIONS_YAML" HOME_DIR="$HOME_DIR" CLAUDE_PROJECT_DIR="$CLAUDE_PROJECT_DIR" LOCAL_BIN="$LOCAL_BIN" \ SUB_TIMEOUT="$SUB_TIMEOUT" SUB_IDLE_TIMEOUT="$SUB_IDLE_TIMEOUT" \ SKILLS_DIR="$SKILLS_DIR" LIB_SH="$LIB_SH" \ "$PYBIN" - <<'PYEOF' @@ -229,6 +229,7 @@ import yaml yaml_path = os.environ['YAML_PATH'] home = os.environ['HOME_DIR'] +claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects") now_iso = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ') @@ -374,7 +375,7 @@ for s in d.get('tmux_sessions', []): if not cwd: continue proj_key = cwd.replace('/', '-').replace('_', '-') - proj_dir = f"{home}/.claude/projects/{proj_key}" + proj_dir = f"{claude_project_dir}/{proj_key}" if not os.path.isdir(proj_dir): continue jsonls = sorted(glob.glob(f"{proj_dir}/*.jsonl"), key=os.path.getmtime, reverse=True) @@ -426,7 +427,7 @@ ai = d.get('agent_identities', {}) or {} cl = (ai.get('claude') or {}) if cl.get('session_id'): sid = cl['session_id'] - if not glob.glob(f"{home}/.claude/projects/*/{sid}.jsonl"): + if not glob.glob(f"{claude_project_dir}/*/{sid}.jsonl"): drifts.append({'class': 'D', 'name': '(claude identity cache)', 'msg': f"stale UUID in agent_identities.claude.session_id: {sid} (jsonl missing)"}) ag = (ai.get('agy') or {}) diff --git a/skills/tmux-agent-orchestrate-resume/SKILL.md b/skills/tmux-agent-orchestrate-resume/SKILL.md index 673cc78..f5539d1 100644 --- a/skills/tmux-agent-orchestrate-resume/SKILL.md +++ b/skills/tmux-agent-orchestrate-resume/SKILL.md @@ -26,7 +26,7 @@ metadata: 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 ` / `agy --conversation `. -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//` for claude. +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 `$HOME_DIR/.gemini/antigravity-cli/cache/last_conversations.json` (defaults to `~/.gemini/...`) for agy, or the latest `*.jsonl` in `$CLAUDE_PROJECT_DIR//` (defaults to `~/.claude/projects/`) for claude. 3. **tmux is alive AND the agent inside is already running** — Just attach. No re-spawn needed. ## UUID resolution order @@ -35,9 +35,9 @@ Three cases this skill handles: 1. **`agent-sessions.yaml` → `agent_identities..session_id` (claude) / `conversation_id` (agy)** — explicit saved value 2. **`agent-sessions.yaml` → `agent_identities..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//*.jsonl | head -1` and parse the `sessionId` from the first line - - agy: `jq -r '.""' ~/.gemini/antigravity-cli/cache/last_conversations.json` +3. **Fallback: scan disk for the workspace's most recent conversation** (Note: `CLAUDE_PROJECT_DIR` overrides the default `~/.claude/projects/` path, and `HOME_DIR` overrides the `~` path) — + - claude: `ls -t $CLAUDE_PROJECT_DIR//*.jsonl | head -1` and parse the `sessionId` from the first line + - agy: `jq -r '.""' $HOME_DIR/.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`. diff --git a/skills/tmux-agent-orchestrate-status/SKILL.md b/skills/tmux-agent-orchestrate-status/SKILL.md index a56e699..1a10a5b 100644 --- a/skills/tmux-agent-orchestrate-status/SKILL.md +++ b/skills/tmux-agent-orchestrate-status/SKILL.md @@ -48,7 +48,7 @@ The script: 3. For each row in `tmux_sessions[]`: - tmux alive? (via `tmux has-session -t `) - pane cmd, cwd (via `tmux list-panes`) - - resume UUID on disk? (claude: `~/.claude/projects//.jsonl`; agy: `~/.gemini/antigravity-cli/conversations/.db`) + - resume UUID on disk? (claude: `$CLAUDE_PROJECT_DIR//.jsonl` with default `~/.claude/projects/`; agy: `$HOME_DIR/.gemini/antigravity-cli/conversations/.db` with default `~/.gemini/...`) 4. For each tmux session matching `*-creator-*` not in YAML → flag as "unregistered" 5. Prints a table (default) or JSON (with `--json`) diff --git a/skills/tmux-agent-orchestrate-status/scripts/status.sh b/skills/tmux-agent-orchestrate-status/scripts/status.sh index 2d28261..76cf920 100755 --- a/skills/tmux-agent-orchestrate-status/scripts/status.sh +++ b/skills/tmux-agent-orchestrate-status/scripts/status.sh @@ -34,6 +34,7 @@ import yaml yaml_path = os.environ['YAML_PATH'] home = os.environ['HOME_DIR'] +claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects") drift = json.loads(os.environ['DRIFT_JSON']) with open(yaml_path) as f: @@ -53,9 +54,9 @@ def resume_on_disk(s): u = s.get('claude_session_id_own') if u: key = cwd.replace('/', '-').replace('_', '-') - return 'yes' if os.path.exists(f"{home}/.claude/projects/{key}/{u}.jsonl") else 'MISSING' + return 'yes' if os.path.exists(f"{claude_project_dir}/{key}/{u}.jsonl") else 'MISSING' key = cwd.replace('/', '-').replace('_', '-') - return 'scan' if glob.glob(f"{home}/.claude/projects/{key}/*.jsonl") else 'no' + return 'scan' if glob.glob(f"{claude_project_dir}/{key}/*.jsonl") else 'no' if name.endswith('-creator-agy'): u = s.get('agy_conversation_id_own') if u: