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."