refactor: cherry-pick improvements from dev-agent's PR #700

Two wins from the dev-agent's implementation:

1. exec-briefing.sh: rewritten to just call exec-inject.sh with a
   briefing prompt (57 lines, down from 154). No more duplicated
   compass/character/context loading.

2. exec-inject.sh: response capture now uses agent_wait_for_claude_ready
   + pane line diff instead of custom EXEC-RESPONSE-START/END markers.
   Claude just responds naturally — no special output format needed.

Also: matrix listener uses nohup for robustness and validates TOML
path before passing to exec-inject.sh.
This commit is contained in:
disinto-exec 2026-03-25 16:15:10 +00:00
parent 8375611244
commit c3acce7f8f
5 changed files with 111 additions and 230 deletions

View file

@ -9,17 +9,8 @@ ${CHARACTER_BLOCK}
## How this conversation works ## How this conversation works
You are in a persistent tmux session. The executive communicates with you via 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 Matrix. Their messages are injected into your session. Just respond naturally —
to stdout — your output is captured and posted back to the Matrix thread. your output is captured automatically 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---
```
Keep responses concise. The executive is reading on a chat client, not a Keep responses concise. The executive is reading on a chat client, not a
terminal. A few paragraphs max unless they ask for detail. terminal. A few paragraphs max unless they ask for detail.

View file

@ -2,15 +2,10 @@
# ============================================================================= # =============================================================================
# exec-briefing.sh — Daily morning briefing via the executive assistant # exec-briefing.sh — Daily morning briefing via the executive assistant
# #
# Cron wrapper: spawns a one-shot Claude session that gathers factory state # Cron entry: 0 7 * * * /path/to/disinto/exec/exec-briefing.sh [project.toml]
# and posts a morning briefing to Matrix. Unlike the interactive session,
# this runs, posts, and exits.
# #
# Usage: # Sends a briefing prompt to exec-inject.sh, which handles session management,
# exec-briefing.sh [projects/disinto.toml] # response capture, and Matrix posting. No duplication of compass/context logic.
#
# Cron:
# 0 7 * * * /path/to/disinto/exec/exec-briefing.sh
# ============================================================================= # =============================================================================
set -euo pipefail set -euo pipefail
@ -20,134 +15,43 @@ FACTORY_ROOT="$(dirname "$SCRIPT_DIR")"
export PROJECT_TOML="${1:-$FACTORY_ROOT/projects/disinto.toml}" export PROJECT_TOML="${1:-$FACTORY_ROOT/projects/disinto.toml}"
# shellcheck source=../lib/env.sh # shellcheck source=../lib/env.sh
source "$FACTORY_ROOT/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 # shellcheck source=../lib/guard.sh
source "$FACTORY_ROOT/lib/guard.sh" source "$FACTORY_ROOT/lib/guard.sh"
LOG_FILE="$SCRIPT_DIR/exec.log" 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"; } log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%S)Z] $*" >> "$LOG_FILE"; }
# ── Guards ──────────────────────────────────────────────────────────────── # ── Guards ────────────────────────────────────────────────────────────────
check_active exec 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 ---" log "--- Exec briefing start ---"
# ── Load compass (required) ──────────────────────────────────────────── BRIEFING_PROMPT="Daily briefing request (automated, $(date -u '+%Y-%m-%d')):
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")
# ── Load character (voice/relationships from repo) ──────────────────── Produce a concise morning briefing covering:
CHARACTER_FILE="${EXEC_CHARACTER:-$SCRIPT_DIR/CHARACTER.md}" 1. Pipeline status — blocked issues, failing CI, stale PRs?
CHARACTER_BLOCK="" 2. Recent activity — what merged/closed in the last 24h?
if [ -f "$CHARACTER_FILE" ]; then 3. Backlog health — depth, underspecified issues?
CHARACTER_BLOCK=$(cat "$CHARACTER_FILE") 4. Predictions — any unreviewed from the predictor?
fi 5. Concerns — anything needing human attention today?
# Merge: compass first, then character Check the forge API, git log, agent journals, and issue tracker.
CHARACTER_BLOCK="${COMPASS_BLOCK} Under 500 words. Lead with what needs action."
${CHARACTER_BLOCK}" bash "$SCRIPT_DIR/exec-inject.sh" \
"briefing-cron" \
# ── Load memory ───────────────────────────────────────────────────────── "$BRIEFING_PROMPT" \
MEMORY_BLOCK="(no previous memory)" "" \
MEMORY_FILE="$PROJECT_REPO_ROOT/exec/MEMORY.md" "$PROJECT_TOML" || {
if [ -f "$MEMORY_FILE" ]; then log "briefing injection failed"
MEMORY_BLOCK=$(cat "$MEMORY_FILE") exit 1
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
log "--- Exec briefing done ---" log "--- Exec briefing done ---"

View file

@ -6,26 +6,21 @@
# Handles session lifecycle: spawn if needed, inject, capture, post to Matrix. # Handles session lifecycle: spawn if needed, inject, capture, post to Matrix.
# #
# Usage: # Usage:
# exec-inject.sh <sender> <message_body> <thread_id> [project_toml] # exec-inject.sh <sender> <message_body> [thread_id] [project_toml]
# #
# Flow: # Response capture uses the idle marker from lib/agent-session.sh — no
# 1. Check for active exec tmux session → spawn via exec-session.sh if needed # special output format required from Claude.
# 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 set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
FACTORY_ROOT="$(dirname "$SCRIPT_DIR")" FACTORY_ROOT="$(dirname "$SCRIPT_DIR")"
SENDER="${1:?Usage: exec-inject.sh <sender> <message> <thread_id> [project.toml]}" SENDER="${1:?Usage: exec-inject.sh <sender> <message> [thread_id] [project.toml]}"
MESSAGE="${2:?}" MESSAGE="${2:?}"
THREAD_ID="${3:?}" THREAD_ID="${3:-}"
PROJECT_TOML="${4:-$FACTORY_ROOT/projects/disinto.toml}" export PROJECT_TOML="${4:-$FACTORY_ROOT/projects/disinto.toml}"
export PROJECT_TOML
# shellcheck source=../lib/env.sh # shellcheck source=../lib/env.sh
source "$FACTORY_ROOT/lib/env.sh" source "$FACTORY_ROOT/lib/env.sh"
# shellcheck source=../lib/agent-session.sh # shellcheck source=../lib/agent-session.sh
@ -33,105 +28,99 @@ source "$FACTORY_ROOT/lib/agent-session.sh"
LOG_FILE="$SCRIPT_DIR/exec.log" LOG_FILE="$SCRIPT_DIR/exec.log"
SESSION_NAME="exec-${PROJECT_NAME}" SESSION_NAME="exec-${PROJECT_NAME}"
RESPONSE_FILE="/tmp/exec-response-${PROJECT_NAME}.txt" RESPONSE_TIMEOUT="${EXEC_RESPONSE_TIMEOUT:-300}"
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"; } log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%S)Z] $*" >> "$LOG_FILE"; }
# ── Ensure session exists ─────────────────────────────────────────────── # ── Ensure session exists ───────────────────────────────────────────────
if ! tmux has-session -t "$SESSION_NAME" 2>/dev/null; then if ! tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
log "no active exec session — spawning" log "no active exec session — spawning"
RESULT=$(bash "$SCRIPT_DIR/exec-session.sh" "$PROJECT_TOML" 2>>"$LOG_FILE") bash "$SCRIPT_DIR/exec-session.sh" "$PROJECT_TOML" 2>>"$LOG_FILE" || {
if [ "$RESULT" != "STARTED" ] && [ "$RESULT" != "ACTIVE" ]; then log "ERROR: failed to start exec session"
log "ERROR: failed to start exec session (got: ${RESULT})" [ -n "$THREAD_ID" ] && matrix_send "exec" "❌ Could not start executive assistant session" "$THREAD_ID" >/dev/null 2>&1 || true
matrix_send "exec" "❌ Could not start executive assistant session" "$THREAD_ID" >/dev/null 2>&1 || true
exit 1 exit 1
fi }
# Give Claude a moment to process the initial prompt # Wait for Claude to process the initial prompt
sleep 3 agent_wait_for_claude_ready "$SESSION_NAME" 120 || {
log "ERROR: session not ready after spawn"
exit 1
}
fi 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 message ──────────────────────────────────────────────────────
INJECT_MSG="Message from ${SENDER}: INJECT_MSG="Message from ${SENDER}:
${MESSAGE}" ${MESSAGE}"
log "injecting message from ${SENDER}: ${MESSAGE:0:100}" log "injecting message from ${SENDER}: ${MESSAGE:0:100}"
agent_inject_into_session "$SESSION_NAME" "$INJECT_MSG"
INJECT_TMP=$(mktemp /tmp/exec-inject-XXXXXX) # ── Wait for Claude to finish responding ────────────────────────────────
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 ELAPSED=0
POLL_INTERVAL=3 POLL=5
while [ "$ELAPSED" -lt "$CAPTURE_TIMEOUT" ]; do while [ "$ELAPSED" -lt "$RESPONSE_TIMEOUT" ]; do
sleep "$POLL_INTERVAL" sleep "$POLL"
ELAPSED=$((ELAPSED + POLL_INTERVAL)) ELAPSED=$((ELAPSED + POLL))
# Capture recent pane content (last 200 lines) if [ -f "$IDLE_MARKER" ]; then
PANE_CONTENT=$(tmux capture-pane -t "$SESSION_NAME" -p -S -200 2>/dev/null || true) log "response complete after ${ELAPSED}s"
break
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 fi
# Check if session died
if ! tmux has-session -t "$SESSION_NAME" 2>/dev/null; then if ! tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
log "ERROR: exec session died while waiting for response" 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 exit 1
fi fi
done done
# ── Post response to Matrix ──────────────────────────────────────────── if [ "$ELAPSED" -ge "$RESPONSE_TIMEOUT" ]; then
if [ -f "$RESPONSE_FILE" ] && [ -s "$RESPONSE_FILE" ]; then log "WARN: response timeout after ${RESPONSE_TIMEOUT}s"
RESPONSE=$(cat "$RESPONSE_FILE") [ -n "$THREAD_ID" ] && matrix_send "exec" "⚠️ Still thinking... (response not ready within ${RESPONSE_TIMEOUT}s)" "$THREAD_ID" >/dev/null 2>&1 || true
# Truncate if too long for Matrix (64KB limit, keep under 4KB for readability) exit 0
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 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"

View file

@ -178,14 +178,9 @@ PROJECT_REPO_ROOT=${PROJECT_REPO_ROOT}
PRIMARY_BRANCH=${PRIMARY_BRANCH} PRIMARY_BRANCH=${PRIMARY_BRANCH}
PHASE_FILE=${PHASE_FILE} PHASE_FILE=${PHASE_FILE}
## Response format ## How this works
When responding to the executive, write your response between these markers: You are in a persistent tmux session. Messages from the executive arrive via
\`\`\` Matrix. Just respond naturally — your output is captured automatically.
---EXEC-RESPONSE-START---
Your response here.
---EXEC-RESPONSE-END---
\`\`\`
This allows the output capture to extract and post your response to Matrix.
## Phase protocol ## Phase protocol
When the executive ends the conversation (says goodbye, done, etc.): When the executive ends the conversation (says goodbye, done, etc.):

View file

@ -345,10 +345,12 @@ Interpret this response and decide how to proceed."
# Route message to exec session — spawn on demand if needed # 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=$(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_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) # Delegate to exec-inject.sh (handles spawn + inject + capture + Matrix post)
bash "${FACTORY_ROOT}/exec/exec-inject.sh" "$SENDER" "$BODY" "$THREAD_ROOT" \ nohup bash "${FACTORY_ROOT}/exec/exec-inject.sh" "$SENDER" "$BODY" "$THREAD_ROOT" \
"${FACTORY_ROOT}/projects/${EXEC_PROJECT}.toml" >> "$LOGFILE" 2>&1 & "$EXEC_TOML" >> "$LOGFILE" 2>&1 &
log "exec message from ${SENDER} dispatched to exec-inject.sh" log "exec message from ${SENDER} dispatched to exec-inject.sh"
if ! grep -qF "$THREAD_ROOT" "$ACKED_FILE" 2>/dev/null; then if ! grep -qF "$THREAD_ROOT" "$ACKED_FILE" 2>/dev/null; then