Community Skills (32): - jat: jat-start, jat-verify, jat-complete - pi-mono: codex-cli, codex-5.3-prompting, interactive-shell - picoclaw: github, weather, tmux, summarize, skill-creator - dyad: 18 skills (swarm-to-plan, multi-pr-review, fix-issue, lint, etc.) - dexter: dcf valuation skill Agents (23): - pi-mono subagents: scout, planner, reviewer, worker - toad: 19 agent configs (Claude, Codex, Gemini, Copilot, OpenCode, etc.) System Prompts (91): - Anthropic: 15 Claude prompts (opus-4.6, code, cowork, etc.) - OpenAI: 49 GPT prompts (gpt-5 series, o3, o4-mini, tools) - Google: 13 Gemini prompts (2.5-pro, 3-pro, workspace, cli) - xAI: 5 Grok prompts - Other: 9 misc prompts (Notion, Raycast, Warp, Kagi, etc.) Hooks (9): - JAT hooks for session management, signal tracking, activity logging Prompts (6): - pi-mono templates for PR review, issue analysis, changelog audit Sources analyzed: jat, ralph-desktop, toad, pi-mono, cmux, pi-interactive-shell, craft-agents-oss, dexter, picoclaw, dyad, system_prompts_leaks, Prometheus, zed, clawdbot, OS-Copilot, and more
277 lines
10 KiB
Bash
Executable File
277 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# session-start-agent-identity.sh - Unified SessionStart hook for JAT
|
|
#
|
|
# Combines agent identity restoration (from tmux/WINDOWID) with workflow
|
|
# state injection (task ID, signal state, next action reminder).
|
|
#
|
|
# This is the GLOBAL hook - installed to ~/.claude/hooks/ by setup-statusline-and-hooks.sh
|
|
# It works with or without .jat/ directory (graceful degradation).
|
|
#
|
|
# Input (stdin): {"session_id": "...", "source": "startup|resume|clear|compact", ...}
|
|
# Output: Context about agent identity + workflow state (if found)
|
|
#
|
|
# Recovery priority:
|
|
# 1. IDE pre-registration file (.tmux-agent-{tmuxSession})
|
|
# 2. WINDOWID-based file (survives /clear, compaction recovery)
|
|
# 3. Existing session file (agent-{sessionId}.txt)
|
|
#
|
|
# Writes: .claude/sessions/agent-{session_id}.txt (in all project dirs)
|
|
|
|
set -euo pipefail
|
|
|
|
DEBUG_LOG="/tmp/jat-session-start-hook.log"
|
|
log() {
|
|
echo "$(date -Iseconds) $*" >> "$DEBUG_LOG"
|
|
}
|
|
|
|
log "=== SessionStart hook triggered ==="
|
|
log "PWD: $(pwd)"
|
|
log "TMUX env: ${TMUX:-NOT_SET}"
|
|
|
|
# Read hook input from stdin
|
|
HOOK_INPUT=$(cat)
|
|
log "Input: ${HOOK_INPUT:0:200}"
|
|
|
|
# Extract session_id and source
|
|
SESSION_ID=$(echo "$HOOK_INPUT" | jq -r '.session_id // ""' 2>/dev/null || echo "")
|
|
SOURCE=$(echo "$HOOK_INPUT" | jq -r '.source // ""' 2>/dev/null || echo "")
|
|
log "Session ID: $SESSION_ID, Source: $SOURCE"
|
|
|
|
if [[ -z "$SESSION_ID" ]]; then
|
|
log "ERROR: No session_id in hook input"
|
|
exit 0
|
|
fi
|
|
|
|
# Write PPID-based session file for other tools
|
|
echo "$SESSION_ID" > "/tmp/claude-session-${PPID}.txt"
|
|
|
|
# ============================================================================
|
|
# DETECT TMUX SESSION - 3 methods for robustness
|
|
# ============================================================================
|
|
|
|
TMUX_SESSION=""
|
|
IN_TMUX=true
|
|
|
|
# Method 1: Use $TMUX env var if available
|
|
if [[ -n "${TMUX:-}" ]]; then
|
|
TMUX_SESSION=$(tmux display-message -p '#S' 2>/dev/null || echo "")
|
|
log "Method 1 (TMUX env): $TMUX_SESSION"
|
|
fi
|
|
|
|
# Method 2: Find tmux session by tty
|
|
if [[ -z "$TMUX_SESSION" ]]; then
|
|
CURRENT_TTY=$(tty 2>/dev/null || echo "")
|
|
if [[ -n "$CURRENT_TTY" ]]; then
|
|
TMUX_SESSION=$(tmux list-panes -a -F '#{pane_tty} #{session_name}' 2>/dev/null | grep "^${CURRENT_TTY} " | head -1 | awk '{print $2}')
|
|
log "Method 2 (tty=$CURRENT_TTY): $TMUX_SESSION"
|
|
fi
|
|
fi
|
|
|
|
# Method 3: Walk parent process tree looking for tmux
|
|
if [[ -z "$TMUX_SESSION" ]]; then
|
|
PPID_CHAIN=$(ps -o ppid= -p $$ 2>/dev/null | tr -d ' ')
|
|
if [[ -n "$PPID_CHAIN" ]]; then
|
|
for _ in 1 2 3 4 5; do
|
|
PPID_CMD=$(ps -o comm= -p "$PPID_CHAIN" 2>/dev/null || echo "")
|
|
if [[ "$PPID_CMD" == "tmux"* ]]; then
|
|
TMUX_SESSION=$(cat /proc/$PPID_CHAIN/environ 2>/dev/null | tr '\0' '\n' | grep '^TMUX=' | head -1 | cut -d',' -f3)
|
|
log "Method 3 (parent process): $TMUX_SESSION"
|
|
break
|
|
fi
|
|
PPID_CHAIN=$(ps -o ppid= -p "$PPID_CHAIN" 2>/dev/null | tr -d ' ')
|
|
[[ -z "$PPID_CHAIN" || "$PPID_CHAIN" == "1" ]] && break
|
|
done
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "$TMUX_SESSION" ]]; then
|
|
IN_TMUX=false
|
|
fi
|
|
|
|
log "Final tmux session: ${TMUX_SESSION:-NONE}"
|
|
|
|
# ============================================================================
|
|
# BUILD SEARCH DIRECTORIES (current dir + all configured projects)
|
|
# ============================================================================
|
|
|
|
PROJECT_DIR="$(pwd)"
|
|
CLAUDE_DIR="$PROJECT_DIR/.claude"
|
|
mkdir -p "$CLAUDE_DIR/sessions"
|
|
|
|
SEARCH_DIRS="$PROJECT_DIR"
|
|
JAT_CONFIG="$HOME/.config/jat/projects.json"
|
|
if [[ -f "$JAT_CONFIG" ]]; then
|
|
PROJECT_PATHS=$(jq -r '.projects[].path // empty' "$JAT_CONFIG" 2>/dev/null | sed "s|^~|$HOME|g")
|
|
for PP in $PROJECT_PATHS; do
|
|
# Skip current dir (already included) and non-existent dirs
|
|
[[ "$PP" == "$PROJECT_DIR" ]] && continue
|
|
[[ -d "${PP}/.claude" ]] && SEARCH_DIRS="$SEARCH_DIRS $PP"
|
|
done
|
|
fi
|
|
log "Search dirs: $SEARCH_DIRS"
|
|
|
|
# ============================================================================
|
|
# RESTORE AGENT IDENTITY (priority order)
|
|
# ============================================================================
|
|
|
|
AGENT_NAME=""
|
|
WINDOW_KEY="${WINDOWID:-$PPID}"
|
|
|
|
# Priority 1: IDE pre-registration file (tmux session name based)
|
|
if [[ -n "$TMUX_SESSION" ]]; then
|
|
for BASE_DIR in $SEARCH_DIRS; do
|
|
CANDIDATE="${BASE_DIR}/.claude/sessions/.tmux-agent-${TMUX_SESSION}"
|
|
if [[ -f "$CANDIDATE" ]]; then
|
|
AGENT_NAME=$(cat "$CANDIDATE" 2>/dev/null | tr -d '\n')
|
|
log "Priority 1 (tmux pre-reg): $AGENT_NAME from $CANDIDATE"
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Priority 2: WINDOWID-based file (compaction recovery)
|
|
if [[ -z "$AGENT_NAME" ]]; then
|
|
PERSISTENT_AGENT_FILE="$CLAUDE_DIR/.agent-identity-${WINDOW_KEY}"
|
|
if [[ -f "$PERSISTENT_AGENT_FILE" ]]; then
|
|
AGENT_NAME=$(cat "$PERSISTENT_AGENT_FILE" 2>/dev/null | tr -d '\n')
|
|
log "Priority 2 (WINDOWID=$WINDOW_KEY): $AGENT_NAME"
|
|
|
|
# Ensure agent is registered in Agent Mail
|
|
if [[ -n "$AGENT_NAME" ]] && command -v am-register &>/dev/null; then
|
|
if ! sqlite3 ~/.agent-mail.db "SELECT 1 FROM agents WHERE name = '$AGENT_NAME'" 2>/dev/null | grep -q 1; then
|
|
am-register --name "$AGENT_NAME" --program claude-code --model opus 2>/dev/null
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Priority 3: Existing session file for this session ID
|
|
if [[ -z "$AGENT_NAME" ]]; then
|
|
for BASE_DIR in $SEARCH_DIRS; do
|
|
CANDIDATE="${BASE_DIR}/.claude/sessions/agent-${SESSION_ID}.txt"
|
|
if [[ -f "$CANDIDATE" ]]; then
|
|
AGENT_NAME=$(cat "$CANDIDATE" 2>/dev/null | tr -d '\n')
|
|
log "Priority 3 (existing session file): $AGENT_NAME from $CANDIDATE"
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [[ -z "$AGENT_NAME" ]]; then
|
|
log "No agent identity found"
|
|
# Still warn about tmux
|
|
if [[ "$IN_TMUX" == false ]]; then
|
|
echo ""
|
|
echo "NOT IN TMUX SESSION - IDE cannot track this session."
|
|
echo "Exit and restart with: jat-projectname (e.g. jat-jat, jat-chimaro)"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# ============================================================================
|
|
# WRITE SESSION FILES to all project directories
|
|
# ============================================================================
|
|
|
|
for BASE_DIR in $SEARCH_DIRS; do
|
|
SESSIONS_DIR="${BASE_DIR}/.claude/sessions"
|
|
if [[ -d "$SESSIONS_DIR" ]]; then
|
|
echo "$AGENT_NAME" > "${SESSIONS_DIR}/agent-${SESSION_ID}.txt"
|
|
log "Wrote session file: ${SESSIONS_DIR}/agent-${SESSION_ID}.txt"
|
|
fi
|
|
done
|
|
|
|
# ============================================================================
|
|
# OUTPUT IDENTITY CONTEXT
|
|
# ============================================================================
|
|
|
|
echo "=== JAT Agent Identity Restored ==="
|
|
echo "Agent: $AGENT_NAME"
|
|
echo "Session: ${SESSION_ID:0:8}..."
|
|
echo "Tmux: ${TMUX_SESSION:-NOT_IN_TMUX}"
|
|
echo "Source: $SOURCE"
|
|
|
|
# ============================================================================
|
|
# INJECT WORKFLOW STATE (if available)
|
|
# ============================================================================
|
|
|
|
PERSISTENT_STATE_FILE="$CLAUDE_DIR/.agent-workflow-state-${WINDOW_KEY}.json"
|
|
TASK_ID=""
|
|
|
|
# Check for saved workflow state from PreCompact hook
|
|
if [[ -f "$PERSISTENT_STATE_FILE" ]]; then
|
|
SIGNAL_STATE=$(jq -r '.signalState // "unknown"' "$PERSISTENT_STATE_FILE" 2>/dev/null)
|
|
TASK_ID=$(jq -r '.taskId // ""' "$PERSISTENT_STATE_FILE" 2>/dev/null)
|
|
TASK_TITLE=$(jq -r '.taskTitle // ""' "$PERSISTENT_STATE_FILE" 2>/dev/null)
|
|
|
|
if [[ -n "$TASK_ID" ]]; then
|
|
case "$SIGNAL_STATE" in
|
|
"starting")
|
|
NEXT_ACTION="Emit 'working' signal with taskId, taskTitle, and approach before continuing work"
|
|
WORKFLOW_STEP="After registration, before implementation"
|
|
;;
|
|
"working")
|
|
NEXT_ACTION="Continue implementation. When done, emit 'review' signal before presenting results"
|
|
WORKFLOW_STEP="Implementation in progress"
|
|
;;
|
|
"needs_input")
|
|
NEXT_ACTION="After user responds, emit 'working' signal to resume, then continue work"
|
|
WORKFLOW_STEP="Waiting for user input"
|
|
;;
|
|
"review")
|
|
NEXT_ACTION="Present findings to user. Run /jat:complete when approved"
|
|
WORKFLOW_STEP="Ready for review"
|
|
;;
|
|
*)
|
|
NEXT_ACTION="Check task status and emit appropriate signal (working/review)"
|
|
WORKFLOW_STEP="Unknown - verify current state"
|
|
;;
|
|
esac
|
|
|
|
echo ""
|
|
echo "[JAT:WORKING task=$TASK_ID]"
|
|
echo ""
|
|
echo "=== JAT WORKFLOW CONTEXT (restored after compaction) ==="
|
|
echo "Agent: $AGENT_NAME"
|
|
echo "Task: $TASK_ID - $TASK_TITLE"
|
|
echo "Last Signal: $SIGNAL_STATE"
|
|
echo "Workflow Step: $WORKFLOW_STEP"
|
|
echo "NEXT ACTION REQUIRED: $NEXT_ACTION"
|
|
echo "========================================================="
|
|
|
|
log "Injected workflow context: state=$SIGNAL_STATE, task=$TASK_ID"
|
|
fi
|
|
fi
|
|
|
|
# Fallback: If no state file but agent has in_progress task, query jt (if available)
|
|
if [[ -z "$TASK_ID" ]] && command -v jt &>/dev/null; then
|
|
# Only try jt if we're in a directory with .jat/ (graceful degradation)
|
|
if [[ -d "$PROJECT_DIR/.jat" ]]; then
|
|
TASK_ID=$(jt list --json 2>/dev/null | jq -r --arg a "$AGENT_NAME" '.[] | select(.assignee == $a and .status == "in_progress") | .id' 2>/dev/null | head -1)
|
|
if [[ -n "$TASK_ID" ]]; then
|
|
TASK_TITLE=$(jt show "$TASK_ID" --json 2>/dev/null | jq -r '.[0].title // ""' 2>/dev/null)
|
|
echo ""
|
|
echo "[JAT:WORKING task=$TASK_ID]"
|
|
echo ""
|
|
echo "=== JAT WORKFLOW CONTEXT (restored from JAT Tasks) ==="
|
|
echo "Agent: $AGENT_NAME"
|
|
echo "Task: $TASK_ID - $TASK_TITLE"
|
|
echo "Last Signal: unknown (no state file)"
|
|
echo "NEXT ACTION: Emit 'working' signal if continuing work, or 'review' signal if done"
|
|
echo "=================================================="
|
|
|
|
log "Fallback context from JAT Tasks: task=$TASK_ID"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Warn if not in tmux
|
|
if [[ "$IN_TMUX" == false ]]; then
|
|
echo ""
|
|
echo "NOT IN TMUX SESSION - IDE cannot track this session."
|
|
echo "Exit and restart with: jat-projectname (e.g. jat-jat, jat-chimaro)"
|
|
fi
|
|
|
|
log "Hook completed successfully"
|
|
exit 0
|