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:
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
|
||||
Reference in New Issue
Block a user