From 1c7bc72d6fcb3e6b1555a0c6c40f39ef711101c0 Mon Sep 17 00:00:00 2001 From: Brian Ruggieri Date: Fri, 20 Mar 2026 08:51:26 -0400 Subject: [PATCH 01/10] feat: Stop-hook haiku summarization for AI context Summarize Claude's last_assistant_message via claude-haiku on the Stop hook. Detached subprocess writes to context file; first-5-words remains as the immediate placeholder. Delivers ~100% AI summaries (up from 0.5% with inline strategy). --- lib/hook_runner.sh | 52 ++++++++++++++++ tests/test-suite.sh | 143 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+) diff --git a/lib/hook_runner.sh b/lib/hook_runner.sh index 30a83ac..641b143 100644 --- a/lib/hook_runner.sh +++ b/lib/hook_runner.sh @@ -8,6 +8,7 @@ # CCP_CONTEXT_FILE - path to write task context (user prompt) # CCP_STATUS_PROFILE - quiet (default) or verbose # CCP_DEBUG_LOG - optional path for debug output +# CCP_CLAUDE_BIN - override claude binary path (for testing) # # Always exits 0 — must never block or fail Claude Code. @@ -494,6 +495,57 @@ case "${mode}" in else atomic_write "${CCP_STATUS_FILE}" "" fi + + # AI context summarization — summarize what Claude just did. + # Uses last_assistant_message from the Stop hook payload (added in + # Claude Code v1.0.17). The detached subprocess survives the hook's + # 5-second timeout — it runs independently after disown. + if [[ "${CCP_ENABLE_AI_CONTEXT:-false}" != "true" ]] || \ + [[ -z "${CCP_CONTEXT_FILE:-}" ]]; then + exit 0 + fi + + last_msg="" + last_msg=$(printf '%s' "${json_input}" | jq -r '.last_assistant_message // ""' 2>/dev/null) || true + [[ -z "${last_msg}" ]] && exit 0 + + _dbg_event "ai_context_stop" "msg_len=${#last_msg}" + + # Truncate: first 500 + last 500 chars + _truncated="${last_msg}" + if [[ ${#last_msg} -gt 1000 ]]; then + _first="${last_msg:0:500}" + _last="${last_msg: -500}" + _truncated="${_first} ... ${_last}" + fi + + _ccp_ctx="${CCP_CONTEXT_FILE}" + _ccp_truncated="${_truncated}" + _ccp_claude_bin="${CCP_CLAUDE_BIN:-}" + ( + set +e + # CLAUDECODE: env var set by Claude Code itself. Must be cleared or + # nested `claude --print` calls fail (discovered by Quickchat.ai). + # CCP_*: unset so hooks fired by the child claude process become no-ops. + unset CLAUDECODE CCP_ENABLE_AI_CONTEXT CCP_STATUS_FILE CCP_CONTEXT_FILE + + claude_bin="${_ccp_claude_bin}" + if [[ -z "${claude_bin}" ]]; then + claude_bin=$(command -v claude 2>/dev/null \ + || command -v claude-code 2>/dev/null || echo "") + fi + [[ -z "${claude_bin}" ]] && exit 0 + + summary=$(printf 'Summarize what this AI assistant just did in 3-5 words. Title-case. No punctuation. No quotes. Reply with only the words, nothing else.\n\nResponse:\n%s' \ + "${_ccp_truncated}" \ + | "${claude_bin}" --print --model claude-haiku-4-5-20251001 \ + 2>/dev/null | head -1 \ + | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') || true + + [[ -n "${summary}" ]] && atomic_write "${_ccp_ctx}" "${summary}" + _dbg_event "ai_summary" "summary=${summary}" "strategy=stop-haiku" + ) & + disown 2>/dev/null || true ;; post-tool) diff --git a/tests/test-suite.sh b/tests/test-suite.sh index bb4108e..2f73412 100755 --- a/tests/test-suite.sh +++ b/tests/test-suite.sh @@ -491,6 +491,149 @@ echo '{}' \ result=$(cat "${TMP_STATUS}" 2>/dev/null || true) assert_empty "stop empties status file" "${result}" +# ── Tests: stop-hook haiku summarization ────────────────────────────────────── + +echo "" +echo "stop-hook haiku summarization" + +# Set up a mock claude binary that captures stdin and outputs a fixed summary +MOCK_CLAUDE_DIR=$(mktemp -d) +cat > "${MOCK_CLAUDE_DIR}/claude" <<'MOCK' +#!/usr/bin/env bash +# Mock claude binary — reads stdin, outputs a fixed summary +cat > /dev/null # consume stdin +echo "Fixed Auth Login Flow" +MOCK +chmod +x "${MOCK_CLAUDE_DIR}/claude" + +# Each test uses a unique context file to avoid races with disowned background subprocesses +HAIKU_CTX_1="${STATE_DIR}/haiku-ctx-1.txt" +HAIKU_CTX_2="${STATE_DIR}/haiku-ctx-2.txt" +HAIKU_CTX_3="${STATE_DIR}/haiku-ctx-3.txt" +HAIKU_CTX_4="${STATE_DIR}/haiku-ctx-4.txt" +HAIKU_CTX_5="${STATE_DIR}/haiku-ctx-5.txt" +HAIKU_CTX_6="${STATE_DIR}/haiku-ctx-6.txt" + +# Test 1: stop hook with AI context enabled + last_assistant_message → haiku writes summary +rm -f "${TMP_STATUS}" "${HAIKU_CTX_1}" +printf '🧪 Testing' > "${TMP_STATUS}" +printf '%s' '{"last_assistant_message":"I fixed the authentication flow by updating the login handler to properly validate tokens."}' \ + | CLAUDECODE=1 \ + CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${HAIKU_CTX_1}" \ + CCP_ENABLE_AI_CONTEXT=true CCP_CLAUDE_BIN="${MOCK_CLAUDE_DIR}/claude" \ + bash "${LIB_DIR}/hook_runner.sh" stop +sleep 2 +result=$(cat "${HAIKU_CTX_1}" 2>/dev/null || true) +assert_equals "stop-haiku: writes summary to context file" "Fixed Auth Login Flow" "${result}" +# Also verify status was still cleared (existing behavior preserved) +status_result=$(cat "${TMP_STATUS}" 2>/dev/null || true) +assert_empty "stop-haiku: status still cleared" "${status_result}" + +# Test 2: stop hook skips haiku when CCP_ENABLE_AI_CONTEXT is not true +rm -f "${TMP_STATUS}" "${HAIKU_CTX_2}" +printf '🧪 Testing' > "${TMP_STATUS}" +printf '%s' '{"last_assistant_message":"I fixed the bug."}' \ + | CLAUDECODE=1 \ + CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${HAIKU_CTX_2}" \ + CCP_ENABLE_AI_CONTEXT=false CCP_CLAUDE_BIN="${MOCK_CLAUDE_DIR}/claude" \ + bash "${LIB_DIR}/hook_runner.sh" stop +sleep 2 +result=$(cat "${HAIKU_CTX_2}" 2>/dev/null || true) +assert_empty "stop-haiku: skips when AI context disabled" "${result}" + +# Test 3: stop hook skips haiku when last_assistant_message is empty +rm -f "${TMP_STATUS}" "${HAIKU_CTX_3}" +printf '🧪 Testing' > "${TMP_STATUS}" +printf '%s' '{"last_assistant_message":""}' \ + | CLAUDECODE=1 \ + CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${HAIKU_CTX_3}" \ + CCP_ENABLE_AI_CONTEXT=true CCP_CLAUDE_BIN="${MOCK_CLAUDE_DIR}/claude" \ + bash "${LIB_DIR}/hook_runner.sh" stop +sleep 2 +result=$(cat "${HAIKU_CTX_3}" 2>/dev/null || true) +assert_empty "stop-haiku: skips when last_assistant_message is empty" "${result}" + +# Test 4: stop hook skips haiku when last_assistant_message field is missing from JSON +rm -f "${TMP_STATUS}" "${HAIKU_CTX_4}" +printf '🧪 Testing' > "${TMP_STATUS}" +printf '%s' '{"some_other_field":"hello"}' \ + | CLAUDECODE=1 \ + CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${HAIKU_CTX_4}" \ + CCP_ENABLE_AI_CONTEXT=true CCP_CLAUDE_BIN="${MOCK_CLAUDE_DIR}/claude" \ + bash "${LIB_DIR}/hook_runner.sh" stop +sleep 2 +result=$(cat "${HAIKU_CTX_4}" 2>/dev/null || true) +assert_empty "stop-haiku: skips when last_assistant_message field missing" "${result}" + +# Test 5: messages >1000 chars are truncated to first 500 + "..." + last 500 +rm -f "${TMP_STATUS}" "${HAIKU_CTX_5}" +printf '🧪 Testing' > "${TMP_STATUS}" +# Build a mock claude that echoes back a snippet of what it received on stdin +# so we can verify truncation happened +MOCK_TRUNC_DIR=$(mktemp -d) +cat > "${MOCK_TRUNC_DIR}/claude" <<'MOCK' +#!/usr/bin/env bash +# Mock claude binary — captures stdin content to a file for inspection +input=$(cat) +# Write the received input to a sidecar file for the test to inspect +echo "${input}" > "${MOCK_TRUNC_DIR_SIDECAR}" +echo "Truncation Test Summary" +MOCK +chmod +x "${MOCK_TRUNC_DIR}/claude" +MOCK_SIDECAR="${MOCK_TRUNC_DIR}/captured_input.txt" + +# Build a 1200-char message: 600 A's + 600 B's +long_msg=$(printf '%0600d' 0 | tr '0' 'A')$(printf '%0600d' 0 | tr '0' 'B') +json_long=$(printf '{"last_assistant_message":"%s"}' "${long_msg}") +printf '%s' "${json_long}" \ + | CLAUDECODE=1 \ + CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${HAIKU_CTX_5}" \ + CCP_ENABLE_AI_CONTEXT=true CCP_CLAUDE_BIN="${MOCK_TRUNC_DIR}/claude" \ + MOCK_TRUNC_DIR_SIDECAR="${MOCK_SIDECAR}" \ + bash "${LIB_DIR}/hook_runner.sh" stop +sleep 2 +result=$(cat "${HAIKU_CTX_5}" 2>/dev/null || true) +assert_equals "stop-haiku: truncated message produces summary" "Truncation Test Summary" "${result}" +# Verify the captured input contains " ... " (truncation marker) +if [[ -f "${MOCK_SIDECAR}" ]]; then + captured=$(cat "${MOCK_SIDECAR}" 2>/dev/null || true) + if [[ "${captured}" == *" ... "* ]]; then + pass "stop-haiku: truncated message contains ' ... ' marker" + else + fail "stop-haiku: truncated message contains ' ... ' marker" "captured input did not contain ' ... '" + fi +else + fail "stop-haiku: truncated message contains ' ... ' marker" "sidecar file not found" +fi +rm -rf "${MOCK_TRUNC_DIR}" + +# Test 6: CLAUDECODE env var is unset in the haiku subprocess +rm -f "${TMP_STATUS}" "${HAIKU_CTX_6}" +printf '🧪 Testing' > "${TMP_STATUS}" +MOCK_CLAUDECODE_DIR=$(mktemp -d) +cat > "${MOCK_CLAUDECODE_DIR}/claude" <<'MOCK' +#!/usr/bin/env bash +cat > /dev/null +if [[ -z "${CLAUDECODE:-}" ]]; then + echo "CLAUDECODE_ABSENT" +else + echo "CLAUDECODE_PRESENT" +fi +MOCK +chmod +x "${MOCK_CLAUDECODE_DIR}/claude" +printf '%s' '{"last_assistant_message":"I fixed the bug by adding a null check."}' \ + | CLAUDECODE=1 \ + CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${HAIKU_CTX_6}" \ + CCP_ENABLE_AI_CONTEXT=true CCP_CLAUDE_BIN="${MOCK_CLAUDECODE_DIR}/claude" \ + bash "${LIB_DIR}/hook_runner.sh" stop +sleep 2 +result=$(cat "${HAIKU_CTX_6}" 2>/dev/null || true) +assert_equals "stop-haiku: CLAUDECODE is unset in haiku subprocess" "CLAUDECODE_ABSENT" "${result}" +rm -rf "${MOCK_CLAUDECODE_DIR}" + +rm -rf "${MOCK_CLAUDE_DIR}" +rm -f "${TMP_STATUS}" "${TMP_CONTEXT}" + # post-tool handler writes completion statuses from Bash output printf '🧪 Testing' > "${TMP_STATUS}" result=$(echo '{"tool_name":"Bash","tool_input":{"command":"npm test"},"tool_response":"3 tests passed"}' \ From c19d77c77bedbf0607fd4950228d379fb0f83416 Mon Sep 17 00:00:00 2001 From: Brian Ruggieri Date: Fri, 20 Mar 2026 09:01:06 -0400 Subject: [PATCH 02/10] feat: sanitize shell prompt prefixes in context strings Strip (venv), user@host, and shell prompt characters ($ % # >) from first-5-words context. Simplify user-prompt handler by removing the haiku subprocess and inline strategy check (now handled by Stop hook). --- lib/hook_runner.sh | 64 +++++++-------------------------------------- tests/test-suite.sh | 30 +++++++++++++++++++++ 2 files changed, 40 insertions(+), 54 deletions(-) diff --git a/lib/hook_runner.sh b/lib/hook_runner.sh index 641b143..4b4d8bf 100644 --- a/lib/hook_runner.sh +++ b/lib/hook_runner.sh @@ -420,65 +420,21 @@ case "${mode}" in exit 0 fi + # Strip shell prompt prefixes from pasted terminal content before + # extracting first-5-words so prefix tokens don't fill the word budget. + sanitized_prompt="" + sanitized_prompt=$(printf '%s' "${raw_prompt}" \ + | sed 's/^([^)]*)[[:space:]]*//' \ + | sed 's/^[a-zA-Z0-9._-]*@[a-zA-Z0-9._-]*[[:space:]]*//' \ + | sed 's/^[%$#>][[:space:]]*//') + # Write first-5-words placeholder immediately so the title updates at once initial="" - initial=$(printf '%s' "${raw_prompt}" \ + initial=$(printf '%s' "${sanitized_prompt}" \ | awk '{n=(NF<5?NF:5); for(i=1;i<=n;i++) printf "%s%s",$i,(i/dev/null \ - || command -v claude-code 2>/dev/null || echo "") - [[ -z "${claude_bin}" ]] && exit 0 - - # Strip project name from prompt so the summary doesn't repeat it - task_text="${_ccp_raw}" - if [[ -n "${_ccp_proj}" ]]; then - task_text=$(printf '%s' "${task_text}" \ - | sed "s/ (${_ccp_proj})[^,]*,\{0,1\}[[:space:]]*/ /g" \ - | sed "s/${_ccp_proj}//g" \ - | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') - fi - - summary=$(printf 'Summarize this developer task in 3-5 words. Title-case. No punctuation. No quotes. Reply with only the words, nothing else. Task: %s' \ - "${task_text}" \ - | "${claude_bin}" --print --model claude-haiku-4-5-20251001 \ - 2>/dev/null | head -1 \ - | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') || true - - [[ -n "${summary}" ]] && atomic_write "${_ccp_ctx}" "${summary}" - _dbg_event "ai_summary" "summary=${summary}" "strategy=haiku" - _dbg "distilled: ${summary}" - ) & - disown 2>/dev/null || true ;; stop) diff --git a/tests/test-suite.sh b/tests/test-suite.sh index 2f73412..efa252f 100755 --- a/tests/test-suite.sh +++ b/tests/test-suite.sh @@ -483,6 +483,36 @@ result=$(printf '%s' '{"prompt":"Refactor the database layer"}' \ bash "${LIB_DIR}/hook_runner.sh" user-prompt && cat "${TMP_CONTEXT}" 2>/dev/null || true) assert_contains "user-prompt: no trailing newline" "Refactor the database" "${result}" +echo "" +echo "user-prompt: context sanitization" + +TMP_STATUS="${STATE_DIR}/test-sanitize-status.txt" +TMP_CONTEXT="${STATE_DIR}/test-sanitize-context.txt" + +# Pasted terminal content with venv prefix +rm -f "${TMP_CONTEXT}" "${TMP_STATUS}" +result=$(printf '{"prompt":"(venv) brianruggieri@Flexias-MacBook-Pro candidate-eval %% fix the bug"}' \ + | CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${TMP_CONTEXT}" \ + bash "${LIB_DIR}/hook_runner.sh" user-prompt && cat "${TMP_CONTEXT}" 2>/dev/null || true) +assert_not_contains "sanitize: venv prefix stripped" "(venv)" "${result}" +assert_contains "sanitize: actual content preserved" "fix the bug" "${result}" + +# Pasted terminal with user@host prefix (no venv) +rm -f "${TMP_CONTEXT}" "${TMP_STATUS}" +result=$(printf '{"prompt":"user@hostname project %% do the thing"}' \ + | CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${TMP_CONTEXT}" \ + bash "${LIB_DIR}/hook_runner.sh" user-prompt && cat "${TMP_CONTEXT}" 2>/dev/null || true) +assert_not_contains "sanitize: user@host stripped" "user@hostname" "${result}" + +# Shell prompt character stripped +rm -f "${TMP_CONTEXT}" "${TMP_STATUS}" +result=$(printf '{"prompt":"$ npm test"}' \ + | CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${TMP_CONTEXT}" \ + bash "${LIB_DIR}/hook_runner.sh" user-prompt && cat "${TMP_CONTEXT}" 2>/dev/null || true) +assert_equals "sanitize: $ prompt stripped" "npm test" "${result}" + +rm -f "${TMP_STATUS}" "${TMP_CONTEXT}" + # stop handler clears status file printf '🧪 Testing' > "${TMP_STATUS}" echo '{}' \ From 3a781d8e9ca166cd4b6d5f3b037d0b182176d232 Mon Sep 17 00:00:00 2001 From: Brian Ruggieri Date: Fri, 20 Mar 2026 09:06:19 -0400 Subject: [PATCH 03/10] feat: capture git commit message as task context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Write the commit subject line to the context file when a git commit is detected in post-tool. Remove the inline AI context marker scanning block and all associated tests — inline strategy is now dead code. --- lib/hook_runner.sh | 47 ++++------- tests/test-suite.sh | 193 +++++--------------------------------------- 2 files changed, 32 insertions(+), 208 deletions(-) diff --git a/lib/hook_runner.sh b/lib/hook_runner.sh index 4b4d8bf..63890b0 100644 --- a/lib/hook_runner.sh +++ b/lib/hook_runner.sh @@ -506,8 +506,8 @@ case "${mode}" in post-tool) # All three output files are checked: status detection writes to - # CCP_STATUS_FILE, inline AI context writes to CCP_CONTEXT_FILE, and - # branch refresh writes to CCP_BRANCH_FILE. Skip only if none are set. + # CCP_STATUS_FILE, commit message capture writes to CCP_CONTEXT_FILE, + # and branch refresh writes to CCP_BRANCH_FILE. Skip only if none are set. [[ -z "${CCP_STATUS_FILE:-}" && -z "${CCP_CONTEXT_FILE:-}" && -z "${CCP_BRANCH_FILE:-}" ]] && exit 0 tool="" @@ -523,37 +523,6 @@ case "${mode}" in command_str="" command_str=$(printf '%s' "${json_input}" | jq -r '.tool_input.command // ""' 2>/dev/null) || true - # Inline AI context: detect the PID-scoped CCP_TASK_SUMMARY marker - # echoed by the main Claude session (injected via --append-system-prompt). - # The marker includes the ccp session PID (CCP_SESSION_PID), making it - # unique per session. Source files on disk always contain the generic - # template string (without a real PID), so grep/cat of hook_runner.sh - # or bin/ccp can never produce a false match. - # - # Once captured, a marker file signals future invocations to skip scanning. - _inline_captured_file="${STATE_DIR:-/tmp}/inline_captured.${CCP_SESSION_PID:-$$}" - if [[ "${CCP_AI_CONTEXT_STRATEGY:-haiku}" == "inline" ]] && \ - [[ -n "${CCP_CONTEXT_FILE:-}" ]] && \ - [[ -n "${CCP_SESSION_PID:-}" ]] && \ - [[ ! -f "${_inline_captured_file}" ]]; then - if [[ "${tool_response}" =~ CCP_TASK_SUMMARY_${CCP_SESSION_PID}:(.+) ]]; then - _inline_summary="${BASH_REMATCH[1]}" - _inline_summary=$(printf '%s' "${_inline_summary}" \ - | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//' \ - | sed "s/^['\"]//;s/['\"]$//") - if [[ -n "${_inline_summary}" ]]; then - atomic_write "${CCP_CONTEXT_FILE}" "${_inline_summary}" - touch "${_inline_captured_file}" 2>/dev/null || true - _dbg_event "ai_summary" "summary=${_inline_summary}" "strategy=inline" - _dbg "inline-summary: ${_inline_summary}" - fi - else - # Bash output didn't contain the inline marker — expected for most - # Bash calls, but useful for tracking whether a summary ever arrives. - _dbg_event "inline_marker_miss" "command=$(printf '%s' "${command_str}" | head -c 80)" "output_len=${#tool_response}" - fi - fi - # Branch detection runs even without CCP_STATUS_FILE — skip status # detection only, not the entire handler. [[ -z "${CCP_STATUS_FILE:-}" && -z "${CCP_BRANCH_FILE:-}" ]] && exit 0 @@ -565,6 +534,18 @@ case "${mode}" in status="❌ Tests failed" elif [[ "${command_str}" =~ git[[:space:]]+commit && "${tool_response}" =~ ^\[ ]]; then status="💾 Committed" + # Capture commit subject line as task context — a human-written summary + if [[ -n "${CCP_CONTEXT_FILE:-}" ]]; then + _commit_subject="" + _commit_subject=$(printf '%s' "${tool_response}" \ + | head -1 \ + | sed 's/^\[[^]]*\][[:space:]]*//' \ + | head -c 80) || true + if [[ -n "${_commit_subject}" ]]; then + atomic_write "${CCP_CONTEXT_FILE}" "${_commit_subject}" + _dbg_event "context_set" "context=${_commit_subject}" "source=commit-message" + fi + fi elif [[ "${command_str}" =~ git[[:space:]]+push ]] && \ [[ "${tool_response}" =~ (error:[[:space:]]+failed[[:space:]]+to[[:space:]]+push|!\ \[rejected\]|!\ \[remote\ rejected\]|ERROR:) ]]; then status="🐛 Push failed" diff --git a/tests/test-suite.sh b/tests/test-suite.sh index efa252f..6804cd9 100755 --- a/tests/test-suite.sh +++ b/tests/test-suite.sh @@ -878,6 +878,24 @@ result=$(echo '{"tool_name":"Bash","tool_input":{"command":"git checkout main"}, assert_equals "post-tool: branch detection fires without CCP_STATUS_FILE" "${expected_branch}" "${result}" rm -f "${TMP_BRANCH}" +echo "" +echo "post-tool: git commit message capture" + +TMP_STATUS="${STATE_DIR}/test-commit-ctx-status.txt" +TMP_CONTEXT="${STATE_DIR}/test-commit-ctx-context.txt" + +# post-tool: git commit writes commit subject to context file +rm -f "${TMP_STATUS}" "${TMP_CONTEXT}" +result=$(echo '{"tool_name":"Bash","tool_input":{"command":"git commit -m \"Fix null check in parser\""},"tool_response":"[main abc1234] Fix null check in parser\n 1 file changed, 2 insertions(+)"}' \ + | CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${TMP_CONTEXT}" \ + bash "${LIB_DIR}/hook_runner.sh" post-tool && cat "${TMP_CONTEXT}" 2>/dev/null || true) +assert_equals "commit: subject written to context file" "Fix null check in parser" "${result}" +# Status should also be set +status_result=$(cat "${TMP_STATUS}" 2>/dev/null || true) +assert_equals "commit: status set to Committed" "💾 Committed" "${status_result}" + +rm -f "${TMP_STATUS}" "${TMP_CONTEXT}" + # event handler: quiet (default) high-signal coverage result=$(echo '{"permission":"needed"}' \ | CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${TMP_CONTEXT}" CCP_STATUS_PROFILE=quiet \ @@ -1158,161 +1176,6 @@ assert_equals "teardown preserves non-CCP hooks" "1" "${remaining}" rm -rf "${HOOKS_TMP_DIR}" rm -f "${TMP_STATUS}" "${TMP_CONTEXT}" -# ── Tests: inline AI context (post-tool CCP_TASK_SUMMARY detection) ─────────── - -echo "" -echo "inline AI context (post-tool CCP_TASK_SUMMARY detection)" - -TMP_STATUS=$(mktemp) -TMP_CONTEXT=$(mktemp) - -# Fixed PID used across all inline tests — mirrors the CCP_SESSION_PID export in bin/ccp -TEST_PID=99999 -# Marker file created by hook_runner.sh after first inline capture -INLINE_CAPTURED="${STATE_DIR}/inline_captured.${TEST_PID}" - -# post-tool: PID-scoped marker in echo output writes to context file -rm -f "${TMP_CONTEXT}" "${TMP_STATUS}" "${INLINE_CAPTURED}" -result=$(echo "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"echo CCP_TASK_SUMMARY_${TEST_PID}:Fix JWT Validation Bug\"},\"tool_response\":\"CCP_TASK_SUMMARY_${TEST_PID}:Fix JWT Validation Bug\"}" \ - | CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${TMP_CONTEXT}" \ - CCP_ENABLE_AI_CONTEXT=true CCP_AI_CONTEXT_STRATEGY=inline CCP_SESSION_PID="${TEST_PID}" \ - bash "${LIB_DIR}/hook_runner.sh" post-tool && cat "${TMP_CONTEXT}" 2>/dev/null || true) -assert_equals "inline: PID-scoped marker extracted" "Fix JWT Validation Bug" "${result}" - -# post-tool: marker with surrounding whitespace is trimmed -rm -f "${TMP_CONTEXT}" "${TMP_STATUS}" "${INLINE_CAPTURED}" -result=$(echo "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"echo CCP_TASK_SUMMARY_${TEST_PID}: Refactor Auth Module \"},\"tool_response\":\"CCP_TASK_SUMMARY_${TEST_PID}: Refactor Auth Module \"}" \ - | CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${TMP_CONTEXT}" \ - CCP_ENABLE_AI_CONTEXT=true CCP_AI_CONTEXT_STRATEGY=inline CCP_SESSION_PID="${TEST_PID}" \ - bash "${LIB_DIR}/hook_runner.sh" post-tool && cat "${TMP_CONTEXT}" 2>/dev/null || true) -assert_equals "inline: whitespace around summary trimmed" "Refactor Auth Module" "${result}" - -# post-tool: marker with quotes is cleaned -rm -f "${TMP_CONTEXT}" "${TMP_STATUS}" "${INLINE_CAPTURED}" -result=$(printf '{"tool_name":"Bash","tool_input":{"command":"echo CCP_TASK_SUMMARY_%s:Update Login UI"},"tool_response":"CCP_TASK_SUMMARY_%s:'\''Update Login UI'\''"}' \ - "${TEST_PID}" "${TEST_PID}" \ - | CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${TMP_CONTEXT}" \ - CCP_ENABLE_AI_CONTEXT=true CCP_AI_CONTEXT_STRATEGY=inline CCP_SESSION_PID="${TEST_PID}" \ - bash "${LIB_DIR}/hook_runner.sh" post-tool && cat "${TMP_CONTEXT}" 2>/dev/null || true) -assert_equals "inline: quotes stripped from summary" "Update Login UI" "${result}" - -# post-tool: GENERIC (unscoped) marker does NOT match — this is the false-positive fix. -# Source files on disk contain CCP_TASK_SUMMARY: without a PID; grep/cat of those -# files must never overwrite a previously captured summary. -rm -f "${TMP_CONTEXT}" "${TMP_STATUS}" -printf 'previously captured summary' > "${TMP_CONTEXT}" -result=$(echo '{"tool_name":"Bash","tool_input":{"command":"grep CCP_TASK_SUMMARY lib/hook_runner.sh"},"tool_response":"CCP_TASK_SUMMARY:Do Not Overwrite"}' \ - | CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${TMP_CONTEXT}" \ - CCP_ENABLE_AI_CONTEXT=true CCP_AI_CONTEXT_STRATEGY=inline CCP_SESSION_PID="${TEST_PID}" \ - bash "${LIB_DIR}/hook_runner.sh" post-tool && cat "${TMP_CONTEXT}" 2>/dev/null || true) -assert_equals "inline: unscoped marker ignored (false-positive guard)" "previously captured summary" "${result}" - -# post-tool: marker from a DIFFERENT PID does not match this session -rm -f "${TMP_CONTEXT}" "${TMP_STATUS}" -printf 'correct summary' > "${TMP_CONTEXT}" -result=$(echo '{"tool_name":"Bash","tool_input":{"command":"echo CCP_TASK_SUMMARY_11111:Wrong Session"},"tool_response":"CCP_TASK_SUMMARY_11111:Wrong Session"}' \ - | CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${TMP_CONTEXT}" \ - CCP_ENABLE_AI_CONTEXT=true CCP_AI_CONTEXT_STRATEGY=inline CCP_SESSION_PID="${TEST_PID}" \ - bash "${LIB_DIR}/hook_runner.sh" post-tool && cat "${TMP_CONTEXT}" 2>/dev/null || true) -assert_equals "inline: wrong-PID marker ignored" "correct summary" "${result}" - -# post-tool: marker is IGNORED when strategy is not inline -rm -f "${TMP_CONTEXT}" "${TMP_STATUS}" -printf 'existing context' > "${TMP_CONTEXT}" -result=$(echo "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"echo CCP_TASK_SUMMARY_${TEST_PID}:Do Not Overwrite\"},\"tool_response\":\"CCP_TASK_SUMMARY_${TEST_PID}:Do Not Overwrite\"}" \ - | CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${TMP_CONTEXT}" \ - CCP_ENABLE_AI_CONTEXT=true CCP_AI_CONTEXT_STRATEGY=haiku CCP_SESSION_PID="${TEST_PID}" \ - bash "${LIB_DIR}/hook_runner.sh" post-tool && cat "${TMP_CONTEXT}" 2>/dev/null || true) -assert_equals "inline: marker ignored when strategy is haiku" "existing context" "${result}" - -# post-tool: marker is IGNORED when AI context is disabled entirely. -# bin/ccp never exports CCP_AI_CONTEXT_STRATEGY=inline when the feature is off, -# so explicitly override to haiku here to simulate a clean non-inline environment. -rm -f "${TMP_CONTEXT}" "${TMP_STATUS}" -printf 'existing context' > "${TMP_CONTEXT}" -result=$(echo "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"echo CCP_TASK_SUMMARY_${TEST_PID}:Do Not Overwrite\"},\"tool_response\":\"CCP_TASK_SUMMARY_${TEST_PID}:Do Not Overwrite\"}" \ - | CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${TMP_CONTEXT}" \ - CCP_AI_CONTEXT_STRATEGY=haiku CCP_SESSION_PID="${TEST_PID}" \ - bash "${LIB_DIR}/hook_runner.sh" post-tool && cat "${TMP_CONTEXT}" 2>/dev/null || true) -assert_equals "inline: marker ignored when AI context disabled" "existing context" "${result}" - -# post-tool: normal Bash output without marker does NOT touch context file -rm -f "${TMP_CONTEXT}" "${TMP_STATUS}" -printf 'existing context' > "${TMP_CONTEXT}" -result=$(echo '{"tool_name":"Bash","tool_input":{"command":"ls -la"},"tool_response":"total 42\ndrwxr-xr-x 5 user staff 160 Jan 1 12:00 ."}' \ - | CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${TMP_CONTEXT}" \ - CCP_ENABLE_AI_CONTEXT=true CCP_AI_CONTEXT_STRATEGY=inline CCP_SESSION_PID="${TEST_PID}" \ - bash "${LIB_DIR}/hook_runner.sh" post-tool && cat "${TMP_CONTEXT}" 2>/dev/null || true) -assert_equals "inline: non-marker Bash output preserves context" "existing context" "${result}" - -# post-tool: PID-scoped summary + test pass — both context and status captured -rm -f "${TMP_CONTEXT}" "${TMP_STATUS}" "${INLINE_CAPTURED}" -result=$(echo "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"npm test\"},\"tool_response\":\"CCP_TASK_SUMMARY_${TEST_PID}:Fix Tests\n3 tests passed\"}" \ - | CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${TMP_CONTEXT}" \ - CCP_ENABLE_AI_CONTEXT=true CCP_AI_CONTEXT_STRATEGY=inline CCP_SESSION_PID="${TEST_PID}" \ - bash "${LIB_DIR}/hook_runner.sh" post-tool && cat "${TMP_STATUS}" 2>/dev/null || true) -assert_equals "inline: summary + test pass both captured (status)" "✅ Tests passed" "${result}" -result=$(cat "${TMP_CONTEXT}" 2>/dev/null || true) -assert_contains "inline: summary + test pass both captured (context)" "Fix Tests" "${result}" - -# user-prompt: inline strategy skips Haiku subprocess (just writes first-5-words) -rm -f "${TMP_CONTEXT}" "${TMP_STATUS}" -result=$(echo '{"prompt":"Fix the login bug in the auth module"}' \ - | CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${TMP_CONTEXT}" \ - CCP_ENABLE_AI_CONTEXT=true CCP_AI_CONTEXT_STRATEGY=inline \ - bash "${LIB_DIR}/hook_runner.sh" user-prompt && cat "${TMP_CONTEXT}" 2>/dev/null || true) -assert_contains "inline: user-prompt still writes first-5-words" "Fix the login bug" "${result}" - -# user-prompt: haiku strategy with AI context enabled (no claude binary in PATH) -# just verify it writes first-5-words and doesn't fail (the background Haiku -# subprocess will silently fail without claude binary — expected in tests) -rm -f "${TMP_CONTEXT}" "${TMP_STATUS}" -result=$(echo '{"prompt":"Refactor the database layer for performance"}' \ - | CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${TMP_CONTEXT}" \ - CCP_ENABLE_AI_CONTEXT=true CCP_AI_CONTEXT_STRATEGY=haiku \ - bash "${LIB_DIR}/hook_runner.sh" user-prompt && cat "${TMP_CONTEXT}" 2>/dev/null || true) -assert_contains "haiku: user-prompt writes first-5-words" "Refactor the database layer" "${result}" - -rm -f "${TMP_STATUS}" "${TMP_CONTEXT}" - -# ── Tests: bin/ccp AI context strategy parsing ──────────────────────────────── - -echo "" -echo "bin/ccp --ai-context-strategy" - -BIN_CCP="${PROJECT_DIR}/bin/ccp" -CLI_TMP_DIR=$(mktemp -d) -CLI_STATE_DIR="${CLI_TMP_DIR}/state" -mkdir -p "${CLI_STATE_DIR}" -CLI_SESSION_FILE="${CLI_STATE_DIR}/sessions.json" -echo '[]' > "${CLI_SESSION_FILE}" - -cli_exit=0 -CCP_CLAUDE_CMD=/usr/bin/true CCP_STATUS_PROFILE=quiet \ -STATE_DIR="${CLI_STATE_DIR}" SESSION_FILE="${CLI_SESSION_FILE}" \ -"${BIN_CCP}" --ai-context --ai-context-strategy inline --no-dynamic "strategy test" \ - >/dev/null 2>/dev/null || cli_exit=$? -assert_equals "bin/ccp: --ai-context-strategy inline succeeds" "0" "${cli_exit}" - -cli_exit=0 -CCP_CLAUDE_CMD=/usr/bin/true CCP_STATUS_PROFILE=quiet \ -STATE_DIR="${CLI_STATE_DIR}" SESSION_FILE="${CLI_SESSION_FILE}" \ -"${BIN_CCP}" --ai-context --ai-context-strategy haiku --no-dynamic "strategy test" \ - >/dev/null 2>/dev/null || cli_exit=$? -assert_equals "bin/ccp: --ai-context-strategy haiku succeeds" "0" "${cli_exit}" - -cli_exit=0 -cli_err="${CLI_TMP_DIR}/invalid-strategy.err" -CCP_CLAUDE_CMD=/usr/bin/true CCP_STATUS_PROFILE=quiet \ -STATE_DIR="${CLI_STATE_DIR}" SESSION_FILE="${CLI_SESSION_FILE}" \ -"${BIN_CCP}" --ai-context --ai-context-strategy bogus --no-dynamic "strategy test" \ - >/dev/null 2>"${cli_err}" || cli_exit=$? -assert_equals "bin/ccp: invalid --ai-context-strategy exits 1" "1" "${cli_exit}" -assert_contains "bin/ccp: invalid strategy emits clear error" \ - "Invalid AI context strategy 'bogus'" "$(cat "${cli_err}" 2>/dev/null || true)" - -rm -rf "${CLI_TMP_DIR}" - # ── Tests: bin/ccp status profile parsing ───────────────────────────────────── echo "" @@ -1530,26 +1393,6 @@ mkdir -p "${CLI_STATE_DIR}" CLI_SESSION_FILE="${CLI_STATE_DIR}/sessions.json" echo '[]' > "${CLI_SESSION_FILE}" -# --append-system-prompt should be filtered from Claude args display -cli_out="${CLI_TMP_DIR}/startup-inline.out" -CCP_CLAUDE_CMD=/usr/bin/true CCP_STATUS_PROFILE=quiet \ -STATE_DIR="${CLI_STATE_DIR}" SESSION_FILE="${CLI_SESSION_FILE}" \ -"${BIN_CCP}" --ai-context --ai-context-strategy inline --no-dynamic "messaging test" \ - >"${cli_out}" 2>&1 || true -assert_not_contains "bin/ccp: inline strategy hides --append-system-prompt from args display" \ - "--append-system-prompt" "$(cat "${cli_out}" 2>/dev/null || true)" - -# Extra user-supplied args should still appear when --append-system-prompt is also injected -cli_out2="${CLI_TMP_DIR}/startup-inline-extra.out" -CCP_CLAUDE_CMD=/usr/bin/true CCP_STATUS_PROFILE=quiet \ -STATE_DIR="${CLI_STATE_DIR}" SESSION_FILE="${CLI_SESSION_FILE}" \ -"${BIN_CCP}" --ai-context --ai-context-strategy inline --no-dynamic "messaging test" -- --some-flag \ - >"${cli_out2}" 2>&1 || true -assert_contains "bin/ccp: extra args still shown when inline strategy active" \ - "--some-flag" "$(cat "${cli_out2}" 2>/dev/null || true)" -assert_not_contains "bin/ccp: --append-system-prompt still hidden when extra args present" \ - "--append-system-prompt" "$(cat "${cli_out2}" 2>/dev/null || true)" - # --goto: matching session emits "Resuming:" line CLI_GOTO_STATE_DIR="${CLI_TMP_DIR}/goto-state" mkdir -p "${CLI_GOTO_STATE_DIR}" From 1406219e306c094e540a89a67184eae816ec9915 Mon Sep 17 00:00:00 2001 From: Brian Ruggieri Date: Fri, 20 Mar 2026 09:09:59 -0400 Subject: [PATCH 04/10] chore: remove inline AI context strategy from bin/ccp Remove --ai-context-strategy flag, inline --append-system-prompt injection, display filtering, strategy validation, and debug event field. AI context is now a simple boolean: --ai-context enables Stop-hook haiku. --- bin/ccp | 68 +++++---------------------------------------------------- 1 file changed, 6 insertions(+), 62 deletions(-) diff --git a/bin/ccp b/bin/ccp index 460e2a5..5a2e02b 100755 --- a/bin/ccp +++ b/bin/ccp @@ -60,16 +60,11 @@ ${BLUE}ccp Options:${NC} --auto-title Auto-detect title from git branch (default) --no-dynamic Disable dynamic updates (static title) --status-profile MODE Status surface: quiet (default) or verbose - --ai-context Summarize prompts in title via claude-haiku (opt-in, - uses your Claude subscription — see docs/ai-context.md) + --ai-context Summarize tasks in title via claude-haiku (opt-in, + uses your Claude subscription) Privacy: prompt text is sent to Anthropic to generate the summary. Do not enable if prompts contain secrets. Also: export CCP_ENABLE_AI_CONTEXT=true for always-on - --ai-context-strategy MODE - How AI context summaries are generated: - haiku — separate claude-haiku call (default) - inline — system prompt injection, zero extra API calls - Also: export CCP_AI_CONTEXT_STRATEGY=inline for always-on --debug-ccp Enable structured JSONL debug logging to ~/.config/claude-code-pulse/logs/ In iTerm2, auto-opens Pulse Monitor in a split pane. @@ -166,7 +161,6 @@ main() { local enable_dynamic=true local status_profile_cli="" local enable_ai_context="${CCP_ENABLE_AI_CONTEXT:-false}" - local ai_context_strategy="${CCP_AI_CONTEXT_STRATEGY:-haiku}" local enable_debug_ccp="${CCP_DEBUG:-false}" local claude_args=() local restored_title="" @@ -205,15 +199,6 @@ main() { enable_ai_context=true shift ;; - --ai-context-strategy) - shift - if [[ $# -lt 1 ]]; then - log_error "Usage: ${SCRIPT_NAME} --ai-context-strategy haiku|inline" - exit 1 - fi - ai_context_strategy="$1" - shift - ;; --status-profile) shift if [[ $# -lt 1 ]]; then @@ -397,18 +382,6 @@ main() { esac export CCP_STATUS_PROFILE="${status_profile}" - # Resolve AI context strategy with precedence: - # CLI flag > environment variable > default haiku. - case "${ai_context_strategy}" in - haiku|inline) - ;; - *) - log_error "Invalid AI context strategy '${ai_context_strategy}'. Use: haiku or inline." - exit 1 - ;; - esac - export CCP_AI_CONTEXT_STRATEGY="${ai_context_strategy}" - # Setup debug logging if enabled if [[ "${enable_debug_ccp}" = true ]]; then local log_dir="${STATE_DIR}/logs" @@ -439,8 +412,7 @@ main() { --arg dir "${directory}" \ --arg profile "${status_profile}" \ --arg ai_ctx "${enable_ai_context}" \ - --arg ai_strat "${ai_context_strategy}" \ - '{ts:$ts, src:"session", pid:$pid, event:"session_start", title:$title, dir:$dir, profile:$profile, ai_context:$ai_ctx, ai_strategy:$ai_strat}' \ + '{ts:$ts, src:"session", pid:$pid, event:"session_start", title:$title, dir:$dir, profile:$profile, ai_context:$ai_ctx}' \ >> "${CCP_DEBUG_LOG}" 2>/dev/null || true fi @@ -466,7 +438,7 @@ main() { cleanup_monitor teardown_ccp_hooks "${ccp_settings_file}" rm -f "${CCP_STATUS_FILE:-}" "${CCP_CONTEXT_FILE:-}" "${CCP_BRANCH_FILE:-}" \ - "${CCP_AGENTS_FILE:-}" "${STATE_DIR}/inline_captured.$$" + "${CCP_AGENTS_FILE:-}" } trap '_ccp_cleanup' EXIT @@ -484,33 +456,14 @@ main() { log_info "Dynamic titles: ${GREEN}enabled${NC}" log_info "Status profile: ${CCP_STATUS_PROFILE}" if [[ "${enable_ai_context}" = true ]]; then - if [[ "${ai_context_strategy}" == "inline" ]]; then - log_info "AI context: ${GREEN}enabled${NC} (inline — summary via system prompt, no extra API calls)" - else - log_info "AI context: ${GREEN}enabled${NC} (prompts summarized via claude-haiku — uses your subscription)" - fi + log_info "AI context: ${GREEN}enabled${NC} (task summaries via claude-haiku — uses your subscription)" fi if [[ "${enable_debug_ccp}" = true ]]; then log_info "Debug log: ${YELLOW}${CCP_DEBUG_LOG}${NC}" fi fi if [[ ${#claude_args[@]} -gt 0 ]]; then - local display_args=() - local skip_next=false - for arg in "${claude_args[@]}"; do - if [[ "${skip_next}" == true ]]; then - skip_next=false - continue - fi - if [[ "${arg}" == "--append-system-prompt" ]]; then - skip_next=true - continue - fi - display_args+=("${arg}") - done - if [[ ${#display_args[@]} -gt 0 ]]; then - log_info "Claude args: ${display_args[*]}" - fi + log_info "Claude args: ${claude_args[*]}" fi echo "" @@ -560,15 +513,6 @@ main() { # Inject hooks into the project's .claude/settings.local.json ccp_settings_file=$(setup_ccp_hooks "${directory}" "${CCP_HOOK_RUNNER}") - # Inline AI context: inject a system prompt instruction asking Claude - # to echo a structured task summary as its first action. The summary - # is captured by the PostToolUse hook (see hook_runner.sh post-tool). - if [[ "${enable_ai_context}" = true && "${ai_context_strategy}" == "inline" ]]; then - local _ccp_inline_prompt - _ccp_inline_prompt="[CCP Terminal Title] As your very first action, run a single bash command: echo 'CCP_TASK_SUMMARY_$$:' followed by a 3-5 word title-case summary of the user's task. Example: echo 'CCP_TASK_SUMMARY_$$:Fix JWT Validation Bug'. Do this once, then proceed with your work normally." - claude_args+=("--append-system-prompt" "${_ccp_inline_prompt}") - fi - # Start background title updater, then run Claude directly. title_updater "${title}" From a1bd3d5c323fb555e65cecbd59c7b8ca710e91b6 Mon Sep 17 00:00:00 2001 From: Brian Ruggieri Date: Fri, 20 Mar 2026 09:16:13 -0400 Subject: [PATCH 05/10] chore: remove inline strategy artifacts from session.sh and ccp-watch Remove inline_captured.* from stale file cleanup and inline_marker_miss event handler from Pulse Monitor. --- bin/ccp-watch | 25 +++---------------------- lib/session.sh | 6 +++--- 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/bin/ccp-watch b/bin/ccp-watch index aceeec6..6e1e277 100755 --- a/bin/ccp-watch +++ b/bin/ccp-watch @@ -72,9 +72,7 @@ fi last_status="" last_status_ts="" last_event_ts="" -pending_summaries=0 delivered_summaries=0 -inline_miss_warned=false total_errors=0 total_events=0 @@ -284,26 +282,11 @@ tail -f "${log}" | while IFS= read -r line; do fi fi - # ── Inline summary tracking ──────────────────────────────────────── + # ── AI summary tracking ──────────────────────────────────────────── case "${event}" in - ai_context_pending) - pending_summaries=$((pending_summaries + 1)) - ;; ai_summary) delivered_summaries=$((delivered_summaries + 1)) - alert "OK" "Summary delivered: '${summary}' (${delivered_summaries}/${pending_summaries})" - ;; - inline_marker_miss) - # Only warn once per session — inline strategy is best-effort, - # Claude may skip the summary echo entirely. - if [[ "${inline_miss_warned}" = false && ${pending_summaries} -gt ${delivered_summaries} ]]; then - miss_count=$(jq -n "[inputs | select(.event == \"inline_marker_miss\")] | length" "${log}" 2>/dev/null) || miss_count=0 - if [[ ${miss_count} -ge 20 ]]; then - cmd=$(printf '%s' "${line}" | jq -r '.command // ""' 2>/dev/null | head -c 40) || cmd="" - alert "INFO" "Inline summary not delivered after ${miss_count} Bash calls (best-effort, using first-5-words fallback)" - inline_miss_warned=true - fi - fi + alert "OK" "Summary delivered: '${summary}' (${delivered_summaries} total)" ;; esac @@ -339,11 +322,9 @@ tail -f "${log}" | while IFS= read -r line; do echo -e " ${MAGENTA}${BOLD}Session Summary${NC}" echo -e " Events: ${total_events}" echo -e " Errors: ${total_errors}" - echo -e " Summaries: ${delivered_summaries}/${pending_summaries} delivered" + echo -e " Summaries: ${delivered_summaries} delivered" if [[ ${total_errors} -gt 0 ]]; then echo -e " ${RED}${BOLD}Issues detected — review log with: ccp-watch --list${NC}" - elif [[ ${pending_summaries} -gt ${delivered_summaries} ]]; then - echo -e " ${YELLOW}Dropped summaries detected${NC}" else echo -e " ${GREEN}Clean session.${NC}" fi diff --git a/lib/session.sh b/lib/session.sh index 0ab7776..afb2787 100644 --- a/lib/session.sh +++ b/lib/session.sh @@ -41,8 +41,8 @@ save_session() { # prune_dead_sessions: remove entries whose process is no longer running. # Rewrites SESSION_FILE in-place so all subsequent reads see only live sessions. -# Also cleans up orphan state files (status, context, branch, monitor PID, -# inline_captured) left behind by sessions killed without their EXIT trap. +# Also cleans up orphan state files (status, context, branch, monitor PID) +# left behind by sessions killed without their EXIT trap. prune_dead_sessions() { [[ ! -f "${SESSION_FILE}" ]] && return @@ -63,7 +63,7 @@ prune_dead_sessions() { local f pid for f in "${STATE_DIR}"/status.*.txt "${STATE_DIR}"/context.*.txt \ "${STATE_DIR}"/branch.*.txt "${STATE_DIR}"/agents.*.txt \ - "${STATE_DIR}"/monitor.*.pid "${STATE_DIR}"/inline_captured.*; do + "${STATE_DIR}"/monitor.*.pid; do [[ -f "${f}" ]] || continue pid=$(basename "${f}" | grep -o '[0-9]\+') || continue if ! kill -0 "${pid}" 2>/dev/null; then From 60dcf2b7c83e441e2cd5874b59f783edc04e4f8a Mon Sep 17 00:00:00 2001 From: Brian Ruggieri Date: Fri, 20 Mar 2026 09:21:25 -0400 Subject: [PATCH 06/10] docs: update documentation for Stop-hook haiku AI context Rewrite AI context docs, remove inline-context-spec, update README with new features (commit message capture, context sanitization), simplify CLI flag documentation. --- README.md | 36 +++------ docs/ai-context.md | 100 +++++++---------------- docs/dynamic-titles.md | 22 +++--- docs/inline-context-spec.md | 154 ------------------------------------ docs/usage.md | 3 +- 5 files changed, 50 insertions(+), 265 deletions(-) delete mode 100644 docs/inline-context-spec.md diff --git a/README.md b/README.md index 4d2f0cc..4b34af6 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,9 @@ Each pane title is independent — project, branch, task, and live status update - **Priority-based display**. Errors always show first. Completions (tests passed, committed) immediately override whatever else is showing. - **Hook-based**. Uses Claude Code's own hook events for status. No output parsing, no regex, just structured JSON. - **Session tracking**. Save and re-open sessions by title with `--goto`. -- **AI task summaries**. `--ai-context` refines your prompt into a clean 3-5 word label shown in the title. Two strategies: `haiku` (separate claude-haiku call, fully invisible) or `inline` (zero extra API calls, uses the already-outgoing session). Both are opt-in. +- **AI task summaries**. `--ai-context` refines each turn into a clean 3-5 word label shown in the title. Uses claude-haiku to summarize after each turn completes. Opt-in only. +- **Git commit context**. When Claude commits, the commit subject line automatically appears as the task context — no flag needed. +- **Context sanitization**. Shell prompt prefixes (`$`, `%`, `user@host`, etc.) are automatically stripped from pasted terminal content before building the title. - **Tested on iTerm2, Terminal.app, and tmux**. Detection logic for WezTerm, Ghostty, and Kitty is included but unverified. - **Full Claude passthrough**. Every Claude Code flag works exactly as expected. @@ -75,14 +77,11 @@ ccp --permission-mode bypassPermissions # AI-summarized task labels in the title (opt-in, uses your subscription) ccp --ai-context "Fix the auth bug" -# Inline strategy — zero extra API calls (one echo visible per session) -ccp --ai-context --ai-context-strategy inline "Fix the auth bug" - # Debug mode — structured JSONL logging + live Pulse Monitor (iTerm2: auto-opens in split pane) ccp --debug-ccp "Fix the auth bug" ``` -> See [AI Task Summaries](#ai-task-summaries) for the `--ai-context` flag and strategy options. +> See [AI Task Summaries](#ai-task-summaries) for the `--ai-context` flag. ## Installation @@ -232,30 +231,19 @@ Precedence: `--status-profile` flag > `CCP_STATUS_PROFILE` env var > `quiet` def ## AI Task Summaries -With `--ai-context`, ccp distills your prompt into a clean 3-5 word label shown in the title. Without it, the first words of your prompt show as-is. - -Two strategies are available: +With `--ai-context`, ccp generates a clean 3-5 word label for each conversation turn. Without it, the first words of your prompt show as-is. -| | `haiku` (default) | `inline` | -|---|---|---| -| Extra API calls | 1 per prompt | 0 | -| Visible to user | No | One `echo` per session | -| Summary context | Prompt text only | Full codebase + history | -| Reliability | High | Model-dependent | +How it works: when Claude finishes responding, the Stop hook sends a truncated excerpt of the response to claude-haiku for a 3-5 word summary. The summary overwrites the first-5-words placeholder in the title. ```bash -# Haiku strategy (default) — invisible, uses your subscription +# Enable AI summaries (opt-in, uses your subscription) ccp --ai-context "PR #89 - Fix auth" -# Inline strategy — zero extra API calls, one echo visible per session -ccp --ai-context --ai-context-strategy inline "PR #89 - Fix auth" - -# Always on, via environment variables +# Always on, via environment variable export CCP_ENABLE_AI_CONTEXT=true -export CCP_AI_CONTEXT_STRATEGY=inline # optional, default is haiku ``` -See [docs/ai-context.md](docs/ai-context.md) for the full trade-off analysis. +See [docs/ai-context.md](docs/ai-context.md) for the full details. ## Claude Flag Passthrough @@ -430,8 +418,7 @@ my-app (bug/login-crash) | login bug | ⬆️ Pushing | `--auto-title` | Auto-detect title from git branch (this is the default) | | `--no-dynamic` | Static title only, no live updates | | `--status-profile quiet\|verbose` | Which events to surface (default: `quiet`) | -| `--ai-context` | Summarize prompts into a 3-5 word title label (opt-in) | -| `--ai-context-strategy haiku\|inline` | Summary strategy: `haiku` (default, invisible) or `inline` (zero extra API calls) | +| `--ai-context` | Summarize each turn into a 3-5 word title label (opt-in, uses your subscription) | | `--debug-ccp` | Structured JSONL debug logging to `~/.config/claude-code-pulse/logs/`; auto-opens Pulse Monitor in iTerm2 | | `--goto TITLE` | Re-open a previous session by title | | `--list`, `-l` | List active ccp sessions | @@ -445,8 +432,7 @@ my-app (bug/login-crash) | login bug | ⬆️ Pushing | Variable | Default | Description | |----------|---------|-------------| | `CCP_STATUS_PROFILE` | `quiet` | Default status profile | -| `CCP_ENABLE_AI_CONTEXT` | `false` | Always-on AI prompt summarization | -| `CCP_AI_CONTEXT_STRATEGY` | `haiku` | Summary strategy: `haiku` or `inline` | +| `CCP_ENABLE_AI_CONTEXT` | `false` | Always-on AI context summarization | | `CCP_DEBUG` | `false` | Always-on structured JSONL debug logging | ## Documentation diff --git a/docs/ai-context.md b/docs/ai-context.md index 2f809ad..811cfd1 100644 --- a/docs/ai-context.md +++ b/docs/ai-context.md @@ -4,7 +4,7 @@ ## What It Does -When enabled, ccp watches the first message you send to Claude in each session and generates a concise 3–5 word label for it. That label appears in the terminal title alongside the current status: +When enabled, ccp generates a concise 3–5 word label for each conversation turn and shows it in the terminal title: ``` ✳ my-project (main) | Fix Auth Bug | ✏️ Editing @@ -26,120 +26,80 @@ With `--ai-context`, it's refined into a clean label: ## How It Works -There are two strategies for generating the AI summary: +Context updates happen in two phases: -### Haiku Strategy (default) +### Phase 1: First-5-words (immediate) -When you send a prompt, `hook_runner.sh`'s UserPromptSubmit handler: +When you send a prompt, `hook_runner.sh`'s UserPromptSubmit handler writes the first 5 words of your prompt to the context file immediately. The title updates within one polling cycle. -1. Writes the first 5 words of your prompt to the context file immediately (instant title update) -2. Fires a **detached background subprocess** with `claude --print --model claude-haiku-4-5-20251001` -3. Sends this exact prompt to Haiku: +### Phase 2: Haiku summary (after turn completes) -``` -Summarize this developer task in 3-5 words. Title-case. No punctuation. No quotes. Reply with only the words, nothing else. Task: -``` +When Claude finishes responding, the Stop hook fires with `last_assistant_message` — the full text of Claude's response. A detached background subprocess: -4. When Haiku responds (~1–3 seconds), overwrites the context file with the summary +1. Truncates the response to first 500 + last 500 characters +2. Sends it to `claude --print --model claude-haiku-4-5-20251001` with a summarization prompt +3. Haiku returns a 3–5 word title-case summary +4. The summary overwrites the first-5-words placeholder in the context file 5. The title_updater picks it up on the next heartbeat The subprocess is fully detached (`& disown`). It never blocks Claude, never delays your session, and never writes to the terminal. -### Inline Strategy (`--ai-context-strategy inline`) - -Instead of a separate API call, ccp piggybacks on the main Claude session: +## Additional Context Sources -1. Writes the first 5 words of your prompt to the context file immediately (instant title update) -2. Injects a lightweight `--append-system-prompt` instruction into Claude's launch args -3. Claude reads the instruction and, as its very first action, runs: `echo 'CCP_TASK_SUMMARY:Fix JWT Auth Bug'` -4. The PostToolUse hook captures the echo output, extracts the summary after the `CCP_TASK_SUMMARY:` marker -5. Writes the summary to the context file -6. The title_updater picks it up on the next heartbeat +Two features provide context automatically, independent of `--ai-context`: -**Key differences from Haiku:** -- **Zero extra API calls** — the summary is part of the main Claude response -- **Zero extra cost** — no additional subscription usage -- **Better summaries** — Claude has full codebase context, not just the prompt text -- **One visible echo** — the `echo 'CCP_TASK_SUMMARY:...'` appears in Claude's tool use output (minimal, one-time) -- **Model-dependent** — Claude may occasionally skip the instruction (falls back to first-5-words) +### Git commit message capture -## What Data Is Sent +When Claude runs `git commit` and the PostToolUse hook detects a successful commit, the commit subject line is extracted and written to the context file. This replaces the prompt-based context with a human-readable summary — e.g., `feat: add OAuth support` appears alongside `💾 Committed`. -### Haiku Strategy +### Context sanitization -**Exactly your prompt text** — the full message you typed into Claude Code, after stripping the project name if it appears in the title. +Shell prompt prefixes are automatically stripped from pasted terminal content before extracting the first-5-words placeholder. Patterns like `user@host`, `(venv)`, `$`, `%`, `#`, and `>` at the start of the prompt are removed so prefix tokens don't waste the 5-word budget. -No other data is sent. No file contents, no tool outputs, no conversation history. Just the user-facing message you already typed. +## What Data Is Sent -### Inline Strategy +**Claude's response text** — the `last_assistant_message` field from the Stop hook, truncated to first 500 + last 500 characters. -**Nothing extra.** Your prompt is already being sent to Claude as part of the normal session. The only addition is a ~250-character system prompt instruction appended via `--append-system-prompt`. No additional user data leaves your machine. +No file contents beyond what Claude already processed. No conversation history. Just a truncated excerpt of what Claude just said. ## Subscription Usage -### Haiku Strategy - -This strategy calls `claude --print` once per user message. That's one Haiku API call per prompt you send. +This feature calls `claude --print` once per conversation turn (when Claude stops responding). That's one Haiku API call per turn. - **Model:** `claude-haiku-4-5-20251001` (the most cost-effective model) - **Call type:** Non-interactive, single-turn (`--print` flag) -- **Context sent:** Your prompt text only (one message, no history) +- **Context sent:** Truncated response excerpt (max ~1000 chars) - **Response:** 3–5 words (minimal token usage) -If you use Claude via subscription (Claude.ai Pro/Max), this counts against your subscription usage. If you use API keys, this counts against your API bill. The per-call cost is minimal (Haiku is Anthropic's cheapest model), but it fires every time you send a message, so usage scales with how much you type. +If you use Claude via subscription (Claude.ai Pro/Max), this counts against your subscription usage. If you use API keys, this counts against your API bill. The per-call cost is minimal (Haiku is Anthropic's cheapest model), but it fires every turn, so usage scales with conversation length. **This is exactly why the feature is opt-in.** We don't think it's appropriate to silently consume your subscription on your behalf. You should choose to enable it knowing what it does. -### Inline Strategy - -**Zero additional subscription usage.** The summary is generated as part of the main Claude session you're already paying for. The only overhead is a single `echo` command in Claude's first response — negligible additional tokens. - ## Privacy -### Haiku Strategy - The data flow is: ``` -Your prompt text → claude CLI (your auth) → Anthropic API → 3-5 word summary +Claude's response text (truncated) → claude CLI (your auth) → Anthropic API → 3-5 word summary ``` -- Your prompt is processed under your own Claude account and credentials +- Claude's response is processed under your own Claude account and credentials - The summary request is subject to Anthropic's standard privacy policy — the same policy that applies to every message you send to Claude - ccp does not see, log, or store the data — the call happens entirely through your local `claude` CLI binary Nothing is routed through ccp's infrastructure (there isn't any). The subprocess calls the same `claude` binary you use interactively. -### Inline Strategy - -The data flow is: - -``` -Your prompt text (already going) + ~250-char system instruction → Claude session → echo summary -``` - -- Your prompt is sent to Anthropic **once** (the main session), not twice -- The only addition is a short system prompt instruction appended via `--append-system-prompt` -- No data leaves your machine beyond what already would without ccp -- ccp does not see, log, or store the data — the echo output is captured by the PostToolUse hook and written to a local temp file +**Note:** Claude's response text is sent to Haiku for summarization. If the response contains sensitive information (secrets, credentials, personal data), that content will be included in the summarization request. ## How to Enable ```bash -# Flag (per session) — uses Haiku strategy by default +# Flag (per session) ccp --ai-context "My task" -# Inline strategy (no extra API calls — recommended) -ccp --ai-context --ai-context-strategy inline "My task" - # Environment variable (always-on — add to ~/.zshrc or ~/.bashrc) export CCP_ENABLE_AI_CONTEXT=true ccp "My task" # AI context active without --ai-context flag - -# Inline strategy always-on -export CCP_ENABLE_AI_CONTEXT=true -export CCP_AI_CONTEXT_STRATEGY=inline -ccp "My task" ``` The flag and env var are equivalent. The env var is the recommended approach if you always want AI context active. @@ -147,22 +107,16 @@ The flag and env var are equivalent. The env var is the recommended approach if When enabled, ccp prints a disclosure at startup: ``` -# Haiku strategy Dynamic titles: enabled Status profile: quiet AI context: enabled (prompts summarized via claude-haiku — uses your subscription) - -# Inline strategy -Dynamic titles: enabled -Status profile: quiet -AI context: enabled (inline — summary via system prompt, no extra API calls) ``` ## How to Disable Don't pass `--ai-context`. That's all. -If you had previously set `CCP_ENABLE_AI_CONTEXT=true` in your shell profile, remove that line. If you also set `CCP_AI_CONTEXT_STRATEGY=inline`, remove that too. +If you had previously set `CCP_ENABLE_AI_CONTEXT=true` in your shell profile, remove that line. ## Without AI Context diff --git a/docs/dynamic-titles.md b/docs/dynamic-titles.md index d647f88..7ea5170 100644 --- a/docs/dynamic-titles.md +++ b/docs/dynamic-titles.md @@ -112,7 +112,7 @@ Fired when a Bash tool finishes. We inspect the output for completion keywords: |-----------|--------| | `N tests passed` / `N specs passed` | `✅ Tests passed` | | `N tests failed` / `N specs failing` | `❌ Tests failed` | -| git output starts with `[` (commit hash) | `💾 Committed` | +| git output starts with `[` (commit hash) | `💾 Committed` (+ commit subject written to context file) | **Completion events always win**, regardless of current priority. A `✅ Tests passed` (priority 60) immediately overrides any active status like `🧪 Testing` (priority 80). @@ -128,14 +128,13 @@ Fired when a tool fails: ### UserPromptSubmit — User sends a message 1. Clears the status file (removes any stale status such as the startup welcome message) -2. Writes the first 5 words of the user's prompt to the context file as a placeholder -3. If `--ai-context` is enabled: spawns a background call to `claude --print` with model `claude-haiku-4-5-20251001` -4. Haiku distills the full prompt into a 3–5 word summary (opt-in only — uses your subscription) -5. Summary overwrites the context file when ready (~1–3 seconds) +2. Sanitizes the prompt (strips shell prompt prefixes like `$`, `%`, `user@host` from pasted content) +3. Writes the first 5 words of the sanitized prompt to the context file as an immediate placeholder ### Stop — Claude finishes responding -Clears the status file. The monitor shows `💤 Idle`. +1. Clears the status file. The monitor shows `💤 Idle`. +2. If `--ai-context` is enabled: reads `last_assistant_message` from the Stop hook payload, truncates to first 500 + last 500 characters, and spawns a detached background call to `claude --print --model claude-haiku-4-5-20251001` for a 3–5 word summary. The summary overwrites the context file when ready (~1–3 seconds). ### Event Hooks (Verbose Mode) @@ -172,7 +171,8 @@ my-project (main) | Fix Auth Bug | ✏️ Editing - Always preserved **Task summary** -- First 5 words of your prompt (always); refined to 3–5 word semantic summary if `--ai-context` is enabled +- First 5 words of your prompt (immediate); refined to 3–5 word summary after the turn completes if `--ai-context` is enabled +- On `💾 Committed`, the commit subject line replaces the prompt-based context automatically - Only appears after the first UserPromptSubmit - Before that, the "Welcome" status is shown instead @@ -242,9 +242,9 @@ Claude Code then reads the updated settings file and registers the hooks. As Claude Code runs: - PreToolUse fires → hook_runner sets PreToolUse status -- PostToolUse fires → hook_runner sets PostToolUse/completion status -- UserPromptSubmit fires → hook_runner sets Thinking; spawns distillation only if `--ai-context` enabled -- Stop fires → hook_runner clears status (monitor shows Idle) +- PostToolUse fires → hook_runner sets PostToolUse/completion status; captures commit subject on `💾 Committed` +- UserPromptSubmit fires → hook_runner sets Thinking; writes first-5-words context +- Stop fires → hook_runner clears status (monitor shows Idle); spawns haiku summarization if `--ai-context` enabled ### Teardown @@ -284,7 +284,7 @@ A separate subshell keeps this work isolated and allows the main session to retu ### Why Haiku distillation? -User prompts can be long and wandering. When `--ai-context` is enabled, after each user message we run `claude --print` (non-interactive, one turn) with the full prompt to generate a 3–5 word summary. This summary fits neatly in the terminal title and gives context at a glance ("Fix Auth Bug", "Refactor Logging", etc.). +User prompts can be long and wandering. When `--ai-context` is enabled, after Claude finishes responding the Stop hook sends a truncated excerpt of the response to `claude --print` (non-interactive, one turn) for a 3–5 word summary. This summary fits neatly in the terminal title and gives context at a glance ("Fix Auth Bug", "Refactor Logging", etc.). This feature is **opt-in** and explicitly uses your Claude subscription. See [docs/ai-context.md](ai-context.md) for the full details, privacy implications, and how to enable it. diff --git a/docs/inline-context-spec.md b/docs/inline-context-spec.md deleted file mode 100644 index 33ddd14..0000000 --- a/docs/inline-context-spec.md +++ /dev/null @@ -1,154 +0,0 @@ -# Inline AI Context: Specification - -> **Status:** Implemented -> **Feature flag:** `--ai-context-strategy inline` / `CCP_AI_CONTEXT_STRATEGY=inline` - -## Problem - -The existing `--ai-context` feature sends a **separate** `claude --print --model claude-haiku` API call to summarize each user prompt. This works, but has trade-offs: - -- **Extra cost** — one Haiku call per prompt (subscription or API credits). -- **Extra latency** — 1–3 seconds for the background call to return. -- **Reduced context** — Haiku only sees the raw prompt text, not the codebase or conversation history. -- **Duplicate data** — the prompt is sent to Anthropic twice (once for the main session, once for summarization). - -## Solution: Inline Context Strategy - -Piggyback on the **already-outgoing** Claude request. Instead of a separate API call, CCP injects a lightweight system prompt instruction into the main Claude session via `--append-system-prompt`. Claude generates a task summary as part of its normal first-turn processing, and CCP captures it through the existing PostToolUse hook. - -### How It Works - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ User types prompt → goes to Claude (as always) │ -│ │ -│ CCP adds --append-system-prompt with a one-line instruction: │ -│ "echo 'CCP_TASK_SUMMARY:Brief Title' as your first action" │ -│ │ -│ Claude reads user prompt + full codebase context │ -│ Claude echoes: CCP_TASK_SUMMARY:Fix JWT Auth Bug │ -│ Claude proceeds with normal work │ -│ │ -│ PostToolUse hook sees the echo output │ -│ hook_runner.sh extracts the summary after the marker │ -│ Writes to CCP_CONTEXT_FILE │ -│ Monitor picks it up → title updates │ -└──────────────────────────────────────────────────────────────────┘ -``` - -### Data Flow - -``` -User prompt ──→ Claude (with appended system prompt) - │ - ├─→ echo 'CCP_TASK_SUMMARY:Fix JWT Auth' (first action) - │ │ - │ └─→ PostToolUse hook → hook_runner.sh - │ │ - │ └─→ extract summary → CCP_CONTEXT_FILE - │ │ - │ └─→ monitor → terminal title - │ - └─→ normal Claude work continues -``` - -## System Prompt Instruction - -The injected instruction is deliberately minimal to avoid wasting context window space: - -``` -[CCP Terminal Title] As your very first action, run: echo 'CCP_TASK_SUMMARY:' followed by -a 3-5 word title-case summary of the user's task. Example: echo 'CCP_TASK_SUMMARY:Fix JWT -Validation Bug'. Do this once, then proceed normally. -``` - -**Key design choices:** - -1. **Structured marker** (`CCP_TASK_SUMMARY:`) — unambiguous prefix for reliable parsing. Not a plausible substring of any real command output. -2. **Bash echo** — the simplest possible tool use. No file writes, no permissions, no side effects. The echo goes to stdout and is captured by the PostToolUse hook's `tool_response` field. -3. **One-shot** — "Do this once" prevents repeated summaries on follow-up prompts. -4. **Minimal instruction** — under 250 characters. Negligible context window impact. - -## Comparison: Haiku vs. Inline - -| Aspect | Haiku (`haiku`) | Inline (`inline`) | -|---|---|---| -| **API calls** | +1 Haiku call per prompt | Zero extra calls | -| **Cost** | Haiku tokens (small but nonzero) | Zero additional cost | -| **Latency** | 1–3s background call | ~0s (part of main response) | -| **Summary quality** | Haiku-quality (good) | Main-model quality (better — full context) | -| **Context available** | Prompt text only | Full codebase + conversation history | -| **User visibility** | Invisible (detached subprocess) | One brief `echo` in Claude's output | -| **Privacy** | Prompt sent twice to Anthropic | Prompt sent once (already going) | -| **Reliability** | High (dedicated call) | Model-dependent (Claude may skip it) | -| **Fallback** | First 5 words if Haiku fails | First 5 words if echo is skipped | - -## Configuration - -```bash -# Enable inline strategy (per session) -ccp --ai-context --ai-context-strategy inline "My task" - -# Enable inline strategy (always-on via env vars) -export CCP_ENABLE_AI_CONTEXT=true -export CCP_AI_CONTEXT_STRATEGY=inline -ccp "My task" -``` - -The strategy setting only matters when `--ai-context` is enabled. Without `--ai-context`, both strategies are inactive and the title shows the raw first-5-words fallback. - -## Privacy & Security - -### What changes - -- **Haiku strategy**: prompt text is sent to Anthropic twice (main session + Haiku call). -- **Inline strategy**: prompt text is sent to Anthropic once (main session only). A ~250-character system prompt instruction is appended. - -### What doesn't change - -- No data is sent to any third party (all calls go through your local `claude` CLI). -- No data is logged, stored, or transmitted by CCP. -- The summary stays local (written to a temp file in `~/.config/claude-code-pulse/`). -- The feature is opt-in (requires `--ai-context`). - -### Transparency - -When the inline strategy is active, CCP prints at startup: - -``` -AI context: enabled (inline — summary via system prompt, no extra API calls) -``` - -The system prompt instruction is visible in Claude's conversation context (Claude Code shows system prompt contents when asked). - -## Terms & Convention Compliance - -1. **`--append-system-prompt`** is Claude Code's official mechanism for extending the system prompt. CCP uses it as documented. -2. **PostToolUse hooks** are Claude Code's official mechanism for observing tool outputs. CCP reads, never modifies. -3. **The appended instruction does not override or conflict** with the user's own `--system-prompt` or `--append-system-prompt` flags. Claude Code concatenates multiple `--append-system-prompt` values. -4. **Claude's behavior is not silently altered** — the system prompt is part of the visible configuration, and the echo output appears in Claude's tool use history. - -## Failure Modes - -| Scenario | Behavior | -|---|---| -| Claude ignores the instruction | First-5-words fallback remains in the title (same as no AI context) | -| Claude includes extra text in the echo | Marker prefix parsing extracts only the summary portion | -| Claude echoes on every prompt | Only the latest summary is used (atomic overwrite) | -| User also passes `--append-system-prompt` | Both are applied (Claude Code concatenates) | -| Inline + Haiku both enabled | Inline takes precedence (Haiku subprocess is skipped) | - -## Implementation Summary - -### `bin/ccp` -- Parses `--ai-context-strategy` flag and `CCP_AI_CONTEXT_STRATEGY` env var -- When inline: injects `--append-system-prompt` with the summary instruction into `claude_args` -- Exports `CCP_AI_CONTEXT_STRATEGY` for `hook_runner.sh` - -### `lib/hook_runner.sh` -- `post-tool` handler: detects `CCP_TASK_SUMMARY:` in Bash tool_response, extracts summary, writes to `CCP_CONTEXT_FILE` -- `user-prompt` handler: skips Haiku subprocess when strategy is `inline` (summary will come from the main session) - -### `docs/ai-context.md` -- Documents both strategies with clear comparison -- Explains when to use each one diff --git a/docs/usage.md b/docs/usage.md index 60fa324..3a0d9df 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -124,8 +124,7 @@ Any unrecognized `-*` flag is also forwarded, so you can pass new Claude flags w | `--auto-title` | Detect title from git branch (default behavior) | | `--no-dynamic` | Static title only — disable monitoring | | `--status-profile quiet\|verbose` | Status surface (default: quiet) | -| `--ai-context` | Summarize prompts into a 3-5 word title label (opt-in) | -| `--ai-context-strategy haiku\|inline` | Summary strategy: `haiku` (default, invisible) or `inline` (zero extra API calls) | +| `--ai-context` | Summarize each turn into a 3-5 word title label (opt-in, uses your subscription) | | `--goto TITLE` | Re-open previous session by title search | | `--list`, `-l` | List all active ccp sessions | | `--help`, `-h` | Show help | From 61a4ac331584cb477a0ff52c900c1e184330f240 Mon Sep 17 00:00:00 2001 From: Brian Ruggieri Date: Fri, 20 Mar 2026 09:22:55 -0400 Subject: [PATCH 07/10] docs: add v1.5.0 CHANGELOG entry for Stop-hook haiku --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ae1e81..d890fb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.5.0] - 2026-03-20 + +### Added +- **Stop-hook haiku summarization** — AI context summaries are now derived from + Claude's actual response (`last_assistant_message`) via the Stop hook, delivering + ~100% of the time (up from 0.5% with the previous inline strategy). Summaries + reflect what Claude *did*, not just what was asked. +- **Two-phase context** — first-5-words appears immediately on prompt submission; + a refined haiku summary overwrites after the turn completes. +- **Git commit message capture** — commit subject line automatically written to + context file on `💾 Committed`, providing a human-written task summary at zero cost. +- **Context sanitization** — shell prompt prefixes (`(venv) user@host %`) are + stripped from first-5-words when users paste terminal output. + +### Fixed +- Clear `CLAUDECODE` env var in haiku subprocess to prevent nested claude call + failures. + +### Removed +- **Inline AI context strategy** — `--ai-context-strategy` flag, + `--append-system-prompt` injection, `CCP_TASK_SUMMARY` markers, and + `inline_captured` sentinel files are all removed. `--ai-context` is now a + simple boolean toggle. + ## [1.2.0] - 2026-03-05 ### Added From 7044a59686556abf43c56482d21991e2f39cc6bb Mon Sep 17 00:00:00 2001 From: Brian Ruggieri Date: Fri, 20 Mar 2026 09:50:41 -0400 Subject: [PATCH 08/10] fix: sanitize directory name before shell prompt character zsh prompts include a directory name between user@host and the prompt character: (venv) user@host dir % command. The previous sed chain stripped (venv) and user@host but left dir % because % wasn't at position 0. Add a sed to strip an optional word before the prompt char. --- lib/hook_runner.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/hook_runner.sh b/lib/hook_runner.sh index 63890b0..962ceb4 100644 --- a/lib/hook_runner.sh +++ b/lib/hook_runner.sh @@ -426,6 +426,7 @@ case "${mode}" in sanitized_prompt=$(printf '%s' "${raw_prompt}" \ | sed 's/^([^)]*)[[:space:]]*//' \ | sed 's/^[a-zA-Z0-9._-]*@[a-zA-Z0-9._-]*[[:space:]]*//' \ + | sed 's/^[a-zA-Z0-9._-]*[[:space:]]*[%$#>][[:space:]]*//' \ | sed 's/^[%$#>][[:space:]]*//') # Write first-5-words placeholder immediately so the title updates at once From b872143fc0eebc9d9688e1b754e43d8146fbcf8c Mon Sep 17 00:00:00 2001 From: Brian Ruggieri Date: Fri, 20 Mar 2026 09:52:33 -0400 Subject: [PATCH 09/10] fix: add exclamation mark to welcome message --- bin/ccp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/ccp b/bin/ccp index 5a2e02b..0dfbb1e 100755 --- a/bin/ccp +++ b/bin/ccp @@ -507,7 +507,7 @@ main() { | awk '{print $1}') || _welcome_name="" [[ -z "${_welcome_name}" ]] && _welcome_name="${USER:-}" if [[ -n "${_welcome_name}" ]]; then - printf '%s' "👋 Welcome back, ${_welcome_name}" > "${CCP_STATUS_FILE}" + printf '%s' "👋 Welcome back, ${_welcome_name}!" > "${CCP_STATUS_FILE}" fi # Inject hooks into the project's .claude/settings.local.json From ad8a8c1909ba843ca83db58469b833d56dae3f80 Mon Sep 17 00:00:00 2001 From: Brian Ruggieri Date: Fri, 20 Mar 2026 10:07:42 -0400 Subject: [PATCH 10/10] fix: address PR review comments and sync stale documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - hook_runner.sh: add race guard to haiku subprocess — only writes context if no newer prompt has arrived since the subprocess launched - hook_runner.sh: sync _status_priority() with monitor.sh — add missing Rebasing and Cherry-picking to priority 75 - tests: replace flaky sleep 2 with polling loops in haiku tests - tests: use printf '%s' for sanitization test payloads to avoid format string fragility with % characters - README: add missing -p/--print to no-value flags list - CHANGELOG: add v1.5.0 comparison link - docs/dynamic-titles.md: fix SubagentStop status text to match code --- CHANGELOG.md | 1 + README.md | 2 +- docs/dynamic-titles.md | 2 +- lib/hook_runner.sh | 18 ++++++++++++++++-- tests/test-suite.sh | 36 ++++++++++++++++++++++++------------ 5 files changed, 43 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d890fb4..442813b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -122,6 +122,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **SECURITY.md** — responsible disclosure policy. - **Issue templates** (bug report + feature request) and PR template. +[1.5.0]: https://github.com/brianruggieri/claude-code-pulse/compare/v1.2.0...v1.5.0 [1.2.0]: https://github.com/brianruggieri/claude-code-pulse/compare/v1.1.0...v1.2.0 [1.1.0]: https://github.com/brianruggieri/claude-code-pulse/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/brianruggieri/claude-code-pulse/releases/tag/v1.0.0 diff --git a/README.md b/README.md index 4b34af6..40deca7 100644 --- a/README.md +++ b/README.md @@ -272,7 +272,7 @@ Any unrecognized `-*` flag is also forwarded, so new Claude flags just work. ### Full forwarded flag list **No value:** -`-c` / `--continue`, `--dangerously-skip-permissions`, `--allow-dangerously-skip-permissions`, `--verbose`, `--ide`, `--no-ide`, `--fork-session`, `--chrome`, `--no-chrome`, `--no-session-persistence`, `--include-partial-messages`, `--replay-user-messages`, `--strict-mcp-config`, `--mcp-debug` +`-c` / `--continue`, `-p` / `--print`, `--dangerously-skip-permissions`, `--allow-dangerously-skip-permissions`, `--verbose`, `--ide`, `--no-ide`, `--fork-session`, `--chrome`, `--no-chrome`, `--no-session-persistence`, `--include-partial-messages`, `--replay-user-messages`, `--strict-mcp-config`, `--mcp-debug` **Required value:** `--model`, `--permission-mode`, `--effort`, `--system-prompt`, `--append-system-prompt`, `--debug-file`, `--session-id`, `--output-format`, `--input-format`, `--settings`, `--setting-sources`, `--fallback-model`, `--max-budget-usd`, `--json-schema`, `--agent` diff --git a/docs/dynamic-titles.md b/docs/dynamic-titles.md index 7ea5170..bf679e3 100644 --- a/docs/dynamic-titles.md +++ b/docs/dynamic-titles.md @@ -149,7 +149,7 @@ Additional hooks fire for lifecycle and permission events. These are only surfac | SessionStart | `🚀 Starting` | | PreCompact | `📦 Compacting` | | SubagentStart | `🤖 Delegating` | -| SubagentStop | `✅ Complete` | +| SubagentStop | `✅ Subagent finished` | | TeammateIdle | `⏸️ Teammate idle` | | ConfigChange | `⚙️ Configuring` | diff --git a/lib/hook_runner.sh b/lib/hook_runner.sh index 962ceb4..18bb2d8 100644 --- a/lib/hook_runner.sh +++ b/lib/hook_runner.sh @@ -118,7 +118,7 @@ _status_priority() { *"Awaiting approval"*) echo 88 ;; *"Input needed"*) echo 85 ;; *Building*|*Testing*|*Installing*) echo 80 ;; - *Pushing*|*Pulling*|*Merging*) echo 75 ;; + *Pushing*|*Pulling*|*Merging*|*Rebasing*|*Cherry-picking*) echo 75 ;; *Docker*|*Thinking*|*Delegating*) echo 70 ;; *Editing*) echo 65 ;; *"Tests passed"*|*Committed*|*Completed*|*"Subagent finished"*) echo 60 ;; @@ -479,6 +479,12 @@ case "${mode}" in _ccp_ctx="${CCP_CONTEXT_FILE}" _ccp_truncated="${_truncated}" _ccp_claude_bin="${CCP_CLAUDE_BIN:-}" + # Snapshot current context so the subprocess can detect if a newer + # prompt has arrived before it finishes (race guard). + _ccp_expected_ctx="" + if [[ -f "${CCP_CONTEXT_FILE}" ]]; then + _ccp_expected_ctx=$(< "${CCP_CONTEXT_FILE}") || _ccp_expected_ctx="" + fi ( set +e # CLAUDECODE: env var set by Claude Code itself. Must be cleared or @@ -499,7 +505,15 @@ case "${mode}" in 2>/dev/null | head -1 \ | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') || true - [[ -n "${summary}" ]] && atomic_write "${_ccp_ctx}" "${summary}" + # Race guard: if a newer prompt has updated the context file + # since we started, skip the write to avoid overwriting it. + if [[ -n "${summary}" ]]; then + _current_ctx="" + [[ -f "${_ccp_ctx}" ]] && _current_ctx=$(< "${_ccp_ctx}") || _current_ctx="" + if [[ "${_current_ctx}" == "${_ccp_expected_ctx}" ]]; then + atomic_write "${_ccp_ctx}" "${summary}" + fi + fi _dbg_event "ai_summary" "summary=${summary}" "strategy=stop-haiku" ) & disown 2>/dev/null || true diff --git a/tests/test-suite.sh b/tests/test-suite.sh index 6804cd9..25638c0 100755 --- a/tests/test-suite.sh +++ b/tests/test-suite.sh @@ -491,7 +491,7 @@ TMP_CONTEXT="${STATE_DIR}/test-sanitize-context.txt" # Pasted terminal content with venv prefix rm -f "${TMP_CONTEXT}" "${TMP_STATUS}" -result=$(printf '{"prompt":"(venv) brianruggieri@Flexias-MacBook-Pro candidate-eval %% fix the bug"}' \ +result=$(printf '%s' '{"prompt":"(venv) brianruggieri@Flexias-MacBook-Pro candidate-eval % fix the bug"}' \ | CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${TMP_CONTEXT}" \ bash "${LIB_DIR}/hook_runner.sh" user-prompt && cat "${TMP_CONTEXT}" 2>/dev/null || true) assert_not_contains "sanitize: venv prefix stripped" "(venv)" "${result}" @@ -499,14 +499,14 @@ assert_contains "sanitize: actual content preserved" "fix the bug" "${result}" # Pasted terminal with user@host prefix (no venv) rm -f "${TMP_CONTEXT}" "${TMP_STATUS}" -result=$(printf '{"prompt":"user@hostname project %% do the thing"}' \ +result=$(printf '%s' '{"prompt":"user@hostname project % do the thing"}' \ | CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${TMP_CONTEXT}" \ bash "${LIB_DIR}/hook_runner.sh" user-prompt && cat "${TMP_CONTEXT}" 2>/dev/null || true) assert_not_contains "sanitize: user@host stripped" "user@hostname" "${result}" # Shell prompt character stripped rm -f "${TMP_CONTEXT}" "${TMP_STATUS}" -result=$(printf '{"prompt":"$ npm test"}' \ +result=$(printf '%s' '{"prompt":"$ npm test"}' \ | CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${TMP_CONTEXT}" \ bash "${LIB_DIR}/hook_runner.sh" user-prompt && cat "${TMP_CONTEXT}" 2>/dev/null || true) assert_equals "sanitize: $ prompt stripped" "npm test" "${result}" @@ -552,8 +552,12 @@ printf '%s' '{"last_assistant_message":"I fixed the authentication flow by updat CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${HAIKU_CTX_1}" \ CCP_ENABLE_AI_CONTEXT=true CCP_CLAUDE_BIN="${MOCK_CLAUDE_DIR}/claude" \ bash "${LIB_DIR}/hook_runner.sh" stop -sleep 2 -result=$(cat "${HAIKU_CTX_1}" 2>/dev/null || true) +result="" +for _ in {1..20}; do + [[ -f "${HAIKU_CTX_1}" ]] && result=$(cat "${HAIKU_CTX_1}" 2>/dev/null || true) + [[ -n "${result}" ]] && break + sleep 0.25 +done assert_equals "stop-haiku: writes summary to context file" "Fixed Auth Login Flow" "${result}" # Also verify status was still cleared (existing behavior preserved) status_result=$(cat "${TMP_STATUS}" 2>/dev/null || true) @@ -567,7 +571,7 @@ printf '%s' '{"last_assistant_message":"I fixed the bug."}' \ CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${HAIKU_CTX_2}" \ CCP_ENABLE_AI_CONTEXT=false CCP_CLAUDE_BIN="${MOCK_CLAUDE_DIR}/claude" \ bash "${LIB_DIR}/hook_runner.sh" stop -sleep 2 +sleep 1 result=$(cat "${HAIKU_CTX_2}" 2>/dev/null || true) assert_empty "stop-haiku: skips when AI context disabled" "${result}" @@ -579,7 +583,7 @@ printf '%s' '{"last_assistant_message":""}' \ CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${HAIKU_CTX_3}" \ CCP_ENABLE_AI_CONTEXT=true CCP_CLAUDE_BIN="${MOCK_CLAUDE_DIR}/claude" \ bash "${LIB_DIR}/hook_runner.sh" stop -sleep 2 +sleep 1 result=$(cat "${HAIKU_CTX_3}" 2>/dev/null || true) assert_empty "stop-haiku: skips when last_assistant_message is empty" "${result}" @@ -591,7 +595,7 @@ printf '%s' '{"some_other_field":"hello"}' \ CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${HAIKU_CTX_4}" \ CCP_ENABLE_AI_CONTEXT=true CCP_CLAUDE_BIN="${MOCK_CLAUDE_DIR}/claude" \ bash "${LIB_DIR}/hook_runner.sh" stop -sleep 2 +sleep 1 result=$(cat "${HAIKU_CTX_4}" 2>/dev/null || true) assert_empty "stop-haiku: skips when last_assistant_message field missing" "${result}" @@ -621,8 +625,12 @@ printf '%s' "${json_long}" \ CCP_ENABLE_AI_CONTEXT=true CCP_CLAUDE_BIN="${MOCK_TRUNC_DIR}/claude" \ MOCK_TRUNC_DIR_SIDECAR="${MOCK_SIDECAR}" \ bash "${LIB_DIR}/hook_runner.sh" stop -sleep 2 -result=$(cat "${HAIKU_CTX_5}" 2>/dev/null || true) +result="" +for _ in {1..20}; do + [[ -f "${HAIKU_CTX_5}" ]] && result=$(cat "${HAIKU_CTX_5}" 2>/dev/null || true) + [[ -n "${result}" ]] && break + sleep 0.25 +done assert_equals "stop-haiku: truncated message produces summary" "Truncation Test Summary" "${result}" # Verify the captured input contains " ... " (truncation marker) if [[ -f "${MOCK_SIDECAR}" ]]; then @@ -656,8 +664,12 @@ printf '%s' '{"last_assistant_message":"I fixed the bug by adding a null check." CCP_STATUS_FILE="${TMP_STATUS}" CCP_CONTEXT_FILE="${HAIKU_CTX_6}" \ CCP_ENABLE_AI_CONTEXT=true CCP_CLAUDE_BIN="${MOCK_CLAUDECODE_DIR}/claude" \ bash "${LIB_DIR}/hook_runner.sh" stop -sleep 2 -result=$(cat "${HAIKU_CTX_6}" 2>/dev/null || true) +result="" +for _ in {1..20}; do + [[ -f "${HAIKU_CTX_6}" ]] && result=$(cat "${HAIKU_CTX_6}" 2>/dev/null || true) + [[ -n "${result}" ]] && break + sleep 0.25 +done assert_equals "stop-haiku: CLAUDECODE is unset in haiku subprocess" "CLAUDECODE_ABSENT" "${result}" rm -rf "${MOCK_CLAUDECODE_DIR}"