Let the build-failure analyst push fixes it cannot suggest inline - #10607
Conversation
GitHub only accepts `suggestion` blocks on lines that are part of a PR's diff. Dependency-flow PRs break exactly that assumption: their diff is nothing but version files, so when a flowed package changes an API and previously-untouched call sites stop compiling, the analysis can only describe the fix and ask a human to commit it (see dotnet/arcade#17348 (comment)). Enable the `push-to-pull-request-branch` safe output on the automatic build-failure-analysis workflow so the agent can append the fix commit instead, with narrow guardrails: * `allowed-files` is an exclusive allowlist, so build infrastructure is out of reach; `protected-files` keeps its default blocked policy. * gh-aw refuses pushes to fork branches, which bounds `roles: all` to same-repo branches (dependency flow + write-access humans). * `max: 1` plus a `[build-failure-analysis]` commit-marker check in the agent playbook (Step 6b) stops a fail -> push -> ADO rebuild -> fail loop from converging on nothing. * Step 6b also requires the fix to be mechanical and provable from the compiler error; anything else stays a comment. Authoring the commit requires the PR's tree, so the agent job now checks out the PR head branch by name (`pr-checkout-ref`, resolved by the fetch job; forks fall back to `refs/pull/<n>/head`). A branch name is required because gh-aw derives the push target from `git rev-parse --abbrev-ref HEAD`. gh-aw's own base-branch config restore is gated on its built-in PR-checkout step, which never fires for `check_run`, so a second sparse checkout plus a `pre-agent-steps` step restores `.github`, `.agents` and the root instruction files from the base branch before the agent starts. No PR code is built or executed: the bash allowlist gains only scoped `git status/diff/log/rev-parse/add/commit`, and the push itself is performed by the safe-outputs job. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7154460-b0d2-4a80-98c8-6fcf6f5a904d
There was a problem hiding this comment.
Pull request overview
Enables automatic build-failure analysis to commit mechanical fixes when affected files are outside the PR diff.
Changes:
- Adds guarded PR-branch push support.
- Checks out the PR head and restores trusted agent configuration.
- Extends the analyst playbook and regenerates workflow locks.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
.github/workflows/shared/build-failure-analysis-shared.md |
Documents conditional fix pushing. |
.github/workflows/shared/build-failure-analysis-fetch.md |
Resolves the appropriate PR checkout ref. |
.github/workflows/build-failure-analysis.md |
Configures checkout, restoration, Git tools, and push output. |
.github/workflows/build-failure-analysis.lock.yml |
Regenerates the automatic workflow. |
.github/workflows/build-failure-analysis-command.lock.yml |
Regenerates shared fetch behavior. |
.github/agents/build-failure-analyst.agent.md |
Adds mechanical-fix commit instructions and guardrails. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Restore of the root agent-config files now consults the base-branch tree (git ls-tree) instead of trusting the sparse checkout to materialize them, so a sparse-checkout change can never turn "restore" into "delete". - Step 6b's loop guard now explicitly reads the PR's commit list through the GitHub tools; the PR-head checkout is depth-1, so git log cannot see it. - Correct the ools: comment: gh-aw itself widens the shell allowlist with git branch/checkout/merge/rm/switch when push-to-pull-request-branch is enabled. git push is still absent, and the playbook forbids the injected verbs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7154460-b0d2-4a80-98c8-6fcf6f5a904d
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (2)
.github/agents/build-failure-analyst.agent.md:205
- The loop guard must explicitly paginate the PR commits. The
get_commitsoperation is paginated, so checking only its default first page can miss the newest[build-failure-analysis]commit on a longer PR and allow another fail → push cycle. Require fetching every page (using the maximum page size and continuing until exhausted) before concluding that the marker is absent.
6. **Loop guard.** List the PR's commits **with the GitHub tools** (the `pull_requests` toolset) — not `git log`: the workspace is a shallow, depth-1 checkout and does not contain the branch's history. If any commit on the branch already carries the marker `[build-failure-analysis]` in its message, do **not** push again: a previous run already attempted a fix and the build still failed, which means the automated fix is not converging and a human must take over. Say exactly that in the summary comment instead.
.github/workflows/build-failure-analysis.md:119
- The generated
safe_outputsjob reuses this checkout expression, but that job only declaresactivation,agent, anddetectioninneeds; it does not directly needfetch-binlog. GitHub Actions therefore cannot resolveneeds.fetch-binlog.outputs.pr-checkout-refthere (see the generated lock around lines 2233 and 2312), so the write-capable checkout falls back to the event/default ref instead of the PR branch and the new push path cannot reliably apply its bundle. Ensurefetch-binlogis a direct dependency ofsafe_outputs(or relay this output through one of its direct dependencies), then regenerate the lock file.
- ref: ${{ needs.fetch-binlog.outputs.pr-checkout-ref }}
Address the second round of review feedback: - Enforce the one-attempt loop guard deterministically instead of relying on the agent obeying a prompt. The fetch job now scans the PR's commits for the `[build-failure-analysis]` marker and publishes a `push-blocked` output; when it is set, `pre-agent-steps` installs a `pre-commit` hook via `core.hooksPath` that refuses every commit. `git config` is not in the agent's tool allowlist, so the agent cannot undo it, and gh-aw builds its patch from agent commits - with no commit there is nothing to push. - Restore the complete set of agent-config paths from the base branch, not just a subset. The sparse-checkout and the restore loops now cover gh-aw's full folder list (.agents .antigravity .claude .codex .crush .gemini .github .opencode .pi) and root files (.crush.json .mcp.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc), closing the gap where a PR could ship an unrestored instruction file. - Document that `GH_AW_CI_TRIGGER_TOKEN` is an optional gh-aw magic secret that is deliberately left unset: it only exists to re-trigger GitHub Actions CI on the pushed commit, and our CI runs in Azure DevOps which rebuilds on its own. Unset, the token is empty and the extra empty-commit step is skipped, so no configuration is required. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7154460-b0d2-4a80-98c8-6fcf6f5a904d
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (4)
.github/agents/build-failure-analyst.agent.md:223
- Step 5 already tells the agent to post exactly one summary before reaching Step 6b, but this instruction asks it to post the summary again and claim the commit was pushed. That either creates duplicate summaries or leaves the first one unable to mention the push; moreover, the push safe output runs in a later job and may still fail. Defer the single
add_commentrequest until after deciding whether to queue a push, and describe the push as requested/pending rather than completed.
5. Post the Step 5 summary comment **as well**, stating near the top that a fix commit was pushed to the branch and still requires human review.
.github/workflows/shared/build-failure-analysis-fetch.md:313
- This loop check fails open. The fetch script has disabled
pipefailand suppressesgherrors, so a transient API or pagination failure makesgrepreturn no marker and leavesPUSH_BLOCKED=false; the next failed rebuild can then push again. Default to blocked and clear it only after the complete commit list was fetched successfully.
if gh api "repos/${GH_AW_REPO}/pulls/${PR_NUMBER}/commits" --paginate \
--jq '.[].commit.message' 2>/dev/null | grep -qF '[build-failure-analysis]'; then
.github/workflows/build-failure-analysis.md:213
- The refusing hook is not visible where
git commitruns. The compiled AWF command mounts only${RUNNER_TEMP}/gh-awplus the workspace (build-failure-analysis.lock.yml:990-991), while this hook lives in the sibling${RUNNER_TEMP}/gh-aw-refuse-commits; Git therefore sees a nonexistent hooks directory and proceeds. Even if mounted, the allowedgit commit:*permits--no-verify. Enforce the block in the trustedsafe_outputspath before processing a push rather than with an agent-side pre-commit hook.
HOOKS_DIR="${RUNNER_TEMP}/gh-aw-refuse-commits"
.github/agents/build-failure-analyst.agent.md:219
- The marker that activates the next-run guard is itself only an agent instruction. If the model omits or misspells it, the fetch job never recognizes the automated commit, so every Azure rebuild can make another attempt despite the claimed deterministic limit. Trusted workflow/handler code must append or validate the marker before allowing the push.
This issue also appears on line 223 of the same file.
[build-failure-analysis]
The previous loop guard did not actually work. It pointed `core.hooksPath`
at `${RUNNER_TEMP}/gh-aw-refuse-commits`, but the agent firewall only
mounts `${RUNNER_TEMP}/gh-aw` and the workspace, so git saw a nonexistent
hooks directory and committed anyway - and even with the directory
mounted, the allowed `git commit:*` permits `--no-verify`. Anything
installed inside the agent's sandbox is a suggestion, not a guarantee.
Move the decision entirely into trusted workflow code:
- The fetch job publishes `push-blocked`, and the automatic workflow's
job-level `if:` consumes it. When it is true the activation and agent
jobs never run, and gh-aw's `safe_outputs` job (conditioned on the
agent not being skipped) is skipped with them, so no push code path
remains. The `/analyze-build-failure` command workflow is comment-only
and ignores the output.
- Stamp the `[build-failure-analysis]` marker with `commit-title-suffix`
so gh-aw's push handler appends it while applying the patch. The guard
no longer depends on the model remembering, or correctly spelling, a
marker of its own; the playbook now tells the agent not to write one.
- Make the check fail closed and read the branch tip directly. It used
`gh api ... 2>/dev/null | grep -q`, so a transient API error silently
produced "not blocked"; it now defaults to blocked and only clears
after the tip commit was read successfully. The tip is resolved from
the pull request's `head.sha` rather than the ambient `HEAD_SHA`, which
can hold the check run's merge commit. Fork pull requests are exempt -
gh-aw refuses to push to them, so the guard must not suppress their
comment-only analysis.
- Scope the guard to the branch tip instead of the whole history, so a
pull request is not abandoned forever after one automated attempt: any
later commit by anyone restores full analysis.
Also fix a genuine defect introduced in the previous commit: two `echo`
statements had been joined onto a single line in the fetch job's outputs
block, which would have written a malformed `$GITHUB_OUTPUT` entry and
lost `ado-build-id`.
Finally, stop the playbook from asking for a second summary comment.
Step 5 already posts exactly one; it is now posted after the push is
requested and describes the commit as requested and pending rather than
completed, since the push happens in a later job and can still fail.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e7154460-b0d2-4a80-98c8-6fcf6f5a904d
|
Addressed the latest review round (including the suppressed comments, which contained the most important findings). The loop guard did not actually work, so it was rebuilt in trusted code.
The check fails closed now. It previously ran Scoped to the branch tip, not the whole history, so one automated attempt does not disable analysis on that PR forever: any later commit by anyone restores it. Real bug fixed. Two No duplicate summary comment. Step 5 posts exactly one; it is now posted after the push is requested and describes the commit as requested and pending rather than completed, since the push runs in a later job and can still be rejected. |
…port Review round 4: * `target: "*"` left the pull request number inside the agent's own tool call: gh-aw's handler resolves the *supplied* number and only then checks whether that pull request is a fork, so an injected agent could aim the push at an unrelated same-repo pull request and escape both the fork boundary and the loop guard. The target is now bound to `check_run.pull_requests[0].number`, a field GitHub fills in itself and that is never routed through the model. Because GitHub leaves it empty for fork-originated check runs, this also removes the push target entirely on fork pull requests while leaving the comment-only analysis untouched (verified against live check runs in all three repositories: same-repo pull requests report exactly one entry, fork ones report none). * `patch-format` defaults to `bundle`, and gh-aw's handler only rewrites commit titles on the `git am` path, so `commit-title-suffix` was silently a no-op: the `[build-failure-analysis]` marker would never have been stamped and the loop guard that keys off it would never have fired. Pinned to `patch-format: am`. * The agent-config restore checked out `github.event.repository.default_branch` rather than the pull request's own base branch, so a release-branch pull request was analysed with `main`'s playbook and agent instructions. It now uses the base ref the fetch job already resolves from the GitHub API, falling back to the default branch when that lookup returns nothing. * The fetch job now refuses the run outright when the check payload and the Azure Pipelines build name different pull requests, so the push target and the loop guard can never end up scoped to different branches. * Reworded a comment that claimed a deliberately non-fatal diagnostic listing would "fail loudly". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7154460-b0d2-4a80-98c8-6fcf6f5a904d
|
Round 4 of review feedback, applied to all three repos (arcade #17353, testfx #10607, sdk #55807). 1. The push target was model-controlled. With Because GitHub leaves that field empty for fork-originated check runs, this removes the push target entirely on fork PRs while leaving the comment-only analysis untouched. Verified against live check runs in all three repos: same-repo PRs report exactly one entry, fork PRs report none. 2. 3. The agent-config restore used the wrong branch. It checked out 4. Target/guard consistency. The fetch job prefers the ADO build's source branch when resolving the PR number. If that ever disagreed with the check payload, the guard and the push would be scoped to different branches, so the run is now refused outright in that case. 5. Reworded a comment that claimed a deliberately non-fatal diagnostic listing would "fail loudly". All locks recompiled |
There was a problem hiding this comment.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
Note
This error may be related to your runner configuration. You can now configure runners for Copilot code review separately from Copilot cloud agent by creating a copilot-code-review.yml file with your setup steps. Read the docs for details.
…bject
Review round 5: the guard matched `[build-failure-analysis]` anywhere in the
tip commit subject, which can false-positive on an unrelated commit that
happens to quote the marker.
It now requires the leading space the handler always inserts, so a subject
that merely opens with the marker no longer counts as an automated fix.
It is deliberately still not anchored to the end of the subject. gh-aw
appends the suffix by rewriting the first `Subject:` line of a
`git format-patch` mbox, and git folds subjects longer than ~72 characters
onto continuation lines, so `git am` reassembles the title with the marker in
the middle:
Fix CS1503 after [build-failure-analysis] Microsoft.DotNet.Product...
Verified end to end against real `git format-patch` output rewritten with the
handler's own regex: the resulting commit subject contains the marker but
does not end with it. Anchoring would silently miss those commits and let the
fail -> push -> rebuild -> fail loop run unbounded, which is the one direction
this guard must never fail in. A false positive only skips one analysis; a
false negative removes the brake.
The reasoning is now recorded next to the check so it is not "simplified"
later, and the analyst playbook asks for commit titles of 60 characters or
fewer so the marker lands at the end in practice.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e7154460-b0d2-4a80-98c8-6fcf6f5a904d
|
Round 5. One finding this time (arcade, suppressed): the loop guard matched Tightened, but deliberately not anchored — anchoring would have introduced a fail-open bug. gh-aw appends the suffix by rewriting the first So an end-of-subject anchor would silently miss exactly the commits the guard exists to catch, and the fail → push → rebuild → fail loop would run unbounded. The asymmetry matters: a false positive skips one analysis (and any later commit on the branch restores it), while a false negative removes the brake entirely. What did change:
Also confirmed there is nothing outstanding elsewhere: the two sdk threads ( All locks recompiled |
There was a problem hiding this comment.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
Note
This error may be related to your runner configuration. You can now configure runners for Copilot code review separately from Copilot cloud agent by creating a copilot-code-review.yml file with your setup steps. Read the docs for details.
# Conflicts: # .github/workflows/build-failure-analysis-command.lock.yml # .github/workflows/build-failure-analysis.lock.yml
There was a problem hiding this comment.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
Note
This error may be related to your runner configuration. You can now configure runners for Copilot code review separately from Copilot cloud agent by creating a copilot-code-review.yml file with your setup steps. Read the docs for details.
There was a problem hiding this comment.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
Note
This error may be related to your runner configuration. You can now configure runners for Copilot code review separately from Copilot cloud agent by creating a copilot-code-review.yml file with your setup steps. Read the docs for details.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (3)
.github/workflows/shared/build-failure-analysis-shared.md:76
- This condition contradicts the tip-scoped loop guard described in Step 6b and the PR: after any human commit, an earlier automated commit remains in history, so this wording can make the analyst refuse all future pushes even though the trusted guard intentionally re-enables them. Refer to the trusted guard allowing the current run, not to absence of a marker anywhere on the branch.
to the PR branch. Every one of its conditions must hold (same-repo PR,
`src/` or `test/` only, provable from the compiler error, no earlier
`[build-failure-analysis]` commit on the branch); otherwise just describe
.github/agents/build-failure-analyst.agent.md:237
- The summarized condition “no previous automated attempt” conflicts with Step 6b item 6, which permits another attempt once a later human commit becomes the branch tip. Since this file is the analyst's instruction set, the contradiction can permanently disable the escape hatch after its first use. Summarize the actual tip-scoped trusted guard instead.
- Do not propose fixes to files outside the PR diff in scan mode unless you are extremely confident — those changes are usually load-bearing across other projects. Prefer to explain the root cause in the comment and let a human apply the fix. The single exception is Step 6b, whose conditions (mechanical fix, provable from the compiler error, `src/` or `test/` only, same-repo PR, no previous automated attempt) exist precisely to keep that confidence bar high.
.github/workflows/build-failure-analysis.md:117
- This describes a full commit-list check, but the trusted guard reads only the current tip subject (
PR_TIP_SHA) and Step 6b explicitly tells the agent not to inspect history. Because this workflow body is runtime-imported into the prompt, the inaccurate comment can reinforce the conflicting “no earlier commit” instruction and prevent the intended re-enable after a human commit. Describe the tip-only API check instead.
# The PR-head checkout is intentionally shallow (`actions/checkout`'s default
# depth of 1): gh-aw bundles only the commits the agent creates on top of it, so
# no history is needed. Step 6b's loop guard therefore reads the PR's commit
# list through the GitHub tools rather than `git log`, which cannot see the
# branch's history here.
The guard has been tip-scoped since it moved into the trusted fetch job: it reads the subject of the PR's head commit and nothing else. Three places still described it as "no earlier [build-failure-analysis] commit on the branch" or "no previous automated attempt". Because the workflow body and the shared playbook are injected into the agent's prompt, that stricter phrasing could teach the analyst to refuse every push after the first one -- permanently disabling the escape hatch instead of re-enabling it once a human commit becomes the tip, which is what the guard actually does. Also correct the token comment. The push runs with `secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN`, so claiming it "is made with GITHUB_TOKEN -- which does not re-trigger GitHub Actions" understates the risk: a repository that sets GH_AW_GITHUB_TOKEN to a PAT or App token gets Actions re-runs as well, which is a further reason the guard is enforced in a trusted job rather than inferred from token behaviour. Drop `git log:*` from the agent's bash allowlist. The checkout is depth-1, so it can only ever show the tip, and the playbook already forbids using it to reconstruct the guard; removing it keeps the allowlist aligned with the documented workflow. Finally, make the commit-title guidance unambiguous. The 60-character budget applies to the title alone -- `commit_title_suffix` is appended by the handler to the already-generated patch, so it does not consume the budget. What does consume it is format-patch's own `Subject: [PATCH] ` prefix, which folds at 62 characters of title (measured, not estimated). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7154460-b0d2-4a80-98c8-6fcf6f5a904d
|
Round 6 — all six suppressed findings triaged; five applied, one respectfully disputed with evidence. Applied
Disputed — the 60-character commit-title guidance The suggestion was to lower the limit to ~47 because the ~25-char suffix is appended. That would be right if the suffix were part of the commit before patchContent.replace(/^Subject: (?:\[PATCH\] )?(.*)$/gm,
(match, title) => `Subject: [PATCH] ${title}${commitTitleSuffix}`);So folding is decided by
61 is the exact limit ( That said, the original sentence was ambiguous enough to invite exactly this reading, so I rewrote it to say explicitly that the budget is for the title alone, that the marker is appended afterwards to the generated patch, and that the real constraint is the Note this is also why the round-5 guard match is deliberately unanchored: if a title does fold, All three repos recompiled with their pinned compilers — 0 errors, 0 warnings. testfx pin audit passes (2274 refs / 49 files). |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (1)
.github/workflows/build-failure-analysis.md:433
- This allowlist makes the escape hatch reject the dependency-flow PRs it is intended to fix. In the pinned gh-aw v0.84.3 implementation, configuring
allowed-filestriggers a preflight over every file inorigin/<base>..<local tip>, not just the analyst's incremental commit. A dependency-flow branch already changes files such aseng/Version.Details.xmloutsidesrc//test/, sopush_to_pull_request_branchreturns an error before generating the patch. Please use a gh-aw version/configuration that applies this policy only to the incremental commit (while retaining apply-time enforcement), then regenerate the locks.
allowed-files:
- "src/**"
- "test/**"
Jan Krivanek (JanKrivanek)
left a comment
There was a problem hiding this comment.
Looks good as is.
If there are any future concerns with agent pushing directly into PRs - lets flip it to create PR targetting the underlying PR
Why
GitHub only accepts inline
suggestionblocks on lines that are part of a PR's diff. Dependency-flow PRs break exactly that assumption: their diff is nothing but version files, so when a flowed package changes an API and previously-untouched call sites stop compiling, the build-failure analyst can only describe the fix and ask a human to commit it.Concrete example that motivated this: dotnet/arcade#17348 (comment) — the analysis correctly identified two
CS1503s inPublishBuildToMaestro.cs(a flowedMicrosoft.DotNet.ProductConstructionService.Clientinserted a parameter beforeCancellationToken) but had to end with "the fix needs to be committed separately", because that file is not in the dependency-bump diff.What
Enable the
push-to-pull-request-branchsafe output on the automaticbuild-failure-analysisworkflow (the/analyze-build-failurecommand workflow is deliberately left comment-only), so the agent can append the fix commit when — and only when — a suggestion structurally cannot carry it.Guardrails
target: "*"gh-aw takes the pull request number out of the agent's own tool call and only then checks whether that pull request is a fork, so an injected agent could redirect the push at an unrelated same-repo PR. The target is instead bound tocheck_run.pull_requests[0].number, which GitHub fills in itself and which never passes through the model.check_run.pull_requestsempty for fork-originated check runs, so a fork PR has no push target at all (verified against live check runs in all three repos: same-repo PRs report exactly one entry, fork ones report none) — and gh-aw's handler refuses fork branches regardless. That boundsroles: allto same-repo branches: dependency flow plus people who already have write access. Pushes are append-only; force-push is impossible. The comment-only analysis still runs on fork PRs.allowed-filesis an exclusive allowlist, so build infrastructure (eng/,global.json,NuGet.config,.github/) is out of reach no matter what the agent produces;protected-fileskeeps its default blocked policy on top.max: 1plus a loop guard enforced entirely in trusted code.commit-title-suffixmakes gh-aw's push handler stamp[build-failure-analysis]onto the commit title as it applies the patch, so the marker is written by the handler and never by the model. This requirespatch-format: am: the defaultbundletransport never rewrites commit titles, which would have made the marker — and therefore the guard — a silent no-op. Before anything else runs, the fetch job reads the PR's head commit; if that tip is already such a commit and the build still fails, it publishespush-blocked, and the workflow's job-levelif:skips the activation and agent jobs — gh-aw'ssafe_outputsjob is itself conditioned on the agent not being skipped, so no push code path remains. The check fails closed (an unreadable commit blocks) and is scoped to the branch tip, so any later commit by anyone restores full analysis rather than abandoning the PR after one attempt. The push usesGITHUB_TOKEN, which does not re-trigger GitHub Actions — but the Azure DevOps GitHub app does rebuild, so this guard is the real brake on a fail → push → rebuild → fail loop.fallback-as-pull-request: falseso a diverged branch cannot silently become a surprise PR.Supporting changes
push-to-pull-request-branchships file contents, so a fix authored againstmainwould silently revert whatever else changed in that file. The fetch job now resolvespr-checkout-ref: the head branch name for same-repo PRs (gh-aw derives the push target fromgit rev-parse --abbrev-ref HEAD, so a detached SHA checkout would break bundle generation), falling back torefs/pull/<n>/headfor forks.check_run(that event carries nopull_requestpayload). Checking out the PR head therefore puts PR-controlled agent-config content in the workspace — where the analyst reads its own playbook. A second sparse checkout of the base branch plus apre-agent-stepsstep restores it before the agent starts, replaying gh-aw's inline sub-agent/skill restores afterwards. That checkout uses the PR's own base ref (resolved from the GitHub API by the fetch job), not the repository default branch, so a release-branch PR is analysed with the playbook that branch actually carries. The restore covers gh-aw's full protected set — folders.agents .antigravity .claude .codex .crush .gemini .github .opencode .piand root files.crush.json .mcp.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc— including paths that do not exist on the base branch, which are deleted rather than left as PR-authored content.GH_AW_CI_TRIGGER_TOKENappears in the lock because gh-aw wires that magic secret into every workflow using this safe output; it only exists to add an empty commit under a PAT so GitHub Actions CI re-runs on the pushed commit. It is deliberately left unset — our CI is Azure DevOps, which rebuilds on its own — and when unset the value is empty and gh-aw simply skips that step.git status/diff/log/rev-parse/add/commit— no interpreters, package managers or build tools. Note that gh-aw itself appendsgit branch/checkout/merge/rm/switchto the generated--allow-toollist wheneverpush-to-pull-request-branchis enabled; that cannot be narrowed from the workflow config, so the analyst playbook forbids those commands explicitly.git pushis not injected — the agent can never write to the remote, and the push is performed by thesafe_outputsjob from a bundle of the agent's local commits.Validation
--strict, clean.contents: writeis added only to thesafe_outputsandconclusionjobs, the agent job stays least-privilege.git bundleof the incremental commit range works from the shallow agent checkout.Residual risks