]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
fix(pre-push): использовать claude CLI и временные файлы для промптов origin/feature_filippov_ERP-48J_scheduler_fix
authorAleksey Filippov <Aleksey.Filippov@erp-flowers.ru>
Fri, 30 Jan 2026 10:25:27 +0000 (13:25 +0300)
committerAleksey Filippov <Aleksey.Filippov@erp-flowers.ru>
Fri, 30 Jan 2026 13:52:42 +0000 (16:52 +0300)
- Добавлена поддержка claude CLI (предпочтительно) в дополнение к cursor-agent
- Промпты формируются во временных файлах вместо передачи через командную строку
- Исправлена ошибка экранирования при передаче diff через аргументы
- Модель по умолчанию изменена на алиас 'sonnet'

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
erp24/scripts/ai/pre_push_multiagent_check.sh

index eabc7508eec89a944f7488f7b2b1776cdab06fae..81a77d45ec3a4eff451ad7c34aee8876f99dab32 100755 (executable)
@@ -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 - <<PY