Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6b68529
Add structured freeform context payload + ExecutingTask/ready recovery
m-nash Apr 30, 2026
a765a5d
Distinguish sampling-classified custom instruction from sampling-unav…
m-nash May 5, 2026
b8bef58
Attach comment context for waiting-for-reply elicitations too
m-nash May 5, 2026
126fed6
Don't leak synthetic 'ready_unresolved' event name into user prompt
m-nash May 5, 2026
0cd0b53
Add 'Treat as comment replied' option to comment-flow recovery prompt
m-nash May 5, 2026
6bb39be
Don't assume comment_addressed for ambiguous push-detected recovery
m-nash May 5, 2026
5a8a976
Preserve waiting-comment context in ExecutingTask recovery prompt
m-nash May 5, 2026
97d597e
Handle 'skip' in all comment-flow sub-states in recovery prompt
m-nash May 5, 2026
42aad23
Fix InvalidOperationException leak in ClassifyFreeformAsync (PR #51 r…
m-nash May 6, 2026
51fd161
Route freeform Path B in waiting-comment context (PR #51 review)
m-nash May 6, 2026
decce7b
Extend (ready) recovery to ApplyingFix state (PR #51 review)
m-nash May 6, 2026
ba7a0c6
Preserve recovery snapshot in EmitComposeReplyAction (PR #51 review)
m-nash May 6, 2026
baaa2a9
Fix duplicate <summary> tag in TryClassifyFreeformViaSamplingAsync (P…
m-nash May 6, 2026
9649219
Hoist "stop" choice above per-flow routing (PR #51 review)
m-nash May 6, 2026
3240197
Make treat_as_replied_externally flow-aware (PR #51 review)
m-nash May 6, 2026
021e99a
Guard ProcessTaskComplete against waiting-thread stale indexing (PR #…
m-nash May 6, 2026
cede694
Add misattributes and rerequest to cspell dictionary
m-nash May 6, 2026
10991be
Rename 'Treat as comment replied' to 'Treat as replied externally' (P…
m-nash May 6, 2026
4de9393
Distinguish HEAD-refresh failure from no-push in recover_from_ready (…
m-nash May 6, 2026
9274c74
Use actual state in BuildRecoverFromReadyAction debug log (PR #51 rev…
m-nash May 6, 2026
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
2 changes: 2 additions & 0 deletions PrCopilot/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
"keepalive",
"LASTEXITCODE",
"MCPEXP",
"misattributes",
"nologo",
"nums",
"osascript",
"pushback",
"rerequest",
"slnx",
"unreplied",
"xunit"
Expand Down
60 changes: 60 additions & 0 deletions PrCopilot/src/PrCopilot/StateMachine/MonitorState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,34 @@ public class MonitorState
/// <summary>Reply text composed by the agent, to be posted by the server via the REST API.</summary>
public string? PendingReplyText { get; set; }

/// <summary>
/// HEAD SHA snapshotted when the state most recently entered <see cref="MonitorStateId.ExecutingTask"/>.
/// Used by the recovery path for <c>(ExecutingTask, "ready")</c> to detect whether the agent
/// pushed during the task — if HEAD has advanced, "ready" is reinterpreted as a completion event
/// (<c>comment_addressed</c> in comment flows, <c>push_completed</c> in CI flows).
/// Set via <see cref="EnterExecutingTask"/>; null/empty means no snapshot was captured.
/// </summary>
public string? HeadShaAtTaskStart { get; set; }

/// <summary>Transient: completion event set by sampling handler for MonitorFlowTools to feed back to state machine.</summary>
public string? SamplingCompletionEvent { get; set; }
/// <summary>Transient: completion event set by EmitComposeReplyAction for the sampling compose_reply handler.</summary>
public string? PendingCompletionEvent { get; set; }

/// <summary>
/// Expected completion event for the currently-executing task, when known unambiguously.
/// Read by the (ExecutingTask, "ready") recovery path: when HEAD has advanced and this
/// value matches a single completion event (e.g., "comment_addressed", "push_completed"),
/// the recovery dispatches that event automatically. When null (ambiguous — e.g., the
/// apply_recommendation task can complete as either "comment_addressed" for an implementation
/// push OR "comment_replied" for a proving-test push), the recovery falls back to the
/// flow-aware ask_user prompt so the user disambiguates instead of the engine guessing.
///
/// Reset to null on every <see cref="EnterExecutingTask"/> call; emitters that know
/// their task's unambiguous completion event must set this explicitly after that call.
/// </summary>
public string? ExecutingTaskExpectedCompletion { get; set; }

/// <summary>
/// When set, ProcessTaskComplete calls AdvanceAfterComment with this summary.
/// Used after posting a thread reply (auto_execute) when there's no subsequent resolve step.
Expand Down Expand Up @@ -130,4 +153,41 @@ public void ClearPendingCommentState()
/// Set when ReviewerReplied terminal state is detected.
/// </summary>
public CommentInfo? RepliedComment { get; set; }

/// <summary>
/// Transition to <see cref="MonitorStateId.ExecutingTask"/> and snapshot the current
/// <see cref="HeadSha"/> as <see cref="HeadShaAtTaskStart"/>. The snapshot lets the
/// recovery path detect post-push resumes (where the agent pushed during the task and
/// then re-entered via the post-push <c>pr_monitor_start</c> hook with event=ready
/// instead of calling the documented completion event).
///
/// Use this instead of assigning <c>CurrentState = MonitorStateId.ExecutingTask</c>
/// directly so the snapshot is never forgotten at a new task-entry site.
/// </summary>
public void EnterExecutingTask()
{
CurrentState = MonitorStateId.ExecutingTask;
SnapshotForRecovery();
}

/// <summary>
/// Transition to <see cref="MonitorStateId.ApplyingFix"/> and snapshot HEAD for
/// post-push recovery. The CI fix flow's apply_fix task tells the agent to push
/// before reporting push_completed; if a post-push hook fires event=ready instead,
/// the (ApplyingFix, "ready") recovery uses this snapshot to detect that the push
/// happened and re-dispatch as push_completed (rather than wiping CiFailureFlow).
/// </summary>
public void EnterApplyingFix()
{
CurrentState = MonitorStateId.ApplyingFix;
SnapshotForRecovery();
}

private void SnapshotForRecovery()
{
HeadShaAtTaskStart = HeadSha;
// Reset expected completion to "ambiguous" on every entry. Emitters that know their
// task's single completion event must set this AFTER calling EnterExecutingTask.
ExecutingTaskExpectedCompletion = null;
}
}
373 changes: 366 additions & 7 deletions PrCopilot/src/PrCopilot/StateMachine/MonitorTransitions.cs

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions PrCopilot/src/PrCopilot/Tools/CiFailureContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace PrCopilot.Tools;

/// <summary>
/// CI failure context, attached to <see cref="FreeformInterpretContext"/> when the
/// user is replying about a failed CI check.
/// </summary>
internal sealed class CiFailureContext
{
[JsonPropertyName("failedChecks")]
public List<FailedCheckContext> FailedChecks { get; set; } = [];
}
28 changes: 28 additions & 0 deletions PrCopilot/src/PrCopilot/Tools/CommentContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace PrCopilot.Tools;

/// <summary>
/// The active comment thread context, attached to <see cref="FreeformInterpretContext"/>
/// when the user is replying about a code review comment.
/// </summary>
internal sealed class CommentContext
{
[JsonPropertyName("author")]
public string Author { get; set; } = "";

[JsonPropertyName("filePath")]
public string FilePath { get; set; } = "";

[JsonPropertyName("line")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? Line { get; set; }

[JsonPropertyName("body")]
public string Body { get; set; } = "";

[JsonPropertyName("url")]
public string Url { get; set; } = "";
}
21 changes: 21 additions & 0 deletions PrCopilot/src/PrCopilot/Tools/ElicitationChoiceContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace PrCopilot.Tools;

/// <summary>
/// A single choice from an elicitation prompt. <see cref="Display"/> is what the
/// user saw; <see cref="Value"/> is the internal mapped value the agent should
/// pass back as <c>choice</c> for a Path A clean choice match.
/// </summary>
internal sealed class ElicitationChoiceContext
{
/// <summary>Human-readable choice label as shown to the user.</summary>
[JsonPropertyName("display")]
public string Display { get; set; } = "";

/// <summary>Internal mapped value (what would be passed back as <c>choice</c>).</summary>
[JsonPropertyName("value")]
public string Value { get; set; } = "";
}
19 changes: 19 additions & 0 deletions PrCopilot/src/PrCopilot/Tools/ElicitationContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace PrCopilot.Tools;

/// <summary>
/// The elicitation prompt that was shown to the user, captured for the
/// <see cref="FreeformInterpretContext"/> payload so the agent knows what
/// question the user was responding to.
/// </summary>
internal sealed class ElicitationContext
{
[JsonPropertyName("question")]
public string Question { get; set; } = "";

[JsonPropertyName("choices")]
public List<ElicitationChoiceContext> Choices { get; set; } = [];
}
20 changes: 20 additions & 0 deletions PrCopilot/src/PrCopilot/Tools/FailedCheckContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace PrCopilot.Tools;

/// <summary>
/// A single failed CI check entry in <see cref="CiFailureContext.FailedChecks"/>.
/// </summary>
internal sealed class FailedCheckContext
{
[JsonPropertyName("name")]
public string Name { get; set; } = "";

[JsonPropertyName("conclusion")]
public string Conclusion { get; set; } = "";

[JsonPropertyName("url")]
public string Url { get; set; } = "";
}
62 changes: 62 additions & 0 deletions PrCopilot/src/PrCopilot/Tools/FreeformInterpretContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace PrCopilot.Tools;

/// <summary>
/// Structured context attached to <c>interpret_freeform</c> execute actions.
/// The agent's chat history has no record of MCP elicitation — it happens out of band.
/// This payload tells the agent everything it needs to act on a freeform reply: what
/// the user was asked, what reply they gave, what comment/CI failure was being
/// discussed, and what server-side analysis or recommendation the user is reacting to.
/// Without this, the agent can mistake a legitimate elicitation reply for "stale state"
/// because the text doesn't match its chat memory of the user's last message.
/// </summary>
internal sealed class FreeformInterpretContext
{
/// <summary>"comment" | "ci_failure" | "generic"</summary>
[JsonPropertyName("flowType")]
public string FlowType { get; set; } = "generic";

/// <summary>
/// Why the freeform fallback was chosen. Distinguishes the two non-routing outcomes:
/// "sampling_classified_as_custom_instruction" — sampling ran successfully and decided
/// the user's text doesn't map to any of the available choices (it's a deliberate
/// custom instruction the agent should execute).
/// "sampling_unavailable" — sampling didn't run or failed (host capability missing,
/// invalid JSON, exception). The agent must interpret the text without a
/// classification hint.
/// The previous version always set this to "sampling_classified_as_custom_instruction"
/// regardless of which path was taken, which made the field useless to the agent.
/// </summary>
[JsonPropertyName("reason")]
public string Reason { get; set; } = "sampling_classified_as_custom_instruction";

/// <summary>The elicitation prompt that was shown to the user.</summary>
[JsonPropertyName("elicitation")]
public ElicitationContext Elicitation { get; set; } = new();

/// <summary>The user's authoritative reply (delivered via MCP elicitation, NOT chat).</summary>
[JsonPropertyName("userReply")]
public UserReplyContext UserReply { get; set; } = new();

/// <summary>The comment thread under discussion (when <c>FlowType</c> is "comment").</summary>
[JsonPropertyName("comment")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public CommentContext? Comment { get; set; }

/// <summary>Failed CI checks under discussion (when <c>FlowType</c> is "ci_failure").</summary>
[JsonPropertyName("ciFailure")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public CiFailureContext? CiFailure { get; set; }

/// <summary>
/// Server-side analysis the user is reacting to. The user's reply often references
/// this implicitly ("write a test that proves this", "fix it that way", "no, do X instead").
/// Without this, pronouns in the reply are unresolvable.
/// </summary>
[JsonPropertyName("serverAnalysis")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public ServerAnalysisContext? ServerAnalysis { get; set; }
}
Loading
Loading