diff --git a/README.md b/README.md
index 7a0f85f..2c43aac 100644
--- a/README.md
+++ b/README.md
@@ -81,18 +81,16 @@ codeflash-cc-plugin/
├── .claude-plugin/
│ ├── marketplace.json # Marketplace manifest
│ └── plugin.json # Plugin manifest
-├── agents/
-│ └── optimizer.md # Background optimization agent
-├── commands/
-│ └── setup.md # /setup command for auto-allow permissions
├── hooks/
│ └── hooks.json # Stop hook for commit detection
├── scripts/
│ ├── find-venv.sh # Shared helper: find and activate a Python venv
│ └── suggest-optimize.sh # Detects Python/Java/JS/TS changes, suggests /optimize
├── skills/
-│ └── optimize/
-│ └── SKILL.md # /optimize slash command
+│ ├── optimize/
+│ │ └── SKILL.md # /optimize slash command
+│ └── setup/
+│ └── SKILL.md # /setup command for installation and configuration
└── README.md
```
diff --git a/scripts/oauth-login.sh b/scripts/oauth-login.sh
deleted file mode 100755
index 68e3aed..0000000
--- a/scripts/oauth-login.sh
+++ /dev/null
@@ -1,405 +0,0 @@
-#!/usr/bin/env bash
-# OAuth PKCE login flow for Codeflash.
-# Opens browser for authentication, exchanges code for API key,
-# and saves it to the user's shell RC file.
-#
-# Usage:
-# ./oauth-login.sh # Full browser flow
-# ./oauth-login.sh --exchange-code STATE_FILE CODE # Complete headless flow
-#
-# Exit codes:
-# 0 = success (API key saved)
-# 1 = error
-# 2 = headless mode (remote URL printed to stdout, state saved to temp file)
-
-set -euo pipefail
-
-CFWEBAPP_BASE_URL="https://app.codeflash.ai"
-TOKEN_URL="${CFWEBAPP_BASE_URL}/codeflash/auth/oauth/token"
-CLIENT_ID="cf-cli-app"
-TIMEOUT=180
-
-# --- Detect if a browser can be launched (matches codeflash's should_attempt_browser_launch) ---
-can_open_browser() {
- # CI/CD or non-interactive environments
- if [ -n "${CI:-}" ] || [ "${DEBIAN_FRONTEND:-}" = "noninteractive" ]; then
- return 1
- fi
-
- # Text-only browsers
- local browser_env="${BROWSER:-}"
- case "$browser_env" in
- www-browser|lynx|links|w3m|elinks|links2) return 1 ;;
- esac
-
- local is_ssh="false"
- if [ -n "${SSH_CONNECTION:-}" ]; then
- is_ssh="true"
- fi
-
- # Linux: require a display server
- if [ "$(uname -s)" = "Linux" ]; then
- if [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ] && [ -z "${MIR_SOCKET:-}" ]; then
- return 1
- fi
- fi
-
- # SSH on non-Linux (e.g. macOS remote) — no browser
- if [ "$is_ssh" = "true" ] && [ "$(uname -s)" != "Linux" ]; then
- return 1
- fi
-
- return 0
-}
-
-# --- Save API key to shell RC (matches codeflash's shell_utils.py logic) ---
-save_api_key() {
- local api_key="$1"
-
- if [ "${OS:-}" = "Windows_NT" ] || [[ "$(uname -s 2>/dev/null)" == MINGW* ]]; then
- # Windows: use dedicated codeflash env files (same as codeflash CLI)
- if [ -n "${PSMODULEPATH:-}" ]; then
- RC_FILE="$HOME/codeflash_env.ps1"
- EXPORT_LINE="\$env:CODEFLASH_API_KEY = \"${api_key}\""
- REMOVE_PATTERN='^\$env:CODEFLASH_API_KEY'
- else
- RC_FILE="$HOME/codeflash_env.bat"
- EXPORT_LINE="set CODEFLASH_API_KEY=\"${api_key}\""
- REMOVE_PATTERN='^set CODEFLASH_API_KEY='
- fi
- else
- # Unix: use shell RC file (same mapping as codeflash CLI)
- SHELL_NAME=$(basename "${SHELL:-/bin/bash}")
- case "$SHELL_NAME" in
- zsh) RC_FILE="$HOME/.zshrc" ;;
- ksh) RC_FILE="$HOME/.kshrc" ;;
- csh|tcsh) RC_FILE="$HOME/.cshrc" ;;
- dash) RC_FILE="$HOME/.profile" ;;
- *) RC_FILE="$HOME/.bashrc" ;;
- esac
- EXPORT_LINE="export CODEFLASH_API_KEY=\"${api_key}\""
- REMOVE_PATTERN='^export CODEFLASH_API_KEY='
- fi
-
- # Remove any existing CODEFLASH_API_KEY lines and append the new one
- if [ -f "$RC_FILE" ]; then
- CLEANED=$(grep -v "$REMOVE_PATTERN" "$RC_FILE" || true)
- printf '%s\n' "$CLEANED" > "$RC_FILE"
- fi
- printf '%s\n' "$EXPORT_LINE" >> "$RC_FILE"
-
- # Also export for the current session
- export CODEFLASH_API_KEY="$api_key"
-}
-
-# --- Exchange code for token and save ---
-exchange_and_save() {
- local auth_code="$1"
- local code_verifier="$2"
- local redirect_uri="$3"
-
- TOKEN_RESPONSE=$(curl -s -X POST "$TOKEN_URL" \
- -H "Content-Type: application/json" \
- -d "{
- \"grant_type\": \"authorization_code\",
- \"code\": \"${auth_code}\",
- \"code_verifier\": \"${code_verifier}\",
- \"redirect_uri\": \"${redirect_uri}\",
- \"client_id\": \"${CLIENT_ID}\"
- }")
-
- API_KEY=$(printf '%s' "$TOKEN_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('access_token',''))" 2>/dev/null || true)
-
- if [ -z "$API_KEY" ] || [[ ! "$API_KEY" == cf-* ]]; then
- exit 1
- fi
-
- save_api_key "$API_KEY"
-}
-
-# ===================================================================
-# Mode: --exchange-code STATE_FILE CODE
-# Complete a headless flow using a previously saved PKCE state file.
-# ===================================================================
-if [ "${1:-}" = "--exchange-code" ]; then
- STATE_FILE="${2:-}"
- MANUAL_CODE="${3:-}"
-
- if [ -z "$STATE_FILE" ] || [ -z "$MANUAL_CODE" ] || [ ! -f "$STATE_FILE" ]; then
- exit 1
- fi
-
- # Read saved state
- CODE_VERIFIER=$(python3 -c "import json; print(json.load(open('${STATE_FILE}')).get('code_verifier',''))" 2>/dev/null || true)
- REMOTE_REDIRECT=$(python3 -c "import json; print(json.load(open('${STATE_FILE}')).get('remote_redirect_uri',''))" 2>/dev/null || true)
-
- rm -f "$STATE_FILE"
-
- if [ -z "$CODE_VERIFIER" ] || [ -z "$REMOTE_REDIRECT" ]; then
- exit 1
- fi
-
- exchange_and_save "$MANUAL_CODE" "$CODE_VERIFIER" "$REMOTE_REDIRECT"
- exit 0
-fi
-
-# ===================================================================
-# Mode: Full OAuth flow (default)
-# ===================================================================
-
-# --- PKCE pair ---
-CODE_VERIFIER=$(openssl rand -base64 48 | tr -d '=/+\n' | head -c 64)
-CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')
-
-# --- State ---
-STATE=$(openssl rand -hex 16)
-
-# --- Find a free port ---
-PORT=$(python3 -c "import socket; s=socket.socket(); s.bind(('',0)); print(s.getsockname()[1]); s.close()")
-
-LOCAL_REDIRECT_URI="http://localhost:${PORT}/callback"
-REMOTE_REDIRECT_URI="${CFWEBAPP_BASE_URL}/codeflash/auth/callback"
-ENCODED_LOCAL_REDIRECT=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${LOCAL_REDIRECT_URI}'))")
-ENCODED_REMOTE_REDIRECT=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${REMOTE_REDIRECT_URI}'))")
-
-AUTH_PARAMS="response_type=code&client_id=${CLIENT_ID}&code_challenge=${CODE_CHALLENGE}&code_challenge_method=sha256&state=${STATE}"
-LOCAL_AUTH_URL="${CFWEBAPP_BASE_URL}/codeflash/auth?${AUTH_PARAMS}&redirect_uri=${ENCODED_LOCAL_REDIRECT}"
-REMOTE_AUTH_URL="${CFWEBAPP_BASE_URL}/codeflash/auth?${AUTH_PARAMS}&redirect_uri=${ENCODED_REMOTE_REDIRECT}"
-
-# --- Headless detection ---
-if ! can_open_browser; then
- # Save PKCE state so --exchange-code can complete the flow later
- HEADLESS_STATE_FILE=$(mktemp /tmp/codeflash-oauth-state-XXXXXX.json)
- python3 -c "
-import json
-json.dump({
- 'code_verifier': '${CODE_VERIFIER}',
- 'remote_redirect_uri': '${REMOTE_REDIRECT_URI}',
- 'state': '${STATE}'
-}, open('${HEADLESS_STATE_FILE}', 'w'))
-"
- # Output JSON for Claude to parse — this is the ONLY stdout in headless mode
- printf '{"headless":true,"url":"%s","state_file":"%s"}\n' "$REMOTE_AUTH_URL" "$HEADLESS_STATE_FILE"
- exit 2
-fi
-
-# --- Temp file for callback result ---
-RESULT_FILE=$(mktemp /tmp/codeflash-oauth-XXXXXX.json)
-trap 'rm -f "$RESULT_FILE"' EXIT
-
-# --- Start local callback server with Codeflash-styled pages ---
-export PORT STATE RESULT_FILE TIMEOUT
-python3 - "$PORT" "$STATE" "$RESULT_FILE" << 'PYEOF' &
-import http.server, urllib.parse, json, sys, threading
-
-port = int(sys.argv[1])
-state = sys.argv[2]
-result_file = sys.argv[3]
-
-STYLE = (
- ":root{"
- "--bg:hsl(0,0%,99%);--fg:hsl(222.2,84%,4.9%);--card:hsl(0,0%,100%);"
- "--card-fg:hsl(222.2,84%,4.9%);--primary:hsl(38,100%,63%);"
- "--muted-fg:hsl(41,8%,46%);--border:hsl(41,30%,90%);"
- "--destructive:hsl(0,84.2%,60.2%);--destructive-fg:#fff;"
- "--success:hsl(142,76%,36%)}"
- "html.dark{"
- "--bg:hsl(0,6%,5%);--fg:#fff;--card:hsl(0,3%,11%);"
- "--card-fg:#fff;--primary:hsl(38,100%,63%);"
- "--muted-fg:hsl(48,20%,65%);--border:hsl(48,20%,25%);"
- "--destructive:hsl(0,62.8%,30.6%)}"
- "*{margin:0;padding:0;box-sizing:border-box}"
- "body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;"
- "background:var(--bg);color:var(--fg);min-height:100vh;"
- "display:flex;align-items:center;justify-content:center;padding:20px;position:relative}"
- "body::before{content:'';position:fixed;inset:0;"
- "background:linear-gradient(to bottom,hsl(38,100%,63%,.1),hsl(38,100%,63%,.05),transparent);"
- "pointer-events:none;z-index:0}"
- "body::after{content:'';position:fixed;inset:0;"
- "background-image:linear-gradient(to right,rgba(128,128,128,.03) 1px,transparent 1px),"
- "linear-gradient(to bottom,rgba(128,128,128,.03) 1px,transparent 1px);"
- "background-size:24px 24px;pointer-events:none;z-index:0}"
- ".ctr{max-width:420px;width:100%;position:relative;z-index:1}"
- ".logo-ctr{display:flex;justify-content:center;margin-bottom:48px}"
- ".logo{height:40px;width:auto}"
- ".ll{display:block}.ld{display:none}"
- "html.dark .ll{display:none}html.dark .ld{display:block}"
- ".card{background:var(--card);color:var(--card-fg);border:1px solid var(--border);"
- "border-radius:16px;box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -2px rgba(0,0,0,.05);"
- "padding:48px;animation:fadeIn .3s ease-out forwards}"
- "@keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}"
- ".ic{width:48px;height:48px;background:hsl(38,100%,63%,.1);border-radius:12px;"
- "display:flex;align-items:center;justify-content:center;margin:0 auto 24px}"
- ".spinner{width:24px;height:24px;border:2px solid var(--border);"
- "border-top-color:var(--primary);border-radius:50%;animation:spin .8s linear infinite}"
- "@keyframes spin{to{transform:rotate(360deg)}}"
- ".si{width:64px;height:64px;background:hsl(142,76%,36%,.1);border-radius:12px;"
- "display:flex;align-items:center;justify-content:center;margin:0 auto 24px}"
- ".sc{width:32px;height:32px;stroke:hsl(142,76%,36%)}"
- "h1{font-size:24px;font-weight:600;margin:0 0 12px;color:var(--card-fg);text-align:center}"
- "p{color:var(--muted-fg);margin:0;font-size:14px;line-height:1.5;text-align:center}"
- ".eb{background:var(--destructive);color:var(--destructive-fg);"
- "padding:14px 18px;border-radius:8px;margin-top:24px;font-size:14px;line-height:1.5;text-align:center}"
- "@media(max-width:480px){.card{padding:32px 24px}h1{font-size:20px}.logo{height:32px}}"
-)
-
-LOGO = (
- '
'
- '

'
- '

'
- '
'
-)
-
-# Pre-built static HTML fragments (no user data)
-SUCCESS_FRAG = (
- 'Success!
'
- 'Authentication completed. You can close this window and return to your terminal.
'
-)
-
-ERR_ICON_FRAG = (
- ''
- '
Authentication Failed
'
-)
-
-
-def loading_page():
- # Inline the static fragments as JS string constants so the polling
- # script only inserts pre-defined trusted HTML, never user data.
- success_js = json.dumps(SUCCESS_FRAG)
- err_icon_js = json.dumps(ERR_ICON_FRAG)
- return (
- ''
- ''
- 'CodeFlash Authentication'
- f''
- ''
- ''
- f'{LOGO}'
- '
'
- '
'
- '
Authenticating
'
- '
Please wait while we verify your credentials...
'
- '
'
- ''
- )
-
-
-class H(http.server.BaseHTTPRequestHandler):
- server_version = "CFHTTP"
-
- def do_GET(self):
- p = urllib.parse.urlparse(self.path)
-
- if p.path == "/status":
- self.send_response(200)
- self.send_header("Content-type", "application/json")
- self.send_header("Access-Control-Allow-Origin", "*")
- self.end_headers()
- body = json.dumps({
- "success": self.server.token_error is None and self.server.auth_code is not None,
- "error": self.server.token_error,
- })
- self.wfile.write(body.encode())
- return
-
- if p.path != "/callback":
- self.send_response(404)
- self.end_headers()
- return
-
- params = urllib.parse.parse_qs(p.query)
- code = params.get("code", [None])[0]
- recv_state = params.get("state", [None])[0]
- error = params.get("error", [None])[0]
-
- if error or not code or recv_state != state:
- self.server.token_error = error or "state_mismatch"
- else:
- self.server.auth_code = code
- with open(result_file, "w") as f:
- json.dump({"code": code}, f)
-
- self.send_response(200)
- self.send_header("Content-type", "text/html")
- self.end_headers()
- self.wfile.write(loading_page().encode())
-
- def log_message(self, *a):
- pass
-
-
-httpd = http.server.HTTPServer(("localhost", port), H)
-httpd.auth_code = None
-httpd.token_error = None
-httpd.serve_forever()
-PYEOF
-SERVER_PID=$!
-
-# --- Open browser (macOS, Linux, WSL) ---
-if [[ "$(uname)" == "Darwin" ]]; then
- open "$LOCAL_AUTH_URL" 2>/dev/null || true
-elif command -v wslview >/dev/null 2>&1; then
- wslview "$LOCAL_AUTH_URL" 2>/dev/null || true
-elif command -v xdg-open >/dev/null 2>&1; then
- xdg-open "$LOCAL_AUTH_URL" 2>/dev/null || true
-elif command -v cmd.exe >/dev/null 2>&1; then
- cmd.exe /c start "" "$LOCAL_AUTH_URL" 2>/dev/null || true
-fi
-
-# --- Wait for callback ---
-WAITED=0
-while [ ! -s "$RESULT_FILE" ] && [ "$WAITED" -lt "$TIMEOUT" ]; do
- sleep 1
- WAITED=$((WAITED + 1))
- if ! kill -0 "$SERVER_PID" 2>/dev/null; then
- break
- fi
-done
-
-if [ ! -s "$RESULT_FILE" ]; then
- kill "$SERVER_PID" 2>/dev/null || true
- wait "$SERVER_PID" 2>/dev/null || true
- exit 1
-fi
-
-# --- Parse callback result ---
-AUTH_CODE=$(python3 -c "import json; print(json.load(open('${RESULT_FILE}')).get('code',''))" 2>/dev/null || true)
-
-if [ -z "$AUTH_CODE" ]; then
- kill "$SERVER_PID" 2>/dev/null || true
- wait "$SERVER_PID" 2>/dev/null || true
- exit 1
-fi
-
-# --- Exchange code for token ---
-exchange_and_save "$AUTH_CODE" "$CODE_VERIFIER" "$LOCAL_REDIRECT_URI"
-
-# Give the browser a moment to poll /status and see success, then shut down
-sleep 2
-kill "$SERVER_PID" 2>/dev/null || true
-wait "$SERVER_PID" 2>/dev/null || true
\ No newline at end of file
diff --git a/skills/optimize/SKILL.md b/skills/optimize/SKILL.md
index 10b808b..8b4c646 100644
--- a/skills/optimize/SKILL.md
+++ b/skills/optimize/SKILL.md
@@ -1,6 +1,6 @@
---
name: optimize
-description: Optimize Python, Java, JavaScript, or TypeScript code for performance using Codeflash
+description: Optimize code for performance using Codeflash
user-invocable: true
argument-hint: "[--file path] [--function name]"
allowed-tools: ["Bash"]
diff --git a/tests/helpers/setup.bash b/tests/helpers/setup.bash
index bc007b1..45f7a94 100644
--- a/tests/helpers/setup.bash
+++ b/tests/helpers/setup.bash
@@ -83,6 +83,17 @@ add_ts_commit() {
git -C "$REPO" commit -m "add $file" >/dev/null 2>&1
}
+add_java_commit() {
+ local file="${1:-Main.java}"
+ mkdir -p "$REPO/$(dirname "$file")"
+ echo "public class Main {}" > "$REPO/$file"
+ git -C "$REPO" add -A >/dev/null 2>&1
+ local ts
+ ts=$(future_timestamp)
+ GIT_COMMITTER_DATE="@$ts" GIT_AUTHOR_DATE="@$ts" \
+ git -C "$REPO" commit -m "add $file" >/dev/null 2>&1
+}
+
add_irrelevant_commit() {
local file="${1:-data.txt}"
echo "some data" > "$REPO/$file"
diff --git a/tests/test_suggest_optimize.bats b/tests/test_suggest_optimize.bats
index ccde695..3e9b2b7 100755
--- a/tests/test_suggest_optimize.bats
+++ b/tests/test_suggest_optimize.bats
@@ -192,81 +192,76 @@ setup() {
# Setup: pyproject.toml with [tool.codeflash]. Fake venv exists but does NOT
# contain a codeflash binary. VIRTUAL_ENV set.
-# Validates: When codeflash is configured but not installed in the venv, the
-# hook should prompt the user to install it before optimization can run.
-# Expected: Block with reason containing "pip install codeflash".
-@test "python: configured + codeflash NOT installed → install prompt" {
+# Validates: The simplified hook delegates all setup/install logic to the
+# codeflash:optimize skill. Regardless of installation state, the
+# hook should block and point to the skill.
+# Expected: Block with reason containing "codeflash:optimize".
+@test "python: configured + codeflash NOT installed → delegates to skill" {
add_python_commit
create_pyproject true
create_fake_venv "$REPO/.venv" false
run run_hook false "VIRTUAL_ENV=$REPO/.venv"
assert_block
- assert_reason_contains "pip install codeflash"
+ assert_reason_contains "codeflash:optimize"
}
# Setup: pyproject.toml exists but has NO [tool.codeflash] section. Fake venv
# with codeflash binary installed. VIRTUAL_ENV set.
-# Validates: When codeflash is installed but not configured, the hook should
-# instruct Claude to discover the project structure (module root,
-# tests folder) and write the [tool.codeflash] config section.
-# Expected: Block with reason containing "[tool.codeflash]" and "module-root"
-# (the config fields to be written).
-@test "python: NOT configured + codeflash installed → setup prompt" {
+# Validates: The hook no longer checks configuration state — it delegates to
+# the skill which handles setup fallback internally.
+# Expected: Block with reason containing "codeflash:optimize".
+@test "python: NOT configured + codeflash installed → delegates to skill" {
add_python_commit
create_pyproject false
create_fake_venv "$REPO/.venv"
run run_hook false "VIRTUAL_ENV=$REPO/.venv"
assert_block
- assert_reason_contains "[tool.codeflash]"
- assert_reason_contains "module-root"
+ assert_reason_contains "codeflash:optimize"
}
# Setup: pyproject.toml without [tool.codeflash]. Fake venv WITHOUT codeflash
# binary. VIRTUAL_ENV set.
-# Validates: When both installation and configuration are missing, the hook
-# should instruct Claude to both install codeflash and set up the
-# config. The install step is embedded within the setup instructions.
-# Expected: Block with reason containing both "[tool.codeflash]" (setup) and
-# "install codeflash" (installation).
-@test "python: NOT configured + NOT installed → setup + install prompt" {
+# Validates: Even when both installation and configuration are missing, the hook
+# delegates to the skill (which handles setup fallback).
+# Expected: Block with reason containing "codeflash:optimize".
+@test "python: NOT configured + NOT installed → delegates to skill" {
add_python_commit
create_pyproject false
create_fake_venv "$REPO/.venv" false
run run_hook false "VIRTUAL_ENV=$REPO/.venv"
assert_block
- assert_reason_contains "[tool.codeflash]"
- assert_reason_contains "install codeflash"
+ assert_reason_contains "codeflash:optimize"
}
# Setup: pyproject.toml with [tool.codeflash]. No .venv or venv directory
# anywhere. VIRTUAL_ENV not set.
-# Validates: Without a virtual environment, codeflash cannot run. The hook
-# reaches the no-venv code path. Currently the script exits non-zero
-# due to an unset SETUP_PERMISSIONS_STEP variable (known issue).
-# Expected: Exit non-zero (script bug: unset variable with set -u).
-@test "python: no venv + configured → exits non-zero (known script bug)" {
+# Validates: The simplified hook no longer inspects venv state — it just detects
+# Python file changes and delegates to the skill.
+# Expected: Block with reason containing "codeflash:optimize".
+@test "python: no venv + configured → delegates to skill" {
add_python_commit
create_pyproject true
# No venv created, no VIRTUAL_ENV set
run run_hook false
- [ "$status" -ne 0 ]
+ assert_block
+ assert_reason_contains "codeflash:optimize"
}
# Setup: pyproject.toml WITHOUT [tool.codeflash]. No venv anywhere.
# VIRTUAL_ENV not set.
-# Validates: Same no-venv code path. Currently the script exits non-zero
-# due to an unset SETUP_PERMISSIONS_STEP variable (known issue).
-# Expected: Exit non-zero (script bug: unset variable with set -u).
-@test "python: no venv + NOT configured → exits non-zero (known script bug)" {
+# Validates: Same as above — hook delegates regardless of project state.
+# Expected: Block with reason containing "codeflash:optimize".
+@test "python: no venv + NOT configured → delegates to skill" {
add_python_commit
create_pyproject false
run run_hook false
- [ "$status" -ne 0 ]
+ assert_block
+ assert_reason_contains "codeflash:optimize"
}
# Setup: pyproject.toml with [tool.codeflash]. Fake venv at $REPO/.venv with
@@ -311,55 +306,47 @@ setup() {
# Setup: package.json with "codeflash" key. Mock npx returns failure for
# `codeflash --version` (package not installed). One .js commit.
-# Validates: When codeflash is configured in package.json but the npm package
-# is not installed, the hook should prompt to install it as a dev
-# dependency before running.
-# Expected: Block with reason containing "npm install --save-dev codeflash".
-@test "js: configured + NOT installed → install prompt" {
+# Validates: The simplified hook delegates all setup/install logic to the
+# codeflash:optimize skill regardless of installation state.
+# Expected: Block with reason containing "codeflash:optimize".
+@test "js: configured + NOT installed → delegates to skill" {
add_js_commit
create_package_json true
setup_mock_npx false
run run_hook false "PATH=$MOCK_BIN:$PATH"
assert_block
- assert_reason_contains "npm install --save-dev codeflash"
+ assert_reason_contains "codeflash:optimize"
}
# Setup: package.json exists but has NO "codeflash" key. Mock npx returns
# success (codeflash is installed). One .js commit.
-# Validates: When codeflash is installed but not configured, the hook should
-# instruct Claude to discover project structure and add the "codeflash"
-# config key to package.json with moduleRoot, testsRoot, etc.
-# Expected: Block with reason containing "moduleRoot" and "testsRoot"
-# (the config fields to be added to package.json).
-@test "js: NOT configured + installed → setup prompt" {
+# Validates: The hook no longer checks configuration state — it delegates to
+# the skill which handles setup fallback internally.
+# Expected: Block with reason containing "codeflash:optimize".
+@test "js: NOT configured + installed → delegates to skill" {
add_js_commit
create_package_json false
setup_mock_npx true
run run_hook false "PATH=$MOCK_BIN:$PATH"
assert_block
- assert_reason_contains "moduleRoot"
- assert_reason_contains "testsRoot"
+ assert_reason_contains "codeflash:optimize"
}
# Setup: package.json without "codeflash" key. Mock npx fails (not installed).
# One .js commit.
-# Validates: When both installation and configuration are missing for a JS
-# project. The setup message should include an install step
-# ("npm install --save-dev codeflash") embedded within the broader
-# config setup instructions.
-# Expected: Block with reason containing both "moduleRoot" (setup) and
-# "npm install --save-dev codeflash" (installation).
-@test "js: NOT configured + NOT installed → setup + install prompt" {
+# Validates: Even when both installation and configuration are missing, the hook
+# delegates to the skill (which handles setup fallback).
+# Expected: Block with reason containing "codeflash:optimize".
+@test "js: NOT configured + NOT installed → delegates to skill" {
add_js_commit
create_package_json false
setup_mock_npx false
run run_hook false "PATH=$MOCK_BIN:$PATH"
assert_block
- assert_reason_contains "moduleRoot"
- assert_reason_contains "npm install --save-dev codeflash"
+ assert_reason_contains "codeflash:optimize"
}
# Setup: Configured package.json + mock npx. Commit touches a .ts file
@@ -393,6 +380,50 @@ setup() {
assert_reason_contains "codeflash:optimize"
}
+# ═══════════════════════════════════════════════════════════════════════════════
+# Java projects
+# ═══════════════════════════════════════════════════════════════════════════════
+
+# Setup: One .java file committed after session start.
+# Validates: The hook detects Java file changes and produces a block decision
+# pointing to the codeflash:optimize skill.
+# Expected: Block with reason containing "codeflash:optimize" and "Java".
+@test "java: .java commit triggers Java path" {
+ add_java_commit
+
+ run run_hook false
+ assert_block
+ assert_reason_contains "codeflash:optimize"
+ assert_reason_contains "Java"
+}
+
+# Setup: One .java file committed. Run the hook twice.
+# Validates: Dedup logic works for Java commits (same as Python/JS).
+# Expected: First run blocks; second run exits silently.
+@test "java: dedup prevents double trigger" {
+ add_java_commit
+
+ run run_hook false
+ assert_block
+
+ run run_hook false
+ assert_no_block
+}
+
+# Setup: One .java file committed. Verify the message does NOT mention JS/TS
+# or Python — it should be Java-specific.
+# Validates: Java commits route through the Java branch, not JS or Python.
+# Expected: Block reason contains "Java", not "JS/TS" or "Python".
+@test "java: message is Java-specific, not JS or Python" {
+ add_java_commit
+
+ run run_hook false
+ assert_block
+ assert_reason_contains "Java"
+ assert_reason_not_contains "JS/TS"
+ assert_reason_not_contains "Python"
+}
+
# ═══════════════════════════════════════════════════════════════════════════════
# Permissions — auto-allow instructions
# ═══════════════════════════════════════════════════════════════════════════════