mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
319 lines
9.6 KiB
Bash
Executable File
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
|