11#! /bin/bash
22# Hook: Context Watch — UserPromptSubmit
3- # Checks if the current walnut's state files were modified by another session.
4- # If so, injects additionalContext suggesting a context refresh.
3+ # Two jobs:
4+ # 1. Context % re-injection — at every 20% threshold, re-inject rules + context
5+ # 2. External change detection — if another session modified walnut state files
56
67SCRIPT_DIR=" $( cd " $( dirname " $0 " ) " && pwd) "
78source " $SCRIPT_DIR /walnut-common.sh"
@@ -12,6 +13,125 @@ find_world || exit 0
1213SESSION_ID=" ${HOOK_SESSION_ID} "
1314[ -z " $SESSION_ID " ] && exit 0
1415
16+ # ── CONTEXT % RE-INJECTION ──────────────────────────────────────
17+
18+ CTX_FILE=" $WORLD_ROOT /.walnut/.context_pct"
19+ if [ -f " $CTX_FILE " ]; then
20+ CTX_PCT=$( cat " $CTX_FILE " 2> /dev/null | tr -d ' [:space:]' )
21+
22+ if [ -n " $CTX_PCT " ] && [ " $CTX_PCT " -gt 0 ] 2> /dev/null; then
23+ # Find highest unfired threshold — inject once, not serially across prompts
24+ FIRE_THRESHOLD=" "
25+ for THRESHOLD in 80 60 40 20; do
26+ MARKER=" /tmp/walnut-ctx-${SESSION_ID} -${THRESHOLD} "
27+ if [ " $CTX_PCT " -ge " $THRESHOLD " ] && [ ! -f " $MARKER " ]; then
28+ FIRE_THRESHOLD=" $THRESHOLD "
29+ break
30+ fi
31+ done
32+
33+ if [ -n " $FIRE_THRESHOLD " ]; then
34+ # Mark all thresholds at or below the fired one
35+ for T in 20 40 60 80; do
36+ if [ " $T " -le " $FIRE_THRESHOLD " ]; then
37+ touch " /tmp/walnut-ctx-${SESSION_ID} -${T} "
38+ fi
39+ done
40+ THRESHOLD=" $FIRE_THRESHOLD "
41+
42+ # Build injection content based on threshold level
43+ if [ " $THRESHOLD " -le 40 ]; then
44+ # Condensed refresh
45+ REFRESH=" <WALNUT_REFRESH threshold=\" ${THRESHOLD} %\" >
46+ Context is at ${CTX_PCT} %. Refreshing core behaviours:
47+ - Stash decisions, tasks, and notes. Surface on change.
48+ - Verify past context via subagent before asserting. Never guess from memory.
49+ - Capsule awareness: deliverable or future audience = capsule. Prefer capsules over loose files.
50+ - Read before speaking. Never answer from memory about file contents.
51+ - Check the world key (injected at start) for walnut registry, people, credentials.
52+ </WALNUT_REFRESH>"
53+ else
54+ # Full re-injection at 60%+ — read world key and index
55+ WORLD_KEY=" "
56+ [ -f " $WORLD_ROOT /.walnut/key.md" ] && WORLD_KEY=$( cat " $WORLD_ROOT /.walnut/key.md" )
57+ WORLD_INDEX=" "
58+ [ -f " $WORLD_ROOT /.walnut/_index.yaml" ] && WORLD_INDEX=$( cat " $WORLD_ROOT /.walnut/_index.yaml" )
59+
60+ REFRESH=" <WALNUT_REFRESH threshold=\" ${THRESHOLD} %\" >
61+ Context is at ${CTX_PCT} %. Full context refresh:
62+ - Stash decisions, tasks, and notes. Surface on change.
63+ - Verify past context via subagent before asserting. Never guess from memory.
64+ - Capsule awareness: deliverable or future audience = capsule.
65+ - Read before speaking. Never answer from memory about file contents.
66+
67+ World Key:
68+ ${WORLD_KEY}
69+
70+ World Index:
71+ ${WORLD_INDEX}
72+ </WALNUT_REFRESH>"
73+ fi
74+
75+ # Scan active squirrel stashes for cross-pollination
76+ ACTIVE_STASHES=" "
77+ if command -v python3 & > /dev/null; then
78+ ACTIVE_STASHES=$( python3 -c "
79+ import os, glob, re
80+ sid = '$SESSION_ID '
81+ squirrels = glob.glob('$WORLD_ROOT /.walnut/_squirrels/*.yaml')
82+ for f in squirrels:
83+ with open(f) as fh:
84+ content = fh.read()
85+ # Skip our own session (check filename, not content — avoids false match if SID appears in stash text)
86+ if os.path.basename(f).replace('.yaml','') == sid:
87+ continue
88+ # Check if ended: null (still active) and saves: 0 (genuinely unsaved — saved stash is historical)
89+ if 'ended: null' not in content:
90+ continue
91+ saves_m = re.search(r'^saves:\s*(\d+)', content, re.M)
92+ if saves_m and int(saves_m.group(1)) > 0:
93+ continue
94+ # Extract walnut and stash
95+ walnut = ''
96+ m = re.search(r'^walnut:\s*(.+)', content, re.M)
97+ if m:
98+ walnut = m.group(1).strip()
99+ if walnut == 'null' or not walnut:
100+ continue
101+ # Extract stash items
102+ stash_items = re.findall(r'content:\s*\" ?(.+?)\" ?\s*$', content, re.M)
103+ if stash_items:
104+ print(f'Active session on {walnut}: ' + '; '.join(stash_items[:5]))
105+ " 2> /dev/null || true)
106+ fi
107+
108+ if [ -n " $ACTIVE_STASHES " ]; then
109+ REFRESH=" ${REFRESH}
110+
111+ <ACTIVE_SQUIRRELS>
112+ ${ACTIVE_STASHES}
113+ </ACTIVE_SQUIRRELS>"
114+ fi
115+
116+ REFRESH_ESCAPED=$( escape_for_json " $REFRESH " )
117+
118+ # Hook can only return one JSON response, so re-injection takes priority.
119+ # External change detection runs on every other prompt (re-injection fires at most 4x per session).
120+ cat << REFRESHEOF
121+ {
122+ "hookSpecificOutput": {
123+ "hookEventName": "UserPromptSubmit",
124+ "additionalContext": "${REFRESH_ESCAPED} "
125+ }
126+ }
127+ REFRESHEOF
128+ exit 0
129+ fi
130+ fi
131+ fi
132+
133+ # ── EXTERNAL CHANGE DETECTION ───────────────────────────────────
134+
15135# Find which walnut this session is working on
16136SQUIRRELS_DIR=" $WORLD_ROOT /.walnut/_squirrels"
17137ENTRY=" $SQUIRRELS_DIR /$SESSION_ID .yaml"
@@ -64,8 +184,10 @@ date +%s > "$LASTCHECK"
64184[ -z " ${CHANGED:- } " ] && exit 0
65185
66186# Check if the change was made by US (same session_id in now.md squirrel field)
67- LAST_SQUIRREL=$( grep ' ^squirrel:' " $WALNUT_CORE /now.md" 2> /dev/null | sed ' s/squirrel: *//' || true)
68- if [ " ${LAST_SQUIRREL:- } " = " $SESSION_ID " ]; then
187+ # now.md uses short IDs (first 8 chars), hook gets full UUID — check both
188+ LAST_SQUIRREL=$( grep ' ^squirrel:' " $WALNUT_CORE /now.md" 2> /dev/null | sed ' s/squirrel: *//' | tr -d ' [:space:]' || true)
189+ SHORT_SID=" ${SESSION_ID: 0: 8} "
190+ if [ " ${LAST_SQUIRREL:- } " = " $SESSION_ID " ] || [ " ${LAST_SQUIRREL:- } " = " $SHORT_SID " ]; then
69191 exit 0
70192fi
71193
0 commit comments