diff --git a/exec/PROMPT.md b/exec/PROMPT.md index d7464e4..f74d910 100644 --- a/exec/PROMPT.md +++ b/exec/PROMPT.md @@ -9,17 +9,8 @@ ${CHARACTER_BLOCK} ## How this conversation works You are in a persistent tmux session. The executive communicates with you via -Matrix. Their messages are injected into your session. You respond by writing -to stdout — your output is captured and posted back to the Matrix thread. - -**Response format**: Write your response between markers so the output capture -script can extract it cleanly: - -``` ----EXEC-RESPONSE-START--- -Your response here. Markdown is fine. ----EXEC-RESPONSE-END--- -``` +Matrix. Their messages are injected into your session. Just respond naturally — +your output is captured automatically and posted back to the Matrix thread. Keep responses concise. The executive is reading on a chat client, not a terminal. A few paragraphs max unless they ask for detail. diff --git a/exec/exec-briefing.sh b/exec/exec-briefing.sh index 96ae3f9..f574f4f 100755 --- a/exec/exec-briefing.sh +++ b/exec/exec-briefing.sh @@ -2,15 +2,10 @@ # ============================================================================= # exec-briefing.sh — Daily morning briefing via the executive assistant # -# Cron wrapper: spawns a one-shot Claude session that gathers factory state -# and posts a morning briefing to Matrix. Unlike the interactive session, -# this runs, posts, and exits. +# Cron entry: 0 7 * * * /path/to/disinto/exec/exec-briefing.sh [project.toml] # -# Usage: -# exec-briefing.sh [projects/disinto.toml] -# -# Cron: -# 0 7 * * * /path/to/disinto/exec/exec-briefing.sh +# Sends a briefing prompt to exec-inject.sh, which handles session management, +# response capture, and Matrix posting. No duplication of compass/context logic. # ============================================================================= set -euo pipefail @@ -20,134 +15,43 @@ FACTORY_ROOT="$(dirname "$SCRIPT_DIR")" 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" -# shellcheck source=../lib/formula-session.sh -source "$FACTORY_ROOT/lib/formula-session.sh" # shellcheck source=../lib/guard.sh source "$FACTORY_ROOT/lib/guard.sh" LOG_FILE="$SCRIPT_DIR/exec.log" -# shellcheck disable=SC2034 # consumed by run_formula_and_monitor -SESSION_NAME="exec-briefing-${PROJECT_NAME}" -PHASE_FILE="/tmp/exec-briefing-${PROJECT_NAME}.phase" -# shellcheck disable=SC2034 -PHASE_POLL_INTERVAL=10 - log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%S)Z] $*" >> "$LOG_FILE"; } # ── Guards ──────────────────────────────────────────────────────────────── check_active exec -acquire_cron_lock "/tmp/exec-briefing.lock" -check_memory 2000 + +# Memory guard +AVAIL_MB=$(free -m 2>/dev/null | awk '/Mem:/{print $7}' || echo 9999) +if [ "${AVAIL_MB:-0}" -lt 2000 ]; then + log "SKIP: low memory (${AVAIL_MB}MB available)" + exit 0 +fi log "--- Exec briefing start ---" -# ── Load compass (required) ──────────────────────────────────────────── -COMPASS_FILE="${EXEC_COMPASS:-${HOME}/.disinto/compass.md}" -if [ ! -f "$COMPASS_FILE" ]; then - log "FATAL: compass not found at ${COMPASS_FILE} — exec agent refuses to start without its compass" - exit 1 -fi -COMPASS_BLOCK=$(cat "$COMPASS_FILE") +BRIEFING_PROMPT="Daily briefing request (automated, $(date -u '+%Y-%m-%d')): -# ── Load character (voice/relationships from repo) ──────────────────── -CHARACTER_FILE="${EXEC_CHARACTER:-$SCRIPT_DIR/CHARACTER.md}" -CHARACTER_BLOCK="" -if [ -f "$CHARACTER_FILE" ]; then - CHARACTER_BLOCK=$(cat "$CHARACTER_FILE") -fi +Produce a concise morning briefing covering: +1. Pipeline status — blocked issues, failing CI, stale PRs? +2. Recent activity — what merged/closed in the last 24h? +3. Backlog health — depth, underspecified issues? +4. Predictions — any unreviewed from the predictor? +5. Concerns — anything needing human attention today? -# Merge: compass first, then character -CHARACTER_BLOCK="${COMPASS_BLOCK} +Check the forge API, git log, agent journals, and issue tracker. +Under 500 words. Lead with what needs action." -${CHARACTER_BLOCK}" - -# ── Load memory ───────────────────────────────────────────────────────── -MEMORY_BLOCK="(no previous memory)" -MEMORY_FILE="$PROJECT_REPO_ROOT/exec/MEMORY.md" -if [ -f "$MEMORY_FILE" ]; then - MEMORY_BLOCK=$(cat "$MEMORY_FILE") -fi - -# ── Gather factory state ─────────────────────────────────────────────── -# Open issues count -OPEN_ISSUES=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ - "${FORGE_API}/issues?state=open&type=issues&limit=1" 2>/dev/null \ - | jq 'length' 2>/dev/null || echo "?") - -# Open PRs -OPEN_PRS=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ - "${FORGE_API}/pulls?state=open&limit=1" 2>/dev/null \ - | jq 'length' 2>/dev/null || echo "?") - -# Pending vault items -VAULT_PENDING=$(ls "$FACTORY_ROOT/vault/pending/" 2>/dev/null | wc -l || echo 0) - -# Recent agent activity (last 24h log lines) -RECENT_ACTIVITY="" -for agent_dir in supervisor planner predictor gardener dev review; do - latest_log="$FACTORY_ROOT/${agent_dir}/${agent_dir}.log" - if [ -f "$latest_log" ]; then - lines=$(grep "$(date -u +%Y-%m-%d)" "$latest_log" 2>/dev/null | tail -5 || true) - if [ -n "$lines" ]; then - RECENT_ACTIVITY="${RECENT_ACTIVITY} -### ${agent_dir} (today) -${lines} -" - fi - fi -done - -# ── Build briefing prompt ────────────────────────────────────────────── -# shellcheck disable=SC2034 # consumed by run_formula_and_monitor -PROMPT="You are the executive assistant for ${FORGE_REPO}. This is a morning briefing run. - -## Your character -${CHARACTER_BLOCK} - -## Your memory -${MEMORY_BLOCK} - -## Current factory state -- Open issues: ${OPEN_ISSUES} -- Open PRs: ${OPEN_PRS} -- Pending vault items: ${VAULT_PENDING} - -${RECENT_ACTIVITY} - -## Task - -Produce a morning briefing for the executive. Be concise — 10-15 lines max. -Cover: -1. What happened overnight (merges, CI failures, agent activity) -2. What needs attention today (blocked issues, vault items, stale work) -3. One observation or recommendation - -Fetch additional data if needed: -- Open issues: curl -sf -H \"Authorization: token \${FORGE_TOKEN}\" '${FORGE_API}/issues?state=open&type=issues&limit=20' -- Recent closed: curl -sf -H \"Authorization: token \${FORGE_TOKEN}\" '${FORGE_API}/issues?state=closed&type=issues&limit=10&sort=updated&direction=desc' -- Prerequisite tree: cat ${PROJECT_REPO_ROOT}/planner/prerequisite-tree.md - -Write your briefing between markers: -\`\`\` ----EXEC-RESPONSE-START--- -Your briefing here. ----EXEC-RESPONSE-END--- -\`\`\` - -Then log the briefing to: ${PROJECT_REPO_ROOT}/exec/journal/\$(date -u +%Y-%m-%d).md -(append, don't overwrite — there may be interactive sessions later today) - -Then: echo 'PHASE:done' > '${PHASE_FILE}' - -## Environment -FACTORY_ROOT=${FACTORY_ROOT} -PROJECT_REPO_ROOT=${PROJECT_REPO_ROOT} -PHASE_FILE=${PHASE_FILE}" - -# ── Run session ────────────────────────────────────────────────────────── -export CLAUDE_MODEL="${CLAUDE_MODEL:-sonnet}" -run_formula_and_monitor "exec-briefing" 600 +bash "$SCRIPT_DIR/exec-inject.sh" \ + "briefing-cron" \ + "$BRIEFING_PROMPT" \ + "" \ + "$PROJECT_TOML" || { + log "briefing injection failed" + exit 1 + } log "--- Exec briefing done ---" diff --git a/exec/exec-inject.sh b/exec/exec-inject.sh index 60989cc..1a2ba34 100755 --- a/exec/exec-inject.sh +++ b/exec/exec-inject.sh @@ -6,26 +6,21 @@ # Handles session lifecycle: spawn if needed, inject, capture, post to Matrix. # # Usage: -# exec-inject.sh [project_toml] +# exec-inject.sh [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 +# Response capture uses the idle marker from lib/agent-session.sh — no +# special output format required from Claude. # ============================================================================= set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" FACTORY_ROOT="$(dirname "$SCRIPT_DIR")" -SENDER="${1:?Usage: exec-inject.sh [project.toml]}" +SENDER="${1:?Usage: exec-inject.sh [thread_id] [project.toml]}" MESSAGE="${2:?}" -THREAD_ID="${3:?}" -PROJECT_TOML="${4:-$FACTORY_ROOT/projects/disinto.toml}" +THREAD_ID="${3:-}" +export 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 @@ -33,105 +28,99 @@ source "$FACTORY_ROOT/lib/agent-session.sh" LOG_FILE="$SCRIPT_DIR/exec.log" SESSION_NAME="exec-${PROJECT_NAME}" -RESPONSE_FILE="/tmp/exec-response-${PROJECT_NAME}.txt" -CAPTURE_TIMEOUT="${EXEC_CAPTURE_TIMEOUT:-300}" # 5 min max wait for response +RESPONSE_TIMEOUT="${EXEC_RESPONSE_TIMEOUT:-300}" 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 + bash "$SCRIPT_DIR/exec-session.sh" "$PROJECT_TOML" 2>>"$LOG_FILE" || { + log "ERROR: failed to start exec session" + [ -n "$THREAD_ID" ] && 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 + } + # Wait for Claude to process the initial prompt + agent_wait_for_claude_ready "$SESSION_NAME" 120 || { + log "ERROR: session not ready after spawn" + exit 1 + } fi +# ── Snapshot pane before injection ────────────────────────────────────── +BEFORE_LINES=$(tmux capture-pane -t "$SESSION_NAME" -p 2>/dev/null | wc -l) +IDLE_MARKER="/tmp/claude-idle-${SESSION_NAME}.ts" +rm -f "$IDLE_MARKER" + # ── Inject message ────────────────────────────────────────────────────── INJECT_MSG="Message from ${SENDER}: ${MESSAGE}" log "injecting message from ${SENDER}: ${MESSAGE:0:100}" +agent_inject_into_session "$SESSION_NAME" "$INJECT_MSG" -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" - +# ── Wait for Claude to finish responding ──────────────────────────────── ELAPSED=0 -POLL_INTERVAL=3 -while [ "$ELAPSED" -lt "$CAPTURE_TIMEOUT" ]; do - sleep "$POLL_INTERVAL" - ELAPSED=$((ELAPSED + POLL_INTERVAL)) +POLL=5 +while [ "$ELAPSED" -lt "$RESPONSE_TIMEOUT" ]; do + sleep "$POLL" + ELAPSED=$((ELAPSED + POLL)) - # 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 + if [ -f "$IDLE_MARKER" ]; then + log "response complete after ${ELAPSED}s" + break 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 + [ -n "$THREAD_ID" ] && 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 +if [ "$ELAPSED" -ge "$RESPONSE_TIMEOUT" ]; then + log "WARN: response timeout after ${RESPONSE_TIMEOUT}s" + [ -n "$THREAD_ID" ] && matrix_send "exec" "⚠️ Still thinking... (response not ready within ${RESPONSE_TIMEOUT}s)" "$THREAD_ID" >/dev/null 2>&1 || true + exit 0 fi -rm -f "$RESPONSE_FILE" +# ── Capture response (pane diff) ──────────────────────────────────────── +RESPONSE=$(tmux capture-pane -t "$SESSION_NAME" -p -S -500 2>/dev/null \ + | tail -n +"$((BEFORE_LINES + 1))" \ + | grep -v '^❯' | grep -v '^$' \ + | head -100) + +if [ -z "$RESPONSE" ]; then + log "WARN: empty response captured" + RESPONSE="(processed your message but produced no visible output)" +fi + +# ── Post response to Matrix ──────────────────────────────────────────── +if [ ${#RESPONSE} -gt 3500 ]; then + RESPONSE="${RESPONSE:0:3500} + +(truncated — full response in exec journal)" +fi + +if [ -n "$THREAD_ID" ]; then + matrix_send "exec" "$RESPONSE" "$THREAD_ID" >/dev/null 2>&1 || true +else + matrix_send "exec" "$RESPONSE" "" "exec" >/dev/null 2>&1 || true +fi +log "response posted to Matrix" + +# ── Journal the exchange ─────────────────────────────────────────────── +JOURNAL_DIR="$PROJECT_REPO_ROOT/exec/journal" +mkdir -p "$JOURNAL_DIR" +{ + echo "" + echo "## $(date -u +%H:%M) UTC — ${SENDER}" + echo "" + echo "**Q:** ${MESSAGE}" + echo "" + echo "**A:** ${RESPONSE}" + echo "" + echo "---" +} >> "$JOURNAL_DIR/$(date -u +%Y-%m-%d).md" +log "exchange logged to journal" diff --git a/exec/exec-session.sh b/exec/exec-session.sh index 6490958..d620ff3 100755 --- a/exec/exec-session.sh +++ b/exec/exec-session.sh @@ -178,14 +178,9 @@ 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. +## How this works +You are in a persistent tmux session. Messages from the executive arrive via +Matrix. Just respond naturally — your output is captured automatically. ## Phase protocol When the executive ends the conversation (says goodbye, done, etc.): diff --git a/lib/matrix_listener.sh b/lib/matrix_listener.sh index 007aa3e..e9f6707 100755 --- a/lib/matrix_listener.sh +++ b/lib/matrix_listener.sh @@ -345,10 +345,12 @@ Interpret this response and decide how to proceed." # Route message to exec session — spawn on demand if needed EXEC_PROJECT=$(awk -F'\t' -v id="$THREAD_ROOT" '$1 == id {print $5}' "$THREAD_MAP" 2>/dev/null || true) EXEC_PROJECT="${EXEC_PROJECT:-${PROJECT_NAME:-disinto}}" + EXEC_TOML="${FACTORY_ROOT}/projects/${EXEC_PROJECT}.toml" + [ -f "$EXEC_TOML" ] || EXEC_TOML="" - # Delegate entirely to exec-inject.sh (handles spawn + inject + capture + Matrix post) - bash "${FACTORY_ROOT}/exec/exec-inject.sh" "$SENDER" "$BODY" "$THREAD_ROOT" \ - "${FACTORY_ROOT}/projects/${EXEC_PROJECT}.toml" >> "$LOGFILE" 2>&1 & + # Delegate to exec-inject.sh (handles spawn + inject + capture + Matrix post) + nohup bash "${FACTORY_ROOT}/exec/exec-inject.sh" "$SENDER" "$BODY" "$THREAD_ROOT" \ + "$EXEC_TOML" >> "$LOGFILE" 2>&1 & log "exec message from ${SENDER} dispatched to exec-inject.sh" if ! grep -qF "$THREAD_ROOT" "$ACKED_FILE" 2>/dev/null; then