diff --git a/bin/revive b/bin/revive index e93ad7f..4ee2d35 100755 --- a/bin/revive +++ b/bin/revive @@ -53,7 +53,18 @@ cmd_version() { echo "revive $VERSION"; } log() { mkdir -p "$LOG_DIR" 2>/dev/null || return 0 - printf '[%s] %s\n' "$(date '+%Y-%m-%dT%H:%M:%S')" "$*" \ + # Tilde-substitute $HOME so multi-project logs stay scannable. Match + # only on a path boundary — `${PWD/#$HOME/~}` would happily rewrite + # `/Users/alice-work/project` to `~-work/project` when HOME is + # `/Users/alice` (codex P2). Use case to require either exact match + # or `$HOME/` prefix. + local cwd_short + case "$PWD" in + "$HOME") cwd_short="~" ;; + "$HOME"/*) cwd_short="~${PWD#"$HOME"}" ;; + *) cwd_short="$PWD" ;; + esac + printf '[%s] [%s] %s\n' "$(date '+%Y-%m-%dT%H:%M:%S')" "$cwd_short" "$*" \ >> "$LOG_FILE" 2>/dev/null || true } @@ -411,8 +422,14 @@ cadence_gate() { # most of its context — that's exactly when re-injecting the brief gives # the highest ROI, so bypass cadence and emit immediately. if [[ -f "$COMPACT_SIGNAL" ]]; then + # The signal file payload identifies the trigger ("compact" / "clear"). + # Sanitize to a-z only — defense-in-depth in case content was mangled. + # Empty or unknown content falls back to a neutral label. + local source + source=$(head -1 "$COMPACT_SIGNAL" 2>/dev/null | tr -cd '[:lower:]') + [[ "$source" == compact || "$source" == clear ]] || source="context-loss" rm -f "$COMPACT_SIGNAL" 2>/dev/null || true - log "post-compact: forcing emit (counter=$n)" + log "post-${source}: forcing emit (counter=$n)" emit=1 elif (( n == 1 )); then emit=1 elif (( n % REFRESH_EVERY == 0 )); then emit=1 @@ -446,14 +463,14 @@ cmd_show() { # path and must never break a Claude Code session. cmd_mark_compact() { mkdir -p "$(dirname "$COMPACT_SIGNAL")" 2>/dev/null || return 0 - date +%s > "$COMPACT_SIGNAL" 2>/dev/null || return 0 + printf 'compact\n' > "$COMPACT_SIGNAL" 2>/dev/null || return 0 log "post-compact signal written: $COMPACT_SIGNAL" return 0 } cmd_mark_clear() { mkdir -p "$(dirname "$COMPACT_SIGNAL")" 2>/dev/null || return 0 - date +%s > "$COMPACT_SIGNAL" 2>/dev/null || return 0 + printf 'clear\n' > "$COMPACT_SIGNAL" 2>/dev/null || return 0 log "post-clear signal written: $COMPACT_SIGNAL" return 0 } diff --git a/tests/revive.bats b/tests/revive.bats index d21c0f9..cedb625 100644 --- a/tests/revive.bats +++ b/tests/revive.bats @@ -1570,3 +1570,64 @@ JSON [[ "$output" == *"PostCompact hook installed"* ]] || return 1 [[ "$output" == *"SessionStart(clear) hook installed"* ]] || return 1 } + +# --- log enrichments --- + +@test "log lines carry the cwd field for multi-project scanability" { + # mark-compact always logs (refresh on a fresh repo doesn't, since the + # counter-1 emit path doesn't go through log()). The mark-compact line + # must carry [] so a global hook.log collected from many projects + # stays disambiguable. + mkdir -p .claude + "$REVIVE" mark-compact + run cat "$HOME/.context-revive/hook.log" + [[ "$output" == *"signal written"* ]] || return 1 + # Format: [timestamp] [cwd] message — must have TWO bracketed fields + # before the message body. Regex held in a variable to avoid the bash + # =~ backslash-quoting differences between macOS bash 3.2 and CI's + # bash 5.x (literal \[ inside the inline pattern is unportable). + local re='\[[^]]+\] \[[^]]+\] post-compact' + [[ "$output" =~ $re ]] || return 1 +} + +@test "refresh log message reflects compact source" { + mkdir -p .claude + printf 'compact\n' > .claude/revive-compact.signal + "$REVIVE" refresh >/dev/null + run cat "$HOME/.context-revive/hook.log" + [[ "$output" == *"post-compact: forcing emit"* ]] || return 1 +} + +@test "refresh log message reflects clear source" { + mkdir -p .claude + printf 'clear\n' > .claude/revive-compact.signal + "$REVIVE" refresh >/dev/null + run cat "$HOME/.context-revive/hook.log" + [[ "$output" == *"post-clear: forcing emit"* ]] || return 1 + [[ "$output" != *"post-compact: forcing emit"* ]] || return 1 +} + +@test "refresh handles a malformed signal payload with neutral label" { + mkdir -p .claude + # Garbage content — must NOT mislabel as compact/clear, must NOT crash. + printf '\x00\x01garbage\n' > .claude/revive-compact.signal + run "$REVIVE" refresh + [ "$status" -eq 0 ] || return 1 + run cat "$HOME/.context-revive/hook.log" + [[ "$output" == *"post-context-loss: forcing emit"* ]] || return 1 +} + +@test "log cwd prefix-match respects path boundary (codex P2)" { + # When HOME is `/tmp/h` and PWD is `/tmp/h-other/project`, the lazy + # `${PWD/#$HOME/~}` would mis-emit `~-other/project` and the global + # log would attribute the message to the wrong project. The case-based + # rewrite must only fire on `$HOME` or `$HOME/...`. + mkdir -p "$WORKDIR-other/project/.claude" + cd "$WORKDIR-other/project" + HOME="$WORKDIR" "$REVIVE" mark-compact + run cat "$WORKDIR/.context-revive/hook.log" + [[ "$output" == *"signal written"* ]] || { rm -rf "$WORKDIR-other"; return 1; } + # The log line must NOT contain the spliced `~-other` artefact. + [[ "$output" != *"~-other"* ]] || { rm -rf "$WORKDIR-other"; return 1; } + rm -rf "$WORKDIR-other" +}