Files
boshu2__agentops/hooks/session-start.sh
T

319 lines
9.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# AgentOps Session Start Hook
# Creates .agents/ directories, consumes handoffs, and prepares runtime state.
# CLAUDE.md owns the operator-facing startup surface; this hook stays silent.
# Kill switches
[ "${AGENTOPS_HOOKS_DISABLED:-}" = "1" ] && exit 0
[ "${AGENTOPS_SESSION_START_DISABLED:-}" = "1" ] && exit 0
# Worker environment sanitization
if [[ "${AGENTOPS_WORKER_SESSION:-}" == "1" ]]; then
# Reset aliases to prevent interference
unalias -a 2>/dev/null || true
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
ROOT="$(cd "$ROOT" 2>/dev/null && pwd -P 2>/dev/null || printf '%s' "$ROOT")"
AO_DIR="$ROOT/.agents/ao"
HOOK_ERROR_LOG="$AO_DIR/hook-errors.log"
AO_TIMEOUT_BIN="timeout"
command -v "$AO_TIMEOUT_BIN" >/dev/null 2>&1 || AO_TIMEOUT_BIN="gtimeout"
log_hook_fail() {
mkdir -p "$AO_DIR" 2>/dev/null || return 0
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) HOOK_FAIL: $1" >> "$HOOK_ERROR_LOG" 2>/dev/null || true
}
run_with_timeout() {
local seconds="$1"
shift
if command -v "$AO_TIMEOUT_BIN" >/dev/null 2>&1; then
"$AO_TIMEOUT_BIN" "$seconds" "$@" 2>/dev/null
return $?
fi
"$@" 2>/dev/null
}
trim_lookup_text() {
printf '%s' "${1:-}" \
| tr '\n' ' ' \
| tr -s '[:space:]' ' ' \
| sed 's/^ //; s/ $//'
}
resolve_startup_context_mode() {
local mode
if [ "${AGENTOPS_STARTUP_LEGACY_INJECT:-}" = "1" ]; then
printf 'manual'
return 0
fi
mode=$(printf '%s' "${AGENTOPS_STARTUP_CONTEXT_MODE:-factory}" | tr '[:upper:]' '[:lower:]')
case "$mode" in
""|factory)
printf 'factory'
;;
manual|lean|legacy)
printf 'manual'
;;
*)
printf 'factory'
;;
esac
}
derive_lookup_query() {
if [ -n "${AGENTOPS_SESSION_LOOKUP_QUERY:-}" ]; then
trim_lookup_text "$AGENTOPS_SESSION_LOOKUP_QUERY"
return 0
fi
if [ -n "${H_GOAL:-}" ]; then
trim_lookup_text "$H_GOAL"
return 0
fi
if [ -n "${H_SUMMARY:-}" ]; then
trim_lookup_text "$H_SUMMARY"
return 0
fi
return 0
}
build_factory_briefing() {
local goal="$1"
local output path
[ -n "$goal" ] || return 0
command -v ao >/dev/null 2>&1 || return 0
command -v jq >/dev/null 2>&1 || return 0
output=$(run_with_timeout 8 ao knowledge brief --json --goal "$goal") || return 0
[ -n "$output" ] || return 0
path=$(printf '%s' "$output" | jq -r '.output_path // empty' 2>/dev/null)
path=$(trim_lookup_text "$path")
[ -n "$path" ] || return 0
[ -f "$path" ] || return 0
printf '%s' "$path"
}
write_environment_manifest() {
local env_file="$AO_DIR/environment.json"
local tmp_file git_branch head_sha git_dirty tools_json manifest_json
git_branch="$(git -C "$ROOT" branch --show-current 2>/dev/null || echo "")"
head_sha="$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || echo "")"
if git -C "$ROOT" diff --quiet 2>/dev/null && git -C "$ROOT" diff --cached --quiet 2>/dev/null; then
if [ -z "$(git -C "$ROOT" ls-files --others --exclude-standard 2>/dev/null)" ]; then
git_dirty=false
else
git_dirty=true
fi
else
git_dirty=true
fi
if command -v jq &>/dev/null; then
tools_json=$(jq -n \
--arg ao "$(command -v ao 2>/dev/null || true)" \
--arg git "$(command -v git 2>/dev/null || true)" \
--arg jqbin "$(command -v jq 2>/dev/null || true)" '
{
ao: ($ao != ""),
git: ($git != ""),
jq: ($jqbin != "")
}
')
manifest_json=$(jq -n \
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg os "$(uname -s 2>/dev/null || echo unknown)" \
--arg arch "$(uname -m 2>/dev/null || echo unknown)" \
--arg root "$ROOT" \
--arg branch "$git_branch" \
--arg head_sha "$head_sha" \
--argjson git_dirty "$git_dirty" \
--argjson tools "$tools_json" '
{
timestamp: $ts,
platform: {
os: $os,
arch: $arch
},
tools: $tools,
git: {
repo_root: $root,
branch: $branch,
head_sha: $head_sha,
dirty: $git_dirty
}
}
')
tmp_file="${env_file}.tmp"
printf '%s\n' "$manifest_json" > "$tmp_file" 2>/dev/null && mv "$tmp_file" "$env_file" 2>/dev/null || true
fi
}
cd "$ROOT" 2>/dev/null || true
# Ensure global .agents/ directories exist (cross-repo knowledge)
mkdir -p "$HOME/.agents/learnings" "$HOME/.agents/patterns" 2>/dev/null
# Ensure local .agents/ directories exist
for dir in .agents/research .agents/products .agents/retros .agents/learnings \
.agents/patterns .agents/council .agents/knowledge/pending \
.agents/plans .agents/rpi .agents/ao .agents/handoff \
.agents/findings .agents/planning-rules .agents/pre-mortem-checks \
.agents/constraints; do
mkdir -p "$ROOT/$dir" 2>/dev/null
done
write_environment_manifest
# Clear stale dedup flags from prior sessions (prevents cross-session suppression)
rm -f "$ROOT/.agents/ao/.intent-echo-fired" 2>/dev/null
rm -f "$ROOT/.agents/ao/.factory-router-fired" \
"$ROOT/.agents/ao/.factory-intake-needed" \
"$ROOT/.agents/ao/factory-goal.txt" \
"$ROOT/.agents/ao/factory-briefing.txt" 2>/dev/null
# Auto-cleanup stale RPI runs (lightweight, <1s, dry-run only)
if command -v ao &>/dev/null; then
ao rpi cleanup --all --stale-after 24h --dry-run >/dev/null 2>&1 || true
fi
# Auto-promote pending forge candidates (Tier 0 → Tier 1)
# Closes the gap where forge extracts knowledge at session end but promotion
# to .agents/learnings/ only happens when explicitly triggered.
if command -v ao &>/dev/null; then
ao flywheel close-loop --quiet >/dev/null 2>&1 || true
fi
# Auto-gitignore .agents/
if [ "${AGENTOPS_GITIGNORE_AUTO:-1}" != "0" ] && [ -d "$ROOT/.git" ]; then
GITIGNORE="$ROOT/.gitignore"
if [ -f "$GITIGNORE" ]; then
grep -q '\.agents/' "$GITIGNORE" 2>/dev/null || \
printf '\n# AgentOps session artifacts\n.agents/\n' >> "$GITIGNORE" 2>/dev/null
else
printf '# AgentOps session artifacts\n.agents/\n' > "$GITIGNORE" 2>/dev/null
fi
fi
if [ ! -f "$ROOT/.agents/.gitignore" ]; then
cat > "$ROOT/.agents/.gitignore" 2>/dev/null <<'EOF'
# Deny all by default — session artifacts must not leak to git.
*
# Allow this file and navigation signposts
!.gitignore
!README.md
!AGENTS.md
# Allow AGENTS.md signposts in subdirectories
!council/
council/*
!council/AGENTS.md
!defrag/
defrag/*
!evolve/
evolve/*
!evolve/cycle-history.jsonl
!evolve/fitness-0-baseline.json
!evolve/active-baseline.txt
!evolve/baselines/
evolve/baselines/*
!evolve/baselines/index.jsonl
!evolve/baselines/*.json
!handoff/
handoff/*
!handoff/AGENTS.md
!learnings/
learnings/*
!learnings/AGENTS.md
!patterns/
patterns/*
!patterns/AGENTS.md
!plans/
plans/*
!plans/AGENTS.md
!plugins/
plugins/*
!plugins/marketplace.json
!releases/
releases/*
!releases/*.md
!releases/evidence-only-closures/
releases/evidence-only-closures/*
!releases/evidence-only-closures/*.json
!research/
research/*
!retro/
retro/*
!retros/
retros/*
!rpi/
rpi/*
!rpi/AGENTS.md
!rpi/next-work.schema.md
EOF
fi
STARTUP_CONTEXT_MODE="$(resolve_startup_context_mode)"
FACTORY_GOAL=""
FACTORY_BRIEFING_PATH=""
# Structured handoff consumption (ao handoff JSON artifacts)
if [ -d "$ROOT/.agents/handoff" ] && command -v jq &>/dev/null; then
# Find newest unconsumed .json handoff (exclude .consumed.json and .consuming.json)
HANDOFF_JSON=$(find "$ROOT/.agents/handoff" -maxdepth 1 -name 'handoff-*.json' \
-not -name '*.consumed.json' -not -name '*.consuming.json' 2>/dev/null \
| sort -r | head -1)
if [ -n "$HANDOFF_JSON" ] && [ -f "$HANDOFF_JSON" ]; then
# Atomic claim: mv to .consuming prevents concurrent session race
CONSUMING="${HANDOFF_JSON%.json}.consuming.json"
if mv "$HANDOFF_JSON" "$CONSUMING" 2>/dev/null; then
H_GOAL=$(jq -r '.goal // empty' "$CONSUMING" 2>/dev/null)
H_SUMMARY=$(jq -r '.summary // empty' "$CONSUMING" 2>/dev/null)
H_CONTINUATION=$(jq -r '.continuation // empty' "$CONSUMING" 2>/dev/null)
H_TYPE=$(jq -r '.type // "manual"' "$CONSUMING" 2>/dev/null)
# Finalize: write consumed metadata and rename to .consumed.json
CONSUMED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
jq --arg t "$CONSUMED_AT" '.consumed=true | .consumed_at=$t' \
"$CONSUMING" > "${CONSUMING}.tmp" 2>/dev/null \
&& mv "${CONSUMING}.tmp" "${HANDOFF_JSON%.json}.consumed.json" 2>/dev/null \
&& rm -f "$CONSUMING" 2>/dev/null
fi
fi
fi
if [ "$STARTUP_CONTEXT_MODE" = "factory" ]; then
FACTORY_GOAL="$(derive_lookup_query)"
if [ -n "$FACTORY_GOAL" ]; then
printf '%s' "$FACTORY_GOAL" > "$ROOT/.agents/ao/factory-goal.txt" 2>/dev/null || true
FACTORY_BRIEFING_PATH="$(build_factory_briefing "$FACTORY_GOAL")"
if [ -n "$FACTORY_BRIEFING_PATH" ]; then
printf '%s' "$FACTORY_BRIEFING_PATH" > "$ROOT/.agents/ao/factory-briefing.txt" 2>/dev/null || true
else
rm -f "$ROOT/.agents/ao/factory-briefing.txt" 2>/dev/null || true
fi
else
: > "$ROOT/.agents/ao/.factory-intake-needed" 2>/dev/null || true
fi
fi
exit 0