From 3e7a3abd38e49c161489770aeb94f23734a1f878 Mon Sep 17 00:00:00 2001 From: Justyna Wojtczak Date: Mon, 27 Apr 2026 23:39:54 +0900 Subject: [PATCH 1/4] feat(log): per-line cwd + source-aware emit label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two annoyances surfaced when dogfooding v0.2.0 across multiple projects: 1. Lines in the global `~/.context-revive/hook.log` did not carry any project context. With revive wired in 5+ repos, the same `post-compact: forcing emit` message could be from any of them — un-debuggable. 2. The forcing-emit log line said `post-compact:` even when the trigger was `/clear`, because both paths shared one signal file and `cadence_gate` had no idea which command wrote it. Fixes: - `log()` now prepends `[]` after the timestamp, with a tilde substitution for paths under $HOME. A subtle bash gotcha: `${VAR/old/\~}` leaks the backslash; bash does NOT tilde-expand inside parameter-substitution replacements, so the literal `~` is correct (and was wrong on my first attempt — caught in smoke before commit). - `cmd_mark_compact` and `cmd_mark_clear` write `compact` / `clear` to the signal file (was: a unix timestamp, never read). The signal filename stays `revive-compact.signal` to keep the on-disk path stable across upgrades. - `cadence_gate` reads the payload, sanitizes via `tr -cd '[:lower:]'`, and emits either `post-compact:` or `post-clear:` accordingly. Unknown / malformed content falls back to a neutral `post-context-loss:` label. 4 new tests cover: cwd field present in log, compact label, clear label, malformed payload → neutral label without crashing. --- bin/revive | 19 +++++++++++++++---- tests/revive.bats | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/bin/revive b/bin/revive index e93ad7f..7a7594f 100755 --- a/bin/revive +++ b/bin/revive @@ -53,7 +53,12 @@ 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. Without + # this each line carries a 60-char absolute path that drowns the message. + # Literal `~` in the replacement (bash does NOT tilde-expand inside the + # right-hand side of ${var/old/new}); a backslash there leaks through. + local cwd_short="${PWD/#$HOME/~}" + printf '[%s] [%s] %s\n' "$(date '+%Y-%m-%dT%H:%M:%S')" "$cwd_short" "$*" \ >> "$LOG_FILE" 2>/dev/null || true } @@ -411,8 +416,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 +457,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..c2f9b93 100644 --- a/tests/revive.bats +++ b/tests/revive.bats @@ -1570,3 +1570,45 @@ 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. + [[ "$output" =~ \[[^]]+\]\ \[[^]]+\]\ post-compact ]] || return 1 +} + +@test "refresh log message reflects compact source" { + printf 'compact\n' > .claude/revive-compact.signal 2>/dev/null || 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 +} From 7988f49b6bc211112509e2ba4fd63612a9e8d999 Mon Sep 17 00:00:00 2001 From: Justyna Wojtczak Date: Mon, 27 Apr 2026 23:42:19 +0900 Subject: [PATCH 2/4] test(log): hold cwd-format regex in a variable for portability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inline `[[ "$output" =~ \[[^]]+\]\ \[[^]]+\]\ post-compact ]]` passed on macOS bats but failed on Ubuntu bats in CI — bash 3.2 vs 5.x differ on how literal `\[` inside the pattern is parsed. The canonical workaround is to bind the pattern to a local variable and reference it as `$re`. No content change to the assertion. --- tests/revive.bats | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/revive.bats b/tests/revive.bats index c2f9b93..0502e31 100644 --- a/tests/revive.bats +++ b/tests/revive.bats @@ -1583,8 +1583,11 @@ JSON 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. - [[ "$output" =~ \[[^]]+\]\ \[[^]]+\]\ post-compact ]] || return 1 + # 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" { From 5969ba41775b69542c98a8518a5ff5d1a9277616 Mon Sep 17 00:00:00 2001 From: Justyna Wojtczak Date: Tue, 28 Apr 2026 00:18:29 +0900 Subject: [PATCH 3/4] fix(log): match $HOME on path boundary only (codex P2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `${PWD/#$HOME/~}` did a string-prefix rewrite without a path boundary check, so a checkout in `$HOME-something/...` (e.g. HOME=/Users/alice and PWD=/Users/alice-work/project) would log as `~-work/project` — wrong project name in a feature that exists specifically to disambiguate projects. Replace with a case statement that fires only on the exact $HOME match or the `$HOME/` prefix; falls through to the absolute path otherwise. New test pins this: stages a sibling worktree at `$WORKDIR-other` with HOME=$WORKDIR, runs mark-compact, and asserts the log line does NOT contain the `~-other` splice artefact. --- bin/revive | 16 +++++++++++----- tests/revive.bats | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/bin/revive b/bin/revive index 7a7594f..4ee2d35 100755 --- a/bin/revive +++ b/bin/revive @@ -53,11 +53,17 @@ cmd_version() { echo "revive $VERSION"; } log() { mkdir -p "$LOG_DIR" 2>/dev/null || return 0 - # Tilde-substitute $HOME so multi-project logs stay scannable. Without - # this each line carries a 60-char absolute path that drowns the message. - # Literal `~` in the replacement (bash does NOT tilde-expand inside the - # right-hand side of ${var/old/new}); a backslash there leaks through. - local cwd_short="${PWD/#$HOME/~}" + # 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 } diff --git a/tests/revive.bats b/tests/revive.bats index 0502e31..2d58cd4 100644 --- a/tests/revive.bats +++ b/tests/revive.bats @@ -1615,3 +1615,18 @@ JSON 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" +} From 82b5c755ccb4ad647eb1916eb5a9781c4e7bfea8 Mon Sep 17 00:00:00 2001 From: Justyna Wojtczak Date: Tue, 28 Apr 2026 00:21:16 +0900 Subject: [PATCH 4/4] test(log): clean up compact-source test setup (copilot) Replaced the convoluted `printf > .claude/... 2>/dev/null || mkdir -p .claude && printf > .claude/...` with the same two-line pattern the sibling clear-source and garbage-payload tests already use. Bash redirect failures land on the original stderr before `2>/dev/null` is applied, so the old form would have leaked an error noise even when functionally correct. --- tests/revive.bats | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/revive.bats b/tests/revive.bats index 2d58cd4..cedb625 100644 --- a/tests/revive.bats +++ b/tests/revive.bats @@ -1591,7 +1591,8 @@ JSON } @test "refresh log message reflects compact source" { - printf 'compact\n' > .claude/revive-compact.signal 2>/dev/null || mkdir -p .claude && printf 'compact\n' > .claude/revive-compact.signal + 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