diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ae1e81..442813b 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 @@ -98,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 4d2f0cc..40deca7 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 @@ -284,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` @@ -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/bin/ccp b/bin/ccp index 460e2a5..0dfbb1e 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 "" @@ -554,21 +507,12 @@ 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 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}" 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/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..bf679e3 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) @@ -150,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` | @@ -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 | diff --git a/lib/hook_runner.sh b/lib/hook_runner.sh index 30a83ac..18bb2d8 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. @@ -117,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 ;; @@ -419,87 +420,109 @@ 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/^[a-zA-Z0-9._-]*[[:space:]]*[%$#>][[: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) || _cur="" + fi + if [[ -z "${_cur}" ]]; then + _dbg_event "status_dedup" "status=" + else + atomic_write "${CCP_STATUS_FILE}" "" fi - # Inline strategy: the summary is produced by the main Claude session - # (via --append-system-prompt injection) and captured in the post-tool - # handler. No separate API call needed β€” skip the Haiku subprocess. - if [[ "${CCP_AI_CONTEXT_STRATEGY:-haiku}" == "inline" ]]; then - _dbg_event "ai_context_pending" "strategy=inline" "waiting_for=post-tool CCP_TASK_SUMMARY marker" - _dbg "AI context strategy=inline β€” skipping haiku subprocess" + # 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 - # Background AI distillation β€” rewrites the context file with a proper - # 3-5 word semantic summary once the haiku call completes (~1-3s). - # CCP vars are unset inside the subshell so any hooks fired by the child - # claude process become no-ops (hook_runner exits early when vars unset). + 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_proj="${CCP_PROJECT_NAME:-}" - _ccp_raw="${raw_prompt}" + _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 - unset CCP_STATUS_FILE CCP_CONTEXT_FILE - - claude_bin=$(command -v claude 2>/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:]]*$//') + # 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 this developer task in 3-5 words. Title-case. No punctuation. No quotes. Reply with only the words, nothing else. Task: %s' \ - "${task_text}" \ + 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=haiku" - _dbg "distilled: ${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 ;; - stop) - [[ -z "${CCP_STATUS_FILE:-}" ]] && exit 0 - _dbg_event "status_cleared" "trigger=stop" - _dbg "clearing status file" - # Empty status signals idle to the monitor on the next heartbeat - _cur="" - if [[ -f "${CCP_STATUS_FILE}" ]]; then - _cur=$(cat "${CCP_STATUS_FILE}" 2>/dev/null) || _cur="" - fi - if [[ -z "${_cur}" ]]; then - _dbg_event "status_dedup" "status=" - else - atomic_write "${CCP_STATUS_FILE}" "" - fi - ;; - 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="" @@ -515,37 +538,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 @@ -557,6 +549,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/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 diff --git a/tests/test-suite.sh b/tests/test-suite.sh index bb4108e..25638c0 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 '%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}" +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 '%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 '%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}" + +rm -f "${TMP_STATUS}" "${TMP_CONTEXT}" + # stop handler clears status file printf 'πŸ§ͺ Testing' > "${TMP_STATUS}" echo '{}' \ @@ -491,6 +521,161 @@ 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 +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) +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 1 +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 1 +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 1 +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 +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 + 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 +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}" + +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"}' \ @@ -705,6 +890,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 \ @@ -985,161 +1188,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 "" @@ -1357,26 +1405,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}"