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:
Boden Fuller
2026-04-02 15:54:48 -04:00
22 changed files with 770 additions and 437 deletions
+11
View File
@@ -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.
+5
View File
@@ -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
+11
View File
@@ -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 |
+146
View File
@@ -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
+5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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) |
---
+1 -1
View File
@@ -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` |
+3 -2
View File
@@ -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
+2 -2
View File
@@ -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 |
+10 -7
View File
@@ -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 |
+146
View File
@@ -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
+5
View File
@@ -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
View File
@@ -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
+33 -11
View File
@@ -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"
+5 -15
View File
@@ -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"
+96 -8
View File
@@ -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
View File
@@ -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 ==="
# ============================================================
+7 -28
View File
@@ -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
# ============================================================