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
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,7 @@ await CommentOnTicketAsync(board, pat, sessions.First(s => s.Id == task.SessionI
var epic = tickets.FirstOrDefault(t => t.Number == close.IssueNumber);
if (epic is { RepoOwner: { } eo, RepoName: { } en } && await sp.GetRequiredService<IPrCreationService>().CloseAsync(
BoardTaskService.RepoDescriptor(board, tenant, eo, en, "main", pat), close.IssueNumber,
$"Closing — {close.Reason}\n\n<sub>Closed by the AgentOS.NET Orchestrator agent: every piece of this epic is delivered. Reopen it if anything is missing.</sub>\n",
ct).ConfigureAwait(false))
ClosedByAgent(close.Reason), ct).ConfigureAwait(false))
{
retried.Add(close.IssueNumber);
log.Add(tenant, board.Id, $"Closed epic #{close.IssueNumber}: {close.Reason}");
Expand All @@ -137,7 +136,12 @@ await CommentOnTicketAsync(board, pat, sessions.First(s => s.Id == task.SessionI
}
var task = stuck.First(s => s.IssueNumber == close.IssueNumber);
var session = sessions.First(s => s.Id == task.SessionId);
if (!await CloseTicketAsync(board, pat, session, PersonAnswers.Acknowledgement($"closing #{close.IssueNumber}: {close.Reason}"), sp, ct).ConfigureAwait(false))
// Thank the person only when they answered; a ticket the agent closes on its own (an epic with a stuck task,
// its scope delivered) says so, like an epic without one.
var closing = task.PersonAnswer is null
? ClosedByAgent(close.Reason)
: PersonAnswers.Acknowledgement($"closing #{close.IssueNumber}: {close.Reason}");
if (!await CloseTicketAsync(board, pat, session, closing, sp, ct).ConfigureAwait(false))
{
continue;
}
Expand All @@ -161,6 +165,10 @@ await CommentOnTicketAsync(board, pat, sessions.First(s => s.Id == task.SessionI
return picked;
}

/// <summary>The comment on a ticket the Orchestrator agent closes on its own (nobody was asked): its reason, and how to undo it.</summary>
internal static string ClosedByAgent(string reason) =>
$"Closing — {reason}\n\n<sub>Closed by the AgentOS.NET Orchestrator agent: everything this ticket covers is delivered. Reopen it if anything is missing.</sub>\n";

private readonly ConcurrentDictionary<(Guid Board, int Issue), (DateTimeOffset At, string Body)> _epicBodies = new();

// Open umbrella tickets (labelled for a person) with their text — read at most every 10 minutes each.
Expand Down
15 changes: 15 additions & 0 deletions tests/AgentOs.Tests/Web/NeedsPersonBlockTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,18 @@ public void HoldsAProposedChange_MergedFinishedOrInFlight_IsLeftAlone(string? st
ProjectOrchestrator.HoldsAProposedChange(status).ShouldBeFalse();
}
}

public sealed class ClosedByAgentTests
{
// VNChess #1 (2026-10-24): an epic with a stuck task was closed with "Posted … after your answer" although nobody
// had answered.
[Fact]
public void ClosedByAgent_SaysTheAgentClosedIt_NeverThanksForAnAnswer()
{
var text = AgentOs.Web.Services.Orchestration.ProjectOrchestrator.ClosedByAgent("every M1 item is merged.");

text.ShouldStartWith("Closing — every M1 item is merged.");
text.ShouldContain("Reopen it if anything is missing");
text.ShouldNotContain("your answer");
}
}
Loading