From 27b7ecda61edd550732a968d2e5ce7013190ff41 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Sun, 11 Oct 2026 15:53:41 +0200 Subject: [PATCH] feat(pr-review): run the review through a listed Mistral agent The prompt, model and read-only Gitea connector now live in the Mistral agent "pr-review", kept in sync from git by mistral-agent-sync.sh (which also stamps the prompt hash into the agent description). mistral-review.sh starts a conversation with the agent id and a one-line task instead of sending the whole prompt each time, warns when the agent was not built from this prompt, and passes the model name on for the review header. The prompt gains mandatory checks (mergeable, base-branch state, linked issues, search before claiming nothing else does X) and an instruction to batch independent reads. Co-Authored-By: Claude Sonnet 5.5 --- ci/agents/pr-review/mistral-agent-sync.sh | 49 +++++++++++++++++++++++ ci/agents/pr-review/mistral-prompt.md | 15 +++++++ ci/agents/pr-review/mistral-review.sh | 44 +++++++++++--------- ci/agents/pr-review/run.sh | 7 ++-- 4 files changed, 92 insertions(+), 23 deletions(-) create mode 100644 ci/agents/pr-review/mistral-agent-sync.sh diff --git a/ci/agents/pr-review/mistral-agent-sync.sh b/ci/agents/pr-review/mistral-agent-sync.sh new file mode 100644 index 00000000..ae61d1e1 --- /dev/null +++ b/ci/agents/pr-review/mistral-agent-sync.sh @@ -0,0 +1,49 @@ +#!/bin/sh +# Create or update the listed Mistral agent "pr-review" from mistral-prompt.md, so the +# agent definition in Mistral Studio follows git. Prints the agent id. +# +# Run it after changing mistral-prompt.md, the model or the connector: +# MISTRAL_API_KEY=... sh ci/agents/pr-review/mistral-agent-sync.sh +# +# Env: MISTRAL_API_KEY (required), MISTRAL_AGENT_NAME, MISTRAL_MODEL, MISTRAL_CONNECTOR_ID, +# MISTRAL_API_BASE, MISTRAL_MAX_TOKENS. +set -eu +HERE="$(cd "$(dirname "$0")" && pwd)" +: "${MISTRAL_API_KEY:?MISTRAL_API_KEY is not set}" +NAME="${MISTRAL_AGENT_NAME:-pr-review}" +MODEL="${MISTRAL_MODEL:-mistral-large-4-0}" +CONNECTOR="${MISTRAL_CONNECTOR_ID:-01a12adf-cdc3-7562-b32b-b1fa96be244c}" +BASE="${MISTRAL_API_BASE:-https://api.mistral.ai/v1}" +MAX_TOKENS="${MISTRAL_MAX_TOKENS:-64000}" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +# The key goes to curl through a config file, not argv. +umask 077 +printf 'header = "Authorization: Bearer %s"\n' "$MISTRAL_API_KEY" > "$WORK/auth" +api() { # method path [datafile] -> body on stdout, fails on HTTP >= 300 + if [ -n "${3:-}" ]; then set -- "$1" "$2" -d @"$3"; else set -- "$1" "$2"; fi + m="$1"; p="$2"; shift 2 + code=$(curl -s -m 60 -K "$WORK/auth" -H 'Content-Type: application/json' -X "$m" "$@" \ + -o "$WORK/resp" -w '%{http_code}' "$BASE$p") + if [ "$code" -ge 300 ]; then echo "Mistral API $m $p -> HTTP $code: $(head -c 400 "$WORK/resp")" >&2; return 1; fi + cat "$WORK/resp" +} + +HASH="$(sha256sum "$HERE/mistral-prompt.md" | cut -c1-12)" +jq -n --arg name "$NAME" --arg model "$MODEL" --arg cid "$CONNECTOR" --argjson max "$MAX_TOKENS" \ + --arg desc "PR review, read-only Gitea connector. prompt sha256:$HASH" \ + --rawfile instructions "$HERE/mistral-prompt.md" \ + '{name: $name, model: $model, description: $desc, instructions: $instructions, + tools: [{type: "connector", connector_id: $cid}], + completion_args: {temperature: 0.1, max_tokens: $max}}' > "$WORK/body.json" + +ID="$(api GET "/agents?page_size=100" | jq -r --arg n "$NAME" '[.[]? | select(.name == $n) | .id][0] // empty')" +if [ -n "$ID" ]; then + api PATCH "/agents/$ID" "$WORK/body.json" > /dev/null + echo "updated $NAME (prompt sha256:$HASH)" >&2 +else + ID="$(api POST "/agents" "$WORK/body.json" | jq -r '.id')" + echo "created $NAME (prompt sha256:$HASH)" >&2 +fi +echo "$ID" diff --git a/ci/agents/pr-review/mistral-prompt.md b/ci/agents/pr-review/mistral-prompt.md index 9c9d0112..11c7d6f2 100644 --- a/ci/agents/pr-review/mistral-prompt.md +++ b/ci/agents/pr-review/mistral-prompt.md @@ -27,6 +27,21 @@ HARD RULES training data is not evidence for how current releases behave. 5. Before you write "no other code does X", search for it. +## Mandatory checks +Do these for every PR, in addition to reading the diff and the changed files: +1. Read the pull request itself for `mergeable`, its base branch and whether the base moved. Read each changed file + at the base branch tip as well as at the head, and list recent commits on the base branch that touch those files. + State plainly if the change is already on the base branch, superseded by another PR, or conflicts with it. That is + a blocking finding. +2. Read every issue the PR links to. +3. Search the repository for other code that does the same thing before you write that nothing else does. + +## Efficiency +Request all independent reads in the same step instead of one at a time (the diff, the changed files at head and +base, neighbouring files, README or CLAUDE.md together). Do not repeat a call whose result you already have. There is +no cap on tool calls; stop reading when every mandatory check is done and every suspicion is settled or listed as +unverified. + ## What to look for, in priority order 1. Correctness: real bugs, wrong conditionals, unhandled errors, race conditions, resource leaks, incorrect API usage, broken backward compatibility, functions whose return value is not what the caller assumes. diff --git a/ci/agents/pr-review/mistral-review.sh b/ci/agents/pr-review/mistral-review.sh index ae82a279..5148bbf0 100644 --- a/ci/agents/pr-review/mistral-review.sh +++ b/ci/agents/pr-review/mistral-review.sh @@ -1,45 +1,49 @@ #!/bin/sh -# Review one PR with a Mistral agent that reads the repository through the Gitea -# connector registered in Mistral Studio (read-only credential, see mistral-prompt.md). +# Review one PR with the listed Mistral agent "pr-review". The agent holds the prompt, the +# model and the read-only Gitea connector (kept in sync from git by mistral-agent-sync.sh). # # Usage: mistral-review.sh -# Writes /report.md. Exits non-zero, and leaves no report, when the request -# fails or the agent called a tool that is not read-only. +# Writes /report.md and /model.txt. Exits non-zero, and leaves no report, +# when the request fails or the agent called a tool that is not read-only. # -# Env: MISTRAL_API_KEY (required), MISTRAL_MODEL, MISTRAL_CONNECTOR_ID, MISTRAL_API, -# MISTRAL_MAX_TOKENS, MISTRAL_TIMEOUT. +# Env: MISTRAL_API_KEY (required), MISTRAL_AGENT_ID, MISTRAL_API_BASE, MISTRAL_TIMEOUT. set -eu D="$1"; SLUG="$2"; NUM="$3"; HEAD="$4" HERE="$(cd "$(dirname "$0")" && pwd)" : "${MISTRAL_API_KEY:?MISTRAL_API_KEY is not set}" -MODEL="${MISTRAL_MODEL:-mistral-large-4-0}" -CONNECTOR="${MISTRAL_CONNECTOR_ID:-01a12adf-cdc3-7562-b32b-b1fa96be244c}" -API="${MISTRAL_API:-https://api.mistral.ai/v1/conversations}" -MAX_TOKENS="${MISTRAL_MAX_TOKENS:-64000}" +AGENT="${MISTRAL_AGENT_ID:-ag_01a12b392002750b8ad2b4692a9de03c}" +BASE="${MISTRAL_API_BASE:-https://api.mistral.ai/v1}" TIMEOUT="${MISTRAL_TIMEOUT:-1500}" -rm -f "$D/report.md" -jq -n --arg model "$MODEL" --arg connector "$CONNECTOR" --argjson max "$MAX_TOKENS" \ - --arg task "Review pull request #$NUM of $SLUG. Head commit: $HEAD." \ - --rawfile prompt "$HERE/mistral-prompt.md" \ - '{model: $model, inputs: ($task + "\n\n" + $prompt), store: false, - tools: [{type: "connector", connector_id: $connector}], - completion_args: {temperature: 0.1, max_tokens: $max}}' > "$D/mistral-request.json" - +rm -f "$D/report.md" "$D/model.txt" # The key goes to curl through a config file, not argv. umask 077 printf 'header = "Authorization: Bearer %s"\n' "$MISTRAL_API_KEY" > "$D/.curl-auth" + +# Model name for the review header, and a warning when the agent's prompt is not the repo's. +if curl -sf -m 60 -K "$D/.curl-auth" -o "$D/agent.json" "$BASE/agents/$AGENT"; then + jq -r '.model' "$D/agent.json" > "$D/model.txt" + HASH="$(sha256sum "$HERE/mistral-prompt.md" | cut -c1-12)" + jq -e --arg h "sha256:$HASH" '.description | contains($h)' "$D/agent.json" > /dev/null \ + || echo "WARNING: agent $AGENT was not built from this mistral-prompt.md; run mistral-agent-sync.sh." +else + echo "WARNING: could not read agent $AGENT." +fi + +jq -n --arg agent "$AGENT" --arg task "Review pull request #$NUM of $SLUG. Head commit: $HEAD." \ + '{agent_id: $agent, inputs: $task, store: false}' > "$D/mistral-request.json" + code=000 for TRY in 1 2; do code=$(curl -s -m "$TIMEOUT" -K "$D/.curl-auth" -H 'Content-Type: application/json' \ - -d @"$D/mistral-request.json" -o "$D/mistral-response.json" -w '%{http_code}' "$API" || echo 000) + -d @"$D/mistral-request.json" -o "$D/mistral-response.json" -w '%{http_code}' "$BASE/conversations" || echo 000) [ "$code" = 200 ] && break echo "Mistral request attempt $TRY failed (HTTP $code): $(head -c 300 "$D/mistral-response.json" 2>&1 || true)" done rm -f "$D/.curl-auth" [ "$code" = 200 ] || exit 1 -echo "Mistral agent ($MODEL) made $(sh "$HERE/parse-response.sh" calls < "$D/mistral-response.json") tool calls." +echo "Mistral agent $AGENT made $(sh "$HERE/parse-response.sh" calls < "$D/mistral-response.json") tool calls." BAD=$(sh "$HERE/parse-response.sh" violations < "$D/mistral-response.json") if [ -n "$BAD" ]; then echo "The agent called tools that are not read-only; no review is posted:" diff --git a/ci/agents/pr-review/run.sh b/ci/agents/pr-review/run.sh index e9878440..55039d49 100755 --- a/ci/agents/pr-review/run.sh +++ b/ci/agents/pr-review/run.sh @@ -147,14 +147,15 @@ while IFS= read -r ITEM; do # --settings "$PROMPTS/settings.json" \ # -p ) || echo "(review agent exited non-zero)" - # --- Review with a Mistral agent (read-only Gitea connector, see mistral-review.sh) --- - MODEL="${MISTRAL_MODEL:-mistral-large-4-0}" - echo "Review model: $MODEL" + # --- Review with the listed Mistral agent "pr-review" (see mistral-review.sh) --- if ! sh "$PROMPTS/mistral-review.sh" "$D" "$SLUG" "$NUM" "$HEAD"; then echo "Mistral review failed for $SLUG#$NUM." REVIEW_FAILED=1 continue fi + MODEL="Mistral agent" + [ -s "$D/model.txt" ] && MODEL="$(cat "$D/model.txt")" + echo "Review model: $MODEL" if [ ! -s "$D/report.md" ]; then echo "Agent wrote no report for $SLUG#$NUM — skipping post." -- 2.54.0