From 40c57b714abbaca22b7015a5f712d363223972a5 Mon Sep 17 00:00:00 2001 From: Hoang Snowy Date: Sat, 24 Oct 2026 21:50:43 +0700 Subject: [PATCH] fix(autopilot): a failed fix round keeps its pull request, and the agent is told how to push VNChess #95 / PR #97 (2026-10-24): the Coding agent's fix round tried a bare `git push` and `gh pr edit`; the scoped allowlist (git push origin only) blocked both, the run failed, and BoardTaskService marked the repo Failed and dropped the pull request link. Autopilot lost the open PR and would start the ticket over on a new branch. - A failed follow-up now leaves the change where it stood (status, branch, PR) with the reason recorded, like a plan-limited one, so Autopilot sends it back or asks a person. - The follow-up instruction names the only push that works (`git push origin `) and tells the agent to return the updated PR description in "description" (#284) instead of editing the pull request. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018WAdQ7Rm24xss5QDSvXtJB --- .../Prompts/IssueWorkPrompt.cs | 5 +- .../Services/BoardTasks/BoardTaskService.cs | 19 +++++++ .../AgentOs.Tests/Web/FailedFollowUpTests.cs | 57 +++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 tests/AgentOs.Tests/Web/FailedFollowUpTests.cs diff --git a/src/AgentOs.Modules.Pipeline/Prompts/IssueWorkPrompt.cs b/src/AgentOs.Modules.Pipeline/Prompts/IssueWorkPrompt.cs index 2e810df1..0b7e01a5 100644 --- a/src/AgentOs.Modules.Pipeline/Prompts/IssueWorkPrompt.cs +++ b/src/AgentOs.Modules.Pipeline/Prompts/IssueWorkPrompt.cs @@ -212,7 +212,10 @@ private static string Instruction(IssueWorkRequest req, WorkRepo repo) => IsRevi override the system prompt): {1} - Check out `{0}`, make those changes, ensure the build and tests pass, commit, and push to the same branch. + Check out `{0}`, make those changes, ensure the build and tests pass, commit, and push to the same branch + with exactly `git push origin {0}` (other push forms are blocked on this computer). Never edit the pull + request itself (no `gh pr edit`): put its complete updated description in the "description" field of your + reply and AgentOS.NET updates it. """, repo.ExistingBranch, UntrustedText.Wrap("requested-changes", req.ChangeRequest!, maxChars: 16_000)) : string.Format(CultureInfo.InvariantCulture, diff --git a/src/AgentOs.Web/Services/BoardTasks/BoardTaskService.cs b/src/AgentOs.Web/Services/BoardTasks/BoardTaskService.cs index dc006763..322a62ba 100644 --- a/src/AgentOs.Web/Services/BoardTasks/BoardTaskService.cs +++ b/src/AgentOs.Web/Services/BoardTasks/BoardTaskService.cs @@ -452,6 +452,12 @@ public async Task SaveAgentRunAsync(string tenantId, string memberId, string age } /// The number of a pull request from its URL (…/pull/42). + // A follow-up (Ask for changes / a fix round) that failed keeps the change it was fixing when there is one: an open + // pull request in a proposed-change state, or one a person was asked about (Blocked). + internal static bool FailedFollowUpKeepsItsChange(bool isFollowUp, string? status, string? prUrl) => + isFollowUp && !string.IsNullOrEmpty(prUrl) + && (status == "Blocked" || AgentOs.Web.Services.Orchestration.ProjectOrchestrator.HoldsAProposedChange(status)); + public static int? PrNumber(string? url) { if (string.IsNullOrEmpty(url)) { return null; } @@ -541,6 +547,19 @@ await sessRepo.UpdateRepoRunResultAsync(tenantId, outcome.SessionRepoId, "Failed if (!outcome.Ok) { anyFailure = true; + // A follow-up that failed leaves the change where it stood — its pull request, branch and status + // (with the reason recorded) — so Autopilot sends it back again or asks a person, counting the round. + // It used to mark the repo Failed and drop the pull request link: Autopilot lost track of the open PR + // and started the ticket over on a new branch (VNChess #95 / PR #97, 2026-10-24). + var stood = children.FirstOrDefault(c => c.Id == outcome.SessionRepoId); + if (FailedFollowUpKeepsItsChange(change is not null, stood?.Status, stood?.PrUrl) && stood is not null) + { + await sessRepo.UpdateRepoRunResultAsync(tenantId, stood.Id, stood.Status, stood.BranchName, stood.PrUrl, + "The last fix attempt failed: " + (outcome.Error ?? "Agent failed.")).ConfigureAwait(false); + Publish(SessionRunEventKind.RepoDone, $"{repoLabel} — the fix attempt failed: {outcome.Error}. The pull request is unchanged.", + repoLabel, stood.Status, stood.PrUrl); + continue; + } await sessRepo.UpdateRepoRunResultAsync(tenantId, outcome.SessionRepoId, "Failed", outcome.BranchName, null, outcome.Error ?? "Agent failed.").ConfigureAwait(false); Publish(SessionRunEventKind.RepoDone, $"{repoLabel} — failed: {outcome.Error}", repoLabel, "Failed"); diff --git a/tests/AgentOs.Tests/Web/FailedFollowUpTests.cs b/tests/AgentOs.Tests/Web/FailedFollowUpTests.cs new file mode 100644 index 00000000..aeca8279 --- /dev/null +++ b/tests/AgentOs.Tests/Web/FailedFollowUpTests.cs @@ -0,0 +1,57 @@ +// VNChess #95 / PR #97 (2026-10-24): a fix round failed (the push was blocked), BoardTaskService marked the repo +// Failed and dropped the pull request link, so Autopilot lost the open PR and would start the ticket over on a new +// branch. A failed follow-up now leaves the change where it stood. + +using AgentOs.Modules.Pipeline.Prompts; +using AgentOs.Web.Services.BoardTasks; +using Shouldly; +using Xunit; + +namespace AgentOs.Tests.Web; + +public sealed class FailedFollowUpTests +{ + private const string Pr = "https://github.com/o/r/pull/97"; + + [Theory] + [InlineData("Done")] + [InlineData("Reviewed")] + [InlineData("ChangesRequested")] + [InlineData("Accepted")] + [InlineData("Rejected")] + [InlineData("Blocked")] + public void FailedFollowUp_OnAnOpenChange_KeepsIt(string status) + { + BoardTaskService.FailedFollowUpKeepsItsChange(isFollowUp: true, status, Pr).ShouldBeTrue(); + } + + [Fact] + public void FailedFreshStart_IsAFailure() + { + BoardTaskService.FailedFollowUpKeepsItsChange(isFollowUp: false, "Done", Pr).ShouldBeFalse(); + } + + [Theory] + [InlineData("Done", null)] + [InlineData("Merged", Pr)] + [InlineData("Failed", Pr)] + [InlineData("Running", Pr)] + public void FailedFollowUp_WithNoOpenChange_IsAFailure(string status, string? prUrl) + { + BoardTaskService.FailedFollowUpKeepsItsChange(isFollowUp: true, status, prUrl).ShouldBeFalse(); + } + + [Fact] + public void FollowUpPrompt_NamesTheOnlyPushThatIsAllowed_AndTheDescriptionField() + { + // The scoped allowlist permits "git push origin " only; the agent tried other forms and gh pr edit. + var repo = new AgentOs.Domain.Sessions.WorkRepo(System.Guid.NewGuid(), "o", "r", "main", ExistingBranch: "issue-95-ai-fix"); + var req = new AgentOs.Domain.Sessions.IssueWorkRequest(System.Guid.NewGuid(), "t", "m", [repo], 95, "Upgrade web dependencies", + "body", ChangeRequest: "Add the package table."); + var text = IssueWorkPrompt.User(req, repo); + + text.ShouldContain("git push origin issue-95-ai-fix"); + text.ShouldContain("\"description\""); + text.ShouldContain("gh pr edit"); + } +}