11#! /usr/bin/env bash
22# watch_session_log.sh
33#
4- # Watches CLAUDE_SESSION_LOG.md for new Chat Integration Requests and
5- # invokes Claude Code CLI non-interactively to process them.
4+ # One-shot processor for CLAUDE_SESSION_LOG.md. Triggered by launchd
5+ # WatchPaths when the log file changes. Checks whether new Chat Integration
6+ # Requests have appeared; if so, invokes Claude Code CLI non-interactively.
7+ # Exits after processing (launchd restarts it on the next file change).
68#
7- # Loop prevention: only triggers when the count of "Chat — Integration Request"
8- # entries increases. Code's own writes (Integration Complete) don't change
9- # that count, so no feedback loop .
9+ # Loop prevention: compares current Chat IR count against stored state.
10+ # Code's own writes (Integration Complete) don't increase the Chat IR count,
11+ # so they don't trigger a Claude invocation .
1012#
1113# Log rotation: delegates to rotate_session_log.py when file exceeds
12- # ROTATION_THRESHOLD lines. Rotation commits automatically.
14+ # ROTATION_THRESHOLD lines.
1315#
14- # Dependencies: fswatch (brew install fswatch), claude CLI
16+ # Dependencies: claude CLI (resolved dynamically from ~/Library/...)
1517# State: .claude/watcher_state (last known IR count), .claude/watcher.lock
1618# Log: .claude/watcher.log
1719#
18- # Run directly for foreground monitoring, or via launchd (install_watcher.sh).
20+ # Install: bash scripts/install_watcher.sh
21+ # Run manually: bash scripts/watch_session_log.sh
1922
2023set -euo pipefail
2124
2225REPO_ROOT=" /Volumes/SanDiskSSD/Developer/Repositories/framework"
2326LOG_FILE=" $REPO_ROOT /CLAUDE_SESSION_LOG.md"
2427STATE_DIR=" $REPO_ROOT /.claude"
2528STATE_FILE=" $STATE_DIR /watcher_state"
29+ LOCK_FILE=" $STATE_DIR /watcher.lock"
30+ ROTATION_THRESHOLD=400
31+
32+ TRIGGER_PATTERN=" — Chat — Integration Request"
2633
27- # Redirect all output (including shell errors) to the watcher log early,
28- # so even pre-startup crashes are captured when running under launchd.
34+ # Route all output to watcher.log early (captures shell-level errors too)
2935mkdir -p " $STATE_DIR "
3036exec >> " $STATE_DIR /watcher.log" 2>&1
3137
32- # Trap to log the exit code whenever the script exits (aids launchd debugging)
33- trap ' echo "[$(date +%Y-%m-%d\ %H:%M:%S)] WATCHER EXIT: status=$?" >> "$STATE_DIR/watcher.log"' EXIT
34- LOCK_FILE=" $STATE_DIR /watcher.lock"
35- ROTATION_THRESHOLD=400 # lines before rotation is triggered
36-
37- TRIGGER_PATTERN=" — Chat — Integration Request"
38+ trap ' echo "[$(date "+%Y-%m-%d %H:%M:%S")] EXIT: status=$?"' EXIT
3839
3940# Find claude binary — resolve latest installed version dynamically
4041CLAUDE_BIN=$( ls -1dt " $HOME /Library/Application Support/Claude/claude-code/" * /claude 2> /dev/null | head -1 || true)
4142if [[ -z " $CLAUDE_BIN " ]]; then
42- echo " ERROR: claude CLI not found" >&2
43+ echo " [ $( date " +%Y-%m-%d %H:%M:%S " ) ] ERROR: claude CLI not found"
4344 exit 1
4445fi
4546
@@ -51,98 +52,53 @@ per the Staging Directory Protocol in CLAUDE_CODE_INSTRUCTIONS.md. \
5152Commit all changes atomically and append an Integration Complete entry \
5253to the session log."
5354
54- # --- Helpers ---
55- log () {
56- echo " [$( date ' +%Y-%m-%d %H:%M:%S' ) ] $* "
57- }
55+ log () { echo " [$( date " +%Y-%m-%d %H:%M:%S" ) ] $* " ; }
5856
5957count_requests () {
6058 grep -c " $TRIGGER_PATTERN " " $LOG_FILE " 2> /dev/null || echo " 0"
6159}
6260
63- # --- Log rotation ---
64- rotate_log () {
65- local line_count
66- line_count=$( wc -l < " $LOG_FILE " | tr -d ' ' )
67- log " Rotation triggered ($line_count lines > $ROTATION_THRESHOLD threshold)"
68- python3 " $REPO_ROOT /scripts/rotate_session_log.py" " $LOG_FILE " \
69- && log " Rotation complete" \
70- || log " WARNING: rotation failed — skipping (log continues to grow)"
71- }
61+ # --- Check for new requests ---
62+ current_count=$( count_requests)
63+ last_count=0
64+ [[ -f " $STATE_FILE " ]] && last_count=$( cat " $STATE_FILE " )
7265
73- # --- Claude invocation ---
74- process_requests () {
75- # Lockfile prevents concurrent runs
76- if [[ -f " $LOCK_FILE " ]]; then
77- log " Claude already running (lockfile present) — skipping this event"
78- return
79- fi
80-
81- log " Triggering Claude Code (non-interactive)"
82- touch " $LOCK_FILE "
83-
84- cd " $REPO_ROOT "
85- # Unset CLAUDECODE so the CLI doesn't refuse to start inside an existing session
86- # stdout/stderr already go to watcher.log via exec redirect at top of script
87- env -u CLAUDECODE \
88- " $CLAUDE_BIN " -p \
89- --permission-mode bypassPermissions \
90- --model sonnet \
91- " $CLAUDE_PROMPT " \
92- && log " Claude Code run complete" \
93- || log " ERROR: Claude Code exited with error (exit $? )"
94-
95- rm -f " $LOCK_FILE "
96-
97- # Update state after run (IR count may have changed if Chat wrote again during run)
98- count_requests > " $STATE_FILE "
99- }
66+ # Always update state (so Code's own writes don't trigger on next run)
67+ echo " $current_count " > " $STATE_FILE "
10068
101- # --- Check file and maybe trigger ---
102- check_and_trigger () {
103- local current_count
104- current_count=$( count_requests)
105- local last_count=0
106- [[ -f " $STATE_FILE " ]] && last_count=$( cat " $STATE_FILE " )
107-
108- if (( current_count > last_count )) ; then
109- log " Integration Request count: $last_count → $current_count "
110- process_requests
111- fi
112-
113- # Check rotation threshold after processing
114- local line_count
115- line_count=$( wc -l < " $LOG_FILE " | tr -d ' ' )
116- if (( line_count > ROTATION_THRESHOLD )) ; then
117- rotate_log
118- # Update state after rotation (line numbers shifted)
119- count_requests > " $STATE_FILE "
120- fi
121- }
69+ if (( current_count <= last_count )) ; then
70+ # No new Chat IR entries — nothing to do (Code's write or no change)
71+ exit 0
72+ fi
12273
123- # --- Init ---
124- if [[ ! -f " $STATE_FILE " ]]; then
125- count_requests > " $STATE_FILE "
126- log " Initialized: $( cat " $STATE_FILE " ) existing Integration Request(s)"
74+ log " Integration Request count: $last_count → $current_count "
75+
76+ # --- Lockfile: prevent concurrent invocations ---
77+ if [[ -f " $LOCK_FILE " ]]; then
78+ log " Claude already running (lockfile present) — skipping"
79+ exit 0
12780fi
12881
129- log " Watcher started — monitoring $LOG_FILE "
130- log " Claude binary: $CLAUDE_BIN "
131-
132- # Check once at startup (in case requests arrived while watcher was down)
133- check_and_trigger
134-
135- # Watch for changes — outer loop restarts fswatch if it exits (e.g. EX_CONFIG
136- # on first launch under launchd before FSEvents is fully initialised).
137- # -o aggregates rapid events into one; -l 2 lets writes settle.
138- # pipefail is disabled for this pipeline so fswatch exits don't kill the script.
139- while true ; do
140- set +o pipefail
141- fswatch -o -l 2 " $LOG_FILE " | while read -r _; do
142- check_and_trigger
143- done
144- EXIT_CODE=$?
145- set -o pipefail
146- log " fswatch exited (code $EXIT_CODE ) — restarting in 10 seconds"
147- sleep 10
148- done
82+ log " Triggering Claude Code (non-interactive)"
83+ touch " $LOCK_FILE "
84+
85+ cd " $REPO_ROOT "
86+ # env -u CLAUDECODE: strip nested-session guard so CLI starts outside a session
87+ env -u CLAUDECODE \
88+ " $CLAUDE_BIN " -p \
89+ --permission-mode bypassPermissions \
90+ --model sonnet \
91+ " $CLAUDE_PROMPT " \
92+ && log " Claude Code run complete" \
93+ || log " ERROR: Claude Code exited with error (exit $? )"
94+
95+ rm -f " $LOCK_FILE "
96+
97+ # --- Log rotation ---
98+ line_count=$( wc -l < " $LOG_FILE " | tr -d ' ' )
99+ if (( line_count > ROTATION_THRESHOLD )) ; then
100+ log " Rotation triggered ($line_count lines)"
101+ python3 " $REPO_ROOT /scripts/rotate_session_log.py" " $LOG_FILE " \
102+ && log " Rotation complete" \
103+ || log " WARNING: rotation failed — log continues to grow"
104+ fi
0 commit comments