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}"
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
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 - <<PY