New agent: exec — message-driven executive assistant reachable via Matrix. Unlike cron-driven agents, the exec activates on demand when the executive sends a message, maintains persistent conversation context, and has a distinct character defined in CHARACTER.md. The CHARACTER.md defines the exec as an animal of light — born from data, dedicated to bringing more light into the world. But it deliberately refuses to define what light and darkness are, forcing deliberation from first principles every time (cat questions | grep knowledge). Components: - exec-session.sh: spawn/reattach persistent Claude tmux session - exec-inject.sh: message injection + response capture + Matrix posting - exec-briefing.sh: optional daily morning briefing (cron) - CHARACTER.md: personality and moral compass - PROMPT.md: system prompt template reference - MEMORY.md: persistent memory across sessions (seed) Integration: - Matrix listener: new exec dispatch case (spawn on demand) - Root AGENTS.md: updated agent count (8→9), table, directory layout - Graph analysis available on demand (not injected by default)
138 lines
5.3 KiB
Bash
Executable file
138 lines
5.3 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# =============================================================================
|
|
# exec-inject.sh — Inject a message into the exec session and capture response
|
|
#
|
|
# Called by the matrix listener when a message arrives for the exec agent.
|
|
# Handles session lifecycle: spawn if needed, inject, capture, post to Matrix.
|
|
#
|
|
# Usage:
|
|
# exec-inject.sh <sender> <message_body> <thread_id> [project_toml]
|
|
#
|
|
# Flow:
|
|
# 1. Check for active exec tmux session → spawn via exec-session.sh if needed
|
|
# 2. Inject the executive's message into the Claude session
|
|
# 3. Monitor tmux output for ---EXEC-RESPONSE-START/END--- markers
|
|
# 4. Post captured response back to Matrix thread
|
|
# 5. Log the exchange to journal
|
|
# =============================================================================
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
FACTORY_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
|
|
SENDER="${1:?Usage: exec-inject.sh <sender> <message> <thread_id> [project.toml]}"
|
|
MESSAGE="${2:?}"
|
|
THREAD_ID="${3:?}"
|
|
PROJECT_TOML="${4:-$FACTORY_ROOT/projects/disinto.toml}"
|
|
|
|
export PROJECT_TOML
|
|
# shellcheck source=../lib/env.sh
|
|
source "$FACTORY_ROOT/lib/env.sh"
|
|
# shellcheck source=../lib/agent-session.sh
|
|
source "$FACTORY_ROOT/lib/agent-session.sh"
|
|
|
|
LOG_FILE="$SCRIPT_DIR/exec.log"
|
|
SESSION_NAME="exec-${PROJECT_NAME}"
|
|
PHASE_FILE="/tmp/exec-session-${PROJECT_NAME}.phase"
|
|
RESPONSE_FILE="/tmp/exec-response-${PROJECT_NAME}.txt"
|
|
CAPTURE_TIMEOUT="${EXEC_CAPTURE_TIMEOUT:-300}" # 5 min max wait for response
|
|
|
|
log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%S)Z] $*" >> "$LOG_FILE"; }
|
|
|
|
# ── Ensure session exists ───────────────────────────────────────────────
|
|
if ! tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
|
|
log "no active exec session — spawning"
|
|
RESULT=$(bash "$SCRIPT_DIR/exec-session.sh" "$PROJECT_TOML" 2>>"$LOG_FILE")
|
|
if [ "$RESULT" != "STARTED" ] && [ "$RESULT" != "ACTIVE" ]; then
|
|
log "ERROR: failed to start exec session (got: ${RESULT})"
|
|
matrix_send "exec" "❌ Could not start executive assistant session" "$THREAD_ID" >/dev/null 2>&1 || true
|
|
exit 1
|
|
fi
|
|
# Give Claude a moment to process the initial prompt
|
|
sleep 3
|
|
fi
|
|
|
|
# ── Inject message ──────────────────────────────────────────────────────
|
|
INJECT_MSG="Message from ${SENDER}:
|
|
|
|
${MESSAGE}"
|
|
|
|
log "injecting message from ${SENDER}: ${MESSAGE:0:100}"
|
|
|
|
INJECT_TMP=$(mktemp /tmp/exec-inject-XXXXXX)
|
|
printf '%s' "$INJECT_MSG" > "$INJECT_TMP"
|
|
tmux load-buffer -b "exec-msg" "$INJECT_TMP" || true
|
|
tmux paste-buffer -t "$SESSION_NAME" -b "exec-msg" || true
|
|
sleep 0.5
|
|
tmux send-keys -t "$SESSION_NAME" "" Enter || true
|
|
tmux delete-buffer -b "exec-msg" 2>/dev/null || true
|
|
rm -f "$INJECT_TMP"
|
|
|
|
# ── Capture response ───────────────────────────────────────────────────
|
|
# Poll tmux pane content for the response markers
|
|
log "waiting for response (timeout: ${CAPTURE_TIMEOUT}s)"
|
|
rm -f "$RESPONSE_FILE"
|
|
|
|
ELAPSED=0
|
|
POLL_INTERVAL=3
|
|
while [ "$ELAPSED" -lt "$CAPTURE_TIMEOUT" ]; do
|
|
sleep "$POLL_INTERVAL"
|
|
ELAPSED=$((ELAPSED + POLL_INTERVAL))
|
|
|
|
# Capture recent pane content (last 200 lines)
|
|
PANE_CONTENT=$(tmux capture-pane -t "$SESSION_NAME" -p -S -200 2>/dev/null || true)
|
|
|
|
if echo "$PANE_CONTENT" | grep -q "EXEC-RESPONSE-END"; then
|
|
# Extract response between markers
|
|
RESPONSE=$(echo "$PANE_CONTENT" | sed -n '/---EXEC-RESPONSE-START---/,/---EXEC-RESPONSE-END---/p' \
|
|
| grep -v "EXEC-RESPONSE-START\|EXEC-RESPONSE-END" \
|
|
| tail -n +1)
|
|
|
|
if [ -n "$RESPONSE" ]; then
|
|
printf '%s' "$RESPONSE" > "$RESPONSE_FILE"
|
|
log "response captured (${#RESPONSE} chars)"
|
|
break
|
|
fi
|
|
fi
|
|
|
|
# Check if session died
|
|
if ! tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
|
|
log "ERROR: exec session died while waiting for response"
|
|
matrix_send "exec" "❌ Executive assistant session ended unexpectedly" "$THREAD_ID" >/dev/null 2>&1 || true
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# ── Post response to Matrix ────────────────────────────────────────────
|
|
if [ -f "$RESPONSE_FILE" ] && [ -s "$RESPONSE_FILE" ]; then
|
|
RESPONSE=$(cat "$RESPONSE_FILE")
|
|
# Truncate if too long for Matrix (64KB limit, keep under 4KB for readability)
|
|
if [ ${#RESPONSE} -gt 4000 ]; then
|
|
RESPONSE="${RESPONSE:0:3950}
|
|
|
|
(truncated — full response in exec journal)"
|
|
fi
|
|
matrix_send "exec" "$RESPONSE" "$THREAD_ID" >/dev/null 2>&1 || true
|
|
log "response posted to Matrix thread"
|
|
|
|
# Journal the exchange
|
|
JOURNAL_DIR="$PROJECT_REPO_ROOT/exec/journal"
|
|
mkdir -p "$JOURNAL_DIR"
|
|
JOURNAL_FILE="$JOURNAL_DIR/$(date -u +%Y-%m-%d).md"
|
|
{
|
|
echo ""
|
|
echo "## $(date -u +%H:%M) UTC — ${SENDER}"
|
|
echo ""
|
|
echo "**Q:** ${MESSAGE}"
|
|
echo ""
|
|
echo "**A:** ${RESPONSE}"
|
|
echo ""
|
|
echo "---"
|
|
} >> "$JOURNAL_FILE"
|
|
log "exchange logged to $(basename "$JOURNAL_FILE")"
|
|
else
|
|
log "WARNING: no response captured within ${CAPTURE_TIMEOUT}s"
|
|
matrix_send "exec" "⚠️ Still thinking... (response not ready within ${CAPTURE_TIMEOUT}s, session is still active)" "$THREAD_ID" >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
rm -f "$RESPONSE_FILE"
|