disinto/exec/exec-briefing.sh
disinto-exec 5c1c91bae2 refactor: extract compass from CHARACTER.md into runtime-loaded secret
The compass (identity, moral core) now lives outside the repo at a path
specified by EXEC_COMPASS in .env or .env.enc. The agent hard-fails if
the compass file is missing — it refuses to start without its soul.

This means the factory (dev agent, gardener, planner) can evolve the
exec's voice and relationships via PRs to CHARACTER.md, but cannot
touch the compass. Only the executive controls it directly.

- exec-session.sh: loads compass from $EXEC_COMPASS, merges with CHARACTER.md
- exec-briefing.sh: same compass loading, hard fail without it
- CHARACTER.md: compass sections replaced with runtime-load comments
- COMPASS.md.example: template for the compass file
- .env.example: added EXEC_COMPASS variable
- exec/AGENTS.md: documented compass separation and EXEC_COMPASS requirement
2026-03-25 15:34:55 +00:00

153 lines
5.7 KiB
Bash
Executable file

#!/usr/bin/env bash
# =============================================================================
# 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.
#
# Usage:
# exec-briefing.sh [projects/disinto.toml]
#
# Cron:
# 0 7 * * * /path/to/disinto/exec/exec-briefing.sh
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
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
log "--- Exec briefing start ---"
# ── Load compass (required) ────────────────────────────────────────────
COMPASS_FILE="${EXEC_COMPASS:-}"
if [ -z "$COMPASS_FILE" ] || [ ! -f "$COMPASS_FILE" ]; then
log "FATAL: EXEC_COMPASS not set or file not found — exec agent refuses to start without its compass"
exit 1
fi
COMPASS_BLOCK=$(cat "$COMPASS_FILE")
# ── 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
# Merge: compass first, then character
CHARACTER_BLOCK="${COMPASS_BLOCK}
${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
log "--- Exec briefing done ---"