diff --git a/.github/workflows/copilot-response.yml b/.github/workflows/copilot-response.yml
index fea99bb..3f6cd61 100644
--- a/.github/workflows/copilot-response.yml
+++ b/.github/workflows/copilot-response.yml
@@ -105,6 +105,8 @@ jobs:
if: needs.gate.outputs.verdict == 'proceed'
runs-on: ubuntu-latest
timeout-minutes: 60
+ outputs:
+ model: ${{ steps.pick.outputs.model }}
permissions:
contents: read
steps:
@@ -176,6 +178,12 @@ jobs:
cargo) cargo fetch ;;
*) true ;;
esac
+ # Resolved once: claude_args and the ai-meta marker both read it.
+ - name: Resolve model
+ id: pick
+ env:
+ MODEL: ${{ inputs.model }}
+ run: echo "model=$MODEL" >> "$GITHUB_OUTPUT"
- name: Claude — verify and address Copilot findings (unprivileged)
id: claude
continue-on-error: true
@@ -222,7 +230,7 @@ jobs:
verdict-deciding fact, backtick identifiers/files/versions, and
use `- ` bullets for anything with more than one item.
claude_args: >-
- --model ${{ inputs.model }}
+ --model ${{ steps.pick.outputs.model }}
--allowedTools "Edit,Write,Read,Glob,Grep,WebFetch,WebSearch,Bash(npm:*),Bash(npx:*),Bash(node:*),Bash(pnpm:*),Bash(corepack:*),Bash(yarn:*),Bash(uv:*),Bash(python:*),Bash(python3:*),Bash(pytest:*),Bash(pip:*),Bash(cargo:*),Bash(terraform:*),Bash(git:*),Bash(ls:*),Bash(cat:*),Bash(grep:*),Bash(rg:*),Bash(find:*),Bash(mkdir:*),Bash(sed:*),Bash(head:*),Bash(tail:*),Bash(wc:*)"
--json-schema '{
"type": "object",
@@ -332,11 +340,20 @@ jobs:
VP_OUTCOME: ${{ steps.vpush.outputs.outcome }}
VP_DETAIL: ${{ steps.vpush.outputs.detail }}
VP_SHA: ${{ steps.vpush.outputs.new-sha }}
+ MODEL: ${{ needs.respond.outputs.model }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
SUMMARY="$RUNNER_TEMP/in/summary.json"
+ # ai-meta / ai-outcome markers for the model-tier report (see
+ # dependabot-upgrade.yml); a pushed round's outcome is recorded
+ # by ci-watch once CI has judged the response commit.
+ META=""
+ marks() { # $1 = outcome, or empty for the model line only
+ printf '%s\n' "$META"
+ [ -z "$1" ] || printf '\n' "$GITHUB_RUN_ID" "$1"
+ }
MAP="$RUNNER_TEMP/in/findings.json"
[ -f "$SUMMARY" ] || { echo "no summary — nothing to do"; echo "pushed=false" >> "$GITHUB_OUTPUT"; exit 0; }
@@ -345,10 +362,12 @@ jobs:
case "${VP_OUTCOME:-}" in
pushed) pushed=true ;;
stale)
- gh pr comment "$PR_NUMBER" --repo "$REPO" --body "Copilot-response round: the branch moved while responding ($VP_DETAIL) — response discarded. Re-request a Copilot review to retry. ([run]($RUN_URL))" || true
+ gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$(marks discarded)
+ Copilot-response round: the branch moved while responding ($VP_DETAIL) — response discarded. Re-request a Copilot review to retry. ([run]($RUN_URL))" || true
echo "pushed=false" >> "$GITHUB_OUTPUT"; exit 0 ;;
violation|error|closed)
- gh pr comment "$PR_NUMBER" --repo "$REPO" --body "Copilot-response round could not push its changes ($VP_OUTCOME: $VP_DETAIL) — review the findings manually. ([run]($RUN_URL))" || true
+ gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$(marks blocked)
+ Copilot-response round could not push its changes ($VP_OUTCOME: $VP_DETAIL) — review the findings manually. ([run]($RUN_URL))" || true
echo "pushed=false" >> "$GITHUB_OUTPUT"; exit 0 ;;
esac
@@ -388,9 +407,14 @@ jobs:
+ "\n\n**Changes:**\n\n" + (if (.changes_made // "") == "" then "none" else .changes_made end)
+ "\n\n**Tests:**\n\n" + (.test_results // "-")
+ (if .error then "\n\n**Error:** " + .error else "" end)' "$SUMMARY" 2>/dev/null || echo "(no summary)")
- gh api "repos/$REPO/issues/$PR_NUMBER/comments" -f body="$disposition${VP_SHA:+
+ # No push means nothing for CI to judge: the round is complete.
+ if [ "$pushed" = "true" ]; then m=$(marks ""); else m=$(marks complete); fi
+ gh api "repos/$REPO/issues/$PR_NUMBER/comments" -f body="$m
+ $disposition${VP_SHA:+
+
+ Response pushed as \`$VP_SHA\`.} ([run]($RUN_URL))
- Response pushed as \`$VP_SHA\`.} ([run]($RUN_URL))" > /dev/null || echo "warning: response comment failed"
+ Model: \`$MODEL\`" > /dev/null || echo "warning: response comment failed"
echo "pushed=$pushed" >> "$GITHUB_OUTPUT"
ci-watch:
@@ -448,7 +472,11 @@ jobs:
run: |
set -euo pipefail
if [ "$OUTCOME" = "success" ] || [ "$OUTCOME" = "none" ]; then
- exit 0 # ai-complete stands
+ # ai-complete stands. Recorded for the model-tier report.
+ gh pr comment "$PR_NUMBER" --repo "$REPO" --body "
+ CI is green on the Copilot-response commit \`$SHA\` — \`ai-complete\` stands." || true
+ exit 0
fi
gh pr edit "$PR_NUMBER" --repo "$REPO" --remove-label ai-complete --add-label ai-ci-failed || true
- gh pr comment "$PR_NUMBER" --repo "$REPO" --body "CI failed on the Copilot-response commit \`$SHA\` — moved to \`ai-ci-failed\`. Review the failing checks." || true
+ gh pr comment "$PR_NUMBER" --repo "$REPO" --body "
+ CI failed on the Copilot-response commit \`$SHA\` — moved to \`ai-ci-failed\`. Review the failing checks." || true
diff --git a/.github/workflows/dependabot-upgrade.yml b/.github/workflows/dependabot-upgrade.yml
index f359370..0ed6390 100644
--- a/.github/workflows/dependabot-upgrade.yml
+++ b/.github/workflows/dependabot-upgrade.yml
@@ -253,6 +253,8 @@ jobs:
if: needs.gate.outputs.verdict == 'proceed' && needs.gate.outputs.moved != 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
+ outputs:
+ model: ${{ steps.pick.outputs.model }}
permissions:
contents: read
actions: read # rescue: deterministic failing-log prefetch (below)
@@ -340,6 +342,13 @@ jobs:
cargo) cargo fetch ;;
*) true ;;
esac
+ # The model is resolved ONCE here: claude_args and the status comment
+ # both read this output, so what is recorded is what actually ran.
+ - name: Resolve round-1 model
+ id: pick
+ env:
+ MODEL: ${{ inputs.model || (needs.gate.outputs.mode == 'upgrade' && 'fable' || 'opus') }}
+ run: echo "model=$MODEL" >> "$GITHUB_OUTPUT"
- name: Claude — analyse and adapt (unprivileged)
id: claude
continue-on-error: true
@@ -432,7 +441,7 @@ jobs:
paragraph longer than two sentences; structure must be
visible at a glance, like a good reviewer's summary.
claude_args: >-
- --model ${{ inputs.model || (needs.gate.outputs.mode == 'upgrade' && 'fable' || 'opus') }}
+ --model ${{ steps.pick.outputs.model }}
--allowedTools "Edit,Write,Read,Glob,Grep,WebFetch,WebSearch,Bash(npm:*),Bash(npx:*),Bash(node:*),Bash(pnpm:*),Bash(corepack:*),Bash(yarn:*),Bash(uv:*),Bash(python:*),Bash(python3:*),Bash(pytest:*),Bash(pip:*),Bash(cargo:*),Bash(terraform:*),Bash(git:*),Bash(ls:*),Bash(cat:*),Bash(grep:*),Bash(rg:*),Bash(find:*),Bash(mkdir:*),Bash(sed:*),Bash(head:*),Bash(tail:*),Bash(wc:*)"
--json-schema '{
"type": "object",
@@ -584,6 +593,7 @@ jobs:
id: decide
if: needs.gate.outputs.mode != 'suggest' || needs.gate.outputs.verdict != 'proceed'
env:
+ MODEL: ${{ needs.agent.outputs.model }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
@@ -604,11 +614,33 @@ jobs:
set -euo pipefail
SUMMARY="$RUNNER_TEMP/ai/summary.json"
+ # ai-meta / ai-outcome: machine-readable record of the model that
+ # ran and what this run concluded, keyed by run id, read by the
+ # automation repo's model-tier report. Both stay empty when the
+ # agent never ran (gate block), so no model is claimed.
+ META=""
+ if [ -n "${MODEL:-}" ]; then
+ META=""
+ fi
+ OUTC=""
+ outcome_line() { # $1 = outcome recorded by the next sticky write
+ OUTC="${META:+}"
+ }
sticky_comment() { # $1 = markdown body (marker prepended); never fatal
- body="${STICKY_MARKER}
- $1"
cid=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments?per_page=100" \
--jq '[.[] | select((.user.login == "'"$APP_SLUG"'[bot]") and (.body | startswith("'"$STICKY_MARKER"'")))][0].id' 2>/dev/null || true)
+ # A retry rewrites this comment: carry earlier runs' ai-meta /
+ # ai-outcome lines forward so their record survives.
+ hist=""
+ if [ -n "$cid" ] && [ "$cid" != "null" ]; then
+ hist=$(gh api "repos/$REPO/issues/comments/$cid" --jq .body 2>/dev/null | grep -E '^"
+ fi
+ marks() { # $1 = outcome -> marker lines to prefix a comment with
+ [ -z "$META" ] || printf '%s\n\n' "$META" "$GITHUB_RUN_ID" "$1"
+ }
park() {
gh pr edit "$PR_NUMBER" --repo "$REPO" --remove-label ai-queued 2>/dev/null || true
for other in ai-complete ai-ci-failed ai-suggested; do
gh pr edit "$PR_NUMBER" --repo "$REPO" --remove-label "$other" 2>/dev/null || true
done
gh pr edit "$PR_NUMBER" --repo "$REPO" --remove-label ai-upgrade --add-label ai-blocked || echo "warning: could not apply ai-blocked"
- gh pr comment "$PR_NUMBER" --repo "$REPO" --body "AI suggest round produced nothing usable: $1 ([run]($RUN_URL))" || true
+ gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$(marks blocked)
+ AI suggest round produced nothing usable: $1 ([run]($RUN_URL))" || true
echo "outcome=blocked" >> "$GITHUB_OUTPUT"
exit 0
}
@@ -734,7 +779,8 @@ jobs:
current=$(gh api "repos/$REPO/pulls/$PR_NUMBER" --jq .head.sha || echo "")
if [ "$current" != "$HEAD_SHA" ]; then
gh pr edit "$PR_NUMBER" --repo "$REPO" --remove-label ai-upgrade --add-label ai-queued || true
- gh pr comment "$PR_NUMBER" --repo "$REPO" --body "AI suggest round: the PR head moved while the analysis ran — re-queued for a fresh pass. ([run]($RUN_URL))" || true
+ gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$(marks requeued)
+ AI suggest round: the PR head moved while the analysis ran — re-queued for a fresh pass. ([run]($RUN_URL))" || true
echo "outcome=stale" >> "$GITHUB_OUTPUT"
exit 0
fi
@@ -766,6 +812,14 @@ jobs:
overview=$(jq -r '"## AI analysis: GitHub-Actions major bump\n\n**Per-action verdicts:**\n\n" + (.breaking_changes // "-")
+ "\n\n**Test/verification notes:**\n\n" + (.test_results // "-")
+ "\n\n**Scoped-apply policy:** suggestions tagged **[apply-safe]** (version/SHA bumps, matrix values, cron strings) are fine to apply from the UI after reading the diff. Anything tagged **[hand-apply]** (run: steps, uses: sources, triggers, permissions) must be retyped after real scrutiny — applying a suggestion commits it as YOU, and the changed workflow executes immediately with org secrets on the resulting push. The reasoning text is agent-written and never substitutes for reading the diff.\n\n**Residual risk:** " + (.residual_risk // "unknown")' "$S")
+ # Delivery failure parks below, so "suggested" only ever sits on
+ # a body that was actually posted.
+ if [ -n "$META" ]; then
+ overview="$(marks suggested)
+ $overview
+
+ Model: \`$MODEL\` (suggest mode)"
+ fi
count=$(jq '.suggestions // [] | length' "$S")
posted="verdicts-only"
if [ "$count" -gt 0 ]; then
@@ -1036,6 +1090,8 @@ jobs:
if: always() && needs.codex.outputs.report == 'true' && needs.codex.outputs.findings != '0'
runs-on: ubuntu-latest
timeout-minutes: 60
+ outputs:
+ model: ${{ steps.pick.outputs.model }}
permissions:
contents: read
steps:
@@ -1113,6 +1169,11 @@ jobs:
cargo) cargo fetch ;;
*) true ;;
esac
+ - name: Resolve round-2 model
+ id: pick
+ env:
+ MODEL: ${{ inputs.review-model || inputs.model || 'opus' }}
+ run: echo "model=$MODEL" >> "$GITHUB_OUTPUT"
- name: Claude — verify and address reviewer findings (unprivileged)
id: claude
continue-on-error: true
@@ -1176,7 +1237,7 @@ jobs:
bold the verdict-deciding fact, and backtick identifiers,
files, and versions.
claude_args: >-
- --model ${{ inputs.review-model || inputs.model || 'opus' }}
+ --model ${{ steps.pick.outputs.model }}
--allowedTools "Edit,Write,Read,Glob,Grep,WebFetch,WebSearch,Bash(npm:*),Bash(npx:*),Bash(node:*),Bash(pnpm:*),Bash(corepack:*),Bash(yarn:*),Bash(uv:*),Bash(python:*),Bash(python3:*),Bash(pytest:*),Bash(pip:*),Bash(cargo:*),Bash(terraform:*),Bash(git:*),Bash(ls:*),Bash(cat:*),Bash(grep:*),Bash(rg:*),Bash(find:*),Bash(mkdir:*),Bash(sed:*),Bash(head:*),Bash(tail:*),Bash(wc:*)"
--json-schema '{
"type": "object",
@@ -1311,6 +1372,8 @@ jobs:
- name: Post disposition
id: decide
env:
+ MODEL: ${{ needs.revise.outputs.model }}
+ MODE: ${{ needs.gate.outputs.mode }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
@@ -1328,8 +1391,11 @@ jobs:
# chronologically next to the Codex review it answers, instead of
# being buried inside the ever-growing status comment (same
# treatment as the Copilot-review response).
- post_comment() { # $1 = markdown body; never fatal
- gh api "repos/$REPO/issues/$PR_NUMBER/comments" -f body="$1" > /dev/null \
+ post_comment() { # $1 = markdown body; never fatal. ai-meta: see round 1
+ gh api "repos/$REPO/issues/$PR_NUMBER/comments" -f body="${MODEL:+
+ }$1${MODEL:+
+
+ Model: \`$MODEL\` (round 2)}" > /dev/null \
|| echo "warning: disposition comment failed"
}
@@ -1399,7 +1465,8 @@ jobs:
echo "outcome=pushed" >> "$GITHUB_OUTPUT" ;;
stale)
gh pr edit "$PR_NUMBER" --repo "$REPO" --remove-label ai-upgrade --add-label ai-queued || true
- post_comment "### Response to the Codex review
+ post_comment "
+ ### Response to the Codex review
The branch moved during the revision round ($VP_DETAIL) — revision discarded, PR re-queued. ([run]($RUN_URL))"
echo "outcome=stale" >> "$GITHUB_OUTPUT" ;;
@@ -1556,6 +1623,9 @@ jobs:
|| echo "review request for $r failed (non-fatal)"
done < <(printf '%s\n' "$MERGE_REVIEWERS" | tr ',' '\n' | tr -d ' ')
fi
+ # ai-outcome (model-tier report): this run's verdict, recorded now
+ # because the label can change later for unrelated reasons.
+ OUTC=""
cid=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments?per_page=100" \
--jq '[.[] | select((.user.login == "'"$APP_SLUG"'[bot]") and (.body | startswith("'"$STICKY_MARKER"'")))][0].id' 2>/dev/null || true)
if [ -n "$cid" ] && [ "$cid" != "null" ]; then
@@ -1563,9 +1633,11 @@ jobs:
gh api -X PATCH "repos/$REPO/issues/comments/$cid" -f body="$existing
---
+ $OUTC
**Final outcome:** $msg ([watch run]($RUN_URL))" > /dev/null
else
gh api "repos/$REPO/issues/$PR_NUMBER/comments" -f body="${STICKY_MARKER}
+ $OUTC
**Final outcome:** $msg ([watch run]($RUN_URL))" > /dev/null
fi
@@ -1648,7 +1720,8 @@ jobs:
gh pr edit "$PR_NUMBER" --repo "$REPO" --remove-label "$other" 2>/dev/null || true
done
gh pr edit "$PR_NUMBER" --repo "$REPO" --remove-label ai-upgrade --add-label ai-blocked || true
- gh pr comment "$PR_NUMBER" --repo "$REPO" --body "AI upgrade run ended without reaching a terminal state (crash or infrastructure failure) — parked as \`ai-blocked\`. Re-queue with \`ai-queued\` to retry. ([run]($RUN_URL))" || true
+ gh pr comment "$PR_NUMBER" --repo "$REPO" --body "
+ AI upgrade run ended without reaching a terminal state (crash or infrastructure failure) — parked as \`ai-blocked\`. Re-queue with \`ai-queued\` to retry. ([run]($RUN_URL))" || true
;;
*) exit 0 ;;
esac