curiosity-auto-trigger: implement flood prevention, anti-self-reference, kill-switch, proposal auto-generation

This commit is contained in:
openclaw
2026-03-27 01:38:02 -04:00
parent 21d91163ca
commit 0bd216458d
@@ -1,152 +1,391 @@
#!/bin/bash
# Curiosity Auto-Trigger — Gap → Action
# Curiosity Auto-Trigger — Gap → Proposal Auto-Generation
# Enhanced per PROPOSAL-curiosity-auto-trigger
# Implementation conditions:
# 1. Flood prevention: max 1 proposal/run, max 3/week
# 2. Anti-self-reference: excludes own output from gap detection
# 3. Kill-switch: reads kill-switch file before executing
# 4. Proposal auto-generation for gaps that meet the proposal bar
# Usage: ./curiosity-auto-trigger.sh [--dry-run] [--verbose]
set -e
WORKSPACE_ROOT="${WORKSPACE_ROOT:-/home/openclaw/.openclaw/workspace}"
DB_PATH="$WORKSPACE_ROOT/.curiosity/curiosity_metrics.db"
CURIOSITY_DIR="$WORKSPACE_ROOT/.curiosity"
STATE_DIR="$WORKSPACE_ROOT/triad/state"
DRAFTS_DIR="$WORKSPACE_ROOT/triad/deliberations/drafts"
KILL_SWITCH="$CURIOSITY_DIR/kill-switch"
COUNTER_FILE="$CURIOSITY_DIR/proposal-counter.json"
METRICS_FILE="$CURIOSITY_DIR/curiosity-metrics.json"
REJECTION_FILE="$CURIOSITY_DIR/auto-rejections.json"
# Ensure .curiosity directory exists
mkdir -p "$WORKSPACE_ROOT/.curiosity"
# Ensure directories exist
mkdir -p "$CURIOSITY_DIR" "$DRAFTS_DIR"
# Load capability gaps from curiosity engine
# === KILL-SWITCH ===
function check_kill_switch() {
if [[ -f "$KILL_SWITCH" ]]; then
local content
content=$(cat "$KILL_SWITCH" 2>/dev/null || echo "")
if [[ "$content" == "ON" ]] || [[ "$content" == "on" ]]; then
echo "⛔ Kill-switch is ON. Curiosity auto-trigger disabled."
echo "To re-enable: echo 'OFF' > $KILL_SWITCH"
exit 0
fi
fi
}
# === FLOOD PREVENTION ===
function check_flood_limits() {
# Initialize counter if missing
if [[ ! -f "$COUNTER_FILE" ]]; then
cat > "$COUNTER_FILE" <<'EOF'
{
"run_count": 0,
"weekly_count": 0,
"week_start": null,
"last_run": null
}
EOF
fi
local counter
counter=$(cat "$COUNTER_FILE")
local week_start
week_start=$(echo "$counter" | jq -r '.week_start')
local current_week
current_week=$(date -u +%Y-W%V)
# Reset weekly counter if new week
if [[ "$week_start" != "$current_week" ]]; then
counter=$(echo "$counter" | jq '.weekly_count = 0 | .week_start = "'$current_week'"')
echo "$counter" > "$COUNTER_FILE"
fi
local run_count
run_count=$(echo "$counter" | jq -r '.run_count')
local weekly_count
weekly_count=$(echo "$counter" | jq -r '.weekly_count')
# Flood check: 1 per run, 3 per week
if [[ "$run_count" -ge 1 ]]; then
echo "⛔ Flood limit reached: already generated $run_count proposal(s) this run."
return 1
fi
if [[ "$weekly_count" -ge 3 ]]; then
echo "⛔ Weekly flood limit reached: $weekly_count/3 proposals this week."
return 1
fi
return 0
}
function increment_counter() {
local counter
counter=$(cat "$COUNTER_FILE")
counter=$(echo "$counter" | jq '.run_count += 1 | .weekly_count += 1 | .last_run = "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"')
echo "$counter" > "$COUNTER_FILE"
}
# === REJECTION TRACKING (5-rejection recalibration) ===
function check_rejection_rate() {
if [[ ! -f "$REJECTION_FILE" ]]; then
return 0 # No rejections, proceed
fi
local rejections
rejections=$(cat "$REJECTION_FILE")
local count
count=$(echo "$rejections" | jq -r '.count // 0')
if [[ "$count" -ge 5 ]]; then
echo "⚠️ 5+ consecutive rejections detected. Recalibrating thresholds..."
# Increase threshold for next run: require higher confidence
local threshold
threshold=$(echo "$rejections" | jq -r '.last_threshold // 0.7')
threshold=$(echo "$threshold + 0.1" | bc -l | head -c 3)
echo "$rejections" | jq '.last_threshold = '"$threshold"' | .count = 0' > "$REJECTION_FILE"
echo " New confidence threshold: $threshold"
return 1 # Temporarily disable
fi
return 0
}
function record_rejection() {
if [[ ! -f "$REJECTION_FILE" ]]; then
echo '{"count": 1, "last_threshold": 0.7}' > "$REJECTION_FILE"
else
local rejections
rejections=$(cat "$REJECTION_FILE")
rejections=$(echo "$rejections" | jq '.count += 1')
echo "$rejections" > "$REJECTION_FILE"
fi
}
# === CAPABILITY GAP DETECTION ===
function get_gaps() {
# Read gap-report.json if it exists
local gap_report="$STATE_DIR/gap-report.json"
if [[ -f "$gap_report" ]]; then
jq -r '.gaps[]?.skill // empty' "$gap_report" 2>/dev/null || echo ""
return
fi
# Fallback: use curiosity engine JSON output
node "$WORKSPACE_ROOT/scripts/curiosity-engine.js" --json 2>/dev/null | \
jq -r '.capability_report[].gap_list[]' 2>/dev/null || echo ""
}
# Relevance ranking: prioritize high-impact gaps
# === ANTI-SELF-REFERENCE ===
# Filter out gaps that are about this skill itself
function filter_anti_self_reference() {
local gaps="$1"
local filtered=""
local self_pattern="curiosity-auto-trigger\|curiosity-engine\|gap-detector\|opportunity-scanner"
for gap in $gaps; do
if echo "$gap" | grep -qE "$self_pattern"; then
echo " 🔄 Excluding (self-reference): $gap"
continue
fi
filtered="$filtered $gap"
done
echo "$filtered" | xargs
}
# === RELEVANCE RANKING ===
function rank_gap() {
local skill="$1"
local priority="medium"
# Triad skills = critical
local confidence=0.5
case "$skill" in
triad-*) priority="critical" ;;
*-triad-*) priority="critical" ;;
detect-*) priority="high" ;;
backup-*) priority="high" ;;
audit-*) priority="medium" ;;
auto-*) priority="medium" ;;
gap-*) priority="high" ;;
opportunity-*) priority="high" ;;
*) priority="low" ;;
triad-*|*-triad-*) priority="critical"; confidence=0.9 ;;
detect-*|backup-*|gap-*|opportunity-*) priority="high"; confidence=0.7 ;;
audit-*|auto-*) priority="medium"; confidence=0.5 ;;
*) priority="low"; confidence=0.3 ;;
esac
echo "$priority"
echo "$priority $confidence"
}
# Auto-install low-risk skills (skip deliberation)
function auto_install() {
# === PROPOSAL BAR CHECK ===
# Returns 0 if gap meets proposal bar, 1 otherwise
function meets_proposal_bar() {
local skill="$1"
local priority="$2"
# Only auto-install low/medium risk
if [[ "$priority" == "critical" ]] || [[ "$priority" == "high" ]]; then
echo "⚠️ High-priority gap: $skill requires deliberation"
return 1
fi
echo "✅ Auto-installing: $skill"
if clawhub install "$skill" 2>&1; then
echo "✅ Installed: $skill"
local confidence="$2"
# Proposal bar: actionable within 2 weeks, less than 1 hour deliberation
# Use confidence threshold
local threshold
threshold=$(cat "$REJECTION_FILE" 2>/dev/null | jq -r '.last_threshold // 0.7')
local meets
meets=$(echo "$confidence >= $threshold" | bc -l)
if [[ "$meets" == "1" ]]; then
return 0
else
echo "❌ Install failed: $skill"
return 1
fi
}
# Log autonomy metrics before/after
function log_metrics() {
local before="$1"
local after="$2"
local skill="$3"
local logfile="$WORKSPACE_ROOT/.curiosity/autonomy_log.md"
# Create header if file doesn't exist
if [[ ! -f "$logfile" ]]; then
cat > "$logfile" <<EOF
# Autonomy Log
# === PROPOSAL DRAFT GENERATION ===
function generate_proposal_draft() {
local skill="$1"
local priority="$2"
local confidence="$3"
| Timestamp | Before | After | Delta | Skills Installed |
| ------------------- | ------ | ----- | ----- | --------------------- |
EOF
fi
local delta
delta=$(echo "$after - $before" | bc 2>/dev/null || echo "0")
local timestamp
timestamp=$(date -Iseconds)
echo "| $timestamp | $before% | $after% | $delta% | $skill |" >> "$logfile"
timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
local proposal_id
proposal_id="AUTO-$(date +%Y%m%d)-$(echo $skill | tr '-' '_')"
# Read gap details from gap-report
local gap_detail=""
local gap_report="$STATE_DIR/gap-report.json"
if [[ -f "$gap_report" ]]; then
gap_detail=$(jq -r ".gaps[] | select(.skill == \"$skill\") | .description // empty" "$gap_report" 2>/dev/null || echo "")
fi
local draft_file="$DRAFTS_DIR/curiosity-$proposal_id.md"
cat > "$draft_file" <<EOF
# [AUTO-GENERATED] Proposal: Auto-Install $skill
**Generated by:** curiosity-auto-trigger
**Timestamp:** $timestamp
**Confidence:** $confidence
**Priority:** $priority
**Tags:** autonomy, auto-generated
---
## Summary
Auto-generated proposal: install skill **$skill** to address capability gap detected by curiosity engine.
## Background
Gap detected: **$skill**
Priority: $priority
Confidence: $confidence
$gap_detail
## The Proposal
Install **$skill** via ClawHub to address the identified capability gap.
## Implementation
\`\`\`bash
clawhub install $skill
\`\`\`
## Success Criteria
- Skill installed and functional
- Skill file exists at \`skills/$skill/SKILL.md\`
- Tests pass (if applicable)
## Risks
- Installation may introduce unexpected behavior (mitigation: test in staging first)
- Skill may conflict with existing capabilities (mitigation: review before production)
## Rollback
\`\`\`bash
clawhub uninstall $skill
\`\`\`
---
*Auto-generated by curiosity-auto-trigger. This proposal was auto-generated because the detected gap met the proposal bar (confidence >= $threshold). The triad still deliberates and votes on this proposal.*
EOF
echo "$draft_file"
}
# Main auto-trigger loop
# === MAIN AUTO-TRIGGER LOOP ===
function run() {
echo "🦞 === Curiosity Auto-Trigger ==="
echo "Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo ""
# Get autonomy score before
local before_score
before_score=$(node "$WORKSPACE_ROOT/scripts/curiosity-engine.js" --json 2>/dev/null | \
jq -r '.autonomy_score' 2>/dev/null || echo "0")
echo "Autonomy score before: $before_score%"
echo ""
# Get gaps
# 1. Kill-switch check
check_kill_switch
# 2. Rejection rate check (recalibration)
if ! check_rejection_rate; then
echo "⛔ Recalibration active. Skipping this cycle."
return 0
fi
# 3. Flood limit check
if ! check_flood_limits; then
return 0
fi
# 4. Get and filter gaps
local gaps
gaps=$(get_gaps)
if [[ -z "$gaps" ]]; then
echo "✅ No gaps detected. Autonomy optimal."
return 0
fi
echo "Gaps detected: $gaps"
echo "Gaps found: $gaps"
# 5. Anti-self-reference filter
local filtered_gaps
filtered_gaps=$(filter_anti_self_reference "$gaps")
if [[ -z "$filtered_gaps" ]]; then
echo "✅ All gaps filtered (self-reference). Autonomy optimal."
return 0
fi
echo "Filtered gaps: $filtered_gaps"
echo ""
# Process each gap
local installed_count=0
for gap in $gaps; do
# 6. Process gaps — generate proposal for first one that meets bar
local generated=0
for gap in $filtered_gaps; do
local ranked
ranked=$(rank_gap "$gap")
local priority
priority=$(rank_gap "$gap")
echo "Gap: $gap (priority: $priority)"
# Attempt auto-install
if auto_install "$gap" "$priority"; then
installed_count=$((installed_count + 1))
local confidence
priority=$(echo "$ranked" | cut -d' ' -f1)
confidence=$(echo "$ranked" | cut -d' ' -f2)
echo "Gap: $gap (priority: $priority, confidence: $confidence)"
# Check proposal bar
if ! meets_proposal_bar "$gap" "$confidence"; then
echo " ⬇️ Below proposal bar (confidence $confidence < threshold). Skipping."
continue
fi
# Generate proposal draft
echo " 📝 Generating proposal draft..."
local draft_file
draft_file=$(generate_proposal_draft "$gap" "$priority" "$confidence")
if [[ -f "$draft_file" ]]; then
echo " ✅ Proposal draft created: $draft_file"
increment_counter
generated=1
echo ""
echo "=== Results ==="
echo "Proposal generated: $(basename "$draft_file")"
echo "Flood counters updated: run=1, weekly incremented"
generated=1
break # Only 1 proposal per run (flood prevention)
else
echo " ❌ Failed to create proposal draft"
fi
done
echo ""
echo "=== Results ==="
echo "Attempted: $(echo "$gaps" | wc -w)"
echo "Installed: $installed_count"
# Get autonomy score after
local after_score
after_score=$(node "$WORKSPACE_ROOT/scripts/curiosity-engine.js" --json 2>/dev/null | \
jq -r '.autonomy_score' 2>/dev/null || echo "0")
echo "Autonomy score after: $after_score%"
echo "Improvement: $((after_score - before_score))%"
# Log metrics
log_metrics "$before_score" "$after_score" "$installed_count skills"
if [[ "$generated" == "0" ]]; then
echo "No gaps met proposal bar. Cycle complete."
fi
echo ""
echo "🦞 Auto-trigger complete."
}
# CLI
# === CLI ===
function enable_trigger() {
echo "OFF" > "$KILL_SWITCH"
echo "✅ Curiosity auto-trigger enabled."
}
function disable_trigger() {
echo "ON" > "$KILL_SWITCH"
echo "⛔ Curiosity auto-trigger disabled (kill-switch ON)."
}
function status() {
if [[ -f "$KILL_SWITCH" ]]; then
echo "Kill-switch: $(cat "$KILL_SWITCH")"
else
echo "Kill-switch: OFF (enabled)"
fi
if [[ -f "$COUNTER_FILE" ]]; then
echo "Counter: $(cat "$COUNTER_FILE")"
fi
}
case "${1:-run}" in
run) run ;;
--dry-run) echo "Dry-run mode: would install gaps without executing" ;;
--dry-run) echo "Dry-run mode: would analyze gaps without generating proposals" ;;
--verbose) set -x; run ;;
*) echo "Usage: $0 [--dry-run|--verbose]"; exit 1 ;;
enable) enable_trigger ;;
disable) disable_trigger ;;
status) status ;;
reset-counter) echo '{"run_count": 0, "weekly_count": 0, "week_start": "'$(date -u +%Y-W%V)'", "last_run": null}' > "$COUNTER_FILE" && echo "Counter reset" ;;
*) echo "Usage: $0 [run|dry-run|enable|disable|status|reset-counter]"; exit 1 ;;
esac