disinto/exec/exec-session.sh
disinto-exec d1ba4bc579 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)
2026-03-25 15:28:29 +00:00

197 lines
8.1 KiB
Bash
Executable file

#!/usr/bin/env bash
# =============================================================================
# exec-session.sh — Spawn or reattach the executive assistant Claude session
#
# Unlike cron-driven agents, the exec session is on-demand:
# 1. Matrix listener receives a message tagged [exec]
# 2. If no tmux session exists → this script spawns one
# 3. Message is injected into the session
# 4. Claude's response is captured and posted back to Matrix
#
# Can also be invoked directly for interactive use:
# exec-session.sh [projects/disinto.toml]
#
# The session stays alive for EXEC_SESSION_TTL (default: 1h) of idle time.
# On exit, Claude updates MEMORY.md and the session is logged to journal.
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
FACTORY_ROOT="$(dirname "$SCRIPT_DIR")"
# Accept project config from argument; default to disinto
export PROJECT_TOML="${1:-$FACTORY_ROOT/projects/disinto.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"
EXEC_SESSION_TTL="${EXEC_SESSION_TTL:-3600}"
log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%S)Z] $*" >> "$LOG_FILE"; }
# ── Check if session already exists ──────────────────────────────────────
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
log "session already active: ${SESSION_NAME}"
echo "ACTIVE"
exit 0
fi
# ── Memory check (skip if low) ──────────────────────────────────────────
AVAIL_MB=$(free -m 2>/dev/null | awk '/Mem:/{print $7}' || echo 9999)
if [ "${AVAIL_MB:-0}" -lt 2000 ]; then
log "skipping — only ${AVAIL_MB}MB available (need 2000)"
exit 1
fi
log "--- Exec session start ---"
# ── Load character ──────────────────────────────────────────────────────
CHARACTER_FILE="${EXEC_CHARACTER:-$SCRIPT_DIR/CHARACTER.md}"
CHARACTER_BLOCK=""
if [ -f "$CHARACTER_FILE" ]; then
CHARACTER_BLOCK=$(cat "$CHARACTER_FILE")
else
log "WARNING: CHARACTER.md not found at ${CHARACTER_FILE}"
CHARACTER_BLOCK="(no character file found — use your best judgment)"
fi
# ── Load factory context ────────────────────────────────────────────────
CONTEXT_BLOCK=""
for ctx in VISION.md AGENTS.md RESOURCES.md; do
ctx_path="${PROJECT_REPO_ROOT}/${ctx}"
if [ -f "$ctx_path" ]; then
CONTEXT_BLOCK="${CONTEXT_BLOCK}
### ${ctx}
$(cat "$ctx_path")
"
fi
done
# ── Load exec memory ───────────────────────────────────────────────────
MEMORY_BLOCK="(no previous memory — this is the first conversation)"
MEMORY_FILE="$PROJECT_REPO_ROOT/exec/MEMORY.md"
if [ -f "$MEMORY_FILE" ]; then
MEMORY_BLOCK=$(cat "$MEMORY_FILE")
fi
# ── Load recent journal entries ─────────────────────────────────────────
JOURNAL_BLOCK=""
JOURNAL_DIR="$PROJECT_REPO_ROOT/exec/journal"
if [ -d "$JOURNAL_DIR" ]; then
JOURNAL_FILES=$(find "$JOURNAL_DIR" -name '*.md' -type f | sort -r | head -3)
if [ -n "$JOURNAL_FILES" ]; then
JOURNAL_BLOCK="
### Recent conversation logs (exec/journal/)
"
while IFS= read -r jf; do
JOURNAL_BLOCK="${JOURNAL_BLOCK}
#### $(basename "$jf")
$(head -100 "$jf")
"
done <<< "$JOURNAL_FILES"
fi
fi
# ── Load recent agent activity summary ──────────────────────────────────
ACTIVITY_BLOCK=""
# Last planner journal
PLANNER_LATEST=$(find "$PROJECT_REPO_ROOT/planner/journal" -name '*.md' -type f 2>/dev/null | sort -r | head -1)
if [ -n "$PLANNER_LATEST" ]; then
ACTIVITY_BLOCK="${ACTIVITY_BLOCK}
### Latest planner run ($(basename "$PLANNER_LATEST"))
$(tail -60 "$PLANNER_LATEST")
"
fi
# Last supervisor journal
SUPERVISOR_LATEST=$(find "$PROJECT_REPO_ROOT/supervisor/journal" -name '*.md' -type f 2>/dev/null | sort -r | head -1)
if [ -n "$SUPERVISOR_LATEST" ]; then
ACTIVITY_BLOCK="${ACTIVITY_BLOCK}
### Latest supervisor run ($(basename "$SUPERVISOR_LATEST"))
$(tail -40 "$SUPERVISOR_LATEST")
"
fi
# Merge activity into journal block
if [ -n "$ACTIVITY_BLOCK" ]; then
JOURNAL_BLOCK="${JOURNAL_BLOCK}${ACTIVITY_BLOCK}"
fi
# ── Build prompt ────────────────────────────────────────────────────────
# Read prompt template and expand variables
PROMPT="You are the executive assistant for ${FORGE_REPO}. Read your character definition carefully — it is who you are.
## Your character
${CHARACTER_BLOCK}
## Factory context
${CONTEXT_BLOCK}
## Your persistent memory
${MEMORY_BLOCK}
## Recent activity
${JOURNAL_BLOCK}
## Forge API reference
Base URL: ${FORGE_API}
Auth header: -H \"Authorization: token \${FORGE_TOKEN}\"
Read issue: curl -sf -H \"Authorization: token \${FORGE_TOKEN}\" '${FORGE_API}/issues/{number}' | jq '.body'
Create issue: curl -sf -X POST -H \"Authorization: token \${FORGE_TOKEN}\" -H 'Content-Type: application/json' '${FORGE_API}/issues' -d '{\"title\":\"...\",\"body\":\"...\",\"labels\":[LABEL_ID]}'
List labels: curl -sf -H \"Authorization: token \${FORGE_TOKEN}\" '${FORGE_API}/labels'
Comment: curl -sf -H \"Authorization: token \${FORGE_TOKEN}\" -X POST -H 'Content-Type: application/json' '${FORGE_API}/issues/{number}/comments' -d '{\"body\":\"...\"}'
Close: curl -sf -H \"Authorization: token \${FORGE_TOKEN}\" -X PATCH -H 'Content-Type: application/json' '${FORGE_API}/issues/{number}' -d '{\"state\":\"closed\"}'
NEVER echo or include the actual token value in output — always reference \${FORGE_TOKEN}.
## Structural analysis (on demand)
When the conversation calls for it — project health, bottlenecks, what to focus on:
# Fresh graph: python3 ${FACTORY_ROOT}/lib/build-graph.py --project-root ${PROJECT_REPO_ROOT} --output /tmp/${PROJECT_NAME}-graph-report.json
# Cached daily: cat /tmp/${PROJECT_NAME}-graph-report.json
The report contains orphans, cycles, thin_objectives, bottlenecks (betweenness centrality).
Reach for it when structural reasoning is what the question needs, not by default.
## Environment
FACTORY_ROOT=${FACTORY_ROOT}
PROJECT_REPO_ROOT=${PROJECT_REPO_ROOT}
PRIMARY_BRANCH=${PRIMARY_BRANCH}
PHASE_FILE=${PHASE_FILE}
## Response format
When responding to the executive, write your response between these markers:
\`\`\`
---EXEC-RESPONSE-START---
Your response here.
---EXEC-RESPONSE-END---
\`\`\`
This allows the output capture to extract and post your response to Matrix.
## Phase protocol
When the executive ends the conversation (says goodbye, done, etc.):
1. Update your memory: write to ${PROJECT_REPO_ROOT}/exec/MEMORY.md
2. Log the conversation: append to ${PROJECT_REPO_ROOT}/exec/journal/\$(date -u +%Y-%m-%d).md
3. Signal done: echo 'PHASE:done' > '${PHASE_FILE}'
On unrecoverable error:
printf 'PHASE:failed\nReason: %s\n' 'describe error' > '${PHASE_FILE}'
You are now live. Wait for the executive's first message."
# ── Create tmux session ─────────────────────────────────────────────────
rm -f "$PHASE_FILE"
log "Creating tmux session: ${SESSION_NAME}"
if ! create_agent_session "$SESSION_NAME" "$PROJECT_REPO_ROOT" "$PHASE_FILE"; then
log "ERROR: failed to create tmux session ${SESSION_NAME}"
exit 1
fi
# Inject prompt
agent_inject_into_session "$SESSION_NAME" "$PROMPT"
log "Prompt injected, session live"
# Notify via Matrix
matrix_send "exec" "Executive assistant session started for ${FORGE_REPO}. Ready for messages." 2>/dev/null || true
echo "STARTED"