feat: add exec agent — interactive executive assistant

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)
This commit is contained in:
disinto-exec 2026-03-25 15:28:29 +00:00
parent d442529ad0
commit d1ba4bc579
10 changed files with 802 additions and 3 deletions

138
exec/exec-inject.sh Executable file
View file

@ -0,0 +1,138 @@
#!/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"