mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
Merge branch 'codex/claude-factory-native' into codex/merge-recent-branches
# Conflicts: # cli/README.md # docs/workflows/session-lifecycle.md
This commit is contained in:
@@ -194,6 +194,17 @@ Research → Plan → Implement → Validate
|
||||
└──── Knowledge Flywheel ────┘
|
||||
```
|
||||
|
||||
## Claude Code Startup Surface
|
||||
|
||||
`CLAUDE.md` is the startup surface in Claude Code. Do not expect `SessionStart`
|
||||
or first-prompt hooks to inject briefings into the conversation.
|
||||
|
||||
- Use the goal stated in the user prompt or recovered handoff as the working objective.
|
||||
- If you want the full software-factory lane, run `/rpi "goal"` explicitly.
|
||||
- If you want a compiled goal-time briefing first, run `ao knowledge brief --goal "goal"`.
|
||||
- Treat `.agents/ao/factory-goal.txt` and `.agents/ao/factory-briefing.txt` as
|
||||
silent runtime state, not operator-facing instructions.
|
||||
|
||||
### Session & Swarm Constraints
|
||||
|
||||
- **Multi-phase work:** Route through `ao rpi` — it enforces 90-min phase timeouts and 10-min stall detection. Raw sessions have neither.
|
||||
|
||||
@@ -87,6 +87,11 @@ Or install via [release binaries](https://github.com/boshu2/agentops/releases) o
|
||||
|
||||
Then type `/quickstart` in your agent chat.
|
||||
|
||||
In Claude Code, `CLAUDE.md` is the startup surface. Installed hooks stay
|
||||
silent: `SessionStart` prepares runtime state and can stage factory goal or
|
||||
briefing files, while `UserPromptSubmit` can capture first-prompt intake
|
||||
without injecting additional context into the session.
|
||||
|
||||
---
|
||||
|
||||
## Start Here
|
||||
|
||||
@@ -44,8 +44,19 @@ ao rpi phased "fix auth startup"
|
||||
If you prefer the skill-first path, use `/rpi "fix auth startup"` after
|
||||
`ao factory start`.
|
||||
|
||||
That's it. In Claude Code, `CLAUDE.md` remains the startup surface. The
|
||||
installed hooks stay silent and only prepare runtime state for the factory lane.
|
||||
|
||||
## Operator Surfaces
|
||||
|
||||
**SessionStart**: Performs startup maintenance, recovers handoff state, and can
|
||||
stage `factory-goal.txt` / `factory-briefing.txt` without injecting context.
|
||||
|
||||
**UserPromptSubmit**: When startup lacked a goal, the first substantive prompt
|
||||
can be captured as silent factory intake and staged for later explicit use.
|
||||
|
||||
**SessionEnd**: Extracts learnings and updates the feedback loop.
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `ao factory start --goal "<goal>"` | Compile briefing-first startup context, then run explicit Codex start |
|
||||
|
||||
Executable
+146
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env bash
|
||||
# factory-router.sh - UserPromptSubmit hook: capture first substantive prompt as
|
||||
# factory state when SessionStart had no goal. This hook stays silent.
|
||||
|
||||
[ "${AGENTOPS_HOOKS_DISABLED:-}" = "1" ] && exit 0
|
||||
[ "${AGENTOPS_FACTORY_ROUTER_DISABLED:-}" = "1" ] && exit 0
|
||||
|
||||
AO_TIMEOUT_BIN="timeout"
|
||||
command -v "$AO_TIMEOUT_BIN" >/dev/null 2>&1 || AO_TIMEOUT_BIN="gtimeout"
|
||||
|
||||
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_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
|
||||
}
|
||||
|
||||
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_text "$path")
|
||||
[ -n "$path" ] || return 0
|
||||
[ -f "$path" ] || return 0
|
||||
printf '%s' "$path"
|
||||
}
|
||||
|
||||
extract_prompt() {
|
||||
local input="$1"
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
printf '%s' "$input" | jq -r '.prompt // ""' 2>/dev/null
|
||||
return 0
|
||||
fi
|
||||
printf '%s' "$input" \
|
||||
| grep -o '"prompt"[[:space:]]*:[[:space:]]*"[^"]*"' \
|
||||
| head -1 \
|
||||
| sed 's/.*"prompt"[[:space:]]*:[[:space:]]*"//;s/"$//'
|
||||
}
|
||||
|
||||
normalize_factory_goal() {
|
||||
local prompt goal
|
||||
prompt="$(trim_text "$1")"
|
||||
[ -n "$prompt" ] || return 0
|
||||
|
||||
case "$prompt" in
|
||||
/session-start*|/session-end*|/quickstart*|/status*|/help*)
|
||||
return 0
|
||||
;;
|
||||
/rpi*)
|
||||
goal="$(trim_text "${prompt#/rpi}")"
|
||||
goal="${goal#\"}"
|
||||
goal="${goal%\"}"
|
||||
prompt="$(trim_text "$goal")"
|
||||
;;
|
||||
/*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$(printf '%s' "$prompt" | tr '[:upper:]' '[:lower:]')" in
|
||||
""|"help"|"status"|"where was i"|"where was i?"|"what should i work on"|"what should i work on?"|"continue"|"resume")
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "${#prompt}" -lt 12 ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '%s' "$prompt"
|
||||
}
|
||||
|
||||
INPUT=$(cat)
|
||||
PROMPT="$(extract_prompt "$INPUT")"
|
||||
[ -n "$PROMPT" ] || exit 0
|
||||
[ "$(resolve_startup_context_mode)" = "factory" ] || exit 0
|
||||
|
||||
ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
||||
STATE_DIR="$ROOT/.agents/ao"
|
||||
INTAKE_FLAG="$STATE_DIR/.factory-intake-needed"
|
||||
ROUTER_FLAG="$STATE_DIR/.factory-router-fired"
|
||||
GOAL_FILE="$STATE_DIR/factory-goal.txt"
|
||||
BRIEFING_FILE="$STATE_DIR/factory-briefing.txt"
|
||||
mkdir -p "$STATE_DIR" 2>/dev/null
|
||||
|
||||
[ -f "$INTAKE_FLAG" ] || exit 0
|
||||
[ ! -s "$GOAL_FILE" ] || exit 0
|
||||
|
||||
if [ -f "$ROUTER_FLAG" ] && find "$ROUTER_FLAG" -mmin -30 2>/dev/null | grep -q .; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
GOAL="$(normalize_factory_goal "$PROMPT")"
|
||||
[ -n "$GOAL" ] || exit 0
|
||||
|
||||
BRIEFING_PATH="$(build_factory_briefing "$GOAL")"
|
||||
printf '%s' "$GOAL" > "$GOAL_FILE" 2>/dev/null || true
|
||||
rm -f "$INTAKE_FLAG" 2>/dev/null || true
|
||||
printf '%s\n' "${CLAUDE_SESSION_ID:-unknown}" > "$ROUTER_FLAG" 2>/dev/null || true
|
||||
|
||||
if [ -n "$BRIEFING_PATH" ]; then
|
||||
printf '%s' "$BRIEFING_PATH" > "$BRIEFING_FILE" 2>/dev/null || true
|
||||
else
|
||||
rm -f "$BRIEFING_FILE" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -41,6 +41,11 @@
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/factory-router.sh",
|
||||
"timeout": 8
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/prompt-nudge.sh",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# AgentOps Session Start Hook (manual mode)
|
||||
# Creates .agents/ directories, consumes handoffs, injects skill context.
|
||||
# MEMORY.md is auto-loaded by Claude Code — no extract/inject needed.
|
||||
# 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
|
||||
@@ -20,12 +20,24 @@ 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' ' ' \
|
||||
@@ -33,6 +45,27 @@ trim_lookup_text() {
|
||||
| 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"
|
||||
@@ -49,11 +82,22 @@ derive_lookup_query() {
|
||||
return 0
|
||||
}
|
||||
|
||||
detect_lookup_bead() {
|
||||
if ! command -v bd >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
timeout 1 bd current 2>/dev/null | head -1 | tr -d '\r' | sed 's/^ //; s/ $//'
|
||||
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() {
|
||||
@@ -130,6 +174,10 @@ 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
|
||||
@@ -224,23 +272,11 @@ rpi/*
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Flywheel behavior
|
||||
INJECTED_KNOWLEDGE=""
|
||||
|
||||
# Derive MEMORY.md path once (used for stale nudge)
|
||||
MEMORY_DIR="$HOME/.claude/projects"
|
||||
PROJECT_PATH=$(printf '%s' "$ROOT" | tr '/' '-')
|
||||
MEMORY_FILE="$MEMORY_DIR/$PROJECT_PATH/memory/MEMORY.md"
|
||||
MEMORY_AGE_DAYS=-1
|
||||
if [ -f "$MEMORY_FILE" ]; then
|
||||
MTIME=$(stat -f %m "$MEMORY_FILE" 2>/dev/null || stat -c %Y "$MEMORY_FILE" 2>/dev/null || echo "")
|
||||
if [ -n "$MTIME" ]; then
|
||||
MEMORY_AGE_DAYS=$(( ($(date +%s) - MTIME) / 86400 ))
|
||||
fi
|
||||
fi
|
||||
STARTUP_CONTEXT_MODE="$(resolve_startup_context_mode)"
|
||||
FACTORY_GOAL=""
|
||||
FACTORY_BRIEFING_PATH=""
|
||||
|
||||
# Structured handoff consumption (ao handoff JSON artifacts)
|
||||
HANDOFF_CONTEXT=""
|
||||
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' \
|
||||
@@ -260,151 +296,23 @@ if [ -d "$ROOT/.agents/handoff" ] && command -v jq &>/dev/null; then
|
||||
"$CONSUMING" > "${CONSUMING}.tmp" 2>/dev/null \
|
||||
&& mv "${CONSUMING}.tmp" "${HANDOFF_JSON%.json}.consumed.json" 2>/dev/null \
|
||||
&& rm -f "$CONSUMING" 2>/dev/null
|
||||
# Build injection context
|
||||
HANDOFF_CONTEXT="### Handoff Context (${H_TYPE})"
|
||||
[ -n "$H_GOAL" ] && HANDOFF_CONTEXT="${HANDOFF_CONTEXT}
|
||||
- **Goal:** ${H_GOAL}"
|
||||
[ -n "$H_SUMMARY" ] && HANDOFF_CONTEXT="${HANDOFF_CONTEXT}
|
||||
- **Summary:** ${H_SUMMARY}"
|
||||
[ -n "$H_CONTINUATION" ] && HANDOFF_CONTEXT="${HANDOFF_CONTEXT}
|
||||
- **Continue:** ${H_CONTINUATION}"
|
||||
HANDOFF_CONTEXT="${HANDOFF_CONTEXT}
|
||||
- **Source:** ${HANDOFF_JSON}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Predecessor handoff discovery
|
||||
PREDECESSOR_FILE="${GT_PREDECESSOR_HANDOFF:-}"
|
||||
if [ -z "$PREDECESSOR_FILE" ] && [ -d "$ROOT/.agents/handoff" ]; then
|
||||
PREDECESSOR_FILE=$(ls -t "$ROOT/.agents/handoff/"*.md 2>/dev/null | head -1)
|
||||
fi
|
||||
|
||||
# Build injection context (manual mode — MEMORY.md auto-loaded by Claude Code)
|
||||
INJECTED_KNOWLEDGE="MEMORY.md is auto-loaded by Claude Code for this project.
|
||||
For on-demand retrieval: \`ao search \"<query>\"\` or \`ao lookup --query \"<query>\"\`"
|
||||
if [ -n "$HANDOFF_CONTEXT" ]; then
|
||||
INJECTED_KNOWLEDGE="${INJECTED_KNOWLEDGE}
|
||||
|
||||
${HANDOFF_CONTEXT}"
|
||||
fi
|
||||
if [ -n "$PREDECESSOR_FILE" ] && [ -f "$PREDECESSOR_FILE" ]; then
|
||||
INJECTED_KNOWLEDGE="${INJECTED_KNOWLEDGE}
|
||||
Predecessor handoff: ${PREDECESSOR_FILE}"
|
||||
fi
|
||||
|
||||
# Point to knowledge signpost (agents search on demand)
|
||||
if [ -f "${ROOT:-.}/.agents/AGENTS.md" ]; then
|
||||
INJECTED_KNOWLEDGE="${INJECTED_KNOWLEDGE}
|
||||
|
||||
Knowledge artifacts are in \`.agents/\`. See \`.agents/AGENTS.md\` for navigation.
|
||||
Use \`ao lookup --query \"topic\"\` for learnings retrieval, or \`Grep\` in \`.agents/learnings/\` as fallback."
|
||||
else
|
||||
INJECTED_KNOWLEDGE="${INJECTED_KNOWLEDGE}
|
||||
|
||||
Knowledge artifacts are in \`.agents/\` (if populated).
|
||||
Use \`ao lookup --query \"topic\"\` for on-demand learnings retrieval."
|
||||
fi
|
||||
|
||||
# Auto-retrieve and cite top learnings for this session (closes citation gap)
|
||||
if command -v ao &>/dev/null; then
|
||||
LOOKUP_QUERY="$(derive_lookup_query)"
|
||||
LOOKUP_BEAD="$(detect_lookup_bead)"
|
||||
LOOKUP_CONTEXT=""
|
||||
LOOKUP_ARGS=(lookup --limit 5)
|
||||
if [ -n "$LOOKUP_QUERY" ]; then
|
||||
LOOKUP_ARGS+=(--query "$LOOKUP_QUERY")
|
||||
LOOKUP_CONTEXT="query=\"$LOOKUP_QUERY\""
|
||||
fi
|
||||
if [ -n "$LOOKUP_BEAD" ]; then
|
||||
LOOKUP_ARGS+=(--bead "$LOOKUP_BEAD")
|
||||
if [ -n "$LOOKUP_CONTEXT" ]; then
|
||||
LOOKUP_CONTEXT="${LOOKUP_CONTEXT}, "
|
||||
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
|
||||
LOOKUP_CONTEXT="${LOOKUP_CONTEXT}bead=${LOOKUP_BEAD}"
|
||||
fi
|
||||
if [ -z "$LOOKUP_CONTEXT" ]; then
|
||||
LOOKUP_CONTEXT="task context unavailable"
|
||||
fi
|
||||
if [ -n "$LOOKUP_QUERY" ] || [ -n "$LOOKUP_BEAD" ]; then
|
||||
FLYWHEEL_KNOWLEDGE=$(timeout 5 ao "${LOOKUP_ARGS[@]}" 2>/dev/null || true)
|
||||
else
|
||||
FLYWHEEL_KNOWLEDGE=""
|
||||
: > "$ROOT/.agents/ao/.factory-intake-needed" 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "$FLYWHEEL_KNOWLEDGE" ]; then
|
||||
INJECTED_KNOWLEDGE="${INJECTED_KNOWLEDGE}
|
||||
|
||||
### Prior Learnings (auto-retrieved: ${LOOKUP_CONTEXT})
|
||||
${FLYWHEEL_KNOWLEDGE}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Keep startup context lean: inject only fresh flywheel knowledge and a short skill pointer.
|
||||
SKILL_FILE="${PLUGIN_ROOT}/skills/using-agentops/SKILL.md"
|
||||
if [ -f "$SKILL_FILE" ]; then
|
||||
using_agentops_hint="AgentOps workflow context is available. Use the Skill tool to read ${SKILL_FILE} when needed."
|
||||
else
|
||||
using_agentops_hint="(AgentOps skill content unavailable at ${SKILL_FILE})"
|
||||
fi
|
||||
|
||||
# Truncation (static cap — no notebook mode logic needed)
|
||||
MAX_INJECT_CHARS=4000
|
||||
if [ -n "$INJECTED_KNOWLEDGE" ] && [ "${#INJECTED_KNOWLEDGE}" -gt "$MAX_INJECT_CHARS" ]; then
|
||||
# Truncate at last newline within budget (never mid-line)
|
||||
trimmed="${INJECTED_KNOWLEDGE:0:$MAX_INJECT_CHARS}"
|
||||
INJECTED_KNOWLEDGE="${trimmed%
|
||||
*}
|
||||
|
||||
*[truncated by session-start hook]*"
|
||||
fi
|
||||
|
||||
# Nudge agent if MEMORY.md is stale or missing
|
||||
if [ -n "${MEMORY_FILE:-}" ] && [ -f "$MEMORY_FILE" ] && [ "${MEMORY_AGE_DAYS:-0}" -gt 7 ]; then
|
||||
INJECTED_KNOWLEDGE="${INJECTED_KNOWLEDGE}
|
||||
|
||||
*Note: Your MEMORY.md hasn't been updated in ${MEMORY_AGE_DAYS} days. Consider running \`ao notebook update\` or updating it manually.*"
|
||||
fi
|
||||
|
||||
if [ -n "$INJECTED_KNOWLEDGE" ]; then
|
||||
full_content=$(cat <<HOOKCTX
|
||||
<AGENTOPS_CONTEXT>
|
||||
${INJECTED_KNOWLEDGE}
|
||||
|
||||
${using_agentops_hint}
|
||||
</AGENTOPS_CONTEXT>
|
||||
HOOKCTX
|
||||
)
|
||||
else
|
||||
full_content=$(cat <<HOOKCTX
|
||||
<AGENTOPS_CONTEXT>
|
||||
No prior flywheel knowledge was injected for this session.
|
||||
${using_agentops_hint}
|
||||
</AGENTOPS_CONTEXT>
|
||||
HOOKCTX
|
||||
)
|
||||
fi
|
||||
|
||||
if command -v jq &>/dev/null; then
|
||||
additional_context=$(printf '%s' "$full_content" | jq -Rs '.')
|
||||
cat <<HOOKEOF
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "SessionStart",
|
||||
"additionalContext": ${additional_context}
|
||||
}
|
||||
}
|
||||
HOOKEOF
|
||||
else
|
||||
# Minimal fallback — escape newlines and quotes
|
||||
escaped=$(printf '%s' "$full_content" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ')
|
||||
cat <<HOOKEOF
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "SessionStart",
|
||||
"additionalContext": "${escaped}"
|
||||
}
|
||||
}
|
||||
HOOKEOF
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -145,6 +145,7 @@ These are the skills every user needs first. Everything else is available when y
|
||||
| `/security` | Continuous repository security scanning and release gating |
|
||||
| `/security-suite` | Binary and prompt-surface security suite — static analysis, dynamic tracing, offline redteam, policy gating |
|
||||
| `/test` | Test generation, coverage analysis, and TDD workflow |
|
||||
| `/red-team` | Persona-based adversarial validation — probe docs and skills from constrained user perspectives |
|
||||
| `/review` | Review incoming PRs, agent output, or diffs — SCORED checklist |
|
||||
| `/refactor` | Safe, verified refactoring with regression testing at each step |
|
||||
| `/deps` | Dependency audit, update, vulnerability scanning, and license compliance |
|
||||
|
||||
+3
-2
@@ -59,8 +59,9 @@ These control the optional hook system installed via `ao init --hooks`. Each hoo
|
||||
|----------|---------|-------------|
|
||||
| `AGENTOPS_HOOKS_DISABLED` | `0` | Set to `1` to disable **all** hooks at once. Global kill switch. Useful for debugging or when hooks interfere with a specific workflow. |
|
||||
| `AGENTOPS_SESSION_START_DISABLED` | `0` | Set to `1` to disable the session-start hook entirely. |
|
||||
| `AGENTOPS_STARTUP_CONTEXT_MODE` | `manual` | Controls SessionStart behavior: `manual` (default — MEMORY.md auto-loaded, no extract/inject), `lean` (extract + inject with reduced budget), `legacy` (extract + inject with full budget). |
|
||||
| `AGENTOPS_STARTUP_LEGACY_INJECT` | (unset) | Set to `1` to force `lean` startup mode regardless of `AGENTOPS_STARTUP_CONTEXT_MODE`. Backward compatibility rollback if `manual` default causes issues. |
|
||||
| `AGENTOPS_STARTUP_CONTEXT_MODE` | `factory` | Controls SessionStart behavior: `factory` (default — silently stage factory goal/briefing state when a goal is recoverable, and mark missing-goal sessions for prompt-time intake) or `manual` (baseline startup maintenance only). Legacy values `lean` and `legacy` are treated as `manual` for compatibility. |
|
||||
| `AGENTOPS_STARTUP_LEGACY_INJECT` | (unset) | Compatibility alias that forces `manual` startup mode regardless of `AGENTOPS_STARTUP_CONTEXT_MODE`. Use only as a rollback for older startup expectations that relied on hidden hook-managed startup context. |
|
||||
| `AGENTOPS_FACTORY_ROUTER_DISABLED` | `0` | Set to `1` to disable the `UserPromptSubmit` factory intake hook that captures the first substantive prompt into `.agents/ao/` state when startup had no goal. |
|
||||
| `AGENTOPS_MEMORY_SYNC` | `0` | Set to `1` to enable repo-root MEMORY.md sync at SessionEnd. Writes session history with managed block markers for cross-runtime access (Codex, OpenCode). |
|
||||
| `AGENTOPS_EVICTION_DISABLED` | `0` | Set to `1` to disable knowledge eviction. Eviction removes stale learnings that have decayed below the retention threshold. Disable if you want to keep all learnings indefinitely. |
|
||||
| `AGENTOPS_GITIGNORE_AUTO` | `1` | Set to `0` to prevent the session-start hook from auto-adding `.agents/` to `.gitignore`. Useful if you want to commit knowledge artifacts to your repo. |
|
||||
|
||||
+1
-1
@@ -446,7 +446,7 @@ These fire automatically and are not directly invoked:
|
||||
| `standards` | Language-specific coding standards (auto-loaded by /vibe, /implement) |
|
||||
| `shared` | Shared reference documents for multi-agent backends |
|
||||
| `beads` | Issue tracking reference |
|
||||
| `using-agentops` | Workflow guide (hook-capable auto-injection, explicit Codex startup fallback) |
|
||||
| `using-agentops` | Workflow guide (hook-capable silent state prep, explicit Codex startup fallback) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ Which `ao` commands each hook invokes.
|
||||
|
||||
| Hook File | Event | ao Commands |
|
||||
|-----------|-------|-------------|
|
||||
| **session-start.sh** | SessionStart | `extract`, `inject`, `lookup`, `notebook update`, `search` |
|
||||
| **session-start.sh** | SessionStart | `flywheel close-loop`, `knowledge brief`, `rpi cleanup` |
|
||||
| **session-end-maintenance.sh** | SessionEnd | `contradict`, `dedup`, `forge transcript`, `maturity`, `memory sync`, `notebook update`, `pool ingest` |
|
||||
| **ao-flywheel-close.sh** | Stop | `flywheel close-loop` |
|
||||
| **ratchet-advance.sh** | PostToolUse | `ratchet record` |
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
| Claude Event | Codex Equivalent | Mechanism |
|
||||
|-------------|-----------------|-----------|
|
||||
| `SessionStart` | `ao codex start` / `ao codex ensure-start` | Explicit command in skill preamble or user invocation |
|
||||
| `SessionStart` | `ao codex start` / `ao codex ensure-start` | Claude native hooks can recover a goal and stage runtime state silently; Codex uses the explicit startup command path |
|
||||
| `SessionEnd` | `ao codex stop` / `ao codex ensure-stop` | Explicit command in skill epilogue or user invocation |
|
||||
| `Stop` | `ao codex stop --close-loop` | Explicit flywheel close via command |
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
| Claude Event | Codex Adaptation | Notes |
|
||||
|-------------|-----------------|-------|
|
||||
| `UserPromptSubmit` | Skill preamble checks | Prompt nudges and intent echo run as inline skill logic, not hook callbacks |
|
||||
| `UserPromptSubmit` | Skill preamble checks | Claude native hooks can capture first-prompt intake silently, emit prompt nudges, and run intent echo; Codex keeps equivalent guidance inside skill/runtime preambles instead of callbacks |
|
||||
| `TaskCompleted` | Skill epilogue validation | Task validation gates run inline after task completion |
|
||||
|
||||
### Unsupported (no Codex equivalent)
|
||||
@@ -46,6 +46,7 @@ ao hook install # Merges hooks.json into ~/.claude/settings.json
|
||||
- Full event manifest from `hooks/hooks.json`
|
||||
- `${CLAUDE_PLUGIN_ROOT}` path resolution
|
||||
- All events wired automatically
|
||||
- `CLAUDE.md` remains the startup surface; hooks prepare factory state silently and keep only explicit safety nudges operator-facing
|
||||
|
||||
### Codex
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ The goal is simple: do not let high-volume `.agents` capture turn into low-trust
|
||||
| finding | canonical | yes | yes | yes | yes | Promoted findings are the highest-trust reusable runtime signal. |
|
||||
| belief-book | canonical | yes | yes | yes | yes | Stable cross-domain doctrine promoted from healthy evidence. |
|
||||
| playbook | canonical | yes | yes | yes | yes | Generated operator workflows from healthy topics and promoted packets. |
|
||||
| knowledge-briefing | runtime-eligible | yes | no | no | yes | Preferred dynamic startup surface for a concrete goal; task-scoped, not universal policy. |
|
||||
| knowledge-briefing | runtime-eligible | yes | no | no | yes | Preferred explicit goal-time briefing surface for a concrete goal; task-scoped, not universal policy. |
|
||||
| planning-rule | canonical | yes | yes | yes | yes | Compiled planning rules are canonical prevention memory. |
|
||||
| known-risk | canonical | yes | yes | yes | yes | Compiled pre-mortem checks are canonical risk memory. |
|
||||
| next-work | runtime-eligible | yes | yes | yes | yes | Ranked next-work gives the next session continuity. |
|
||||
@@ -44,7 +44,7 @@ Suppression does not mean deletion. These classes remain valid lookup, provenanc
|
||||
Session Intelligence should prefer this order when building startup or planning context:
|
||||
|
||||
1. canonical artifacts
|
||||
2. matched knowledge briefings as the primary dynamic startup surface
|
||||
2. matched knowledge briefings when the operator or runtime explicitly assembles goal-time context
|
||||
3. runtime-eligible artifacts
|
||||
4. experimental families only after explicit health review
|
||||
5. discovery-only or archive-only artifacts only by explicit lookup or post-mortem use
|
||||
|
||||
@@ -113,7 +113,7 @@ The active runtime manifest currently declares **7 hook event sections** in `hoo
|
||||
|
||||
| Hook surface | Trigger | What it does | Gap closed |
|
||||
|--------------|---------|--------------|------------|
|
||||
| Session start | `SessionStart` | Runs `session-start.sh` — startup maintenance, lightweight retrieval, and continuity hints | Durable learning (retrieval) |
|
||||
| Session start | `SessionStart` | Runs `session-start.sh` — startup maintenance, handoff recovery, and silent factory-state staging | Runtime continuity |
|
||||
| Session end maintenance | `SessionEnd` | Runs `session-end-maintenance.sh` (transcript mining, maturity management) and `athena-session-defrag.sh` (knowledge deduplication and defrag) | Durable learning (extraction), Loop closure |
|
||||
| Flywheel close | `Stop` | Runs `ao-flywheel-close.sh` — closes the feedback loop via `ao flywheel close-loop` | Loop closure |
|
||||
|
||||
@@ -121,7 +121,7 @@ The active runtime manifest currently declares **7 hook event sections** in `hoo
|
||||
|
||||
| Hook surface | Trigger | What it does | Gap closed |
|
||||
|--------------|---------|--------------|------------|
|
||||
| Prompt guidance | `UserPromptSubmit` | Runs `prompt-nudge.sh` (nudges missing intent and ratchet status) and `intent-echo.sh` (confirms task understanding) | Judgment validation |
|
||||
| Prompt guidance | `UserPromptSubmit` | Runs `factory-router.sh` (captures first-goal intake when startup had none), `prompt-nudge.sh` (ratchet nudges), and `intent-echo.sh` (confirms high-stakes intent) without adding startup briefings to the conversation | Judgment validation |
|
||||
| Pre-tool gates | `PreToolUse` | `pre-mortem-gate.sh` (blocks `/crank` without plan review), `commit-review-gate.sh` (pre-commit checks), `go-test-precommit.sh`, `git-worker-guard.sh` (worker isolation), `edit-knowledge-surface.sh`, `codex-parity-warn.sh` | Judgment validation |
|
||||
| Post-tool checks | `PostToolUse` | `write-time-quality.sh` (edit quality), `go-complexity-precommit.sh`, `go-vet-post-edit.sh`, `research-loop-detector.sh` (detects stalled loops), `context-monitor.sh` | Judgment validation, Loop closure |
|
||||
| Task completion gate | `TaskCompleted` | Runs `task-validation-gate.sh` — executes compiled constraints from `.agents/constraints/index.json` before accepting task completion | Judgment validation, Loop closure |
|
||||
|
||||
@@ -14,8 +14,8 @@ Just describe what you want:
|
||||
|
||||
| Say This | What Happens |
|
||||
|----------|--------------|
|
||||
| "Continue working on X" | Loads context, shows progress, finds next task when runtime hooks are available |
|
||||
| "I need to add Y" | Checks for plan, guides you if needed |
|
||||
| "Continue working on X" | `CLAUDE.md` provides the startup surface; hooks recover handoff and stage any factory state silently |
|
||||
| "I need to add Y" | If no startup goal exists yet, the first substantive prompt can be captured as silent factory intake for later explicit `/rpi` or `ao knowledge brief` use |
|
||||
| "I'm done for today" | Saves progress, offers retrospective |
|
||||
| "What should I work on?" | Shows status, suggests next task |
|
||||
| "Where was I?" | Shows last session, current state, blockers |
|
||||
@@ -83,7 +83,7 @@ with `ao codex ensure-stop`.
|
||||
|
||||
| Mode | Start | Closeout | Notes |
|
||||
|------|-------|----------|-------|
|
||||
| Hook-capable | Natural language, `/session-start`, or startup hooks | Natural language, `/session-end`, or session-end hooks | Best fit for Claude/OpenCode when hooks are installed |
|
||||
| Hook-capable | Natural language, `/session-start`, or startup hooks | Natural language, `/session-end`, or session-end hooks | Best fit for Claude/OpenCode when hooks are installed; `CLAUDE.md` is the startup surface and hooks stage state silently |
|
||||
| Codex hookless fallback | `ao factory start --goal "<goal>"`, `ao codex start`, or skill-driven `ao codex ensure-start` | `ao codex stop` or skill-driven `ao codex ensure-stop` | No startup/session-end hook surface under `~/.codex`; lifecycle is explicit |
|
||||
| Manual fallback | `ao inject`, `ao lookup` | `ao forge transcript`, `ao flywheel close-loop` | Lowest-level portable path |
|
||||
|
||||
@@ -96,10 +96,10 @@ with `ao codex ensure-stop`.
|
||||
│ SESSION START │
|
||||
│ "Continue the API work" or /session-start │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ • Load bundle (if exists) │
|
||||
│ • Read claude-progress.json │
|
||||
│ • Show: last session, current work, next steps │
|
||||
│ • Suggest: next feature to work on │
|
||||
│ • Recover handoff / tracker goal when available │
|
||||
│ • Keep operator framing in CLAUDE.md │
|
||||
│ • Stage factory goal / briefing files silently │
|
||||
│ • Mark missing-goal sessions for prompt-time intake │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
@@ -107,6 +107,8 @@ with `ao codex ensure-stop`.
|
||||
│ WORK │
|
||||
│ "Add the validation layer" or just start coding │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ • First substantive prompt can become silent factory intake│
|
||||
│ • Run /rpi or ao knowledge brief explicitly when needed │
|
||||
│ • Implement features │
|
||||
│ • Fix bugs │
|
||||
│ • Research when needed │
|
||||
@@ -306,6 +308,7 @@ Next time, just say "continue the caching work" and I'll pick up where you left
|
||||
| `ao codex start` | Start an explicit Codex hookless session | Start of work in Codex |
|
||||
| `ao codex stop` | Close out a Codex session without runtime hooks | End of work in Codex |
|
||||
| `ao codex status` | Inspect Codex lifecycle and flywheel health | Any time in Codex |
|
||||
| Natural-language goal | Native factory intake when hooks are installed | Preferred operator entrypoint in Claude/OpenCode |
|
||||
| `/session-start` | Initialize session, load context | Start of work in hook-capable runtimes |
|
||||
| `/session-end` | Save progress, close gracefully | End of work in hook-capable runtimes |
|
||||
| `/progress-update` | Update progress files | During work |
|
||||
|
||||
Executable
+146
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env bash
|
||||
# factory-router.sh - UserPromptSubmit hook: capture first substantive prompt as
|
||||
# factory state when SessionStart had no goal. This hook stays silent.
|
||||
|
||||
[ "${AGENTOPS_HOOKS_DISABLED:-}" = "1" ] && exit 0
|
||||
[ "${AGENTOPS_FACTORY_ROUTER_DISABLED:-}" = "1" ] && exit 0
|
||||
|
||||
AO_TIMEOUT_BIN="timeout"
|
||||
command -v "$AO_TIMEOUT_BIN" >/dev/null 2>&1 || AO_TIMEOUT_BIN="gtimeout"
|
||||
|
||||
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_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
|
||||
}
|
||||
|
||||
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_text "$path")
|
||||
[ -n "$path" ] || return 0
|
||||
[ -f "$path" ] || return 0
|
||||
printf '%s' "$path"
|
||||
}
|
||||
|
||||
extract_prompt() {
|
||||
local input="$1"
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
printf '%s' "$input" | jq -r '.prompt // ""' 2>/dev/null
|
||||
return 0
|
||||
fi
|
||||
printf '%s' "$input" \
|
||||
| grep -o '"prompt"[[:space:]]*:[[:space:]]*"[^"]*"' \
|
||||
| head -1 \
|
||||
| sed 's/.*"prompt"[[:space:]]*:[[:space:]]*"//;s/"$//'
|
||||
}
|
||||
|
||||
normalize_factory_goal() {
|
||||
local prompt goal
|
||||
prompt="$(trim_text "$1")"
|
||||
[ -n "$prompt" ] || return 0
|
||||
|
||||
case "$prompt" in
|
||||
/session-start*|/session-end*|/quickstart*|/status*|/help*)
|
||||
return 0
|
||||
;;
|
||||
/rpi*)
|
||||
goal="$(trim_text "${prompt#/rpi}")"
|
||||
goal="${goal#\"}"
|
||||
goal="${goal%\"}"
|
||||
prompt="$(trim_text "$goal")"
|
||||
;;
|
||||
/*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$(printf '%s' "$prompt" | tr '[:upper:]' '[:lower:]')" in
|
||||
""|"help"|"status"|"where was i"|"where was i?"|"what should i work on"|"what should i work on?"|"continue"|"resume")
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "${#prompt}" -lt 12 ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '%s' "$prompt"
|
||||
}
|
||||
|
||||
INPUT=$(cat)
|
||||
PROMPT="$(extract_prompt "$INPUT")"
|
||||
[ -n "$PROMPT" ] || exit 0
|
||||
[ "$(resolve_startup_context_mode)" = "factory" ] || exit 0
|
||||
|
||||
ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
||||
STATE_DIR="$ROOT/.agents/ao"
|
||||
INTAKE_FLAG="$STATE_DIR/.factory-intake-needed"
|
||||
ROUTER_FLAG="$STATE_DIR/.factory-router-fired"
|
||||
GOAL_FILE="$STATE_DIR/factory-goal.txt"
|
||||
BRIEFING_FILE="$STATE_DIR/factory-briefing.txt"
|
||||
mkdir -p "$STATE_DIR" 2>/dev/null
|
||||
|
||||
[ -f "$INTAKE_FLAG" ] || exit 0
|
||||
[ ! -s "$GOAL_FILE" ] || exit 0
|
||||
|
||||
if [ -f "$ROUTER_FLAG" ] && find "$ROUTER_FLAG" -mmin -30 2>/dev/null | grep -q .; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
GOAL="$(normalize_factory_goal "$PROMPT")"
|
||||
[ -n "$GOAL" ] || exit 0
|
||||
|
||||
BRIEFING_PATH="$(build_factory_briefing "$GOAL")"
|
||||
printf '%s' "$GOAL" > "$GOAL_FILE" 2>/dev/null || true
|
||||
rm -f "$INTAKE_FLAG" 2>/dev/null || true
|
||||
printf '%s\n' "${CLAUDE_SESSION_ID:-unknown}" > "$ROUTER_FLAG" 2>/dev/null || true
|
||||
|
||||
if [ -n "$BRIEFING_PATH" ]; then
|
||||
printf '%s' "$BRIEFING_PATH" > "$BRIEFING_FILE" 2>/dev/null || true
|
||||
else
|
||||
rm -f "$BRIEFING_FILE" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -41,6 +41,11 @@
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/factory-router.sh",
|
||||
"timeout": 8
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/prompt-nudge.sh",
|
||||
|
||||
+69
-161
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# AgentOps Session Start Hook (manual mode)
|
||||
# Creates .agents/ directories, consumes handoffs, injects skill context.
|
||||
# MEMORY.md is auto-loaded by Claude Code — no extract/inject needed.
|
||||
# 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
|
||||
@@ -20,12 +20,24 @@ 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' ' ' \
|
||||
@@ -33,6 +45,27 @@ trim_lookup_text() {
|
||||
| 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"
|
||||
@@ -49,11 +82,22 @@ derive_lookup_query() {
|
||||
return 0
|
||||
}
|
||||
|
||||
detect_lookup_bead() {
|
||||
if ! command -v bd >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
timeout 1 bd current 2>/dev/null | head -1 | tr -d '\r' | sed 's/^ //; s/ $//'
|
||||
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() {
|
||||
@@ -130,6 +174,10 @@ 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
|
||||
@@ -224,23 +272,11 @@ rpi/*
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Flywheel behavior
|
||||
INJECTED_KNOWLEDGE=""
|
||||
|
||||
# Derive MEMORY.md path once (used for stale nudge)
|
||||
MEMORY_DIR="$HOME/.claude/projects"
|
||||
PROJECT_PATH=$(printf '%s' "$ROOT" | tr '/' '-')
|
||||
MEMORY_FILE="$MEMORY_DIR/$PROJECT_PATH/memory/MEMORY.md"
|
||||
MEMORY_AGE_DAYS=-1
|
||||
if [ -f "$MEMORY_FILE" ]; then
|
||||
MTIME=$(stat -f %m "$MEMORY_FILE" 2>/dev/null || stat -c %Y "$MEMORY_FILE" 2>/dev/null || echo "")
|
||||
if [ -n "$MTIME" ]; then
|
||||
MEMORY_AGE_DAYS=$(( ($(date +%s) - MTIME) / 86400 ))
|
||||
fi
|
||||
fi
|
||||
STARTUP_CONTEXT_MODE="$(resolve_startup_context_mode)"
|
||||
FACTORY_GOAL=""
|
||||
FACTORY_BRIEFING_PATH=""
|
||||
|
||||
# Structured handoff consumption (ao handoff JSON artifacts)
|
||||
HANDOFF_CONTEXT=""
|
||||
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' \
|
||||
@@ -260,151 +296,23 @@ if [ -d "$ROOT/.agents/handoff" ] && command -v jq &>/dev/null; then
|
||||
"$CONSUMING" > "${CONSUMING}.tmp" 2>/dev/null \
|
||||
&& mv "${CONSUMING}.tmp" "${HANDOFF_JSON%.json}.consumed.json" 2>/dev/null \
|
||||
&& rm -f "$CONSUMING" 2>/dev/null
|
||||
# Build injection context
|
||||
HANDOFF_CONTEXT="### Handoff Context (${H_TYPE})"
|
||||
[ -n "$H_GOAL" ] && HANDOFF_CONTEXT="${HANDOFF_CONTEXT}
|
||||
- **Goal:** ${H_GOAL}"
|
||||
[ -n "$H_SUMMARY" ] && HANDOFF_CONTEXT="${HANDOFF_CONTEXT}
|
||||
- **Summary:** ${H_SUMMARY}"
|
||||
[ -n "$H_CONTINUATION" ] && HANDOFF_CONTEXT="${HANDOFF_CONTEXT}
|
||||
- **Continue:** ${H_CONTINUATION}"
|
||||
HANDOFF_CONTEXT="${HANDOFF_CONTEXT}
|
||||
- **Source:** ${HANDOFF_JSON}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Predecessor handoff discovery
|
||||
PREDECESSOR_FILE="${GT_PREDECESSOR_HANDOFF:-}"
|
||||
if [ -z "$PREDECESSOR_FILE" ] && [ -d "$ROOT/.agents/handoff" ]; then
|
||||
PREDECESSOR_FILE=$(ls -t "$ROOT/.agents/handoff/"*.md 2>/dev/null | head -1)
|
||||
fi
|
||||
|
||||
# Build injection context (manual mode — MEMORY.md auto-loaded by Claude Code)
|
||||
INJECTED_KNOWLEDGE="MEMORY.md is auto-loaded by Claude Code for this project.
|
||||
For on-demand retrieval: \`ao search \"<query>\"\` or \`ao lookup --query \"<query>\"\`"
|
||||
if [ -n "$HANDOFF_CONTEXT" ]; then
|
||||
INJECTED_KNOWLEDGE="${INJECTED_KNOWLEDGE}
|
||||
|
||||
${HANDOFF_CONTEXT}"
|
||||
fi
|
||||
if [ -n "$PREDECESSOR_FILE" ] && [ -f "$PREDECESSOR_FILE" ]; then
|
||||
INJECTED_KNOWLEDGE="${INJECTED_KNOWLEDGE}
|
||||
Predecessor handoff: ${PREDECESSOR_FILE}"
|
||||
fi
|
||||
|
||||
# Point to knowledge signpost (agents search on demand)
|
||||
if [ -f "${ROOT:-.}/.agents/AGENTS.md" ]; then
|
||||
INJECTED_KNOWLEDGE="${INJECTED_KNOWLEDGE}
|
||||
|
||||
Knowledge artifacts are in \`.agents/\`. See \`.agents/AGENTS.md\` for navigation.
|
||||
Use \`ao lookup --query \"topic\"\` for learnings retrieval, or \`Grep\` in \`.agents/learnings/\` as fallback."
|
||||
else
|
||||
INJECTED_KNOWLEDGE="${INJECTED_KNOWLEDGE}
|
||||
|
||||
Knowledge artifacts are in \`.agents/\` (if populated).
|
||||
Use \`ao lookup --query \"topic\"\` for on-demand learnings retrieval."
|
||||
fi
|
||||
|
||||
# Auto-retrieve and cite top learnings for this session (closes citation gap)
|
||||
if command -v ao &>/dev/null; then
|
||||
LOOKUP_QUERY="$(derive_lookup_query)"
|
||||
LOOKUP_BEAD="$(detect_lookup_bead)"
|
||||
LOOKUP_CONTEXT=""
|
||||
LOOKUP_ARGS=(lookup --limit 5)
|
||||
if [ -n "$LOOKUP_QUERY" ]; then
|
||||
LOOKUP_ARGS+=(--query "$LOOKUP_QUERY")
|
||||
LOOKUP_CONTEXT="query=\"$LOOKUP_QUERY\""
|
||||
fi
|
||||
if [ -n "$LOOKUP_BEAD" ]; then
|
||||
LOOKUP_ARGS+=(--bead "$LOOKUP_BEAD")
|
||||
if [ -n "$LOOKUP_CONTEXT" ]; then
|
||||
LOOKUP_CONTEXT="${LOOKUP_CONTEXT}, "
|
||||
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
|
||||
LOOKUP_CONTEXT="${LOOKUP_CONTEXT}bead=${LOOKUP_BEAD}"
|
||||
fi
|
||||
if [ -z "$LOOKUP_CONTEXT" ]; then
|
||||
LOOKUP_CONTEXT="task context unavailable"
|
||||
fi
|
||||
if [ -n "$LOOKUP_QUERY" ] || [ -n "$LOOKUP_BEAD" ]; then
|
||||
FLYWHEEL_KNOWLEDGE=$(timeout 5 ao "${LOOKUP_ARGS[@]}" 2>/dev/null || true)
|
||||
else
|
||||
FLYWHEEL_KNOWLEDGE=""
|
||||
: > "$ROOT/.agents/ao/.factory-intake-needed" 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "$FLYWHEEL_KNOWLEDGE" ]; then
|
||||
INJECTED_KNOWLEDGE="${INJECTED_KNOWLEDGE}
|
||||
|
||||
### Prior Learnings (auto-retrieved: ${LOOKUP_CONTEXT})
|
||||
${FLYWHEEL_KNOWLEDGE}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Keep startup context lean: inject only fresh flywheel knowledge and a short skill pointer.
|
||||
SKILL_FILE="${PLUGIN_ROOT}/skills/using-agentops/SKILL.md"
|
||||
if [ -f "$SKILL_FILE" ]; then
|
||||
using_agentops_hint="AgentOps workflow context is available. Use the Skill tool to read ${SKILL_FILE} when needed."
|
||||
else
|
||||
using_agentops_hint="(AgentOps skill content unavailable at ${SKILL_FILE})"
|
||||
fi
|
||||
|
||||
# Truncation (static cap — no notebook mode logic needed)
|
||||
MAX_INJECT_CHARS=4000
|
||||
if [ -n "$INJECTED_KNOWLEDGE" ] && [ "${#INJECTED_KNOWLEDGE}" -gt "$MAX_INJECT_CHARS" ]; then
|
||||
# Truncate at last newline within budget (never mid-line)
|
||||
trimmed="${INJECTED_KNOWLEDGE:0:$MAX_INJECT_CHARS}"
|
||||
INJECTED_KNOWLEDGE="${trimmed%
|
||||
*}
|
||||
|
||||
*[truncated by session-start hook]*"
|
||||
fi
|
||||
|
||||
# Nudge agent if MEMORY.md is stale or missing
|
||||
if [ -n "${MEMORY_FILE:-}" ] && [ -f "$MEMORY_FILE" ] && [ "${MEMORY_AGE_DAYS:-0}" -gt 7 ]; then
|
||||
INJECTED_KNOWLEDGE="${INJECTED_KNOWLEDGE}
|
||||
|
||||
*Note: Your MEMORY.md hasn't been updated in ${MEMORY_AGE_DAYS} days. Consider running \`ao notebook update\` or updating it manually.*"
|
||||
fi
|
||||
|
||||
if [ -n "$INJECTED_KNOWLEDGE" ]; then
|
||||
full_content=$(cat <<HOOKCTX
|
||||
<AGENTOPS_CONTEXT>
|
||||
${INJECTED_KNOWLEDGE}
|
||||
|
||||
${using_agentops_hint}
|
||||
</AGENTOPS_CONTEXT>
|
||||
HOOKCTX
|
||||
)
|
||||
else
|
||||
full_content=$(cat <<HOOKCTX
|
||||
<AGENTOPS_CONTEXT>
|
||||
No prior flywheel knowledge was injected for this session.
|
||||
${using_agentops_hint}
|
||||
</AGENTOPS_CONTEXT>
|
||||
HOOKCTX
|
||||
)
|
||||
fi
|
||||
|
||||
if command -v jq &>/dev/null; then
|
||||
additional_context=$(printf '%s' "$full_content" | jq -Rs '.')
|
||||
cat <<HOOKEOF
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "SessionStart",
|
||||
"additionalContext": ${additional_context}
|
||||
}
|
||||
}
|
||||
HOOKEOF
|
||||
else
|
||||
# Minimal fallback — escape newlines and quotes
|
||||
escaped=$(printf '%s' "$full_content" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ')
|
||||
cat <<HOOKEOF
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "SessionStart",
|
||||
"additionalContext": "${escaped}"
|
||||
}
|
||||
}
|
||||
HOOKEOF
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -55,20 +55,14 @@ assert_hook_schema() {
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# 1. session-start.sh (SessionStart) — registered
|
||||
# 1. session-start.sh (SessionStart) — registered but intentionally silent
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
@test "session-start: output conforms to hookSpecificOutput schema with hookEventName=SessionStart" {
|
||||
@test "session-start: produces no operator-facing JSON output" {
|
||||
run bash -c 'cd "$1" && CLAUDE_SESSION_ID="bats-schema-sess" bash "$2" 2>&1' \
|
||||
-- "$MOCK_REPO" "$HOOKS_DIR/session-start.sh"
|
||||
[ "$status" -eq 0 ]
|
||||
[ -n "$output" ]
|
||||
# session-start emits multi-line pretty-printed JSON (heredoc) possibly preceded
|
||||
# by non-JSON diagnostic lines. Extract from first '{' to end and parse.
|
||||
local json_blob
|
||||
json_blob=$(echo "$output" | sed -n '/^{/,/^}/p' | jq -c '.' 2>/dev/null)
|
||||
[ -n "$json_blob" ]
|
||||
assert_hook_schema "$json_blob" "SessionStart"
|
||||
[ -z "$output" ]
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
@@ -76,6 +70,34 @@ assert_hook_schema() {
|
||||
# Only emits JSON when there is a nudge to give (ratchet gate active).
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
@test "factory-router: intake prompt stays silent while staging state" {
|
||||
local mock="$TMP_TEST_DIR/mock-factory-router"
|
||||
mkdir -p "$mock/.agents/ao" "$mock/bin"
|
||||
git -C "$mock" init -q >/dev/null 2>&1
|
||||
touch "$mock/.agents/ao/.factory-intake-needed"
|
||||
cat > "$mock/bin/ao" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
if [ "${1:-}" = "knowledge" ] && [ "${2:-}" = "brief" ]; then
|
||||
briefing_path="${MOCK_FACTORY_BRIEFING_PATH:?}"
|
||||
mkdir -p "$(dirname "$briefing_path")"
|
||||
cat > "$briefing_path" <<'BRIEF'
|
||||
# Briefing: fix auth bootstrap
|
||||
BRIEF
|
||||
jq -n --arg path "$briefing_path" '{"output_path":$path}'
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$mock/bin/ao"
|
||||
local briefing_path="$mock/.agents/briefings/2026-04-02-fix-auth-bootstrap.md"
|
||||
run bash -c 'cd "$1" && printf "%s" "$3" | PATH="$1/bin:$PATH" MOCK_FACTORY_BRIEFING_PATH="$2" AGENTOPS_STARTUP_CONTEXT_MODE=factory bash "$4" 2>&1' \
|
||||
-- "$mock" "$briefing_path" '{"prompt":"fix auth bootstrap"}' "$HOOKS_DIR/factory-router.sh"
|
||||
[ "$status" -eq 0 ]
|
||||
[ -z "$output" ]
|
||||
[ "$(cat "$mock/.agents/ao/factory-goal.txt" 2>/dev/null)" = "fix auth bootstrap" ]
|
||||
[ "$(cat "$mock/.agents/ao/factory-briefing.txt" 2>/dev/null)" = "$briefing_path" ]
|
||||
}
|
||||
|
||||
@test "prompt-nudge: when nudge fires, output conforms to schema with hookEventName=UserPromptSubmit" {
|
||||
# Set up ratchet state so prompt-nudge has something to nudge about.
|
||||
# Simulate: research step done, plan step pending, user says "implement".
|
||||
@@ -336,10 +358,10 @@ AOEOF
|
||||
local hooks_json="$HOOKS_DIR/hooks.json"
|
||||
[ -f "$hooks_json" ]
|
||||
|
||||
# Parse hooks.json to build event-to-hook mapping for all known JSON emitters
|
||||
# Parse hooks.json to build event-to-hook mapping for registered JSON emitters.
|
||||
# SessionStart and factory-router are intentionally silent.
|
||||
local -A hook_event_map
|
||||
hook_event_map=(
|
||||
["session-start.sh"]="SessionStart"
|
||||
["prompt-nudge.sh"]="UserPromptSubmit"
|
||||
["intent-echo.sh"]="UserPromptSubmit"
|
||||
["commit-review-gate.sh"]="PreToolUse"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# Verifies each hook produces correct side effects in the expected order.
|
||||
#
|
||||
# Lifecycle sequence:
|
||||
# 1. SessionStart → session-start.sh (directory creation, JSON output)
|
||||
# 1. SessionStart → session-start.sh (directory creation, silent runtime prep)
|
||||
# 2. PreToolUse(Read) → citation-tracker.sh (citation recorded)
|
||||
# 3. PreToolUse(Bash) → git-worker-guard.sh (worker git commit blocked)
|
||||
# 4. PostToolUse(Bash) → ratchet-advance.sh (advance suggestion on ratchet record)
|
||||
@@ -163,21 +163,10 @@ else
|
||||
SESSION_JSON=$(echo "$SESSION_OUTPUT" | LC_ALL=C awk '/^[[:space:]]*\{/{found=1; buf=""} found{buf=buf $0 "\n"} /^[[:space:]]*\}/{if(found) last=buf; found=0} END{printf "%s", last}')
|
||||
fi
|
||||
|
||||
if echo "$SESSION_JSON" | jq -e '.hookSpecificOutput.hookEventName == "SessionStart"' >/dev/null 2>&1; then
|
||||
pass "step 1: emits SessionStart hookEventName"
|
||||
if [ -z "$SESSION_OUTPUT" ]; then
|
||||
pass "step 1: session-start remains silent"
|
||||
else
|
||||
# Fallback: grep raw output
|
||||
if echo "$SESSION_OUTPUT" | grep -q '"SessionStart"'; then
|
||||
pass "step 1: emits SessionStart hookEventName"
|
||||
else
|
||||
fail "step 1: emits SessionStart hookEventName"
|
||||
fi
|
||||
fi
|
||||
|
||||
if echo "$SESSION_JSON" | jq -e '.hookSpecificOutput.additionalContext' >/dev/null 2>&1; then
|
||||
pass "step 1: emits additionalContext"
|
||||
else
|
||||
fail "step 1: emits additionalContext"
|
||||
fail "step 1: session-start remains silent"
|
||||
fi
|
||||
|
||||
# Verify directory creation (session-start creates .agents/ dirs)
|
||||
@@ -591,6 +580,7 @@ echo "=== Cross-Cutting: Kill Switch Verification ==="
|
||||
# Verify ALL hooks respect the AGENTOPS_HOOKS_DISABLED kill switch
|
||||
HOOKS_WITH_KILL_SWITCH=(
|
||||
"session-start.sh"
|
||||
"factory-router.sh"
|
||||
"citation-tracker.sh"
|
||||
"git-worker-guard.sh"
|
||||
"ratchet-advance.sh"
|
||||
|
||||
@@ -28,15 +28,9 @@ teardown() {
|
||||
# session-start.sh / precompact-snapshot.sh
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
@test "session-start: emits SessionStart JSON" {
|
||||
@test "session-start: stays silent while preparing runtime state" {
|
||||
SESSION_RAW=$(bash "$HOOKS_DIR/session-start.sh" 2>/dev/null || true)
|
||||
# Extract JSON from potentially noisy output
|
||||
if echo "$SESSION_RAW" | grep -q '"hookEventName".*"SessionStart"'; then
|
||||
true # Found SessionStart in output
|
||||
else
|
||||
SESSION_JSON=$(echo "$SESSION_RAW" | LC_ALL=C awk '/^[[:space:]]*\{/{found=1; buf=""} found{buf=buf $0 "\n"} /^[[:space:]]*\}/{if(found) last=buf; found=0} END{printf "%s", last}')
|
||||
echo "$SESSION_JSON" | jq -e '.hookSpecificOutput.hookEventName == "SessionStart"' >/dev/null 2>&1
|
||||
fi
|
||||
[ -z "$SESSION_RAW" ]
|
||||
}
|
||||
|
||||
@test "session-start: kill switch suppresses output" {
|
||||
@@ -52,6 +46,54 @@ teardown() {
|
||||
[ -d "$mock/.agents/research" ] && [ ! -d "$mock/subdir/.agents/research" ]
|
||||
}
|
||||
|
||||
@test "session-start: factory mode stages a matched briefing without injecting it" {
|
||||
local mock="$TMP_TEST_DIR/mock-factory-start"
|
||||
mkdir -p "$mock/.agents/handoff" "$mock/bin"
|
||||
git -C "$mock" init -q >/dev/null 2>&1
|
||||
cat > "$mock/.agents/handoff/handoff-20260402T192000Z.json" <<'EOF'
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "handoff-factory-test",
|
||||
"created_at": "2026-04-02T19:20:00Z",
|
||||
"type": "manual",
|
||||
"goal": "stabilize auth startup",
|
||||
"summary": "route this session into the factory lane"
|
||||
}
|
||||
EOF
|
||||
cat > "$mock/bin/ao" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
if [ -n "${AO_ARGS_FILE:-}" ]; then
|
||||
printf '%s\n' "$*" >> "$AO_ARGS_FILE"
|
||||
fi
|
||||
if [ "${1:-}" = "knowledge" ] && [ "${2:-}" = "brief" ]; then
|
||||
briefing_path="${MOCK_FACTORY_BRIEFING_PATH:?}"
|
||||
mkdir -p "$(dirname "$briefing_path")"
|
||||
cat > "$briefing_path" <<'BRIEF'
|
||||
# Briefing: stabilize auth startup
|
||||
|
||||
## Relevant Topics
|
||||
- `auth-startup` (healthy)
|
||||
BRIEF
|
||||
jq -n --arg path "$briefing_path" '{"output_path":$path}'
|
||||
exit 0
|
||||
fi
|
||||
if [ "${1:-}" = "lookup" ]; then
|
||||
printf '[lookup] supporting result\n'
|
||||
fi
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$mock/bin/ao"
|
||||
local args_file="$mock/ao-args.log"
|
||||
local briefing_path="$mock/.agents/briefings/2026-04-02-stabilize-auth-startup.md"
|
||||
run bash -c 'cd "$1" && PATH="$1/bin:$PATH" AO_ARGS_FILE="$2" MOCK_FACTORY_BRIEFING_PATH="$3" AGENTOPS_STARTUP_CONTEXT_MODE=factory bash "$4" 2>&1' \
|
||||
-- "$mock" "$args_file" "$briefing_path" "$HOOKS_DIR/session-start.sh"
|
||||
[ "$status" -eq 0 ]
|
||||
[ -z "$output" ]
|
||||
grep -q '^knowledge brief --json --goal stabilize auth startup$' "$args_file"
|
||||
[ "$(cat "$mock/.agents/ao/factory-goal.txt" 2>/dev/null)" = "stabilize auth startup" ]
|
||||
[ "$(cat "$mock/.agents/ao/factory-briefing.txt" 2>/dev/null)" = "$briefing_path" ]
|
||||
}
|
||||
|
||||
@test "precompact-snapshot: emits additionalContext JSON" {
|
||||
local mock="$TMP_TEST_DIR/mock-precompact"
|
||||
mkdir -p "$mock/.agents"
|
||||
@@ -359,6 +401,48 @@ EOF
|
||||
rm -f "$REPO_ROOT/.agents/ao/.intent-echo-fired" 2>/dev/null
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# factory-router.sh
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
@test "factory-router: first substantive prompt builds briefing and clears intake" {
|
||||
local mock="$TMP_TEST_DIR/mock-factory-router"
|
||||
mkdir -p "$mock/.agents/ao" "$mock/bin"
|
||||
git -C "$mock" init -q >/dev/null 2>&1
|
||||
touch "$mock/.agents/ao/.factory-intake-needed"
|
||||
cat > "$mock/bin/ao" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
if [ -n "${AO_ARGS_FILE:-}" ]; then
|
||||
printf '%s\n' "$*" >> "$AO_ARGS_FILE"
|
||||
fi
|
||||
if [ "${1:-}" = "knowledge" ] && [ "${2:-}" = "brief" ]; then
|
||||
briefing_path="${MOCK_FACTORY_BRIEFING_PATH:?}"
|
||||
mkdir -p "$(dirname "$briefing_path")"
|
||||
cat > "$briefing_path" <<'BRIEF'
|
||||
# Briefing: fix auth bootstrap
|
||||
|
||||
## Relevant Topics
|
||||
- `auth-bootstrap` (healthy)
|
||||
BRIEF
|
||||
jq -n --arg path "$briefing_path" '{"output_path":$path}'
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$mock/bin/ao"
|
||||
local args_file="$mock/ao-args.log"
|
||||
local briefing_path="$mock/.agents/briefings/2026-04-02-fix-auth-bootstrap.md"
|
||||
run bash -c 'cd "$1" && printf "%s" "$4" | PATH="$1/bin:$PATH" AO_ARGS_FILE="$2" MOCK_FACTORY_BRIEFING_PATH="$3" AGENTOPS_STARTUP_CONTEXT_MODE=factory bash "$5" 2>&1' \
|
||||
-- "$mock" "$args_file" "$briefing_path" '{"prompt":"fix auth bootstrap"}' "$HOOKS_DIR/factory-router.sh"
|
||||
[ "$status" -eq 0 ]
|
||||
[ -z "$output" ]
|
||||
grep -q '^knowledge brief --json --goal fix auth bootstrap$' "$args_file"
|
||||
[ ! -f "$mock/.agents/ao/.factory-intake-needed" ]
|
||||
[ "$(cat "$mock/.agents/ao/factory-goal.txt" 2>/dev/null)" = "fix auth bootstrap" ]
|
||||
[ "$(cat "$mock/.agents/ao/factory-briefing.txt" 2>/dev/null)" = "$briefing_path" ]
|
||||
[ -f "$mock/.agents/ao/.factory-router-fired" ]
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# research-loop-detector.sh
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
@@ -442,6 +526,10 @@ AOEOF
|
||||
jq -e '.hooks.UserPromptSubmit[].hooks[] | select(.command | contains("intent-echo.sh"))' "$HOOKS_DIR/hooks.json" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
@test "hooks.json: factory-router wired in UserPromptSubmit" {
|
||||
jq -e '.hooks.UserPromptSubmit[].hooks[] | select(.command | contains("factory-router.sh"))' "$HOOKS_DIR/hooks.json" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
@test "hooks.json: research-loop-detector wired in PostToolUse" {
|
||||
jq -e '.hooks.PostToolUse[].hooks[] | select(.command | contains("research-loop-detector.sh"))' "$HOOKS_DIR/hooks.json" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
+139
-36
@@ -84,20 +84,12 @@ echo ""
|
||||
echo "=== session-start.sh / precompact-snapshot.sh ==="
|
||||
# ============================================================
|
||||
|
||||
# Test 12: session-start emits valid JSON (extract last JSON object from output,
|
||||
# since ao extract may emit non-JSON to stdout before the hook's JSON)
|
||||
# Test 12: session-start stays silent while preparing runtime state
|
||||
SESSION_RAW=$(bash "$HOOKS_DIR/session-start.sh" 2>/dev/null || true)
|
||||
# Extract the last valid JSON block by finding the final { ... } spanning multiple lines
|
||||
SESSION_JSON=$(echo "$SESSION_RAW" | LC_ALL=C awk '/^[[:space:]]*\{/{found=1; buf=""} found{buf=buf $0 "\n"} /^[[:space:]]*\}/{if(found) last=buf; found=0} END{printf "%s", last}')
|
||||
if echo "$SESSION_JSON" | jq -e '.hookSpecificOutput.hookEventName == "SessionStart"' >/dev/null 2>&1; then
|
||||
pass "session-start emits SessionStart JSON"
|
||||
if [ -z "$SESSION_RAW" ]; then
|
||||
pass "session-start stays silent"
|
||||
else
|
||||
# Fallback: check if hookEventName appears anywhere in raw output
|
||||
if echo "$SESSION_RAW" | grep -q '"hookEventName".*"SessionStart"'; then
|
||||
pass "session-start emits SessionStart JSON"
|
||||
else
|
||||
fail "session-start emits SessionStart JSON"
|
||||
fi
|
||||
fail "session-start stays silent"
|
||||
fi
|
||||
|
||||
# Test 13: session-start kill switch suppresses output
|
||||
@@ -115,7 +107,7 @@ else
|
||||
fail "session-start writes .agents to repo root"
|
||||
fi
|
||||
|
||||
# Test 14b: session-start lookup uses handoff goal + active bead, not commit subject
|
||||
# Test 14b: session-start no longer performs hidden lookup injection
|
||||
MOCK_LOOKUP="$TMPDIR/mock-session-lookup"
|
||||
mkdir -p "$MOCK_LOOKUP/.agents/handoff" "$MOCK_LOOKUP/bin"
|
||||
git -C "$MOCK_LOOKUP" init -q >/dev/null 2>&1
|
||||
@@ -130,8 +122,8 @@ cat > "$MOCK_LOOKUP/.agents/handoff/handoff-20260322T160000Z.json" <<'EOF'
|
||||
"id": "handoff-test",
|
||||
"created_at": "2026-03-22T16:00:00Z",
|
||||
"type": "manual",
|
||||
"goal": "task-scoped lookup queries",
|
||||
"summary": "use the handoff goal for retrieval"
|
||||
"goal": "task-scoped silent startup",
|
||||
"summary": "startup should materialize state without injected lookup chatter"
|
||||
}
|
||||
EOF
|
||||
cat > "$MOCK_LOOKUP/bin/ao" <<'EOF'
|
||||
@@ -139,36 +131,82 @@ cat > "$MOCK_LOOKUP/bin/ao" <<'EOF'
|
||||
if [ -n "${AO_ARGS_FILE:-}" ]; then
|
||||
printf '%s\n' "$*" >> "$AO_ARGS_FILE"
|
||||
fi
|
||||
if [ "${1:-}" = "lookup" ]; then
|
||||
printf '[lookup] stub result\n'
|
||||
fi
|
||||
exit 0
|
||||
EOF
|
||||
cat > "$MOCK_LOOKUP/bin/bd" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
if [ "${1:-}" = "current" ]; then
|
||||
printf 'ag-73u.5\n'
|
||||
fi
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$MOCK_LOOKUP/bin/ao" "$MOCK_LOOKUP/bin/bd"
|
||||
chmod +x "$MOCK_LOOKUP/bin/ao"
|
||||
AO_ARGS_FILE="$MOCK_LOOKUP/ao-args.log"
|
||||
LOOKUP_OUTPUT=$(cd "$MOCK_LOOKUP" && PATH="$MOCK_LOOKUP/bin:$PATH" AO_ARGS_FILE="$AO_ARGS_FILE" bash "$HOOKS_DIR/session-start.sh" 2>/dev/null || true)
|
||||
if grep -q '^lookup --limit 5 --query task-scoped lookup queries --bead ag-73u.5$' "$AO_ARGS_FILE"; then
|
||||
pass "session-start lookup is scoped by handoff goal and bead"
|
||||
if ! grep -qE '^(lookup|knowledge brief)\b' "$AO_ARGS_FILE" 2>/dev/null; then
|
||||
pass "session-start performs no hidden lookup or briefing work in manual mode"
|
||||
else
|
||||
fail "session-start lookup is scoped by handoff goal and bead"
|
||||
fail "session-start performs no hidden lookup or briefing work in manual mode"
|
||||
fi
|
||||
if ! grep -q 'commit subject should not drive lookup' "$AO_ARGS_FILE"; then
|
||||
pass "session-start no longer falls back to commit subject when task context exists"
|
||||
if [ -z "$LOOKUP_OUTPUT" ]; then
|
||||
pass "session-start remains silent in manual mode"
|
||||
else
|
||||
fail "session-start no longer falls back to commit subject when task context exists"
|
||||
fail "session-start remains silent in manual mode"
|
||||
fi
|
||||
LOOKUP_CONTEXT=$(echo "$LOOKUP_OUTPUT" | jq -r '.hookSpecificOutput.additionalContext // ""' 2>/dev/null)
|
||||
if echo "$LOOKUP_CONTEXT" | grep -q 'auto-retrieved: query="task-scoped lookup queries", bead=ag-73u.5'; then
|
||||
pass "session-start reports lookup scope in injected context"
|
||||
|
||||
# Test 14c: session-start can stage a factory briefing silently when startup goal exists
|
||||
MOCK_FACTORY_START="$TMPDIR/mock-factory-start"
|
||||
mkdir -p "$MOCK_FACTORY_START/.agents/handoff" "$MOCK_FACTORY_START/bin"
|
||||
git -C "$MOCK_FACTORY_START" init -q >/dev/null 2>&1
|
||||
cat > "$MOCK_FACTORY_START/.agents/handoff/handoff-20260402T192000Z.json" <<'EOF'
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "handoff-factory-test",
|
||||
"created_at": "2026-04-02T19:20:00Z",
|
||||
"type": "manual",
|
||||
"goal": "stabilize auth startup",
|
||||
"summary": "route this session into the factory lane"
|
||||
}
|
||||
EOF
|
||||
cat > "$MOCK_FACTORY_START/bin/ao" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
if [ -n "${AO_ARGS_FILE:-}" ]; then
|
||||
printf '%s\n' "$*" >> "$AO_ARGS_FILE"
|
||||
fi
|
||||
if [ "${1:-}" = "knowledge" ] && [ "${2:-}" = "brief" ]; then
|
||||
briefing_path="${MOCK_FACTORY_BRIEFING_PATH:?}"
|
||||
mkdir -p "$(dirname "$briefing_path")"
|
||||
cat > "$briefing_path" <<'BRIEF'
|
||||
# Briefing: stabilize auth startup
|
||||
|
||||
## Relevant Topics
|
||||
- `auth-startup` (healthy)
|
||||
BRIEF
|
||||
jq -n --arg path "$briefing_path" '{"output_path":$path}'
|
||||
exit 0
|
||||
fi
|
||||
if [ "${1:-}" = "lookup" ]; then
|
||||
printf '[lookup] supporting result\n'
|
||||
fi
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$MOCK_FACTORY_START/bin/ao"
|
||||
AO_ARGS_FILE="$MOCK_FACTORY_START/ao-args.log"
|
||||
MOCK_FACTORY_BRIEFING_PATH="$MOCK_FACTORY_START/.agents/briefings/2026-04-02-stabilize-auth-startup.md"
|
||||
FACTORY_OUTPUT=$(cd "$MOCK_FACTORY_START" && \
|
||||
PATH="$MOCK_FACTORY_START/bin:$PATH" \
|
||||
AO_ARGS_FILE="$AO_ARGS_FILE" \
|
||||
MOCK_FACTORY_BRIEFING_PATH="$MOCK_FACTORY_BRIEFING_PATH" \
|
||||
AGENTOPS_STARTUP_CONTEXT_MODE=factory \
|
||||
bash "$HOOKS_DIR/session-start.sh" 2>/dev/null || true)
|
||||
if grep -q '^knowledge brief --json --goal stabilize auth startup$' "$AO_ARGS_FILE"; then
|
||||
pass "session-start builds a factory briefing from startup goal"
|
||||
else
|
||||
fail "session-start reports lookup scope in injected context"
|
||||
fail "session-start builds a factory briefing from startup goal"
|
||||
fi
|
||||
if [ "$(cat "$MOCK_FACTORY_START/.agents/ao/factory-goal.txt" 2>/dev/null)" = "stabilize auth startup" ] && \
|
||||
[ "$(cat "$MOCK_FACTORY_START/.agents/ao/factory-briefing.txt" 2>/dev/null)" = "$MOCK_FACTORY_BRIEFING_PATH" ]; then
|
||||
pass "session-start records staged factory state"
|
||||
else
|
||||
fail "session-start records staged factory state"
|
||||
fi
|
||||
if [ -z "$FACTORY_OUTPUT" ]; then
|
||||
pass "session-start does not inject startup context in factory mode"
|
||||
else
|
||||
fail "session-start does not inject startup context in factory mode"
|
||||
fi
|
||||
|
||||
# Test 15: precompact emits JSON when data exists
|
||||
@@ -1714,6 +1752,71 @@ else
|
||||
fail "intent-echo.sh not found in hooks.json"
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
echo "=== factory-router.sh ==="
|
||||
# ============================================================
|
||||
|
||||
FACTORY_ROUTER_MOCK="$TMPDIR/factory-router-mock"
|
||||
mkdir -p "$FACTORY_ROUTER_MOCK/.agents/ao" "$FACTORY_ROUTER_MOCK/bin"
|
||||
git -C "$FACTORY_ROUTER_MOCK" init -q >/dev/null 2>&1
|
||||
touch "$FACTORY_ROUTER_MOCK/.agents/ao/.factory-intake-needed"
|
||||
cat > "$FACTORY_ROUTER_MOCK/bin/ao" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
if [ -n "${AO_ARGS_FILE:-}" ]; then
|
||||
printf '%s\n' "$*" >> "$AO_ARGS_FILE"
|
||||
fi
|
||||
if [ "${1:-}" = "knowledge" ] && [ "${2:-}" = "brief" ]; then
|
||||
briefing_path="${MOCK_FACTORY_BRIEFING_PATH:?}"
|
||||
mkdir -p "$(dirname "$briefing_path")"
|
||||
cat > "$briefing_path" <<'BRIEF'
|
||||
# Briefing: fix auth bootstrap
|
||||
|
||||
## Relevant Topics
|
||||
- `auth-bootstrap` (healthy)
|
||||
BRIEF
|
||||
jq -n --arg path "$briefing_path" '{"output_path":$path}'
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$FACTORY_ROUTER_MOCK/bin/ao"
|
||||
AO_ARGS_FILE="$FACTORY_ROUTER_MOCK/ao-args.log"
|
||||
MOCK_FACTORY_BRIEFING_PATH="$FACTORY_ROUTER_MOCK/.agents/briefings/2026-04-02-fix-auth-bootstrap.md"
|
||||
ROUTER_OUTPUT=$(cd "$FACTORY_ROUTER_MOCK" && \
|
||||
printf '%s' '{"prompt":"fix auth bootstrap"}' | \
|
||||
PATH="$FACTORY_ROUTER_MOCK/bin:$PATH" \
|
||||
AO_ARGS_FILE="$AO_ARGS_FILE" \
|
||||
MOCK_FACTORY_BRIEFING_PATH="$MOCK_FACTORY_BRIEFING_PATH" \
|
||||
AGENTOPS_STARTUP_CONTEXT_MODE=factory \
|
||||
bash "$HOOKS_DIR/factory-router.sh" 2>/dev/null || true)
|
||||
if grep -q '^knowledge brief --json --goal fix auth bootstrap$' "$AO_ARGS_FILE"; then
|
||||
pass "factory-router builds briefing from first substantive prompt"
|
||||
else
|
||||
fail "factory-router builds briefing from first substantive prompt"
|
||||
fi
|
||||
if [ ! -f "$FACTORY_ROUTER_MOCK/.agents/ao/.factory-intake-needed" ] && \
|
||||
[ "$(cat "$FACTORY_ROUTER_MOCK/.agents/ao/factory-goal.txt" 2>/dev/null)" = "fix auth bootstrap" ]; then
|
||||
pass "factory-router records the captured goal and clears intake state"
|
||||
else
|
||||
fail "factory-router records the captured goal and clears intake state"
|
||||
fi
|
||||
if [ "$(cat "$FACTORY_ROUTER_MOCK/.agents/ao/factory-briefing.txt" 2>/dev/null)" = "$MOCK_FACTORY_BRIEFING_PATH" ] && \
|
||||
[ -f "$FACTORY_ROUTER_MOCK/.agents/ao/.factory-router-fired" ]; then
|
||||
pass "factory-router stages briefing state and dedup marker"
|
||||
else
|
||||
fail "factory-router stages briefing state and dedup marker"
|
||||
fi
|
||||
if [ -z "$ROUTER_OUTPUT" ]; then
|
||||
pass "factory-router stays silent"
|
||||
else
|
||||
fail "factory-router stays silent"
|
||||
fi
|
||||
if jq -e '.hooks.UserPromptSubmit[].hooks[] | select(.command | contains("factory-router.sh"))' "$HOOKS_DIR/hooks.json" >/dev/null 2>&1; then
|
||||
pass "factory-router.sh wired in hooks.json"
|
||||
else
|
||||
fail "factory-router.sh not found in hooks.json"
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
echo "=== research-loop-detector.sh ==="
|
||||
# ============================================================
|
||||
|
||||
@@ -142,42 +142,21 @@ else
|
||||
fail "session-start.sh exits with code 0 (got $EXIT_CODE)"
|
||||
fi
|
||||
|
||||
if [ -n "$OUTPUT" ]; then
|
||||
pass "session-start.sh produces output"
|
||||
if [ -z "$OUTPUT" ]; then
|
||||
pass "session-start.sh stays silent"
|
||||
else
|
||||
fail "session-start.sh produces output"
|
||||
fail "session-start.sh stays silent"
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
echo ""
|
||||
echo "=== Test 2: session-start.sh JSON output validation ==="
|
||||
echo "=== Test 2: session-start.sh silent runtime prep ==="
|
||||
# ============================================================
|
||||
|
||||
# Validate JSON structure
|
||||
if echo "$OUTPUT" | jq . >/dev/null 2>&1; then
|
||||
pass "session-start.sh produces valid JSON"
|
||||
if [ -z "$OUTPUT" ]; then
|
||||
pass "session-start.sh emits no operator-facing context"
|
||||
else
|
||||
fail "session-start.sh produces valid JSON"
|
||||
fi
|
||||
|
||||
# Check for required fields
|
||||
if echo "$OUTPUT" | jq -e '.hookSpecificOutput.hookEventName' >/dev/null 2>&1; then
|
||||
pass "JSON contains hookEventName field"
|
||||
else
|
||||
fail "JSON contains hookEventName field"
|
||||
fi
|
||||
|
||||
if echo "$OUTPUT" | jq -e '.hookSpecificOutput.additionalContext' >/dev/null 2>&1; then
|
||||
pass "JSON contains additionalContext field"
|
||||
else
|
||||
fail "JSON contains additionalContext field"
|
||||
fi
|
||||
|
||||
EVENT_NAME=$(echo "$OUTPUT" | jq -r '.hookSpecificOutput.hookEventName')
|
||||
if [ "$EVENT_NAME" = "SessionStart" ]; then
|
||||
pass "hookEventName is 'SessionStart'"
|
||||
else
|
||||
fail "hookEventName is 'SessionStart' (got: $EVENT_NAME)"
|
||||
fail "session-start.sh emits no operator-facing context"
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
|
||||
Reference in New Issue
Block a user