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)
197 lines
8.1 KiB
Bash
Executable file
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"
|