Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/AgentOs.Modules.Pipeline/Prompts/IssueWorkPrompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
19 changes: 19 additions & 0 deletions src/AgentOs.Web/Services/BoardTasks/BoardTaskService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,12 @@
}

/// <summary>The number of a pull request from its URL (…/pull/42).</summary>
// 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; }
Expand Down Expand Up @@ -541,6 +547,19 @@
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");
Expand Down
57 changes: 57 additions & 0 deletions tests/AgentOs.Tests/Web/FailedFollowUpTests.cs
Original file line number Diff line number Diff line change
@@ -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 <branch>" 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");
}
}
Loading