Add community skills, agents, system prompts from 22+ sources
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
This commit is contained in:
113
hooks/community/jat/log-tool-activity.sh
Executable file
113
hooks/community/jat/log-tool-activity.sh
Executable file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# log-tool-activity.sh - Claude hook to log tool usage
|
||||
#
|
||||
# This hook is called after any tool use by Claude
|
||||
# Hook receives tool info via stdin (JSON format)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Read tool info from stdin
|
||||
TOOL_INFO=$(cat)
|
||||
|
||||
# Extract session ID from hook data (preferred - always available in hooks)
|
||||
SESSION_ID=$(echo "$TOOL_INFO" | jq -r '.session_id // ""' 2>/dev/null || echo "")
|
||||
if [[ -z "$SESSION_ID" ]]; then
|
||||
# Fallback to PPID-based file if session_id not in JSON (shouldn't happen with hooks)
|
||||
# Note: PPID here is the hook's parent, which may not be correct
|
||||
SESSION_ID=$(cat /tmp/claude-session-${PPID}.txt 2>/dev/null | tr -d '\n' || echo "")
|
||||
fi
|
||||
|
||||
if [[ -z "$SESSION_ID" ]]; then
|
||||
exit 0 # Can't determine session, skip logging
|
||||
fi
|
||||
|
||||
# Parse tool name and parameters (correct JSON paths)
|
||||
TOOL_NAME=$(echo "$TOOL_INFO" | jq -r '.tool_name // "Unknown"' 2>/dev/null || echo "Unknown")
|
||||
|
||||
# Build preview based on tool type
|
||||
case "$TOOL_NAME" in
|
||||
Read)
|
||||
FILE_PATH=$(echo "$TOOL_INFO" | jq -r '.tool_input.file_path // ""' 2>/dev/null || echo "")
|
||||
PREVIEW="Reading $(basename "$FILE_PATH")"
|
||||
log-agent-activity \
|
||||
--session "$SESSION_ID" \
|
||||
--type tool \
|
||||
--tool "Read" \
|
||||
--file "$FILE_PATH" \
|
||||
--preview "$PREVIEW" \
|
||||
--content "Read file: $FILE_PATH"
|
||||
;;
|
||||
Write)
|
||||
FILE_PATH=$(echo "$TOOL_INFO" | jq -r '.tool_input.file_path // ""' 2>/dev/null || echo "")
|
||||
PREVIEW="Writing $(basename "$FILE_PATH")"
|
||||
log-agent-activity \
|
||||
--session "$SESSION_ID" \
|
||||
--type tool \
|
||||
--tool "Write" \
|
||||
--file "$FILE_PATH" \
|
||||
--preview "$PREVIEW" \
|
||||
--content "Write file: $FILE_PATH"
|
||||
;;
|
||||
Edit)
|
||||
FILE_PATH=$(echo "$TOOL_INFO" | jq -r '.tool_input.file_path // ""' 2>/dev/null || echo "")
|
||||
PREVIEW="Editing $(basename "$FILE_PATH")"
|
||||
log-agent-activity \
|
||||
--session "$SESSION_ID" \
|
||||
--type tool \
|
||||
--tool "Edit" \
|
||||
--file "$FILE_PATH" \
|
||||
--preview "$PREVIEW" \
|
||||
--content "Edit file: $FILE_PATH"
|
||||
;;
|
||||
Bash)
|
||||
COMMAND=$(echo "$TOOL_INFO" | jq -r '.tool_input.command // ""' 2>/dev/null || echo "")
|
||||
# Truncate long commands
|
||||
SHORT_CMD=$(echo "$COMMAND" | head -c 50)
|
||||
[[ ${#COMMAND} -gt 50 ]] && SHORT_CMD="${SHORT_CMD}..."
|
||||
PREVIEW="Running: $SHORT_CMD"
|
||||
log-agent-activity \
|
||||
--session "$SESSION_ID" \
|
||||
--type tool \
|
||||
--tool "Bash" \
|
||||
--preview "$PREVIEW" \
|
||||
--content "Bash: $COMMAND"
|
||||
;;
|
||||
Grep|Glob)
|
||||
PATTERN=$(echo "$TOOL_INFO" | jq -r '.tool_input.pattern // ""' 2>/dev/null || echo "")
|
||||
PREVIEW="Searching: $PATTERN"
|
||||
log-agent-activity \
|
||||
--session "$SESSION_ID" \
|
||||
--type tool \
|
||||
--tool "$TOOL_NAME" \
|
||||
--preview "$PREVIEW" \
|
||||
--content "$TOOL_NAME: $PATTERN"
|
||||
;;
|
||||
AskUserQuestion)
|
||||
# Note: Question file writing is handled by pre-ask-user-question.sh (PreToolUse hook)
|
||||
# This PostToolUse hook only logs the activity
|
||||
QUESTIONS_JSON=$(echo "$TOOL_INFO" | jq -c '.tool_input.questions // []' 2>/dev/null || echo "[]")
|
||||
FIRST_QUESTION=$(echo "$QUESTIONS_JSON" | jq -r '.[0].question // "Question"' 2>/dev/null || echo "Question")
|
||||
SHORT_Q=$(echo "$FIRST_QUESTION" | head -c 40)
|
||||
[[ ${#FIRST_QUESTION} -gt 40 ]] && SHORT_Q="${SHORT_Q}..."
|
||||
PREVIEW="Asking: $SHORT_Q"
|
||||
log-agent-activity \
|
||||
--session "$SESSION_ID" \
|
||||
--type tool \
|
||||
--tool "AskUserQuestion" \
|
||||
--preview "$PREVIEW" \
|
||||
--content "Question: $FIRST_QUESTION"
|
||||
;;
|
||||
*)
|
||||
# Generic tool logging
|
||||
PREVIEW="Using tool: $TOOL_NAME"
|
||||
log-agent-activity \
|
||||
--session "$SESSION_ID" \
|
||||
--type tool \
|
||||
--tool "$TOOL_NAME" \
|
||||
--preview "$PREVIEW" \
|
||||
--content "Tool: $TOOL_NAME"
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
85
hooks/community/jat/monitor-output.sh
Executable file
85
hooks/community/jat/monitor-output.sh
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# monitor-output.sh - Real-time output activity monitor
|
||||
#
|
||||
# Monitors tmux pane output to detect when agent is actively generating text.
|
||||
# Writes ephemeral state to /tmp/jat-activity-{session}.json for IDE polling.
|
||||
#
|
||||
# Usage: monitor-output.sh <tmux-session-name>
|
||||
# Started by: user-prompt-signal.sh (on user message)
|
||||
# Terminates: After 30 seconds of no output change
|
||||
#
|
||||
# States:
|
||||
# generating - Output is growing (agent writing text)
|
||||
# thinking - Output stable for 2+ seconds (agent processing)
|
||||
# idle - Output stable for 30+ seconds (agent waiting)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMUX_SESSION="${1:-}"
|
||||
if [[ -z "$TMUX_SESSION" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ACTIVITY_FILE="/tmp/jat-activity-${TMUX_SESSION}.json"
|
||||
PID_FILE="/tmp/jat-monitor-${TMUX_SESSION}.pid"
|
||||
|
||||
# Write our PID so we can be killed by other hooks
|
||||
echo $$ > "$PID_FILE"
|
||||
|
||||
# Cleanup on exit
|
||||
trap "rm -f '$PID_FILE'" EXIT
|
||||
|
||||
prev_len=0
|
||||
idle_count=0
|
||||
last_state=""
|
||||
touch_count=0
|
||||
|
||||
write_state() {
|
||||
local state="$1"
|
||||
local force="${2:-false}"
|
||||
# Write if state changed OR if force=true (to update mtime for freshness check)
|
||||
if [[ "$state" != "$last_state" ]] || [[ "$force" == "true" ]]; then
|
||||
echo "{\"state\":\"${state}\",\"since\":\"$(date -Iseconds)\",\"tmux_session\":\"${TMUX_SESSION}\"}" > "$ACTIVITY_FILE"
|
||||
last_state="$state"
|
||||
touch_count=0
|
||||
fi
|
||||
}
|
||||
|
||||
# Initial state
|
||||
write_state "generating"
|
||||
|
||||
while true; do
|
||||
# Capture current pane content length
|
||||
curr_len=$(tmux capture-pane -t "$TMUX_SESSION" -p 2>/dev/null | wc -c || echo "0")
|
||||
|
||||
if [[ "$curr_len" -gt "$prev_len" ]]; then
|
||||
# Output is growing = agent generating text
|
||||
write_state "generating"
|
||||
idle_count=0
|
||||
else
|
||||
# Output stable
|
||||
((idle_count++)) || true
|
||||
|
||||
if [[ $idle_count -gt 20 ]]; then
|
||||
# 2+ seconds of no change = thinking/processing
|
||||
write_state "thinking"
|
||||
fi
|
||||
|
||||
if [[ $idle_count -gt 300 ]]; then
|
||||
# 30+ seconds of no change = idle, self-terminate
|
||||
write_state "idle"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Keep file timestamp fresh for IDE staleness check (every ~2 seconds)
|
||||
# IDE considers activity older than 30s as stale, so we update at least every 20 iterations
|
||||
((touch_count++)) || true
|
||||
if [[ $touch_count -gt 20 ]]; then
|
||||
write_state "$last_state" true
|
||||
fi
|
||||
|
||||
prev_len="$curr_len"
|
||||
sleep 0.1
|
||||
done
|
||||
41
hooks/community/jat/post-bash-agent-state-refresh.sh
Executable file
41
hooks/community/jat/post-bash-agent-state-refresh.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Post-Bash Hook: Agent State Refresh
|
||||
#
|
||||
# Detects when agent coordination commands are executed and triggers
|
||||
# statusline refresh by outputting a message (which becomes a conversation
|
||||
# message, which triggers statusline update).
|
||||
#
|
||||
# Monitored commands:
|
||||
# - am-* (Agent Mail: reserve, release, send, reply, ack, etc.)
|
||||
# - jt (JAT Tasks: create, update, close, etc.)
|
||||
# - /jat:* slash commands (via SlashCommand tool)
|
||||
#
|
||||
# Hook input (stdin): JSON with tool name, input, and output
|
||||
# Hook output (stdout): Message to display (triggers statusline refresh)
|
||||
|
||||
# Read JSON input from stdin
|
||||
input_json=$(cat)
|
||||
|
||||
# Extract the bash command that was executed
|
||||
command=$(echo "$input_json" | jq -r '.tool_input.command // empty')
|
||||
|
||||
# Check if command is empty or null
|
||||
if [[ -z "$command" || "$command" == "null" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Detect agent coordination commands
|
||||
# Pattern: am-* (Agent Mail tools) or jt followed by space (JAT Tasks commands)
|
||||
if echo "$command" | grep -qE '^(am-|jt\s)'; then
|
||||
# Extract the base command for display (first word)
|
||||
base_cmd=$(echo "$command" | awk '{print $1}')
|
||||
|
||||
# Output a brief message - this triggers statusline refresh!
|
||||
# Keep it minimal to avoid cluttering the conversation
|
||||
echo "✓ $base_cmd executed"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# No agent coordination command detected - stay silent
|
||||
exit 0
|
||||
346
hooks/community/jat/post-bash-jat-signal.sh
Executable file
346
hooks/community/jat/post-bash-jat-signal.sh
Executable file
@@ -0,0 +1,346 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# post-bash-jat-signal.sh - PostToolUse hook for jat-signal commands
|
||||
#
|
||||
# Detects when agent runs jat-signal and writes structured data to temp file
|
||||
# for IDE consumption via SSE.
|
||||
#
|
||||
# Signal format: [JAT-SIGNAL:<type>] <json-payload>
|
||||
# Types: working, review, needs_input, idle, completing, completed,
|
||||
# starting, compacting, question, tasks, action, complete
|
||||
#
|
||||
# Input: JSON with tool name, input (command), output, session_id
|
||||
# Output: Writes to /tmp/jat-signal-{session}.json
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Read tool info from stdin (must do this before any exit)
|
||||
TOOL_INFO=$(cat)
|
||||
|
||||
# WORKAROUND: Claude Code calls hooks twice per tool use (bug)
|
||||
# Use atomic mkdir for locking - only one process can create a directory
|
||||
LOCK_DIR="/tmp/jat-signal-locks"
|
||||
mkdir -p "$LOCK_DIR" 2>/dev/null || true
|
||||
|
||||
# Create a lock based on session_id + command hash (first 50 chars of command)
|
||||
SESSION_ID_EARLY=$(echo "$TOOL_INFO" | jq -r '.session_id // ""' 2>/dev/null || echo "")
|
||||
COMMAND_EARLY=$(echo "$TOOL_INFO" | jq -r '.tool_input.command // ""' 2>/dev/null || echo "")
|
||||
COMMAND_HASH=$(echo "${SESSION_ID_EARLY}:${COMMAND_EARLY:0:50}" | md5sum | cut -c1-16)
|
||||
LOCK_FILE="${LOCK_DIR}/hook-${COMMAND_HASH}"
|
||||
|
||||
# Try to atomically create lock directory - only first process succeeds
|
||||
if ! mkdir "$LOCK_FILE" 2>/dev/null; then
|
||||
# Lock exists - check if it's stale (older than 5 seconds)
|
||||
if [[ -d "$LOCK_FILE" ]]; then
|
||||
# Get lock file mtime (cross-platform: Linux uses -c, macOS uses -f)
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
LOCK_MTIME=$(stat -f %m "$LOCK_FILE" 2>/dev/null || echo "0")
|
||||
else
|
||||
LOCK_MTIME=$(stat -c %Y "$LOCK_FILE" 2>/dev/null || echo "0")
|
||||
fi
|
||||
LOCK_AGE=$(( $(date +%s) - LOCK_MTIME ))
|
||||
if [[ $LOCK_AGE -lt 5 ]]; then
|
||||
# Recent duplicate invocation, skip silently
|
||||
exit 0
|
||||
fi
|
||||
# Stale lock, remove and recreate
|
||||
rmdir "$LOCK_FILE" 2>/dev/null || true
|
||||
mkdir "$LOCK_FILE" 2>/dev/null || exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clean up lock on exit (after 1 second to ensure second invocation sees it)
|
||||
trap "sleep 1; rmdir '$LOCK_FILE' 2>/dev/null || true" EXIT
|
||||
|
||||
# Only process Bash tool calls
|
||||
TOOL_NAME=$(echo "$TOOL_INFO" | jq -r '.tool_name // ""' 2>/dev/null || echo "")
|
||||
if [[ "$TOOL_NAME" != "Bash" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Extract the command that was executed
|
||||
COMMAND=$(echo "$TOOL_INFO" | jq -r '.tool_input.command // ""' 2>/dev/null || echo "")
|
||||
|
||||
# Extract the tool output first - check if it contains a signal marker
|
||||
OUTPUT=$(echo "$TOOL_INFO" | jq -r '.tool_response.stdout // ""' 2>/dev/null || echo "")
|
||||
|
||||
# Check if output contains a jat-signal marker (regardless of what command was run)
|
||||
# This handles both direct jat-signal calls AND scripts that call jat-signal internally (like jat-step)
|
||||
if ! echo "$OUTPUT" | grep -qE '\[JAT-SIGNAL:[a-z_]+\]'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Extract session ID
|
||||
SESSION_ID=$(echo "$TOOL_INFO" | jq -r '.session_id // ""' 2>/dev/null || echo "")
|
||||
if [[ -z "$SESSION_ID" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# OUTPUT already extracted above when checking for signal marker
|
||||
|
||||
# Check for validation warnings in stderr
|
||||
STDERR=$(echo "$TOOL_INFO" | jq -r '.tool_response.stderr // ""' 2>/dev/null || echo "")
|
||||
VALIDATION_WARNING=""
|
||||
if echo "$STDERR" | grep -q 'Warning:'; then
|
||||
VALIDATION_WARNING=$(echo "$STDERR" | grep -o 'Warning: .*' | head -1)
|
||||
fi
|
||||
|
||||
# Parse the signal from output - format: [JAT-SIGNAL:<type>] <json>
|
||||
SIGNAL_TYPE=""
|
||||
SIGNAL_DATA=""
|
||||
|
||||
if echo "$OUTPUT" | grep -qE '\[JAT-SIGNAL:[a-z_]+\]'; then
|
||||
# Extract signal type from marker
|
||||
SIGNAL_TYPE=$(echo "$OUTPUT" | grep -oE '\[JAT-SIGNAL:[a-z_]+\]' | head -1 | sed 's/\[JAT-SIGNAL://;s/\]//')
|
||||
# Extract JSON payload after marker (take only the first match, trim whitespace)
|
||||
SIGNAL_DATA=$(echo "$OUTPUT" | grep -oE '\[JAT-SIGNAL:[a-z_]+\] \{.*' | head -1 | sed 's/\[JAT-SIGNAL:[a-z_]*\] *//')
|
||||
fi
|
||||
|
||||
if [[ -z "$SIGNAL_TYPE" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Get tmux session name for IDE lookup
|
||||
TMUX_SESSION=""
|
||||
|
||||
# Build list of directories to search: current dir + configured projects
|
||||
SEARCH_DIRS="."
|
||||
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 PROJECT_PATH in $PROJECT_PATHS; do
|
||||
if [[ -d "${PROJECT_PATH}/.claude" ]]; then
|
||||
SEARCH_DIRS="$SEARCH_DIRS $PROJECT_PATH"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
for BASE_DIR in $SEARCH_DIRS; do
|
||||
for SUBDIR in "sessions" ""; do
|
||||
if [[ -n "$SUBDIR" ]]; then
|
||||
AGENT_FILE="${BASE_DIR}/.claude/${SUBDIR}/agent-${SESSION_ID}.txt"
|
||||
else
|
||||
AGENT_FILE="${BASE_DIR}/.claude/agent-${SESSION_ID}.txt"
|
||||
fi
|
||||
if [[ -f "$AGENT_FILE" ]]; then
|
||||
AGENT_NAME=$(cat "$AGENT_FILE" 2>/dev/null | tr -d '\n')
|
||||
if [[ -n "$AGENT_NAME" ]]; then
|
||||
TMUX_SESSION="jat-${AGENT_NAME}"
|
||||
break 2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# Parse signal data as JSON (validate first to avoid || echo appending extra output)
|
||||
if [[ -n "$SIGNAL_DATA" ]] && echo "$SIGNAL_DATA" | jq -e . >/dev/null 2>&1; then
|
||||
PARSED_DATA=$(echo "$SIGNAL_DATA" | jq -c .)
|
||||
else
|
||||
PARSED_DATA='{}'
|
||||
fi
|
||||
|
||||
# Extract task_id from payload if present
|
||||
TASK_ID=$(echo "$PARSED_DATA" | jq -r '.taskId // ""' 2>/dev/null)
|
||||
TASK_ID="${TASK_ID:-}"
|
||||
|
||||
# Determine if this is a state signal or data signal
|
||||
# State signals: working, review, needs_input, idle, completing, completed, starting, compacting, question
|
||||
# Data signals: tasks, action, complete
|
||||
STATE_SIGNALS="working review needs_input idle completing completed starting compacting question"
|
||||
IS_STATE_SIGNAL=false
|
||||
for s in $STATE_SIGNALS; do
|
||||
if [[ "$SIGNAL_TYPE" == "$s" ]]; then
|
||||
IS_STATE_SIGNAL=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Defense-in-depth: Validate required fields for state signals
|
||||
# This catches signals that somehow bypassed jat-signal validation
|
||||
if [[ "$IS_STATE_SIGNAL" == "true" ]]; then
|
||||
case "$SIGNAL_TYPE" in
|
||||
working)
|
||||
# working requires taskId and taskTitle
|
||||
HAS_TASK_ID=$(echo "$PARSED_DATA" | jq -r '.taskId // ""' 2>/dev/null)
|
||||
HAS_TASK_TITLE=$(echo "$PARSED_DATA" | jq -r '.taskTitle // ""' 2>/dev/null)
|
||||
if [[ -z "$HAS_TASK_ID" ]] || [[ -z "$HAS_TASK_TITLE" ]]; then
|
||||
exit 0 # Silently skip incomplete working signals
|
||||
fi
|
||||
;;
|
||||
review)
|
||||
# review requires taskId
|
||||
HAS_TASK_ID=$(echo "$PARSED_DATA" | jq -r '.taskId // ""' 2>/dev/null)
|
||||
if [[ -z "$HAS_TASK_ID" ]]; then
|
||||
exit 0 # Silently skip incomplete review signals
|
||||
fi
|
||||
;;
|
||||
needs_input)
|
||||
# needs_input requires taskId, question, questionType
|
||||
HAS_TASK_ID=$(echo "$PARSED_DATA" | jq -r '.taskId // ""' 2>/dev/null)
|
||||
HAS_QUESTION=$(echo "$PARSED_DATA" | jq -r '.question // ""' 2>/dev/null)
|
||||
HAS_TYPE=$(echo "$PARSED_DATA" | jq -r '.questionType // ""' 2>/dev/null)
|
||||
if [[ -z "$HAS_TASK_ID" ]] || [[ -z "$HAS_QUESTION" ]] || [[ -z "$HAS_TYPE" ]]; then
|
||||
exit 0 # Silently skip incomplete needs_input signals
|
||||
fi
|
||||
;;
|
||||
completing|completed)
|
||||
# completing/completed require taskId
|
||||
HAS_TASK_ID=$(echo "$PARSED_DATA" | jq -r '.taskId // ""' 2>/dev/null)
|
||||
if [[ -z "$HAS_TASK_ID" ]]; then
|
||||
exit 0 # Silently skip incomplete completing/completed signals
|
||||
fi
|
||||
;;
|
||||
question)
|
||||
# question requires question and questionType
|
||||
HAS_QUESTION=$(echo "$PARSED_DATA" | jq -r '.question // ""' 2>/dev/null)
|
||||
HAS_TYPE=$(echo "$PARSED_DATA" | jq -r '.questionType // ""' 2>/dev/null)
|
||||
if [[ -z "$HAS_QUESTION" ]] || [[ -z "$HAS_TYPE" ]]; then
|
||||
exit 0 # Silently skip incomplete question signals
|
||||
fi
|
||||
;;
|
||||
# idle, starting, compacting are more flexible
|
||||
esac
|
||||
fi
|
||||
|
||||
# Build signal JSON - use "type: state" + "state: <signal>" for state signals
|
||||
# This matches what the SSE server expects for rich signal card rendering
|
||||
if [[ "$IS_STATE_SIGNAL" == "true" ]]; then
|
||||
SIGNAL_JSON=$(jq -c -n \
|
||||
--arg state "$SIGNAL_TYPE" \
|
||||
--arg session "$SESSION_ID" \
|
||||
--arg tmux "$TMUX_SESSION" \
|
||||
--arg task "$TASK_ID" \
|
||||
--argjson data "$PARSED_DATA" \
|
||||
'{
|
||||
type: "state",
|
||||
state: $state,
|
||||
session_id: $session,
|
||||
tmux_session: $tmux,
|
||||
task_id: $task,
|
||||
timestamp: (now | todate),
|
||||
data: $data
|
||||
}' 2>/dev/null || echo "{}")
|
||||
else
|
||||
# Data signals keep signal type in type field
|
||||
SIGNAL_JSON=$(jq -c -n \
|
||||
--arg type "$SIGNAL_TYPE" \
|
||||
--arg session "$SESSION_ID" \
|
||||
--arg tmux "$TMUX_SESSION" \
|
||||
--arg task "$TASK_ID" \
|
||||
--argjson data "$PARSED_DATA" \
|
||||
'{
|
||||
type: $type,
|
||||
session_id: $session,
|
||||
tmux_session: $tmux,
|
||||
task_id: $task,
|
||||
timestamp: (now | todate),
|
||||
data: $data
|
||||
}' 2>/dev/null || echo "{}")
|
||||
fi
|
||||
|
||||
# Get current git SHA for rollback capability
|
||||
GIT_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo "")
|
||||
|
||||
# Add git_sha to signal JSON if available
|
||||
if [[ -n "$GIT_SHA" ]]; then
|
||||
SIGNAL_JSON=$(echo "$SIGNAL_JSON" | jq -c --arg sha "$GIT_SHA" '. + {git_sha: $sha}' 2>/dev/null || echo "$SIGNAL_JSON")
|
||||
fi
|
||||
|
||||
# Write to temp file by session ID (current state - overwrites)
|
||||
SIGNAL_FILE="/tmp/jat-signal-${SESSION_ID}.json"
|
||||
echo "$SIGNAL_JSON" > "$SIGNAL_FILE" 2>/dev/null || true
|
||||
|
||||
# Also write by tmux session name for easy lookup (current state - overwrites)
|
||||
if [[ -n "$TMUX_SESSION" ]]; then
|
||||
TMUX_SIGNAL_FILE="/tmp/jat-signal-tmux-${TMUX_SESSION}.json"
|
||||
echo "$SIGNAL_JSON" > "$TMUX_SIGNAL_FILE" 2>/dev/null || true
|
||||
|
||||
# Append to timeline log (JSONL format - preserves history)
|
||||
TIMELINE_FILE="/tmp/jat-timeline-${TMUX_SESSION}.jsonl"
|
||||
echo "$SIGNAL_JSON" >> "$TIMELINE_FILE" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# For question signals, also write to /tmp/jat-question-*.json files
|
||||
# This allows the IDE to poll for questions separately from other signals
|
||||
if [[ "$SIGNAL_TYPE" == "question" ]]; then
|
||||
# Build question-specific JSON with fields expected by IDE
|
||||
QUESTION_JSON=$(jq -c -n \
|
||||
--arg session "$SESSION_ID" \
|
||||
--arg tmux "$TMUX_SESSION" \
|
||||
--argjson data "$PARSED_DATA" \
|
||||
'{
|
||||
session_id: $session,
|
||||
tmux_session: $tmux,
|
||||
timestamp: (now | todate),
|
||||
question: $data.question,
|
||||
questionType: $data.questionType,
|
||||
options: ($data.options // []),
|
||||
timeout: ($data.timeout // null)
|
||||
}' 2>/dev/null || echo "{}")
|
||||
|
||||
# Write to session ID file
|
||||
QUESTION_FILE="/tmp/jat-question-${SESSION_ID}.json"
|
||||
echo "$QUESTION_JSON" > "$QUESTION_FILE" 2>/dev/null || true
|
||||
|
||||
# Also write to tmux session name file for easy IDE lookup
|
||||
if [[ -n "$TMUX_SESSION" ]]; then
|
||||
TMUX_QUESTION_FILE="/tmp/jat-question-tmux-${TMUX_SESSION}.json"
|
||||
echo "$QUESTION_JSON" > "$TMUX_QUESTION_FILE" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Write per-task signal timeline for TaskDetailDrawer
|
||||
# Stored in .jat/signals/{taskId}.jsonl so it persists with the repo
|
||||
if [[ -n "$TASK_ID" ]]; then
|
||||
|
||||
# Extract project prefix from task ID (e.g., "jat-abc" -> "jat")
|
||||
TASK_PROJECT=""
|
||||
if [[ "$TASK_ID" =~ ^([a-zA-Z0-9_-]+)- ]]; then
|
||||
TASK_PROJECT="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
|
||||
# Find the project root - prioritize project matching task ID prefix
|
||||
TARGET_DIR=""
|
||||
FALLBACK_DIR=""
|
||||
for BASE_DIR in $SEARCH_DIRS; do
|
||||
if [[ -d "${BASE_DIR}/.jat" ]]; then
|
||||
DIR_NAME=$(basename "$BASE_DIR")
|
||||
# If directory name matches task project prefix, use it
|
||||
if [[ -n "$TASK_PROJECT" ]] && [[ "$DIR_NAME" == "$TASK_PROJECT" ]]; then
|
||||
TARGET_DIR="$BASE_DIR"
|
||||
break
|
||||
fi
|
||||
# Otherwise save first match as fallback
|
||||
if [[ -z "$FALLBACK_DIR" ]]; then
|
||||
FALLBACK_DIR="$BASE_DIR"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Use target dir or fall back to first found
|
||||
CHOSEN_DIR="${TARGET_DIR:-$FALLBACK_DIR}"
|
||||
|
||||
if [[ -n "$CHOSEN_DIR" ]]; then
|
||||
SIGNALS_DIR="${CHOSEN_DIR}/.jat/signals"
|
||||
mkdir -p "$SIGNALS_DIR" 2>/dev/null || true
|
||||
|
||||
# Add agent name to the signal for task context
|
||||
AGENT_FROM_TMUX=""
|
||||
if [[ -n "$TMUX_SESSION" ]] && [[ "$TMUX_SESSION" =~ ^jat-(.+)$ ]]; then
|
||||
AGENT_FROM_TMUX="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
|
||||
# Enrich signal with agent name if available
|
||||
if [[ -n "$AGENT_FROM_TMUX" ]]; then
|
||||
TASK_SIGNAL_JSON=$(echo "$SIGNAL_JSON" | jq -c --arg agent "$AGENT_FROM_TMUX" '. + {agent_name: $agent}' 2>/dev/null || echo "$SIGNAL_JSON")
|
||||
else
|
||||
TASK_SIGNAL_JSON="$SIGNAL_JSON"
|
||||
fi
|
||||
|
||||
# Append to task-specific timeline
|
||||
TASK_TIMELINE_FILE="${SIGNALS_DIR}/${TASK_ID}.jsonl"
|
||||
echo "$TASK_SIGNAL_JSON" >> "$TASK_TIMELINE_FILE" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
||||
126
hooks/community/jat/pre-ask-user-question.sh
Normal file
126
hooks/community/jat/pre-ask-user-question.sh
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# pre-ask-user-question.sh - Claude PreToolUse hook for AskUserQuestion
|
||||
#
|
||||
# This hook captures the question data BEFORE the user answers,
|
||||
# writing it to a temp file for the IDE to display.
|
||||
#
|
||||
# PreToolUse is required because PostToolUse runs after the user
|
||||
# has already answered, making the question data irrelevant.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Read tool info from stdin
|
||||
TOOL_INFO=$(cat)
|
||||
|
||||
# Extract session ID from hook data
|
||||
SESSION_ID=$(echo "$TOOL_INFO" | jq -r '.session_id // ""' 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$SESSION_ID" ]]; then
|
||||
exit 0 # Can't determine session, skip
|
||||
fi
|
||||
|
||||
# Get tmux session name - try multiple methods
|
||||
TMUX_SESSION=""
|
||||
# Method 1: From TMUX env var (may not be passed to hook subprocess)
|
||||
if [[ -n "${TMUX:-}" ]]; then
|
||||
TMUX_SESSION=$(tmux display-message -p '#S' 2>/dev/null || echo "")
|
||||
fi
|
||||
# Method 2: From agent session file (more reliable)
|
||||
if [[ -z "$TMUX_SESSION" ]]; then
|
||||
# Build list of directories to search: current dir + configured projects
|
||||
SEARCH_DIRS="."
|
||||
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 PROJECT_PATH in $PROJECT_PATHS; do
|
||||
if [[ -d "${PROJECT_PATH}/.claude" ]]; then
|
||||
SEARCH_DIRS="$SEARCH_DIRS $PROJECT_PATH"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Check both .claude/sessions/agent-{id}.txt (current) and .claude/agent-{id}.txt (legacy)
|
||||
for BASE_DIR in $SEARCH_DIRS; do
|
||||
for SUBDIR in "sessions" ""; do
|
||||
if [[ -n "$SUBDIR" ]]; then
|
||||
AGENT_FILE="${BASE_DIR}/.claude/${SUBDIR}/agent-${SESSION_ID}.txt"
|
||||
else
|
||||
AGENT_FILE="${BASE_DIR}/.claude/agent-${SESSION_ID}.txt"
|
||||
fi
|
||||
if [[ -f "$AGENT_FILE" ]]; then
|
||||
AGENT_NAME=$(cat "$AGENT_FILE" 2>/dev/null | tr -d '\n')
|
||||
if [[ -n "$AGENT_NAME" ]]; then
|
||||
TMUX_SESSION="jat-${AGENT_NAME}"
|
||||
break 2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
# Build question data JSON
|
||||
QUESTION_DATA=$(echo "$TOOL_INFO" | jq -c --arg tmux "$TMUX_SESSION" '{
|
||||
session_id: .session_id,
|
||||
tmux_session: $tmux,
|
||||
timestamp: (now | todate),
|
||||
questions: .tool_input.questions
|
||||
}' 2>/dev/null || echo "{}")
|
||||
|
||||
# Write to session ID file
|
||||
QUESTION_FILE="/tmp/claude-question-${SESSION_ID}.json"
|
||||
echo "$QUESTION_DATA" > "$QUESTION_FILE" 2>/dev/null || true
|
||||
|
||||
# Also write to tmux session name file for easy IDE lookup
|
||||
if [[ -n "$TMUX_SESSION" ]]; then
|
||||
TMUX_QUESTION_FILE="/tmp/claude-question-tmux-${TMUX_SESSION}.json"
|
||||
echo "$QUESTION_DATA" > "$TMUX_QUESTION_FILE" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Also emit a needs_input signal so the IDE transitions to needs-input state
|
||||
# This triggers the question polling in SessionCard
|
||||
if [[ -n "$TMUX_SESSION" ]]; then
|
||||
# Extract the first question text for the signal
|
||||
QUESTION_TEXT=$(echo "$TOOL_INFO" | jq -r '.tool_input.questions[0].question // "Question from agent"' 2>/dev/null || echo "Question from agent")
|
||||
QUESTION_TYPE=$(echo "$TOOL_INFO" | jq -r 'if .tool_input.questions[0].multiSelect then "multi-select" else "choice" end' 2>/dev/null || echo "choice")
|
||||
|
||||
# Get current task ID from JAT Tasks if available
|
||||
TASK_ID=""
|
||||
if command -v jt &>/dev/null && [[ -n "$AGENT_NAME" ]]; then
|
||||
TASK_ID=$(jt list --json 2>/dev/null | jq -r --arg agent "$AGENT_NAME" '.[] | select(.assignee == $agent and .status == "in_progress") | .id' 2>/dev/null | head -1 || echo "")
|
||||
fi
|
||||
|
||||
# Build signal data - use type: "state" and state: "needs_input"
|
||||
# This matches the format expected by the SSE server in +server.ts
|
||||
# which maps signal states using SIGNAL_STATE_MAP (needs_input -> needs-input)
|
||||
SIGNAL_DATA=$(jq -n -c \
|
||||
--arg state "needs_input" \
|
||||
--arg session_id "$SESSION_ID" \
|
||||
--arg tmux "$TMUX_SESSION" \
|
||||
--arg task_id "$TASK_ID" \
|
||||
--arg question "$QUESTION_TEXT" \
|
||||
--arg question_type "$QUESTION_TYPE" \
|
||||
'{
|
||||
type: "state",
|
||||
state: $state,
|
||||
session_id: $session_id,
|
||||
tmux_session: $tmux,
|
||||
timestamp: (now | todate),
|
||||
task_id: $task_id,
|
||||
data: {
|
||||
taskId: $task_id,
|
||||
question: $question,
|
||||
questionType: $question_type
|
||||
}
|
||||
}' 2>/dev/null || echo "{}")
|
||||
|
||||
# Write signal files
|
||||
echo "$SIGNAL_DATA" > "/tmp/jat-signal-${SESSION_ID}.json" 2>/dev/null || true
|
||||
echo "$SIGNAL_DATA" > "/tmp/jat-signal-tmux-${TMUX_SESSION}.json" 2>/dev/null || true
|
||||
|
||||
# Also append to timeline for history tracking (JSONL format)
|
||||
TIMELINE_FILE="/tmp/jat-timeline-${TMUX_SESSION}.jsonl"
|
||||
echo "$SIGNAL_DATA" >> "$TIMELINE_FILE" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
113
hooks/community/jat/pre-compact-save-agent.sh
Executable file
113
hooks/community/jat/pre-compact-save-agent.sh
Executable file
@@ -0,0 +1,113 @@
|
||||
#!/bin/bash
|
||||
# Pre-compact hook: Save current agent identity, workflow state, and terminal scrollback before compaction
|
||||
# This ensures we can restore identity, workflow context, AND terminal history after compaction
|
||||
#
|
||||
# Uses WINDOWID-based file - stable across /clear (unlike PPID which changes)
|
||||
# Each terminal window has unique WINDOWID, avoiding race conditions
|
||||
|
||||
PROJECT_DIR="$(pwd)"
|
||||
CLAUDE_DIR="$PROJECT_DIR/.claude"
|
||||
JAT_LOGS_DIR="$PROJECT_DIR/.jat/logs"
|
||||
|
||||
# Use WINDOWID for persistence (stable across /clear, unique per terminal)
|
||||
# Falls back to PPID if WINDOWID not available
|
||||
WINDOW_KEY="${WINDOWID:-$PPID}"
|
||||
PERSISTENT_AGENT_FILE="$CLAUDE_DIR/.agent-identity-${WINDOW_KEY}"
|
||||
PERSISTENT_STATE_FILE="$CLAUDE_DIR/.agent-workflow-state-${WINDOW_KEY}.json"
|
||||
|
||||
# Find the current session's agent file
|
||||
SESSION_ID=$(cat /tmp/claude-session-${PPID}.txt 2>/dev/null | tr -d '\n')
|
||||
# Use sessions/ subdirectory to keep .claude/ clean
|
||||
AGENT_FILE="$CLAUDE_DIR/sessions/agent-${SESSION_ID}.txt"
|
||||
|
||||
# Get tmux session name for signal file lookup and scrollback capture
|
||||
TMUX_SESSION=""
|
||||
if [[ -n "${TMUX:-}" ]]; then
|
||||
TMUX_SESSION=$(tmux display-message -p '#S' 2>/dev/null)
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# CAPTURE TERMINAL SCROLLBACK BEFORE COMPACTION
|
||||
# This preserves the pre-compaction terminal history that would otherwise be lost
|
||||
# Uses unified session log: .jat/logs/session-{sessionName}.log
|
||||
# ============================================================================
|
||||
if [[ -n "$TMUX_SESSION" ]]; then
|
||||
# Use the unified capture script
|
||||
CAPTURE_SCRIPT="$HOME/.local/bin/capture-session-log.sh"
|
||||
if [[ -x "$CAPTURE_SCRIPT" ]]; then
|
||||
PROJECT_DIR="$PROJECT_DIR" "$CAPTURE_SCRIPT" "$TMUX_SESSION" "compacted" 2>/dev/null || true
|
||||
echo "[PreCompact] Captured scrollback for $TMUX_SESSION (compacted)" >> "$CLAUDE_DIR/.agent-activity.log"
|
||||
else
|
||||
# Fallback: inline capture if script not found
|
||||
mkdir -p "$JAT_LOGS_DIR" 2>/dev/null
|
||||
LOG_FILE="$JAT_LOGS_DIR/session-${TMUX_SESSION}.log"
|
||||
TIMESTAMP=$(date -Iseconds)
|
||||
|
||||
SCROLLBACK=$(tmux capture-pane -t "$TMUX_SESSION" -p -S - -E - 2>/dev/null || true)
|
||||
if [[ -n "$SCROLLBACK" ]]; then
|
||||
# Add header if new file
|
||||
if [[ ! -f "$LOG_FILE" ]]; then
|
||||
echo "# Session Log: $TMUX_SESSION" > "$LOG_FILE"
|
||||
echo "# Created: $TIMESTAMP" >> "$LOG_FILE"
|
||||
echo "================================================================================" >> "$LOG_FILE"
|
||||
echo "" >> "$LOG_FILE"
|
||||
fi
|
||||
# Append scrollback with separator
|
||||
echo "$SCROLLBACK" >> "$LOG_FILE"
|
||||
echo "" >> "$LOG_FILE"
|
||||
echo "════════════════════════════════════════════════════════════════════════════════" >> "$LOG_FILE"
|
||||
echo "📦 CONTEXT COMPACTED at $TIMESTAMP" >> "$LOG_FILE"
|
||||
echo "════════════════════════════════════════════════════════════════════════════════" >> "$LOG_FILE"
|
||||
echo "" >> "$LOG_FILE"
|
||||
|
||||
echo "[PreCompact] Captured scrollback to: $LOG_FILE" >> "$CLAUDE_DIR/.agent-activity.log"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -f "$AGENT_FILE" ]]; then
|
||||
AGENT_NAME=$(cat "$AGENT_FILE" | tr -d '\n')
|
||||
|
||||
# Save agent name to window-specific location
|
||||
echo "$AGENT_NAME" > "$PERSISTENT_AGENT_FILE"
|
||||
|
||||
# Build workflow state JSON
|
||||
SIGNAL_STATE="unknown"
|
||||
TASK_ID=""
|
||||
TASK_TITLE=""
|
||||
|
||||
# Try to get last signal state from signal file
|
||||
SIGNAL_FILE="/tmp/jat-signal-tmux-${TMUX_SESSION}.json"
|
||||
if [[ -f "$SIGNAL_FILE" ]]; then
|
||||
# Signal file may have .state or .signalType depending on source
|
||||
SIGNAL_STATE=$(jq -r '.state // .signalType // .type // "unknown"' "$SIGNAL_FILE" 2>/dev/null)
|
||||
# Task ID may be in .task_id or .data.taskId
|
||||
TASK_ID=$(jq -r '.task_id // .data.taskId // .taskId // ""' "$SIGNAL_FILE" 2>/dev/null)
|
||||
TASK_TITLE=$(jq -r '.data.taskTitle // .taskTitle // ""' "$SIGNAL_FILE" 2>/dev/null)
|
||||
fi
|
||||
|
||||
# If no signal file, try to get task from JAT Tasks
|
||||
if [[ -z "$TASK_ID" ]] && command -v jt &>/dev/null; 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)
|
||||
fi
|
||||
fi
|
||||
|
||||
# Save workflow state
|
||||
cat > "$PERSISTENT_STATE_FILE" << EOF
|
||||
{
|
||||
"agentName": "$AGENT_NAME",
|
||||
"signalState": "$SIGNAL_STATE",
|
||||
"taskId": "$TASK_ID",
|
||||
"taskTitle": "$TASK_TITLE",
|
||||
"savedAt": "$(date -Iseconds)"
|
||||
}
|
||||
EOF
|
||||
|
||||
# Output marker for IDE state detection
|
||||
echo "[JAT:COMPACTING]"
|
||||
|
||||
# Log for debugging
|
||||
echo "[PreCompact] Saved agent: $AGENT_NAME, state: $SIGNAL_STATE, task: $TASK_ID (WINDOWID=$WINDOW_KEY)" >> "$CLAUDE_DIR/.agent-activity.log"
|
||||
fi
|
||||
276
hooks/community/jat/session-start-agent-identity.sh
Executable file
276
hooks/community/jat/session-start-agent-identity.sh
Executable file
@@ -0,0 +1,276 @@
|
||||
#!/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
|
||||
163
hooks/community/jat/session-start-restore-agent.sh
Executable file
163
hooks/community/jat/session-start-restore-agent.sh
Executable file
@@ -0,0 +1,163 @@
|
||||
#!/bin/bash
|
||||
# Session start hook: Restore agent identity and inject workflow context after compaction
|
||||
# This ensures the agent file exists AND the agent knows where it was in the workflow
|
||||
#
|
||||
# Uses WINDOWID-based file - stable across /clear (unlike PPID which changes)
|
||||
# Each terminal window has unique WINDOWID, avoiding race conditions
|
||||
|
||||
PROJECT_DIR="$(pwd)"
|
||||
CLAUDE_DIR="$PROJECT_DIR/.claude"
|
||||
|
||||
# Check if running inside tmux - agents require tmux for IDE tracking
|
||||
IN_TMUX=true
|
||||
if [[ -z "${TMUX:-}" ]] && ! tmux display-message -p '#S' &>/dev/null; then
|
||||
IN_TMUX=false
|
||||
fi
|
||||
|
||||
# Use WINDOWID for persistence (matches pre-compact hook)
|
||||
# Falls back to PPID if WINDOWID not available
|
||||
WINDOW_KEY="${WINDOWID:-$PPID}"
|
||||
PERSISTENT_AGENT_FILE="$CLAUDE_DIR/.agent-identity-${WINDOW_KEY}"
|
||||
PERSISTENT_STATE_FILE="$CLAUDE_DIR/.agent-workflow-state-${WINDOW_KEY}.json"
|
||||
|
||||
# Read session ID from stdin JSON (provided by Claude Code)
|
||||
INPUT=$(cat)
|
||||
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
|
||||
|
||||
if [[ -z "$SESSION_ID" ]]; then
|
||||
# No session ID - can't do anything
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Also update the PPID-based session file for other tools
|
||||
echo "$SESSION_ID" > "/tmp/claude-session-${PPID}.txt"
|
||||
|
||||
# Use sessions/ subdirectory to keep .claude/ clean
|
||||
mkdir -p "$CLAUDE_DIR/sessions"
|
||||
AGENT_FILE="$CLAUDE_DIR/sessions/agent-${SESSION_ID}.txt"
|
||||
|
||||
# Track if we restored or already had agent
|
||||
AGENT_NAME=""
|
||||
|
||||
# If agent file already exists for this session, read the name
|
||||
if [[ -f "$AGENT_FILE" ]]; then
|
||||
AGENT_NAME=$(cat "$AGENT_FILE" | tr -d '\n')
|
||||
fi
|
||||
|
||||
# Priority 1: Check for IDE-spawned agent identity (tmux session name based)
|
||||
# The IDE writes .claude/sessions/.tmux-agent-{tmuxSessionName} before spawning
|
||||
# This MUST be checked first because WINDOWID-based files persist across sessions
|
||||
if [[ -z "$AGENT_NAME" ]]; then
|
||||
# Get tmux session name (e.g., "jat-SwiftRiver")
|
||||
TMUX_SESSION=$(tmux display-message -p '#S' 2>/dev/null || echo "")
|
||||
if [[ -n "$TMUX_SESSION" ]]; then
|
||||
TMUX_AGENT_FILE="$CLAUDE_DIR/sessions/.tmux-agent-${TMUX_SESSION}"
|
||||
if [[ -f "$TMUX_AGENT_FILE" ]]; then
|
||||
AGENT_NAME=$(cat "$TMUX_AGENT_FILE" | tr -d '\n')
|
||||
|
||||
if [[ -n "$AGENT_NAME" ]]; then
|
||||
# Write the session ID-based agent file
|
||||
echo "$AGENT_NAME" > "$AGENT_FILE"
|
||||
|
||||
# Log for debugging
|
||||
echo "[SessionStart] Restored agent from tmux: $AGENT_NAME for session $SESSION_ID (tmux=$TMUX_SESSION)" >> "$CLAUDE_DIR/.agent-activity.log"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Priority 2: WINDOWID-based file (for compaction recovery in the same terminal)
|
||||
# Only used if tmux-based lookup didn't find anything
|
||||
if [[ -z "$AGENT_NAME" ]] && [[ -f "$PERSISTENT_AGENT_FILE" ]]; then
|
||||
AGENT_NAME=$(cat "$PERSISTENT_AGENT_FILE" | tr -d '\n')
|
||||
|
||||
if [[ -n "$AGENT_NAME" ]]; then
|
||||
# Restore the agent file for this new session ID
|
||||
echo "$AGENT_NAME" > "$AGENT_FILE"
|
||||
|
||||
# Ensure agent is registered in Agent Mail
|
||||
if command -v am-register &>/dev/null; then
|
||||
# Check if already registered
|
||||
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-4.5 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
# Log for debugging
|
||||
echo "[SessionStart] Restored agent: $AGENT_NAME for session $SESSION_ID (WINDOWID=$WINDOW_KEY)" >> "$CLAUDE_DIR/.agent-activity.log"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for saved workflow state and inject context reminder
|
||||
TASK_ID=""
|
||||
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)
|
||||
|
||||
# Only output context if we have meaningful state
|
||||
if [[ -n "$TASK_ID" ]]; then
|
||||
# Determine what signal should be emitted next based on last state
|
||||
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
|
||||
|
||||
# Output as compact marker for IDE + structured context for agent
|
||||
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 "========================================================="
|
||||
|
||||
echo "[SessionStart] Injected workflow context: state=$SIGNAL_STATE, task=$TASK_ID" >> "$CLAUDE_DIR/.agent-activity.log"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fallback: If no state file but agent has in_progress task, still output working marker
|
||||
if [[ -z "$TASK_ID" ]] && [[ -n "$AGENT_NAME" ]] && command -v jt &>/dev/null; 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 "[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 "=================================================="
|
||||
|
||||
echo "[SessionStart] Fallback context from JAT Tasks: task=$TASK_ID" >> "$CLAUDE_DIR/.agent-activity.log"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Warn if not in tmux - agents need tmux for IDE tracking
|
||||
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)"
|
||||
echo "Or: jat projectname 1 --claude"
|
||||
fi
|
||||
139
hooks/community/jat/user-prompt-signal.sh
Executable file
139
hooks/community/jat/user-prompt-signal.sh
Executable file
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# user-prompt-signal.sh - UserPromptSubmit hook for tracking user messages
|
||||
#
|
||||
# Fires when user submits a prompt to Claude Code.
|
||||
# Writes user_input event to timeline for IDE visibility.
|
||||
#
|
||||
# Input: JSON via stdin with format: {"session_id": "...", "prompt": "...", ...}
|
||||
# Output: Appends to /tmp/jat-timeline-{tmux-session}.jsonl
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Read JSON input from stdin
|
||||
HOOK_INPUT=$(cat)
|
||||
|
||||
# Skip empty input
|
||||
if [[ -z "$HOOK_INPUT" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Parse session_id and prompt from the JSON input
|
||||
SESSION_ID=$(echo "$HOOK_INPUT" | jq -r '.session_id // ""' 2>/dev/null || echo "")
|
||||
USER_PROMPT=$(echo "$HOOK_INPUT" | jq -r '.prompt // ""' 2>/dev/null || echo "")
|
||||
|
||||
# Skip empty prompts or missing session_id
|
||||
if [[ -z "$USER_PROMPT" ]] || [[ -z "$SESSION_ID" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Skip /jat:start commands - these cause a race condition where the event gets
|
||||
# written to the OLD agent's timeline before /jat:start updates the agent file.
|
||||
# The /jat:start command emits its own "starting" signal which is the proper
|
||||
# event for the new session.
|
||||
if [[ "$USER_PROMPT" =~ ^/jat:start ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Get tmux session name by looking up agent file from session_id
|
||||
# (Cannot use tmux display-message in subprocess - no TMUX env var)
|
||||
TMUX_SESSION=""
|
||||
|
||||
# Build list of directories to search: current dir + configured projects
|
||||
SEARCH_DIRS="."
|
||||
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 PROJECT_PATH in $PROJECT_PATHS; do
|
||||
if [[ -d "${PROJECT_PATH}/.claude" ]]; then
|
||||
SEARCH_DIRS="$SEARCH_DIRS $PROJECT_PATH"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
for BASE_DIR in $SEARCH_DIRS; do
|
||||
for SUBDIR in "sessions" ""; do
|
||||
if [[ -n "$SUBDIR" ]]; then
|
||||
AGENT_FILE="${BASE_DIR}/.claude/${SUBDIR}/agent-${SESSION_ID}.txt"
|
||||
else
|
||||
AGENT_FILE="${BASE_DIR}/.claude/agent-${SESSION_ID}.txt"
|
||||
fi
|
||||
if [[ -f "$AGENT_FILE" ]]; then
|
||||
AGENT_NAME=$(cat "$AGENT_FILE" 2>/dev/null | tr -d '\n')
|
||||
if [[ -n "$AGENT_NAME" ]]; then
|
||||
TMUX_SESSION="jat-${AGENT_NAME}"
|
||||
break 2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if [[ -z "$TMUX_SESSION" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Detect if the prompt contains an image (by checking for common image paths/patterns)
|
||||
# Image paths typically match: /path/to/file.(png|jpg|jpeg|gif|webp|svg)
|
||||
# Also check for task-images directory and upload patterns
|
||||
HAS_IMAGE="false"
|
||||
if [[ "$USER_PROMPT" =~ \.(png|jpg|jpeg|gif|webp|svg|PNG|JPG|JPEG|GIF|WEBP|SVG)($|[[:space:]]) ]] || \
|
||||
[[ "$USER_PROMPT" =~ task-images/ ]] || \
|
||||
[[ "$USER_PROMPT" =~ upload-.*\.(png|jpg|jpeg|gif|webp|svg) ]] || \
|
||||
[[ "$USER_PROMPT" =~ /tmp/.*\.(png|jpg|jpeg|gif|webp|svg) ]]; then
|
||||
HAS_IMAGE="true"
|
||||
fi
|
||||
|
||||
# Truncate long prompts for timeline display (keep first 500 chars)
|
||||
PROMPT_PREVIEW="${USER_PROMPT:0:500}"
|
||||
if [[ ${#USER_PROMPT} -gt 500 ]]; then
|
||||
PROMPT_PREVIEW="${PROMPT_PREVIEW}..."
|
||||
fi
|
||||
|
||||
# REMOVED: Task ID lookup from signal file
|
||||
# Previously we read task_id from /tmp/jat-signal-tmux-{session}.json, but this caused
|
||||
# signal leaking when a user started a new task - the user_input event would inherit
|
||||
# the OLD task_id from the previous signal file.
|
||||
#
|
||||
# User input events should not be associated with a specific task since they represent
|
||||
# what the user typed, which may be switching to a new task entirely (e.g., /jat:start).
|
||||
# The task context is better represented by subsequent agent signals that actually
|
||||
# emit the new task ID.
|
||||
TASK_ID=""
|
||||
|
||||
# Build event JSON
|
||||
EVENT_JSON=$(jq -c -n \
|
||||
--arg type "user_input" \
|
||||
--arg session "$SESSION_ID" \
|
||||
--arg tmux "$TMUX_SESSION" \
|
||||
--arg task "$TASK_ID" \
|
||||
--arg prompt "$PROMPT_PREVIEW" \
|
||||
--argjson hasImage "$HAS_IMAGE" \
|
||||
'{
|
||||
type: $type,
|
||||
session_id: $session,
|
||||
tmux_session: $tmux,
|
||||
task_id: $task,
|
||||
timestamp: (now | todate),
|
||||
data: {
|
||||
prompt: $prompt,
|
||||
hasImage: $hasImage
|
||||
}
|
||||
}' 2>/dev/null || echo "{}")
|
||||
|
||||
# Append to timeline log (JSONL format - preserves history)
|
||||
TIMELINE_FILE="/tmp/jat-timeline-${TMUX_SESSION}.jsonl"
|
||||
echo "$EVENT_JSON" >> "$TIMELINE_FILE" 2>/dev/null || true
|
||||
|
||||
# Start output monitor for real-time activity detection (shimmer effect)
|
||||
# Kill any existing monitor for this session first
|
||||
PID_FILE="/tmp/jat-monitor-${TMUX_SESSION}.pid"
|
||||
if [[ -f "$PID_FILE" ]]; then
|
||||
kill "$(cat "$PID_FILE")" 2>/dev/null || true
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
|
||||
# Start new monitor in background
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
nohup "$SCRIPT_DIR/monitor-output.sh" "$TMUX_SESSION" &>/dev/null &
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user