feat(nudge): ReContext-grounded cue reactivation + analyst refinements

Analyse claude-adam through ReContext (arXiv:2607.02509): close the
access!=utilization gap with query-conditioned nudge replay + analyst tweaks.

- nudge: optional cue.cwd gates SessionStart replay (adam-nudge.mjs) and
  new mid-session reactivation on UserPromptSubmit (adam-observe.mjs,
  cwd-gated, once/session, ADAM_NO_MIDSESSION_NUDGE opt-out).
- new adam-apply-nudge.mjs builds message + cue deterministically from
  proposal frontmatter (wired into SKILL.md, install.sh, pending-check).
- adam-batch: optional --max-batches/--top-fraction output cap (default off),
  reports capped.
- analyst: utility-lift rubric penalty + skill_utility_path fed to both
  stages; adaptive re-diagnose for unclear/top-severity clusters;
  cross-type associative pass (agents/adam.md, SKILL.md).
- tests: 140 -> 144 (cue gate, batch cap, apply-nudge, mid-session).
This commit is contained in:
2026-07-06 13:14:17 +01:00
parent 4d1276a73f
commit 2b6e8da885
9 changed files with 337 additions and 26 deletions
+110
View File
@@ -19,6 +19,7 @@ UPGRADE="$REAL_HOME/.claude/adam/scripts/adam-upgrade.mjs"
BATCH="$REAL_HOME/.claude/adam/scripts/adam-batch.mjs"
ROLLBACK="$REAL_HOME/.claude/adam/scripts/adam-rollback.mjs"
SKILLUTIL="$REAL_HOME/.claude/adam/scripts/adam-skill-utility.mjs"
APPLYNUDGE="$REAL_HOME/.claude/adam/scripts/adam-apply-nudge.mjs"
TMP_HOME="$(mktemp -d -t adam-test.XXXXXX)"
trap 'rm -rf "$TMP_HOME"' EXIT INT TERM
@@ -39,6 +40,7 @@ UPGRADE_RUN() { HOME="$TMP_HOME" node "$UPGRADE" "$@"; }
BATCH_RUN() { HOME="$TMP_HOME" node "$BATCH" "$@"; }
ROLLBACK_RUN(){ HOME="$TMP_HOME" node "$ROLLBACK" "$@"; }
SKILLUTIL_RUN(){ HOME="$TMP_HOME" node "$SKILLUTIL" "$@"; }
APPLYNUDGE_RUN(){ HOME="$TMP_HOME" node "$APPLYNUDGE" "$@" --home "$TMP_HOME/.claude"; }
PASS=0
FAIL=0
@@ -2176,6 +2178,114 @@ else
fi
rm -f "$SU_INPUT"
# --- Test 120: nudge cue.cwd gates surfacing on the current session's cwd ---
echo "Test 120: nudge cue.cwd — matching cwd prints, mismatch deferred (kept, not consumed)"
reset_state
cue_now=$(node -e 'console.log(Date.now())')
cue_future=$(node -e 'console.log(Date.now() + 7*86400000)')
cat > "$ROOT/active-nudges.json" <<EOF
[{"kind":"dead_end_reminder","message":"adam: cue-match checkpoint","created_at":$cue_now,"expires_at_ts":$cue_future,"max_displays":3,"displays_used":0,"source_session":"sOLD","cue":{"cwd":"/proj/a"}}]
EOF
out_match=$(echo '{"hook_event_name":"SessionStart","session_id":"sNEW","cwd":"/proj/a"}' | NUDGE_RUN 2>&1 || true)
cat > "$ROOT/active-nudges.json" <<EOF
[{"kind":"dead_end_reminder","message":"adam: cue-miss checkpoint","created_at":$cue_now,"expires_at_ts":$cue_future,"max_displays":3,"displays_used":0,"source_session":"sOLD","cue":{"cwd":"/proj/a"}}]
EOF
out_miss=$(echo '{"hook_event_name":"SessionStart","session_id":"sNEW","cwd":"/proj/b"}' | NUDGE_RUN 2>&1 || true)
cue_remaining=$(node -e 'const j=require("'"$ROOT"'/active-nudges.json");console.log(j.length)')
cue_used=$(node -e 'const j=require("'"$ROOT"'/active-nudges.json");console.log((j[0]||{}).displays_used)')
if echo "$out_match" | grep -q "cue-match checkpoint" \
&& ! echo "$out_miss" | grep -q "cue-miss checkpoint" \
&& [ "$cue_remaining" = "1" ] && [ "$cue_used" = "0" ]; then
echo " PASS: cue.cwd match prints; mismatch deferred without consuming a display"; PASS=$((PASS+1))
else
echo " FAIL: match=$out_match miss=$out_miss remaining=$cue_remaining used=$cue_used"; FAIL=$((FAIL+1))
fi
rm -f "$ROOT/active-nudges.json"
# --- Test 121: adam-batch output cap (--max-batches / --top-fraction), default off ---
echo "Test 121: adam-batch caps ranked output and reports capped; no flag = no cap"
cap_input=$(cat <<'EOF'
{"ts":"2026-05-20T10:00:00Z","type":"correction","session":"s1","cwd":"/a","phrase":"alpha alpha"}
{"ts":"2026-05-20T11:00:00Z","type":"correction","session":"s2","cwd":"/a","phrase":"alpha alpha"}
{"ts":"2026-05-20T12:00:00Z","type":"retry_loop","session":"s1","cwd":"/a","tool":"Bash"}
{"ts":"2026-05-21T10:00:00Z","type":"tool_error_loop","session":"s1","cwd":"/a","fp":"ENOENT:x","count":3}
EOF
)
GET() { node -e 'let b="";process.stdin.on("data",d=>b+=d).on("end",()=>{const j=JSON.parse(b);console.log(eval("j."+process.argv[1]))})' "$1"; }
mb_out=$(echo "$cap_input" | BATCH_RUN --max-batches 2 2>/dev/null)
mb_bc=$(echo "$mb_out" | GET "batches.length"); mb_cap=$(echo "$mb_out" | GET "capped"); mb_top=$(echo "$mb_out" | GET "batches[0].signal_type")
none_out=$(echo "$cap_input" | BATCH_RUN 2>/dev/null)
none_bc=$(echo "$none_out" | GET "batches.length"); none_cap=$(echo "$none_out" | GET "capped")
tf_out=$(echo "$cap_input" | BATCH_RUN --top-fraction 0.5 2>/dev/null)
tf_bc=$(echo "$tf_out" | GET "batches.length"); tf_cap=$(echo "$tf_out" | GET "capped")
if [ "$mb_bc" = "2" ] && [ "$mb_cap" = "1" ] && [ "$mb_top" = "correction" ] \
&& [ "$none_bc" = "3" ] && [ "$none_cap" = "0" ] \
&& [ "$tf_bc" = "2" ] && [ "$tf_cap" = "1" ]; then
echo " PASS: --max-batches 2 → 2 batches (capped=1, head=correction); no flag → 3/capped=0; --top-fraction 0.5 → 2/capped=1"; PASS=$((PASS+1))
else
echo " FAIL: mb($mb_bc,$mb_cap,$mb_top) none($none_bc,$none_cap) tf($tf_bc,$tf_cap)"; FAIL=$((FAIL+1))
fi
# --- Test 122: adam-apply-nudge builds a cue'd entry deterministically; gates non-nudge ---
echo "Test 122: adam-apply-nudge populates cue.cwd + generated message from frontmatter"
reset_state
rm -f "$ROOT/active-nudges.json"
cat > "$ROOT/proposals/nudge-prop-001.md" <<'EOF'
---
type: nudge
source_session: sNUDGE
cwd: /Users/me/proj-alpha
dead_end_count: 4
confidence: 4
blast_radius: low
---
# Nudge
checkpoint reminder
EOF
an_out=$(APPLYNUDGE_RUN "$ROOT/proposals/nudge-prop-001.md" 2>&1 || true)
an_status=$(echo "$an_out" | node -e 'let b="";process.stdin.on("data",d=>b+=d).on("end",()=>{try{console.log(JSON.parse(b).status)}catch{console.log("parse-error")}})')
an_cue=$(node -e 'const j=require("'"$ROOT"'/active-nudges.json");console.log((j[0]&&j[0].cue&&j[0].cue.cwd)||"")')
an_src=$(node -e 'const j=require("'"$ROOT"'/active-nudges.json");console.log((j[0]||{}).source_session||"")')
an_msg=$(node -e 'const j=require("'"$ROOT"'/active-nudges.json");console.log((j[0]||{}).message||"")')
cat > "$ROOT/proposals/nudge-prop-002.md" <<'EOF'
---
type: memory
source_session: sX
---
EOF
gate_status=$(APPLYNUDGE_RUN "$ROOT/proposals/nudge-prop-002.md" 2>&1 | node -e 'let b="";process.stdin.on("data",d=>b+=d).on("end",()=>{try{console.log(JSON.parse(b).status)}catch{console.log("parse-error")}})')
if [ "$an_status" = "applied" ] && [ "$an_cue" = "/Users/me/proj-alpha" ] && [ "$an_src" = "sNUDGE" ] \
&& echo "$an_msg" | grep -q "4 dead-ends in proj-alpha" && [ "$gate_status" = "gated" ]; then
echo " PASS: applied with cue.cwd + generated message; non-nudge proposal gated"; PASS=$((PASS+1))
else
echo " FAIL: status=$an_status cue=$an_cue src=$an_src msg=$an_msg gate=$gate_status"; FAIL=$((FAIL+1))
fi
rm -f "$ROOT/proposals/nudge-prop-00"*.md "$ROOT/active-nudges.json"
# --- Test 123: mid-session nudge reactivation on UserPromptSubmit (cue.cwd gated) ---
echo "Test 123: adam-observe surfaces a cue-matched nudge mid-session — once/session, cwd-gated, opt-out"
ms_future=$(node -e 'console.log(Date.now() + 7*86400000)')
ms_nudge() { cat > "$ROOT/active-nudges.json" <<EOF
[{"kind":"dead_end_reminder","message":"adam: midsession checkpoint alpha","created_at":111,"expires_at_ts":$ms_future,"max_displays":3,"displays_used":0,"source_session":"sOLD","cue":{"cwd":"/proj/x"}}]
EOF
}
reset_state; ms_nudge
ms1=$(echo '{"hook_event_name":"UserPromptSubmit","session_id":"sMS","cwd":"/proj/x","prompt":"hello"}' | HOOK_RUN 2>&1 || true)
ms2=$(echo '{"hook_event_name":"UserPromptSubmit","session_id":"sMS","cwd":"/proj/x","prompt":"again"}' | HOOK_RUN 2>&1 || true)
reset_state; ms_nudge
ms_miss=$(echo '{"hook_event_name":"UserPromptSubmit","session_id":"sMS2","cwd":"/proj/y","prompt":"hello"}' | HOOK_RUN 2>&1 || true)
reset_state; ms_nudge
ms_off=$(echo '{"hook_event_name":"UserPromptSubmit","session_id":"sMS3","cwd":"/proj/x","prompt":"hello"}' | HOME="$TMP_HOME" ADAM_NO_MIDSESSION_NUDGE=1 node "$HOOK" 2>&1 || true)
if echo "$ms1" | grep -q "midsession checkpoint alpha" \
&& ! echo "$ms2" | grep -q "midsession checkpoint alpha" \
&& ! echo "$ms_miss" | grep -q "midsession checkpoint alpha" \
&& ! echo "$ms_off" | grep -q "midsession checkpoint alpha"; then
echo " PASS: surfaced once on cwd match; deduped 2nd prompt; silent on cwd mismatch + opt-out"; PASS=$((PASS+1))
else
echo " FAIL: ms1=$ms1 || ms2=$ms2 || miss=$ms_miss || off=$ms_off"; FAIL=$((FAIL+1))
fi
rm -f "$ROOT/active-nudges.json"
echo
echo "Results: $PASS passed, $FAIL failed"
[ "$FAIL" = "0" ]