From: Aleksey Filippov Date: Fri, 30 Jan 2026 10:25:27 +0000 (+0300) Subject: fix(pre-push): использовать claude CLI и временные файлы для промптов X-Git-Url: https://gitweb.erp-flowers.ru/?a=commitdiff_plain;h=b68649e6c662b362e13effb8cbc1152b4585b377;p=erp24_rep%2Fyii-erp24%2F.git fix(pre-push): использовать claude CLI и временные файлы для промптов - Добавлена поддержка claude CLI (предпочтительно) в дополнение к cursor-agent - Промпты формируются во временных файлах вместо передачи через командную строку - Исправлена ошибка экранирования при передаче diff через аргументы - Модель по умолчанию изменена на алиас 'sonnet' Co-Authored-By: Claude Opus 4.5 --- diff --git a/erp24/scripts/ai/pre_push_multiagent_check.sh b/erp24/scripts/ai/pre_push_multiagent_check.sh index eabc7508..81a77d45 100755 --- a/erp24/scripts/ai/pre_push_multiagent_check.sh +++ b/erp24/scripts/ai/pre_push_multiagent_check.sh @@ -9,6 +9,17 @@ set -euo pipefail log() { echo "[pre-push-ai] $*"; } fail() { echo "[pre-push-ai] FAIL: $*" >&2; exit 1; } +# Массив для отслеживания временных файлов +declare -a TEMP_FILES=() + +# Очистка временных файлов при выходе +cleanup_temp_files() { + for f in "${TEMP_FILES[@]:-}"; do + [[ -f "${f}" ]] && rm -f "${f}" + done +} +trap cleanup_temp_files EXIT ERR SIGINT SIGTERM + repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" [[ -n "${repo_root}" ]] || fail "не удалось определить корень git-репозитория" cd "${repo_root}" @@ -79,14 +90,46 @@ if [[ "${diff_lines_count}" -gt "${max_lines}" ]]; then diff_text="$(printf "%s\n" "${diff_text}" | head -n "${max_lines}")"$'\n'"[TRUNCATED: original_lines=${diff_lines_count}, max_lines=${max_lines}]" fi -if ! command -v cursor-agent >/dev/null 2>&1; then - fail "не найден 'cursor-agent' в PATH. Установите/включите CLI для запуска агентов (см. erp24/agents/README.md) или используйте bypass: ERP24_PREPUSH_BYPASS=1 git push" +# Поддерживаем claude CLI (предпочтительно) или cursor-agent +AGENT_CMD="" +if command -v claude >/dev/null 2>&1; then + # Healthcheck: проверяем что claude CLI работает + if claude --version >/dev/null 2>&1; then + AGENT_CMD="claude" + else + log "предупреждение: claude CLI найден, но не работает, пробуем cursor-agent" + fi +fi +if [[ -z "${AGENT_CMD}" ]] && command -v cursor-agent >/dev/null 2>&1; then + AGENT_CMD="cursor-agent" +fi +if [[ -z "${AGENT_CMD}" ]]; then + fail "не найден рабочий 'claude' или 'cursor-agent' в PATH. Установите Claude Code CLI (https://claude.ai/code) или используйте bypass: ERP24_PREPUSH_BYPASS=1 git push" fi + +# Timeout для вызовов CLI (в секундах) +CLI_TIMEOUT="${ERP24_PREPUSH_TIMEOUT:-120}" if ! command -v python3 >/dev/null 2>&1; then fail "не найден 'python3' в PATH (нужен для валидации JSON-ответов агентов)" fi -model="${ERP24_PREPUSH_MODEL:-claude-sonnet-4-20250514}" +# Модель для проверки +# claude CLI: sonnet, opus, haiku, или полное имя (claude-sonnet-4-20250514) +# cursor-agent: sonnet-4, gpt-5 +# Используем алиас по умолчанию, но проверяем whitelist для безопасности +model="${ERP24_PREPUSH_MODEL:-sonnet}" +allowed_models=("sonnet" "opus" "haiku" "claude-sonnet-4-20250514" "claude-opus-4-5-20251101" "sonnet-4" "gpt-5") +model_valid=0 +for m in "${allowed_models[@]}"; do + if [[ "${model}" == "${m}" ]]; then + model_valid=1 + break + fi +done +if [[ "${model_valid}" -ne 1 ]]; then + log "предупреждение: модель '${model}' не в whitelist, используем 'sonnet'" + model="sonnet" +fi mode="fast" if [[ "${ERP24_PREPUSH_FULL:-}" == "1" ]]; then @@ -119,31 +162,82 @@ for persona in "${personas[@]}"; do review_out="${reviews_dir}/persona-${persona}.md" json_out="${reviews_dir}/persona-${persona}.json" - log "persona=${persona} → запуск агента" + log "persona=${persona} → запуск агента (${AGENT_CMD})" # В prompt мы просим вернуть JSON строго последней строкой. # Сохраняем полный вывод в .md и вытаскиваем JSON в отдельный файл. + + # Формируем промпт во временном файле (избегаем проблем с экранированием в командной строке) + # Используем mktemp с chmod 600 для безопасности (diff может содержать чувствительные данные) + # Санитизируем persona для защиты от path traversal + persona_safe="${persona//[^a-zA-Z0-9_-]/}" + prompt_tmp="$(mktemp "${artifact_dir}/prompt-${persona_safe}.XXXXXX.tmp")" + # ВАЖНО: добавляем в TEMP_FILES сразу после mktemp, до записи данных + TEMP_FILES+=("${prompt_tmp}") + chmod 600 "${prompt_tmp}" + + cat "${prompt_file}" > "${prompt_tmp}" + printf '\n\n## Changed files\n%s\n\n## Diff\n```diff\n%s\n```\n' "${changed_files}" "${diff_text}" >> "${prompt_tmp}" + + # Проверяем успешность записи + if [[ ! -s "${prompt_tmp}" ]]; then + fail "не удалось записать промпт во временный файл ${prompt_tmp}" + fi + set +e - agent_output="$( - cursor-agent -f --model "${model}" -p "$(cat "${prompt_file}")"$'\n\n'"## Changed files"$'\n'"${changed_files}"$'\n\n'"## Diff"$'\n'"```diff"$'\n'"${diff_text}"$'\n'"```" 2>&1 - )" + if [[ "${AGENT_CMD}" == "claude" ]]; then + # Claude Code CLI: передаём промпт через stdin с флагом -p (print mode) + agent_output="$( + timeout "${CLI_TIMEOUT}" claude -p --model "${model}" < "${prompt_tmp}" 2>&1 + )" + else + # cursor-agent: передаём промпт через stdin (избегаем command injection через подстановку команды) + agent_output="$( + timeout "${CLI_TIMEOUT}" cursor-agent -f --model "${model}" -p < "${prompt_tmp}" 2>&1 + )" + fi exit_code=$? set -e + # Проверка timeout (exit code 124) + if [[ "${exit_code}" -eq 124 ]]; then + log "prompt сохранён для отладки: ${prompt_tmp}" + fail "агент ${AGENT_CMD} (model=${model}, persona=${persona}) превысил timeout ${CLI_TIMEOUT}s. Возможно проблема с API. Используйте bypass: ERP24_PREPUSH_BYPASS=1 git push" + fi + printf "%s\n" "${agent_output}" > "${review_out}" if [[ "${exit_code}" -ne 0 ]]; then - fail "агент (persona=${persona}) завершился с кодом ${exit_code}. См. ${review_out}" + # Сохраняем prompt_tmp для отладки при ошибке + log "prompt сохранён для отладки: ${prompt_tmp}" + fail "агент ${AGENT_CMD} (model=${model}, persona=${persona}) завершился с кодом ${exit_code}. См. ${review_out}" + fi + + # Удаляем временный файл после успешной обработки (trap очистит при ошибке) + rm -f "${prompt_tmp}" + + # JSON ожидается в блоке ```json ... ``` или как последняя непустая строка. + # Сначала пробуем извлечь из markdown code block, затем как последнюю строку. + json_content="" + + # Попытка 1: извлечь из ```json ... ``` + json_content="$(printf "%s\n" "${agent_output}" | awk ' + /^```json/ { capture=1; next } + /^```$/ && capture { capture=0; next } + capture { print } + ')" + + # Попытка 2: если не нашли code block, берём последнюю непустую строку + if [[ -z "${json_content}" ]]; then + json_content="$(printf "%s\n" "${agent_output}" | awk 'NF{p=$0} END{print p}')" fi - # JSON ожидается последней непустой строкой. - json_line="$(printf "%s\n" "${agent_output}" | awk 'NF{p=$0} END{print p}')" - if [[ -z "${json_line}" ]]; then + if [[ -z "${json_content}" ]]; then fail "не удалось извлечь JSON из ответа агента (persona=${persona}). См. ${review_out}" fi # Валидируем JSON python'ом (jq может отсутствовать). - printf "%s" "${json_line}" | python3 -c 'import json,sys; data=json.load(sys.stdin); print(json.dumps(data, ensure_ascii=False, indent=2))' > "${json_out}" + printf "%s" "${json_content}" | python3 -c 'import json,sys; data=json.load(sys.stdin); print(json.dumps(data, ensure_ascii=False, indent=2))' > "${json_out}" # Проверяем critical=true is_critical="$(python3 - <