fix: auto-fix GitHub PR merge conflicts (#3418)
This commit is contained in:
@@ -3066,7 +3066,6 @@ func (s *Store) RefreshTaskCIFixCheckpoint(ctx context.Context, taskID, reposito
|
||||
last_fix_signature = excluded.last_fix_signature,
|
||||
last_fix_checkpoint_json = excluded.last_fix_checkpoint_json,
|
||||
last_fix_enqueued_at = NULL,
|
||||
last_fix_session_id = NULL,
|
||||
last_error = NULL,
|
||||
last_error_kind = '',
|
||||
updated_at = excluded.updated_at`),
|
||||
@@ -3230,8 +3229,9 @@ func (s *Store) RecordTaskCIMergeAttemptResult(
|
||||
// RecordTaskCIMergeQueueObservation persists an active queue attempt or a
|
||||
// conservative current-head baseline when a removal is observed first. The
|
||||
// baseline is written only when no queue attempt has been recorded yet, so a
|
||||
// later poll cannot move the guard to a newer head and accidentally requeue a
|
||||
// removal that belongs to an older attempt.
|
||||
// later poll cannot move the automatic requeue guard to a newer head. The
|
||||
// passive baseline is not auto-fix provenance; queue-removal auto-fix also
|
||||
// requires the durable merge-attempt signature.
|
||||
func (s *Store) RecordTaskCIMergeQueueObservation(ctx context.Context, observation TaskCIMergeQueueObservation) error {
|
||||
return s.mutateTaskCIPRState(ctx, observation.TaskID, func(ctx context.Context, tx *sqlx.Tx, now time.Time) error {
|
||||
baselineHead := ""
|
||||
|
||||
@@ -911,7 +911,7 @@ func TestStoreTaskCIPRState_MarkExhaustedAndResetOnReenable(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreTaskCIPRState_RefreshCheckpointClearsPromptDispatchMetadata(t *testing.T) {
|
||||
func TestStoreTaskCIPRState_RefreshCheckpointPreservesSessionPinning(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
enqueuedAt := time.Date(2026, 6, 18, 12, 0, 0, 0, time.UTC)
|
||||
@@ -938,8 +938,8 @@ func TestStoreTaskCIPRState_RefreshCheckpointClearsPromptDispatchMetadata(t *tes
|
||||
if state.LastFixSignature != "after" || state.LastFixCheckpointJSON != `{"failed_checks":[]}` {
|
||||
t.Fatalf("checkpoint was not refreshed: %+v", state)
|
||||
}
|
||||
if state.LastFixSessionID != nil {
|
||||
t.Fatalf("LastFixSessionID=%v, want nil", state.LastFixSessionID)
|
||||
if state.LastFixSessionID == nil || *state.LastFixSessionID != "session-1" {
|
||||
t.Fatalf("LastFixSessionID=%v, want session-1", state.LastFixSessionID)
|
||||
}
|
||||
if state.LastFixEnqueuedAt != nil {
|
||||
t.Fatalf("LastFixEnqueuedAt=%v, want nil", state.LastFixEnqueuedAt)
|
||||
|
||||
@@ -21,18 +21,19 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ciAutomationOrigin = "ci_automation"
|
||||
ciAutomationCheckSuccess = "success"
|
||||
ciAutomationCheckFailure = "failure"
|
||||
ciAutomationCheckError = "error"
|
||||
ciAutomationCheckCompleted = "completed"
|
||||
ciAutomationCheckPending = "pending"
|
||||
ciAutomationChangesRequested = "changes_requested"
|
||||
ciAutomationPRFeedbackToken = "{{pr.feedback}}"
|
||||
ciAutomationFixBlockWindow = time.Hour
|
||||
ciAutomationMaxFixRounds = github.TaskCIAutoFixMaxRounds
|
||||
ciAutomationKindAutoFix = "ci_auto_fix"
|
||||
ciAutomationStateEventSource = "ci_automation_state"
|
||||
ciAutomationOrigin = "ci_automation"
|
||||
ciAutomationCheckSuccess = "success"
|
||||
ciAutomationCheckFailure = "failure"
|
||||
ciAutomationCheckError = "error"
|
||||
ciAutomationCheckCompleted = "completed"
|
||||
ciAutomationCheckPending = "pending"
|
||||
ciAutomationChangesRequested = "changes_requested"
|
||||
ciAutomationMergeableStateDirty = "dirty"
|
||||
ciAutomationPRFeedbackToken = "{{pr.feedback}}"
|
||||
ciAutomationFixBlockWindow = time.Hour
|
||||
ciAutomationMaxFixRounds = github.TaskCIAutoFixMaxRounds
|
||||
ciAutomationKindAutoFix = "ci_auto_fix"
|
||||
ciAutomationStateEventSource = "ci_automation_state"
|
||||
)
|
||||
|
||||
var ciAutomationSnapshotFieldReplacer = strings.NewReplacer("\r", " ", "\n", " ", "<", "", ">", "")
|
||||
@@ -40,6 +41,7 @@ var ciAutomationSnapshotFieldReplacer = strings.NewReplacer("\r", " ", "\n", " "
|
||||
type ciAutomationCheckpoint struct {
|
||||
FailedChecks []ciAutomationCheckSnapshot `json:"failed_checks"`
|
||||
Comments []ciAutomationCommentSnapshot `json:"comments"`
|
||||
Conflict *ciAutomationConflictSnapshot `json:"conflict,omitempty"`
|
||||
QueueRemovals []ciAutomationQueueRemovalSnapshotData `json:"queue_removals,omitempty"`
|
||||
}
|
||||
|
||||
@@ -57,6 +59,13 @@ type ciAutomationCommentSnapshot struct {
|
||||
Line int `json:"line,omitempty"`
|
||||
}
|
||||
|
||||
type ciAutomationConflictSnapshot struct {
|
||||
MergeableState string `json:"mergeable_state"`
|
||||
HeadSHA string `json:"head_sha,omitempty"`
|
||||
HeadBranch string `json:"head_branch,omitempty"`
|
||||
BaseBranch string `json:"base_branch,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
ciAutomationQueueRemovalCauseChecksFailed = "checks_failed"
|
||||
ciAutomationQueueRemovalCauseChecksTimedOut = "checks_timed_out"
|
||||
@@ -300,8 +309,8 @@ func (s *Service) handleTaskPRCIAutoFix(ctx context.Context, pr *github.TaskPR,
|
||||
}
|
||||
feedback = ciAutomationFilterFeedbackForPR(pr, feedback)
|
||||
previous := decodeCIAutomationCheckpoint(state)
|
||||
delta := ciAutomationBuildDelta(feedback, previous)
|
||||
checkpoint := ciAutomationCurrentCheckpoint(feedback)
|
||||
delta := ciAutomationBuildDeltaForPR(pr, feedback, previous)
|
||||
checkpoint := ciAutomationCurrentCheckpointForPR(pr, feedback, previous)
|
||||
queueRemoval, queueRecovery := ciAutomationNewQueueRemoval(pr, state)
|
||||
if queueRecovery {
|
||||
delta.QueueRemovals = append(delta.QueueRemovals, *queueRemoval)
|
||||
@@ -382,7 +391,7 @@ func (s *Service) handleTaskPRCIAutoFixEmptyDelta(ctx context.Context, pr *githu
|
||||
if state != nil && state.LastFixSignature == signature && ciAutomationDuplicateFixAttemptBlocksMerge(state) {
|
||||
return true
|
||||
}
|
||||
if state != nil && len(previous.FailedChecks)+len(previous.Comments) > 0 {
|
||||
if state != nil && !ciAutomationCheckpointEmpty(previous) {
|
||||
if err := s.githubService.RefreshTaskCIFixCheckpoint(context.WithoutCancel(ctx), pr.TaskID, pr.RepositoryID, pr.PRNumber, signature, checkpointJSON); err != nil {
|
||||
s.logger.Debug("record CI auto-fix checkpoint refresh failed", zap.String("task_id", pr.TaskID), zap.Error(err))
|
||||
}
|
||||
@@ -718,6 +727,15 @@ func ciAutomationBuildDelta(feedback *github.PRFeedback, previous ciAutomationCh
|
||||
return delta
|
||||
}
|
||||
|
||||
func ciAutomationBuildDeltaForPR(pr *github.TaskPR, feedback *github.PRFeedback, previous ciAutomationCheckpoint) ciAutomationCheckpoint {
|
||||
delta := ciAutomationBuildDelta(feedback, previous)
|
||||
currentConflict := ciAutomationCurrentConflictSnapshot(pr, previous.Conflict)
|
||||
if !ciAutomationConflictSnapshotEqual(currentConflict, previous.Conflict) && currentConflict != nil {
|
||||
delta.Conflict = currentConflict
|
||||
}
|
||||
return delta
|
||||
}
|
||||
|
||||
func ciAutomationCheckConclusionNeedsFix(conclusion string) bool {
|
||||
return conclusion == ciAutomationCheckFailure ||
|
||||
conclusion == "timed_out" ||
|
||||
@@ -783,8 +801,41 @@ func ciAutomationCurrentCheckpoint(feedback *github.PRFeedback) ciAutomationChec
|
||||
return ciAutomationBuildDelta(feedback, ciAutomationCheckpoint{})
|
||||
}
|
||||
|
||||
func ciAutomationCurrentCheckpointForPR(pr *github.TaskPR, feedback *github.PRFeedback, previous ciAutomationCheckpoint) ciAutomationCheckpoint {
|
||||
checkpoint := ciAutomationCurrentCheckpoint(feedback)
|
||||
checkpoint.Conflict = ciAutomationCurrentConflictSnapshot(pr, previous.Conflict)
|
||||
return checkpoint
|
||||
}
|
||||
|
||||
func ciAutomationCheckpointEmpty(checkpoint ciAutomationCheckpoint) bool {
|
||||
return len(checkpoint.FailedChecks) == 0 && len(checkpoint.Comments) == 0 && len(checkpoint.QueueRemovals) == 0
|
||||
return len(checkpoint.FailedChecks) == 0 && len(checkpoint.Comments) == 0 && checkpoint.Conflict == nil && len(checkpoint.QueueRemovals) == 0
|
||||
}
|
||||
|
||||
func ciAutomationCurrentConflictSnapshot(pr *github.TaskPR, previous *ciAutomationConflictSnapshot) *ciAutomationConflictSnapshot {
|
||||
if pr == nil {
|
||||
return previous
|
||||
}
|
||||
mergeableState := strings.ToLower(strings.TrimSpace(pr.MergeableState))
|
||||
switch mergeableState {
|
||||
case ciAutomationMergeableStateDirty:
|
||||
return &ciAutomationConflictSnapshot{
|
||||
MergeableState: mergeableState,
|
||||
HeadSHA: strings.TrimSpace(pr.HeadSHA),
|
||||
HeadBranch: strings.TrimSpace(pr.HeadBranch),
|
||||
BaseBranch: strings.TrimSpace(pr.BaseBranch),
|
||||
}
|
||||
case "", ciAutomationQueueRemovalCauseUnknown:
|
||||
return previous
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func ciAutomationConflictSnapshotEqual(left, right *ciAutomationConflictSnapshot) bool {
|
||||
if left == nil || right == nil {
|
||||
return left == right
|
||||
}
|
||||
return *left == *right
|
||||
}
|
||||
|
||||
func ciAutomationNewQueueRemoval(pr *github.TaskPR, state *github.TaskCIPRAutomationState) (*ciAutomationQueueRemovalSnapshotData, bool) {
|
||||
@@ -803,11 +854,12 @@ func ciAutomationQueueRemovalBelongsToCurrentHead(pr *github.TaskPR, state *gith
|
||||
return false
|
||||
}
|
||||
attemptHead := strings.TrimSpace(state.LastQueueAttemptHeadSHA)
|
||||
// A removal-only observation may establish a current-head baseline, but it
|
||||
// does not prove that Kandev ever queued this head. Require the merge
|
||||
// signature written by an actual merge attempt or active-entry adoption so
|
||||
// an old removal cannot spend a repair round after automation is enabled.
|
||||
return attemptHead != "" && attemptHead == strings.TrimSpace(pr.HeadSHA) && strings.TrimSpace(state.LastMergeSignature) != ""
|
||||
// A removal-only observation can establish a passive same-head baseline,
|
||||
// but it does not prove which pull-request head produced the removal. Auto-
|
||||
// fix therefore requires the merge signature written by an attempted or
|
||||
// adopted queue entry in addition to the matching durable head.
|
||||
return attemptHead != "" && attemptHead == strings.TrimSpace(pr.HeadSHA) &&
|
||||
strings.TrimSpace(state.LastMergeSignature) != ""
|
||||
}
|
||||
|
||||
func ciAutomationHasActiveMergeQueueEntry(pr *github.TaskPR) bool {
|
||||
@@ -882,7 +934,7 @@ func ciAutomationQueueRemovalConflictEvidence(pr *github.TaskPR) bool {
|
||||
}
|
||||
mergeable := strings.ToLower(strings.TrimSpace(pr.MergeableState))
|
||||
queueState := strings.ToLower(strings.TrimSpace(pr.MergeQueueState))
|
||||
return mergeable == "dirty" || strings.Contains(mergeable, "conflict") ||
|
||||
return mergeable == ciAutomationMergeableStateDirty || strings.Contains(mergeable, "conflict") ||
|
||||
queueState == "unmergeable" || strings.Contains(queueState, "unmergeable")
|
||||
}
|
||||
|
||||
@@ -940,6 +992,17 @@ func ciAutomationRenderSnapshot(pr *github.TaskPR, delta ciAutomationCheckpoint)
|
||||
b.WriteString(fmt.Sprintf("\n- %s:%d %s", ciAutomationSanitizeSnapshotField(comment.Path), comment.Line, ciAutomationSanitizeSnapshotField(strings.TrimSpace(comment.Body))))
|
||||
}
|
||||
}
|
||||
if delta.Conflict != nil {
|
||||
b.WriteString("\n\nMerge conflict:")
|
||||
b.WriteString("\n- mergeability: ")
|
||||
b.WriteString(ciAutomationSanitizeSnapshotField(delta.Conflict.MergeableState))
|
||||
b.WriteString("; head branch: ")
|
||||
b.WriteString(ciAutomationSanitizeSnapshotField(delta.Conflict.HeadBranch))
|
||||
b.WriteString("; base branch: ")
|
||||
b.WriteString(ciAutomationSanitizeSnapshotField(delta.Conflict.BaseBranch))
|
||||
b.WriteString("; head commit: ")
|
||||
b.WriteString(ciAutomationSanitizeSnapshotField(delta.Conflict.HeadSHA))
|
||||
}
|
||||
if len(delta.QueueRemovals) > 0 {
|
||||
b.WriteString("\n\nMerge queue removal recovery:")
|
||||
for _, removal := range delta.QueueRemovals {
|
||||
|
||||
@@ -334,6 +334,65 @@ func TestCIAutomationFeedbackDeltaIncludesKnownFailingConclusions(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCIAutomationConflictSignal(t *testing.T) {
|
||||
pr := &github.TaskPR{
|
||||
MergeableState: "DIRTY",
|
||||
HeadSHA: "head-a",
|
||||
HeadBranch: "feature-a",
|
||||
BaseBranch: "main",
|
||||
}
|
||||
feedback := &github.PRFeedback{}
|
||||
|
||||
first := ciAutomationBuildDeltaForPR(pr, feedback, ciAutomationCheckpoint{})
|
||||
if first.Conflict == nil {
|
||||
t.Fatal("expected a dirty pull request to produce a conflict delta")
|
||||
}
|
||||
if first.Conflict.MergeableState != "dirty" || first.Conflict.HeadSHA != "head-a" ||
|
||||
first.Conflict.HeadBranch != "feature-a" || first.Conflict.BaseBranch != "main" {
|
||||
t.Fatalf("unexpected conflict snapshot: %+v", first.Conflict)
|
||||
}
|
||||
|
||||
current := ciAutomationCurrentCheckpointForPR(pr, feedback, ciAutomationCheckpoint{})
|
||||
if repeated := ciAutomationBuildDeltaForPR(pr, feedback, current); repeated.Conflict != nil {
|
||||
t.Fatalf("unchanged conflict produced a duplicate delta: %+v", repeated.Conflict)
|
||||
}
|
||||
|
||||
clean := *pr
|
||||
clean.MergeableState = "clean"
|
||||
if checkpoint := ciAutomationCurrentCheckpointForPR(&clean, feedback, current); checkpoint.Conflict != nil {
|
||||
t.Fatalf("authoritative clean state kept conflict checkpoint: %+v", checkpoint.Conflict)
|
||||
}
|
||||
|
||||
unknown := *pr
|
||||
unknown.MergeableState = "unknown"
|
||||
if checkpoint := ciAutomationCurrentCheckpointForPR(&unknown, feedback, current); checkpoint.Conflict == nil {
|
||||
t.Fatal("unknown mergeability state cleared the prior conflict checkpoint")
|
||||
}
|
||||
|
||||
changed := *pr
|
||||
changed.HeadSHA = "head-b"
|
||||
if delta := ciAutomationBuildDeltaForPR(&changed, feedback, current); delta.Conflict == nil {
|
||||
t.Fatal("changed conflict head did not re-arm conflict repair")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCIAutomationRenderSnapshotIncludesConflict(t *testing.T) {
|
||||
delta := ciAutomationCheckpoint{
|
||||
Conflict: &ciAutomationConflictSnapshot{
|
||||
MergeableState: "dirty",
|
||||
HeadSHA: "head-<a>",
|
||||
HeadBranch: "feature/<a>",
|
||||
BaseBranch: "main/<b>",
|
||||
},
|
||||
}
|
||||
|
||||
snapshot := ciAutomationRenderSnapshot(&github.TaskPR{Owner: "acme", Repo: "widget", PRNumber: 42}, delta)
|
||||
if !strings.Contains(snapshot, "Merge conflict") || !strings.Contains(snapshot, "feature/a") ||
|
||||
!strings.Contains(snapshot, "main/b") || !strings.Contains(snapshot, "head-a") {
|
||||
t.Fatalf("conflict snapshot missing expected details:\n%s", snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCIAutomationRenderSnapshotSanitizesUntrustedFields(t *testing.T) {
|
||||
delta := ciAutomationCheckpoint{
|
||||
FailedChecks: []ciAutomationCheckSnapshot{{Name: "unit\n<script>", Conclusion: "failure\r<p>", HTMLURL: "https://ci/<job>"}},
|
||||
@@ -441,6 +500,94 @@ func TestHandleTaskPRCIAutomationQueuesFixDedupesAndMerges(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleTaskPRCIAutomationAutoFixesConflict(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repo := setupTestRepo(t)
|
||||
seedTaskAndSession(t, repo, "task-1", "session-1", models.TaskSessionStateRunning)
|
||||
svc := createTestService(repo, newMockStepGetter(), newMockTaskRepo())
|
||||
pr := &github.TaskPR{
|
||||
TaskID: "task-1",
|
||||
RepositoryID: "repo-1",
|
||||
Owner: "acme",
|
||||
Repo: "widget",
|
||||
PRNumber: 42,
|
||||
State: "open",
|
||||
ChecksState: "success",
|
||||
MergeableState: "dirty",
|
||||
HeadSHA: "head-a",
|
||||
HeadBranch: "feature-a",
|
||||
BaseBranch: "main",
|
||||
}
|
||||
ghSvc := &mockGitHubService{
|
||||
ciOptionsResp: &github.TaskCIOptionsResponse{
|
||||
TaskID: "task-1",
|
||||
AutoFixEnabled: true,
|
||||
EffectiveAutoFixPrompt: "Fix the PR\n\n{{pr.feedback}}",
|
||||
},
|
||||
prFeedback: &github.PRFeedback{},
|
||||
}
|
||||
svc.SetGitHubService(ghSvc)
|
||||
|
||||
if err := svc.handleTaskPRCIAutomationWithRefresh(ctx, pr, false); err != nil {
|
||||
t.Fatalf("handle conflict auto-fix: %v", err)
|
||||
}
|
||||
status := svc.messageQueue.GetStatus(ctx, "session-1")
|
||||
if status.Count != 1 || !strings.Contains(status.Entries[0].Content, "Merge conflict") ||
|
||||
!strings.Contains(status.Entries[0].Content, "feature-a") || !strings.Contains(status.Entries[0].Content, "main") {
|
||||
t.Fatalf("expected one conflict repair prompt, got %+v", status)
|
||||
}
|
||||
if len(ghSvc.fixAttempts) != 1 || !ghSvc.fixAttempts[0].IncrementRound {
|
||||
t.Fatalf("expected one round-consuming conflict repair, got %+v", ghSvc.fixAttempts)
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
firstSessionID := "session-1"
|
||||
ghSvc.ciPRState = &github.TaskCIPRAutomationState{
|
||||
LastFixSignature: ghSvc.fixAttempts[0].Signature,
|
||||
LastFixCheckpointJSON: ghSvc.fixAttempts[0].CheckpointJSON,
|
||||
LastFixEnqueuedAt: &now,
|
||||
LastFixSessionID: &firstSessionID,
|
||||
}
|
||||
if err := svc.handleTaskPRCIAutomationWithRefresh(ctx, pr, false); err != nil {
|
||||
t.Fatalf("handle duplicate conflict auto-fix: %v", err)
|
||||
}
|
||||
if status := svc.messageQueue.GetStatus(ctx, "session-1"); status.Count != 1 || len(ghSvc.fixAttempts) != 1 {
|
||||
t.Fatalf("unchanged conflict was dispatched twice: status=%+v attempts=%+v", status, ghSvc.fixAttempts)
|
||||
}
|
||||
|
||||
pr.MergeableState = "clean"
|
||||
if err := svc.handleTaskPRCIAutomationWithRefresh(ctx, pr, false); err != nil {
|
||||
t.Fatalf("handle cleared conflict: %v", err)
|
||||
}
|
||||
if len(ghSvc.fixCheckpointRefresh) != 1 {
|
||||
t.Fatalf("expected conflict checkpoint clear without a round, got %+v", ghSvc.fixCheckpointRefresh)
|
||||
}
|
||||
if ghSvc.ciPRState.LastFixSessionID == nil || *ghSvc.ciPRState.LastFixSessionID != "session-1" {
|
||||
t.Fatalf("conflict clear lost the pinned session: %+v", ghSvc.ciPRState)
|
||||
}
|
||||
if ghSvc.ciPRState.LastFixEnqueuedAt != nil {
|
||||
t.Fatalf("conflict clear retained an active dispatch timestamp: %+v", ghSvc.ciPRState.LastFixEnqueuedAt)
|
||||
}
|
||||
|
||||
if err := repo.CreateTaskSession(ctx, &models.TaskSession{
|
||||
ID: "session-2", TaskID: "task-1", State: models.TaskSessionStateRunning,
|
||||
StartedAt: now, UpdatedAt: now,
|
||||
}); err != nil {
|
||||
t.Fatalf("create newer session: %v", err)
|
||||
}
|
||||
pr.HeadSHA = "head-b"
|
||||
pr.MergeableState = "dirty"
|
||||
if err := svc.handleTaskPRCIAutomationWithRefresh(ctx, pr, false); err != nil {
|
||||
t.Fatalf("handle reappeared conflict: %v", err)
|
||||
}
|
||||
if len(ghSvc.fixAttempts) != 2 || ghSvc.fixAttempts[1].SessionID != "session-1" {
|
||||
t.Fatalf("reappeared conflict moved sessions: attempts=%+v", ghSvc.fixAttempts)
|
||||
}
|
||||
if got := svc.messageQueue.GetStatus(ctx, "session-2"); got.Count != 0 {
|
||||
t.Fatalf("newer session received pinned conflict repair: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleTaskPRLifecycleDeliveryFailureRecordsAndPublishesError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repo := setupTestRepo(t)
|
||||
|
||||
+40
-3
@@ -96,7 +96,7 @@ func TestCIAutomationMergeQueueRecoveryClassifiesReviewedReasons(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCIAutomationMergeQueueRecoveryRequiresDurableAttemptEvidence(t *testing.T) {
|
||||
func TestCIAutomationMergeQueueRecoveryRequiresAttemptEvidence(t *testing.T) {
|
||||
pr := &github.TaskPR{HeadSHA: "head-a"}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -105,7 +105,7 @@ func TestCIAutomationMergeQueueRecoveryRequiresDurableAttemptEvidence(t *testing
|
||||
}{
|
||||
{name: "no state", state: nil, want: false},
|
||||
{
|
||||
name: "passive baseline has no attempt signature",
|
||||
name: "removal-only baseline fails closed",
|
||||
state: &github.TaskCIPRAutomationState{LastQueueAttemptHeadSHA: "head-a"},
|
||||
want: false,
|
||||
},
|
||||
@@ -132,6 +132,42 @@ func TestCIAutomationMergeQueueRecoveryRequiresDurableAttemptEvidence(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
func TestCIAutomationMergeQueueRecoveryRejectsRemovalObservedAfterHeadChange(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repo := setupTestRepo(t)
|
||||
seedTaskAndSession(t, repo, "task-1", "session-1", models.TaskSessionStateRunning)
|
||||
svc := createTestService(repo, newMockStepGetter(), newMockTaskRepo())
|
||||
ghSvc := &mergeQueueRecoveryGitHubService{mockGitHubService: &mockGitHubService{
|
||||
ciOptionsResp: &github.TaskCIOptionsResponse{
|
||||
TaskID: "task-1", AutoFixEnabled: true, EffectiveAutoFixPrompt: "Repair the PR\n\n{{pr.feedback}}",
|
||||
},
|
||||
prFeedback: &github.PRFeedback{},
|
||||
}}
|
||||
svc.SetGitHubService(ghSvc)
|
||||
pr := &github.TaskPR{
|
||||
TaskID: "task-1", RepositoryID: "repo-1", Owner: "acme", Repo: "widget", PRNumber: 42,
|
||||
State: "open", ChecksState: "success", HeadSHA: "head-b",
|
||||
MergeQueueLastRemovalID: "removal-a", MergeQueueLastRemovalBeforeSHA: "head-a",
|
||||
MergeQueueLastRemovalReason: "CHECKS_FAILED",
|
||||
}
|
||||
|
||||
if err := svc.handleTaskPRCIAutomationWithRefresh(ctx, pr, false); err != nil {
|
||||
t.Fatalf("stale queue recovery evaluation: %v", err)
|
||||
}
|
||||
if got := svc.messageQueue.GetStatus(ctx, "session-1"); got.Count != 0 {
|
||||
t.Fatalf("stale removal queued an auto-fix prompt: %+v", got)
|
||||
}
|
||||
if len(ghSvc.fixAttempts) != 0 {
|
||||
t.Fatalf("stale removal consumed an auto-fix round: %+v", ghSvc.fixAttempts)
|
||||
}
|
||||
if ghSvc.ciPRState == nil || ghSvc.ciPRState.LastQueueAttemptHeadSHA != "head-b" {
|
||||
t.Fatalf("expected passive baseline to be recorded for merge retry guarding, got %+v", ghSvc.ciPRState)
|
||||
}
|
||||
if ghSvc.ciPRState.LastMergeSignature != "" {
|
||||
t.Fatalf("passive baseline became merge-attempt evidence: %+v", ghSvc.ciPRState)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCIAutomationMergeQueueRecoveryUsesConflictEvidenceForUnknownReason(t *testing.T) {
|
||||
pr := &github.TaskPR{MergeableState: "dirty"}
|
||||
if got := ciAutomationQueueRemovalCause("provider changed this", pr); got != ciAutomationQueueRemovalCauseConflict {
|
||||
@@ -183,7 +219,8 @@ func TestCIAutomationMergeQueueRecoveryQueuesOneRepairPerRemoval(t *testing.T) {
|
||||
}}
|
||||
ghSvc.ciPRState = &github.TaskCIPRAutomationState{
|
||||
TaskID: "task-1", RepositoryID: "repo-1", PRNumber: 42,
|
||||
LastQueueAttemptHeadSHA: "head-a", LastMergeSignature: "merge-a",
|
||||
LastQueueAttemptHeadSHA: "head-a",
|
||||
LastMergeSignature: "merge-a",
|
||||
}
|
||||
svc.SetGitHubService(ghSvc)
|
||||
pr := &github.TaskPR{
|
||||
|
||||
@@ -225,6 +225,13 @@ func (m *mockGitHubService) RefreshTaskCIFixCheckpoint(_ context.Context, taskID
|
||||
Signature: signature,
|
||||
CheckpointJSON: checkpointJSON,
|
||||
})
|
||||
if m.ciPRState != nil {
|
||||
m.ciPRState.LastFixSignature = signature
|
||||
m.ciPRState.LastFixCheckpointJSON = checkpointJSON
|
||||
m.ciPRState.LastFixEnqueuedAt = nil
|
||||
// A prompt-free refresh clears the active dispatch timestamp but keeps
|
||||
// the session selected for the next repair round.
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *mockGitHubService) RecordTaskCIMergeAttempt(_ context.Context, attempt github.TaskCIMergeAttempt) error {
|
||||
|
||||
@@ -70,6 +70,20 @@ async function openPromptDialog(session: SessionPage) {
|
||||
await editButton.click({ force: true });
|
||||
}
|
||||
|
||||
async function primarySessionId(apiClient: ApiClient, taskId: string) {
|
||||
const { sessions } = await apiClient.listTaskSessions(taskId);
|
||||
const session = sessions.find((item) => item.is_primary) ?? sessions[0];
|
||||
if (!session) throw new Error(`Task ${taskId} has no session`);
|
||||
return session.id;
|
||||
}
|
||||
|
||||
async function listAutoFixMessages(apiClient: ApiClient, sessionId: string) {
|
||||
const { messages } = await apiClient.listSessionMessages(sessionId);
|
||||
return messages.filter(
|
||||
(message) => message.author_type === "user" && message.content.includes("@ci-auto-fix"),
|
||||
);
|
||||
}
|
||||
|
||||
async function interceptLifecycleError(
|
||||
testPage: import("@playwright/test").Page,
|
||||
repositoryId: string,
|
||||
@@ -229,7 +243,9 @@ test.describe("PR CI automation options", () => {
|
||||
|
||||
await popover.getByLabel("Explain CI automation options").hover();
|
||||
const queueRecoveryHelp = testPage.getByRole("tooltip");
|
||||
await expect(queueRecoveryHelp).toContainText("Auto-fix repairs actionable queue removals.");
|
||||
await expect(queueRecoveryHelp).toContainText(
|
||||
"Auto-fix repairs ordinary merge conflicts and actionable queue removals.",
|
||||
);
|
||||
await expect(queueRecoveryHelp).toContainText(
|
||||
"Auto-merge submits an eligible head or requeues it after a new commit.",
|
||||
);
|
||||
@@ -253,6 +269,8 @@ test.describe("PR CI automation options", () => {
|
||||
const feedbackHelp = promptDialog.getByTestId("ci-auto-fix-pr-feedback-help");
|
||||
await expect(feedbackHelp).toContainText("new or changed failing checks");
|
||||
await expect(feedbackHelp).toContainText("pull or fetch the branch");
|
||||
await expect(promptDialog).toContainText("ordinary merge-conflict context");
|
||||
await expect(promptDialog).toContainText("actionable queue-removal context");
|
||||
await testPage.getByLabel("Task auto-fix prompt").fill("Please fix only the new CI issues.");
|
||||
await testPage.getByRole("button", { name: "Save prompt" }).click();
|
||||
|
||||
@@ -277,6 +295,112 @@ test.describe("PR CI automation options", () => {
|
||||
).toBeChecked();
|
||||
});
|
||||
|
||||
test("desktop auto-fix repairs an existing merge conflict once", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
test.setTimeout(120_000);
|
||||
const headSHA = "head-conflict-desktop";
|
||||
const taskId = await seedTaskWithPR(apiClient, seedData, "CI conflict auto-fix desktop", {
|
||||
head_sha: headSHA,
|
||||
checks_state: "success",
|
||||
checks_total: 1,
|
||||
checks_passing: 1,
|
||||
unresolved_review_threads: 0,
|
||||
mergeable_state: "dirty",
|
||||
});
|
||||
const sessionId = await primarySessionId(apiClient, taskId);
|
||||
const session = await openTask(testPage, taskId);
|
||||
await session.waitForChatIdle();
|
||||
|
||||
await session.hoverPRTopbar();
|
||||
await session.prTopbarPopover().hover();
|
||||
const popover = session.prTopbarPopover();
|
||||
await expect(popover.getByTestId("pr-ci-automation-controls")).toBeVisible();
|
||||
await popover.getByLabel("Explain CI automation options").hover();
|
||||
const help = testPage.getByRole("tooltip");
|
||||
await expect(help).toContainText(
|
||||
"Auto-fix repairs ordinary merge conflicts and actionable queue removals.",
|
||||
);
|
||||
|
||||
await openPromptDialog(session);
|
||||
const promptDialog = testPage.getByRole("dialog", { name: "Auto-fix prompt" });
|
||||
await expect(promptDialog).toContainText("ordinary merge-conflict context");
|
||||
await expect(promptDialog).toContainText("actionable queue-removal context");
|
||||
await testPage.getByRole("button", { name: "Cancel" }).click();
|
||||
|
||||
await popover.getByRole("switch", { name: "Auto-fix CI and address comments" }).click();
|
||||
await apiClient.mockGitHubAssociateTaskPR({
|
||||
task_id: taskId,
|
||||
workspace_id: seedData.workspaceId,
|
||||
repository_id: seedData.repositoryId,
|
||||
owner: OWNER,
|
||||
repo: REPO,
|
||||
pr_number: PR_NUMBER,
|
||||
pr_url: PR_URL,
|
||||
pr_title: "Add CI automation options",
|
||||
head_branch: "feat/ci-automation",
|
||||
base_branch: "main",
|
||||
author_login: "test-user",
|
||||
state: "open",
|
||||
head_sha: headSHA,
|
||||
review_state: "approved",
|
||||
checks_state: "success",
|
||||
checks_total: 1,
|
||||
checks_passing: 1,
|
||||
unresolved_review_threads: 0,
|
||||
mergeable_state: "dirty",
|
||||
});
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const options = await apiClient.getTaskCIAutomationOptions(taskId);
|
||||
return options.pr_states?.find((item) => item.pr_number === PR_NUMBER)
|
||||
?.auto_fix_round_count;
|
||||
})
|
||||
.toBe(1);
|
||||
await expect(session.prStatusChip().getByTestId("pr-status-auto-fix-chip")).toContainText(
|
||||
"Auto-fix 1/10",
|
||||
);
|
||||
await expect.poll(() => listAutoFixMessages(apiClient, sessionId)).toHaveLength(1);
|
||||
const [firstMessage] = await listAutoFixMessages(apiClient, sessionId);
|
||||
expect(firstMessage).toBeDefined();
|
||||
expect(firstMessage?.content).toContain("Merge conflict");
|
||||
expect(firstMessage?.content).toContain("feat/ci-automation");
|
||||
expect(firstMessage?.content).toContain("main");
|
||||
|
||||
await apiClient.mockGitHubAssociateTaskPR({
|
||||
task_id: taskId,
|
||||
workspace_id: seedData.workspaceId,
|
||||
repository_id: seedData.repositoryId,
|
||||
owner: OWNER,
|
||||
repo: REPO,
|
||||
pr_number: PR_NUMBER,
|
||||
pr_url: PR_URL,
|
||||
pr_title: "Add CI automation options",
|
||||
head_branch: "feat/ci-automation",
|
||||
base_branch: "main",
|
||||
author_login: "test-user",
|
||||
state: "open",
|
||||
head_sha: headSHA,
|
||||
review_state: "approved",
|
||||
checks_state: "success",
|
||||
checks_total: 1,
|
||||
checks_passing: 1,
|
||||
unresolved_review_threads: 0,
|
||||
mergeable_state: "dirty",
|
||||
});
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const options = await apiClient.getTaskCIAutomationOptions(taskId);
|
||||
return options.pr_states?.find((item) => item.pr_number === PR_NUMBER)
|
||||
?.auto_fix_round_count;
|
||||
})
|
||||
.toBe(1);
|
||||
await expect.poll(() => listAutoFixMessages(apiClient, sessionId)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("desktop popover keeps two linked PRs' automation switches independent", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
|
||||
@@ -106,6 +106,20 @@ async function interceptTallPRFeedback(testPage: import("@playwright/test").Page
|
||||
);
|
||||
}
|
||||
|
||||
async function primarySessionId(apiClient: ApiClient, taskId: string) {
|
||||
const { sessions } = await apiClient.listTaskSessions(taskId);
|
||||
const session = sessions.find((item) => item.is_primary) ?? sessions[0];
|
||||
if (!session) throw new Error(`Task ${taskId} has no session`);
|
||||
return session.id;
|
||||
}
|
||||
|
||||
async function listAutoFixMessages(apiClient: ApiClient, sessionId: string) {
|
||||
const { messages } = await apiClient.listSessionMessages(sessionId);
|
||||
return messages.filter(
|
||||
(message) => message.author_type === "user" && message.content.includes("@ci-auto-fix"),
|
||||
);
|
||||
}
|
||||
|
||||
test.describe("mobile PR CI automation options", () => {
|
||||
test("drawer exposes automation controls and task prompt settings link", async ({
|
||||
testPage,
|
||||
@@ -185,6 +199,110 @@ test.describe("mobile PR CI automation options", () => {
|
||||
await expect(promptDialog.getByTestId("ci-auto-fix-pr-feedback-help")).toContainText(
|
||||
"new or changed review comments",
|
||||
);
|
||||
await expect(promptDialog).toContainText("ordinary merge-conflict context");
|
||||
await expect(promptDialog).toContainText("actionable queue-removal context");
|
||||
await testPage.keyboard.press("Escape");
|
||||
});
|
||||
|
||||
test("mobile auto-fix repairs an existing merge conflict once", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
test.setTimeout(120_000);
|
||||
const headSHA = "head-conflict-mobile";
|
||||
const taskId = await seedTaskWithPR(apiClient, seedData, "CI conflict auto-fix mobile", {
|
||||
head_sha: headSHA,
|
||||
checks_state: "success",
|
||||
checks_total: 1,
|
||||
checks_passing: 1,
|
||||
unresolved_review_threads: 0,
|
||||
mergeable_state: "dirty",
|
||||
});
|
||||
const sessionId = await primarySessionId(apiClient, taskId);
|
||||
|
||||
await testPage.goto(`/t/${taskId}`);
|
||||
const session = new SessionPage(testPage);
|
||||
await session.waitForLoad();
|
||||
await session.waitForChatIdle();
|
||||
await session.tapPRStatusChip();
|
||||
const drawer = session.prStatusChipDrawer();
|
||||
await expect(drawer.getByTestId("pr-ci-automation-controls")).toBeVisible();
|
||||
await drawer.getByLabel("Explain CI automation options").tap();
|
||||
await expect(testPage.getByRole("tooltip")).toContainText(
|
||||
"Auto-fix repairs ordinary merge conflicts and actionable queue removals.",
|
||||
);
|
||||
await testPage.keyboard.press("Escape");
|
||||
|
||||
await drawer.getByRole("switch", { name: "Auto-fix CI and address comments" }).tap();
|
||||
await apiClient.mockGitHubAssociateTaskPR({
|
||||
task_id: taskId,
|
||||
workspace_id: seedData.workspaceId,
|
||||
repository_id: seedData.repositoryId,
|
||||
owner: OWNER,
|
||||
repo: REPO,
|
||||
pr_number: PR_NUMBER,
|
||||
pr_url: PR_URL,
|
||||
pr_title: "Add mobile CI automation options",
|
||||
head_branch: "feat/mobile-ci-automation",
|
||||
base_branch: "main",
|
||||
author_login: "test-user",
|
||||
state: "open",
|
||||
head_sha: headSHA,
|
||||
review_state: "approved",
|
||||
checks_state: "success",
|
||||
checks_total: 1,
|
||||
checks_passing: 1,
|
||||
unresolved_review_threads: 0,
|
||||
mergeable_state: "dirty",
|
||||
});
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const options = await apiClient.getTaskCIAutomationOptions(taskId);
|
||||
return options.pr_states?.find((item) => item.pr_number === PR_NUMBER)
|
||||
?.auto_fix_round_count;
|
||||
})
|
||||
.toBe(1);
|
||||
await expect(session.prStatusChip().getByTestId("pr-status-auto-fix-chip")).toContainText(
|
||||
"Auto-fix 1/10",
|
||||
);
|
||||
await expect.poll(() => listAutoFixMessages(apiClient, sessionId)).toHaveLength(1);
|
||||
const [firstMessage] = await listAutoFixMessages(apiClient, sessionId);
|
||||
expect(firstMessage).toBeDefined();
|
||||
expect(firstMessage?.content).toContain("Merge conflict");
|
||||
expect(firstMessage?.content).toContain("feat/mobile-ci-automation");
|
||||
expect(firstMessage?.content).toContain("main");
|
||||
|
||||
await apiClient.mockGitHubAssociateTaskPR({
|
||||
task_id: taskId,
|
||||
workspace_id: seedData.workspaceId,
|
||||
repository_id: seedData.repositoryId,
|
||||
owner: OWNER,
|
||||
repo: REPO,
|
||||
pr_number: PR_NUMBER,
|
||||
pr_url: PR_URL,
|
||||
pr_title: "Add mobile CI automation options",
|
||||
head_branch: "feat/mobile-ci-automation",
|
||||
base_branch: "main",
|
||||
author_login: "test-user",
|
||||
state: "open",
|
||||
head_sha: headSHA,
|
||||
review_state: "approved",
|
||||
checks_state: "success",
|
||||
checks_total: 1,
|
||||
checks_passing: 1,
|
||||
unresolved_review_threads: 0,
|
||||
mergeable_state: "dirty",
|
||||
});
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const options = await apiClient.getTaskCIAutomationOptions(taskId);
|
||||
return options.pr_states?.find((item) => item.pr_number === PR_NUMBER)
|
||||
?.auto_fix_round_count;
|
||||
})
|
||||
.toBe(1);
|
||||
await expect.poll(() => listAutoFixMessages(apiClient, sessionId)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("drawer keeps two linked PRs' automation switches independent", async ({
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
"autoFixExhaustedAria": "auto-fix paused after reaching {{current}} of {{max}} rounds",
|
||||
"autoFixCiAndAddressComments": "Auto-fix CI and address comments",
|
||||
"autoFixPrompt": "Auto-fix prompt",
|
||||
"autoFixPromptDescription": "This prompt is used only for this task. Leave it blank to use the default prompt. Add <0>{{placeholder}}</0> when you want Kandev to include PR feedback, queue-removal context, and available failed merge-group checks.",
|
||||
"autoFixRoundExplanation": "Auto-fix has used {{current}} of {{max}} rounds for this PR. A round is counted when Kandev sends or queues a CI auto-fix message. Kandev waits for all PR checks to finish before starting a new CI auto-fix turn, so the agent gets the final failed checks and current comments together. Updating an already queued auto-fix message does not use another round. When this is at {{max}}/{{max}} and there is no pending auto-fix message left to update, Kandev pauses auto-fix for this PR so it cannot loop forever. Disable and re-enable auto-fix after manual review to start over.",
|
||||
"autoFixPromptDescription": "This prompt is used only for this task. Leave it blank to use the default prompt. Add <0>{{placeholder}}</0> when you want Kandev to include PR feedback, ordinary merge-conflict context, actionable queue-removal context, and available failed merge-group checks.",
|
||||
"autoFixRoundExplanation": "Auto-fix has used {{current}} of {{max}} rounds for this PR. A round is counted when Kandev sends or queues a CI auto-fix message. Kandev waits for all PR checks to finish before starting a new CI auto-fix turn, so the agent gets the final failed checks, current comments, ordinary merge conflicts, and actionable merge-queue removals together. Updating an already queued auto-fix message does not use another round. When this is at {{max}}/{{max}} and there is no pending auto-fix message left to update, Kandev pauses auto-fix for this PR so it cannot loop forever. Disable and re-enable auto-fix after manual review to start over.",
|
||||
"autoMerge": "Auto-merge",
|
||||
"autoMergeEnabledAria": "auto-merge enabled",
|
||||
"autoMergeWhenReady": "Auto-merge when ready",
|
||||
@@ -611,7 +611,7 @@
|
||||
"automationForPrQueued": "PR #{{number}} is in the merge queue",
|
||||
"automationForPrRemoved": "PR #{{number}} was removed: {{cause}}",
|
||||
"automationForPrRemovedGeneric": "PR #{{number}} was removed from the merge queue",
|
||||
"ciAutomationQueueRecoveryHelp": "Auto-fix repairs actionable queue removals. Auto-merge submits an eligible head or requeues it after a new commit. Both controls form the repair and requeue loop. Kandev never requeues the same head after removal.",
|
||||
"ciAutomationQueueRecoveryHelp": "Auto-fix repairs ordinary merge conflicts and actionable queue removals. Auto-merge submits an eligible head or requeues it after a new commit. Both controls form the repair and requeue loop. Kandev never requeues the same head after removal.",
|
||||
"mergeQueueAutomation": "Merge queue automation",
|
||||
"mergeQueueRecovery": "Merge queue recovery",
|
||||
"mergeQueueRemovalCauseChecksFailed": "checks failed",
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
"autoFixExhaustedAria": "àũţō-ƒĩx ƥàũśēď àƒţēŕ ŕēàćĥĩńĝ {{current}} ōƒ {{max}} ŕōũńďś",
|
||||
"autoFixCiAndAddressComments": "Àũţō-ƒĩx ĆĨ àńď àďďŕēśś ćōḿḿēńţś",
|
||||
"autoFixPrompt": "Àũţō-ƒĩx ƥŕōḿƥţ",
|
||||
"autoFixPromptDescription": "Ţĥĩś ƥŕōḿƥţ ĩś ũśēď ōńĺŷ ƒōŕ ţĥĩś ţàśķ. Ĺēàvē ĩţ ƀĺàńķ ţō ũśē ţĥē ďēƒàũĺţ ƥŕōḿƥţ. Àďď <0>{{placeholder}}</0> ŵĥēń ŷōũ ŵàńţ Ķàńďēv ţō ĩńćĺũďē ƤŔ ƒēēďƀàćķ, qũēũē-ŕēḿōvàĺ ćōńţēxţ, àńď àvàĩĺàƀĺē ƒàĩĺēď ḿēŕĝē-ĝŕōũƥ ćĥēćķś.",
|
||||
"autoFixRoundExplanation": "Àũţō-ƒĩx ĥàś ũśēď {{current}} ōƒ {{max}} ŕōũńďś ƒōŕ ţĥĩś ƤŔ. À ŕōũńď ĩś ćōũńţēď ŵĥēń Ķàńďēv śēńďś ōŕ qũēũēś à ĆĨ àũţō-ƒĩx ḿēśśàĝē. Ķàńďēv ŵàĩţś ƒōŕ àĺĺ ƤŔ ćĥēćķś ţō ƒĩńĩśĥ ƀēƒōŕē śţàŕţĩńĝ à ńēŵ ĆĨ àũţō-ƒĩx ţũŕń, śō ţĥē àĝēńţ ĝēţś ţĥē ƒĩńàĺ ƒàĩĺēď ćĥēćķś àńď ćũŕŕēńţ ćōḿḿēńţś ţōĝēţĥēŕ. Ũƥďàţĩńĝ àń àĺŕēàďŷ qũēũēď àũţō-ƒĩx ḿēśśàĝē ďōēś ńōţ ũśē àńōţĥēŕ ŕōũńď. Ŵĥēń ţĥĩś ĩś àţ {{max}}/{{max}} àńď ţĥēŕē ĩś ńō ƥēńďĩńĝ àũţō-ƒĩx ḿēśśàĝē ĺēƒţ ţō ũƥďàţē, Ķàńďēv ƥàũśēś àũţō-ƒĩx ƒōŕ ţĥĩś ƤŔ śō ĩţ ćàńńōţ ĺōōƥ ƒōŕēvēŕ. Ďĩśàƀĺē àńď ŕē-ēńàƀĺē àũţō-ƒĩx àƒţēŕ ḿàńũàĺ ŕēvĩēŵ ţō śţàŕţ ōvēŕ.",
|
||||
"autoFixPromptDescription": "Ţĥĩś ƥŕōḿƥţ ĩś ũśēď ōńĺŷ ƒōŕ ţĥĩś ţàśķ. Ĺēàvē ĩţ ƀĺàńķ ţō ũśē ţĥē ďēƒàũĺţ ƥŕōḿƥţ. Àďď <0>{{placeholder}}</0> ŵĥēń ŷōũ ŵàńţ Ķàńďēv ţō ĩńćĺũďē ƤŔ ƒēēďƀàćķ, ōŕďĩńàŕŷ ḿēŕĝē-ćōńƒĺĩćţ ćōńţēxţ, àćţĩōńàƀĺē qũēũē-ŕēḿōvàĺ ćōńţēxţ, àńď àvàĩĺàƀĺē ƒàĩĺēď ḿēŕĝē-ĝŕōũƥ ćĥēćķś.",
|
||||
"autoFixRoundExplanation": "Àũţō-ƒĩx ĥàś ũśēď {{current}} ōƒ {{max}} ŕōũńďś ƒōŕ ţĥĩś ƤŔ. À ŕōũńď ĩś ćōũńţēď ŵĥēń Ķàńďēv śēńďś ōŕ qũēũēś à ĆĨ àũţō-ƒĩx ḿēśśàĝē. Ķàńďēv ŵàĩţś ƒōŕ àĺĺ ƤŔ ćĥēćķś ţō ƒĩńĩśĥ ƀēƒōŕē śţàŕţĩńĝ à ńēŵ ĆĨ àũţō-ƒĩx ţũŕń, śō ţĥē àĝēńţ ĝēţś ţĥē ƒĩńàĺ ƒàĩĺēď ćĥēćķś, ćũŕŕēńţ ćōḿḿēńţś, ōŕďĩńàŕŷ ḿēŕĝē ćōńƒĺĩćţś, àńď àćţĩōńàƀĺē ḿēŕĝē-qũēũē ŕēḿōvàĺś ţōĝēţĥēŕ. Ũƥďàţĩńĝ àń àĺŕēàďŷ qũēũēď àũţō-ƒĩx ḿēśśàĝē ďōēś ńōţ ũśē àńōţĥēŕ ŕōũńď. Ŵĥēń ţĥĩś ĩś àţ {{max}}/{{max}} àńď ţĥēŕē ĩś ńō ƥēńďĩńĝ àũţō-ƒĩx ḿēśśàĝē ĺēƒţ ţō ũƥďàţē, Ķàńďēv ƥàũśēś àũţō-ƒĩx ƒōŕ ţĥĩś ƤŔ śō ĩţ ćàńńōţ ĺōōƥ ƒōŕēvēŕ. Ďĩśàƀĺē àńď ŕē-ēńàƀĺē àũţō-ƒĩx àƒţēŕ ḿàńũàĺ ŕēvĩēŵ ţō śţàŕţ ōvēŕ.",
|
||||
"autoMerge": "Àũţō-ḿēŕĝē",
|
||||
"autoMergeEnabledAria": "àũţō-ḿēŕĝē ēńàƀĺēď",
|
||||
"autoMergeWhenReady": "Àũţō-ḿēŕĝē ŵĥēń ŕēàďŷ",
|
||||
@@ -611,7 +611,7 @@
|
||||
"automationForPrQueued": "ƤŔ #{{number}} ĩś ĩń ţĥē ḿēŕĝē qũēũē",
|
||||
"automationForPrRemoved": "ƤŔ #{{number}} ŵàś ŕēḿōvēď: {{cause}}",
|
||||
"automationForPrRemovedGeneric": "ƤŔ #{{number}} ŵàś ŕēḿōvēď ƒŕōḿ ţĥē ḿēŕĝē qũēũē",
|
||||
"ciAutomationQueueRecoveryHelp": "Àũţō-ƒĩx ŕēƥàĩŕś àćţĩōńàƀĺē qũēũē ŕēḿōvàĺś. Àũţō-ḿēŕĝē śũƀḿĩţś àń ēĺĩĝĩƀĺē ĥēàď ōŕ ŕēqũēũēś ĩţ àƒţēŕ à ńēŵ ćōḿḿĩţ. Ɓōţĥ ćōńţŕōĺś ƒōŕḿ ţĥē ŕēƥàĩŕ àńď ŕēqũēũē ĺōōƥ. Ķàńďēv ńēvēŕ ŕēqũēũēś ţĥē śàḿē ĥēàď àƒţēŕ ŕēḿōvàĺ.",
|
||||
"ciAutomationQueueRecoveryHelp": "Àũţō-ƒĩx ŕēƥàĩŕś ōŕďĩńàŕŷ ḿēŕĝē ćōńƒĺĩćţś àńď àćţĩōńàƀĺē qũēũē ŕēḿōvàĺś. Àũţō-ḿēŕĝē śũƀḿĩţś àń ēĺĩĝĩƀĺē ĥēàď ōŕ ŕēqũēũēś ĩţ àƒţēŕ à ńēŵ ćōḿḿĩţ. Ɓōţĥ ćōńţŕōĺś ƒōŕḿ ţĥē ŕēƥàĩŕ àńď ŕēqũēũē ĺōōƥ. Ķàńďēv ńēvēŕ ŕēqũēũēś ţĥē śàḿē ĥēàď àƒţēŕ ŕēḿōvàĺ.",
|
||||
"mergeQueueAutomation": "Ḿēŕĝē qũēũē àũţōḿàţĩōń",
|
||||
"mergeQueueRecovery": "Ḿēŕĝē qũēũē ŕēćōvēŕŷ",
|
||||
"mergeQueueRemovalCauseChecksFailed": "ćĥēćķś ƒàĩĺēď",
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
"autoFixExhaustedAria": "correção automática pausada após atingir {{current}} de {{max}} rondas",
|
||||
"autoFixCiAndAddressComments": "Corrigir automaticamente o CI e responder aos comentários",
|
||||
"autoFixPrompt": "Prompt da correção automática",
|
||||
"autoFixPromptDescription": "Este prompt é usado apenas nesta tarefa. Deixe-o em branco para usar o prompt pré-definido. Adicione <0>{{placeholder}}</0> quando quiser que o Kandev inclua feedback do PR, contexto de remoção da fila e verificações falhadas de merge-group disponíveis.",
|
||||
"autoFixRoundExplanation": "A correção automática já usou {{current}} de {{max}} rondas neste PR. Uma ronda é contada quando o Kandev envia ou coloca em fila uma mensagem de correção automática do CI. O Kandev espera que todas as verificações do PR terminem antes de iniciar um novo turno de correção automática do CI, para que o agente receba em conjunto as verificações falhadas finais e os comentários atuais. Atualizar uma mensagem de correção automática já em fila não consome outra ronda. Quando isto estiver em {{max}}/{{max}} e não restar nenhuma mensagem de correção automática pendente para atualizar, o Kandev suspende a correção automática neste PR para que não entre num ciclo infinito. Desative e reative a correção automática após revisão manual para recomeçar.",
|
||||
"autoFixPromptDescription": "Este prompt é usado apenas nesta tarefa. Deixe-o em branco para usar o prompt pré-definido. Adicione <0>{{placeholder}}</0> quando quiser que o Kandev inclua feedback do PR, contexto de conflitos de merge normais, remoções acionáveis da fila e verificações falhadas de merge-group disponíveis.",
|
||||
"autoFixRoundExplanation": "A correção automática já usou {{current}} de {{max}} rondas neste PR. Uma ronda é contada quando o Kandev envia ou coloca em fila uma mensagem de correção automática do CI. O Kandev espera que todas as verificações do PR terminem antes de iniciar um novo turno de correção automática do CI, para que o agente receba em conjunto as verificações falhadas finais, os comentários atuais, os conflitos de merge normais e as remoções acionáveis da fila. Atualizar uma mensagem de correção automática já em fila não consome outra ronda. Quando isto estiver em {{max}}/{{max}} e não restar nenhuma mensagem de correção automática pendente para atualizar, o Kandev suspende a correção automática neste PR para que não entre num ciclo infinito. Desative e reative a correção automática após revisão manual para recomeçar.",
|
||||
"autoMerge": "Merge automático",
|
||||
"autoMergeWhenReady": "Fazer merge automático quando estiver pronto",
|
||||
"automaticallyCreateTasksForGithubIssues": "Cria automaticamente tarefas para as issues do GitHub que correspondam aos seus critérios.",
|
||||
@@ -611,7 +611,7 @@
|
||||
"automationForPrQueued": "O PR #{{number}} está na fila de merge",
|
||||
"automationForPrRemoved": "O PR #{{number}} foi removido: {{cause}}",
|
||||
"automationForPrRemovedGeneric": "O PR #{{number}} foi removido da fila de merge",
|
||||
"ciAutomationQueueRecoveryHelp": "A correção automática repara remoções acionáveis da fila. O merge automático envia um head elegível ou volta a colocá-lo na fila depois de um novo commit. As duas opções formam o ciclo de reparação e fila. O Kandev nunca volta a colocar o mesmo head na fila depois de uma remoção.",
|
||||
"ciAutomationQueueRecoveryHelp": "A correção automática repara conflitos de merge normais e remoções acionáveis da fila. O merge automático envia um head elegível ou volta a colocá-lo na fila depois de um novo commit. As duas opções formam o ciclo de reparação e fila. O Kandev nunca volta a colocar o mesmo head na fila depois de uma remoção.",
|
||||
"mergeQueueAutomation": "Automatização da fila de merge",
|
||||
"mergeQueueRecovery": "Recuperação da fila de merge",
|
||||
"mergeQueueRemovalCauseChecksFailed": "as verificações falharam",
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
"autoFixExhaustedAria": "达到 {{current}} / {{max}} 轮后,自动修复已暂停",
|
||||
"autoFixCiAndAddressComments": "自动修复 CI 并处理评论",
|
||||
"autoFixPrompt": "自动修复提示词",
|
||||
"autoFixPromptDescription": "此提示词仅用于本任务。留空则使用默认提示词。若希望 Kandev 附上 PR 反馈、队列移除上下文和可用的合并组失败检查,请添加 <0>{{placeholder}}</0>。",
|
||||
"autoFixRoundExplanation": "自动修复已为该 PR 使用 {{current}} / {{max}} 轮。Kandev 每次发送或排队一条 CI 自动修复消息即计为一轮。Kandev 会等待全部 PR 检查完成后再开始新一轮自动修复,以便智能体同时获得最终的失败检查与当前评论。更新已排队的自动修复消息不会额外消耗轮次。当已达 {{max}}/{{max}} 且没有待更新的自动修复消息时,Kandev 会暂停该 PR 的自动修复,避免无限循环。人工审阅后,请关闭并重新启用自动修复以重新开始。",
|
||||
"autoFixPromptDescription": "此提示词仅用于本任务。留空则使用默认提示词。若希望 Kandev 附上 PR 反馈、普通合并冲突上下文、可处理的合并队列移除上下文和可用的合并组失败检查,请添加 <0>{{placeholder}}</0>。",
|
||||
"autoFixRoundExplanation": "自动修复已为该 PR 使用 {{current}} / {{max}} 轮。Kandev 每次发送或排队一条 CI 自动修复消息即计为一轮。Kandev 会等待全部 PR 检查完成后再开始新一轮自动修复,以便智能体同时获得最终的失败检查、当前评论、普通合并冲突和可处理的合并队列移除。更新已排队的自动修复消息不会额外消耗轮次。当已达 {{max}}/{{max}} 且没有待更新的自动修复消息时,Kandev 会暂停该 PR 的自动修复,避免无限循环。人工审阅后,请关闭并重新启用自动修复以重新开始。",
|
||||
"autoMerge": "自动合并",
|
||||
"autoMergeEnabledAria": "自动合并已启用",
|
||||
"autoMergeWhenReady": "就绪时自动合并",
|
||||
@@ -611,7 +611,7 @@
|
||||
"automationForPrQueued": "PR #{{number}} 已在合并队列中",
|
||||
"automationForPrRemoved": "PR #{{number}} 已被移除:{{cause}}",
|
||||
"automationForPrRemovedGeneric": "PR #{{number}} 已从合并队列中移除",
|
||||
"ciAutomationQueueRecoveryHelp": "自动修复会处理可操作的队列移除。自动合并会提交符合条件的 head,或在新提交后重新排队。两项设置共同完成修复和重新排队流程。移除后 Kandev 不会让同一个 head 再次排队。",
|
||||
"ciAutomationQueueRecoveryHelp": "自动修复会处理普通合并冲突和可操作的队列移除。自动合并会提交符合条件的 head,或在新提交后重新排队。两项设置共同完成修复和重新排队流程。移除后 Kandev 不会让同一个 head 再次排队。",
|
||||
"mergeQueueAutomation": "合并队列自动化",
|
||||
"mergeQueueRecovery": "合并队列恢复",
|
||||
"mergeQueueRemovalCauseChecksFailed": "检查失败",
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
"autoFixExhaustedAria": "達到 {{current}} / {{max}} 輪後,自動修復已暫停",
|
||||
"autoFixCiAndAddressComments": "自動修復 CI 並處理評論",
|
||||
"autoFixPrompt": "自動修復提示詞",
|
||||
"autoFixPromptDescription": "此提示詞僅用於本任務。留空則使用預設提示詞。若希望 Kandev 附上 PR 反饋、佇列移除上下文和可用的合併組失敗檢查,請新增 <0>{{placeholder}}</0>。",
|
||||
"autoFixRoundExplanation": "自動修復已為該 PR 使用 {{current}} / {{max}} 輪。Kandev 每次傳送或排隊一條 CI 自動修復訊息即計為一輪。Kandev 會等待全部 PR 檢查完成後再開始新一輪自動修復,以便代理程式同時獲得最終的失敗檢查與目前評論。更新已排隊的自動修復訊息不會額外消耗輪次。當已達 {{max}}/{{max}} 且沒有待更新的自動修復訊息時,Kandev 會暫停該 PR 的自動修復,避免無限循環。人工審閱後,請關閉並重新啓用自動修復以重新開始。",
|
||||
"autoFixPromptDescription": "此提示詞僅用於本任務。留空則使用預設提示詞。若希望 Kandev 附上 PR 反饋、普通合併衝突上下文、可處理的合併佇列移除上下文和可用的合併組失敗檢查,請新增 <0>{{placeholder}}</0>。",
|
||||
"autoFixRoundExplanation": "自動修復已為該 PR 使用 {{current}} / {{max}} 輪。Kandev 每次傳送或排隊一條 CI 自動修復訊息即計為一輪。Kandev 會等待全部 PR 檢查完成後再開始新一輪自動修復,以便代理程式同時獲得最終的失敗檢查、目前評論、普通合併衝突和可處理的合併佇列移除。更新已排隊的自動修復訊息不會額外消耗輪次。當已達 {{max}}/{{max}} 且沒有待更新的自動修復訊息時,Kandev 會暫停該 PR 的自動修復,避免無限循環。人工審閱後,請關閉並重新啓用自動修復以重新開始。",
|
||||
"autoMerge": "自動合併",
|
||||
"autoMergeEnabledAria": "自動合併已啓用",
|
||||
"autoMergeWhenReady": "就緒時自動合併",
|
||||
@@ -611,7 +611,7 @@
|
||||
"automationForPrQueued": "PR #{{number}} 已在合併佇列中",
|
||||
"automationForPrRemoved": "PR #{{number}} 已被移除:{{cause}}",
|
||||
"automationForPrRemovedGeneric": "PR #{{number}} 已從合併佇列中移除",
|
||||
"ciAutomationQueueRecoveryHelp": "自動修復會處理可操作的佇列移除。自動合併會提交符合條件的 head,或在新提交後重新排隊。兩項設定共同完成修復和重新排隊流程。移除後 Kandev 不會讓同一個 head 再次排隊。",
|
||||
"ciAutomationQueueRecoveryHelp": "自動修復會處理普通合併衝突和可操作的佇列移除。自動合併會提交符合條件的 head,或在新提交後重新排隊。兩項設定共同完成修復和重新排隊流程。移除後 Kandev 不會讓同一個 head 再次排隊。",
|
||||
"mergeQueueAutomation": "合併佇列自動化",
|
||||
"mergeQueueRecovery": "合併佇列恢復",
|
||||
"mergeQueueRemovalCauseChecksFailed": "檢查失敗",
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
"autoFixExhaustedAria": "達到 {{current}} / {{max}} 輪後,自動修復已暫停",
|
||||
"autoFixCiAndAddressComments": "自動修復 CI 並處理評論",
|
||||
"autoFixPrompt": "自動修復提示詞",
|
||||
"autoFixPromptDescription": "此提示詞僅用於本任務。留空則使用預設提示詞。若希望 Kandev 附上 PR 意見回饋、佇列移除上下文和可用的合併組失敗檢查,請新增 <0>{{placeholder}}</0>。",
|
||||
"autoFixRoundExplanation": "自動修復已為該 PR 使用 {{current}} / {{max}} 輪。Kandev 每次傳送或排隊一條 CI 自動修復訊息即計為一輪。Kandev 會等待全部 PR 檢查完成後再開始新一輪自動修復,以便代理程式同時獲得最終的失敗檢查與目前評論。更新已排隊的自動修復訊息不會額外消耗輪次。當已達 {{max}}/{{max}} 且沒有待更新的自動修復訊息時,Kandev 會暫停該 PR 的自動修復,避免無限迴圈。人工審閱後,請關閉並重新啟用自動修復以重新開始。",
|
||||
"autoFixPromptDescription": "此提示詞僅用於本任務。留空則使用預設提示詞。若希望 Kandev 附上 PR 意見回饋、普通合併衝突上下文、可處理的合併佇列移除上下文和可用的合併組失敗檢查,請新增 <0>{{placeholder}}</0>。",
|
||||
"autoFixRoundExplanation": "自動修復已為該 PR 使用 {{current}} / {{max}} 輪。Kandev 每次傳送或排隊一條 CI 自動修復訊息即計為一輪。Kandev 會等待全部 PR 檢查完成後再開始新一輪自動修復,以便代理程式同時獲得最終的失敗檢查、目前評論、普通合併衝突和可處理的合併佇列移除。更新已排隊的自動修復訊息不會額外消耗輪次。當已達 {{max}}/{{max}} 且沒有待更新的自動修復訊息時,Kandev 會暫停該 PR 的自動修復,避免無限迴圈。人工審閱後,請關閉並重新啟用自動修復以重新開始。",
|
||||
"autoMerge": "自動合併",
|
||||
"autoMergeEnabledAria": "自動合併已啟用",
|
||||
"autoMergeWhenReady": "就緒時自動合併",
|
||||
@@ -611,7 +611,7 @@
|
||||
"automationForPrQueued": "PR #{{number}} 已在合併佇列中",
|
||||
"automationForPrRemoved": "PR #{{number}} 已被移除:{{cause}}",
|
||||
"automationForPrRemovedGeneric": "PR #{{number}} 已從合併佇列中移除",
|
||||
"ciAutomationQueueRecoveryHelp": "自動修復會處理可操作的佇列移除。自動合併會提交符合條件的 head,或在新提交後重新排隊。兩項設定共同完成修復和重新排隊流程。移除後 Kandev 不會讓同一個 head 再次排隊。",
|
||||
"ciAutomationQueueRecoveryHelp": "自動修復會處理普通合併衝突和可操作的佇列移除。自動合併會提交符合條件的 head,或在新提交後重新排隊。兩項設定共同完成修復和重新排隊流程。移除後 Kandev 不會讓同一個 head 再次排隊。",
|
||||
"mergeQueueAutomation": "合併佇列自動化",
|
||||
"mergeQueueRecovery": "合併佇列恢復",
|
||||
"mergeQueueRemovalCauseChecksFailed": "檢查失敗",
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
---
|
||||
created: 2026-09-05
|
||||
status: done
|
||||
requirements:
|
||||
- REQ-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001
|
||||
- REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002
|
||||
- REQ-UI-CI-PR-AUTOMATION-001
|
||||
- REQ-UI-CI-PR-MERGE-QUEUE-RECOVERY-001
|
||||
system_design:
|
||||
- ../../specs/integrations/system-design/github-pr-auto-fix-conflicts.md
|
||||
- ../../specs/integrations/system-design/github-pr-merge-queue-recovery.md
|
||||
- ../../specs/ui/system-design/ci-pr-automation-01.md
|
||||
- ../../specs/ui/system-design/ci-pr-merge-queue-recovery-controls.md
|
||||
legacy_specs: []
|
||||
---
|
||||
# Implementation Plan: GitHub PR Auto-Fix Conflict Recovery
|
||||
|
||||
## Overview
|
||||
|
||||
Add ordinary merge conflicts to the per-PR auto-fix checkpoint. Correct the
|
||||
queue-removal guard so a retained failed-check removal can start repair after
|
||||
Kandev misses the active queue entry.
|
||||
|
||||
Backend signal behavior lands first. Localized help and public documentation
|
||||
then describe the complete trigger set. Desktop and mobile E2E tests prove the
|
||||
user outcome last.
|
||||
|
||||
## Confirmed root cause
|
||||
|
||||
`ciAutomationBuildDelta` includes failed checks and comments. It does not
|
||||
include `TaskPR.MergeableState == "dirty"`, so an ordinary conflict produces an
|
||||
empty delta and no prompt.
|
||||
|
||||
`ciAutomationQueueRemovalBelongsToCurrentHead` must distinguish a passive
|
||||
removal-only baseline from queue-attempt evidence. The evaluator therefore
|
||||
requires a matching `last_queue_attempt_head_sha` and non-empty
|
||||
`last_merge_signature`; it rejects a retained actionable removal when Kandev
|
||||
missed the active queue entry and cannot prove its head.
|
||||
|
||||
## Scope
|
||||
|
||||
### In scope
|
||||
|
||||
- Add ordinary merge conflicts to the current auto-fix checkpoint and prompt.
|
||||
- Deduplicate stable conflicts across polls and restarts.
|
||||
- Re-arm conflict repair after resolution, a new head, or a target change.
|
||||
- Accept failed-check queue removals only when durable attempted or adopted
|
||||
queue evidence matches the current head.
|
||||
- Preserve queue-removal event deduplication and same-head requeue protection.
|
||||
- Update localized help and public GitHub automation documentation.
|
||||
- Prove the behavior through backend tests and responsive E2E tests.
|
||||
|
||||
### Out of scope
|
||||
|
||||
- Changing the auto-fix switch label.
|
||||
- Prompting before pull-request checks settle.
|
||||
- Automatic selection of a merge strategy.
|
||||
- Same-head requeue after a merge-queue removal.
|
||||
- GitLab merge-request behavior.
|
||||
- A database migration or new automation option.
|
||||
|
||||
## Technical approach
|
||||
|
||||
### Auto-fix checkpoint
|
||||
|
||||
Extend `ciAutomationCheckpoint` in
|
||||
`apps/backend/internal/orchestrator/event_handlers_github_ci_automation.go` with
|
||||
an additive conflict snapshot. Include the normalized state, head commit, head
|
||||
branch, and base branch.
|
||||
|
||||
Build the conflict delta from `TaskPR` beside the current full-feedback delta.
|
||||
Refresh the checkpoint when a prior conflict clears. Render conflict details
|
||||
through the existing sanitized `{{pr.feedback}}` snapshot.
|
||||
|
||||
### Merge-queue removal eligibility
|
||||
|
||||
Change `ciAutomationQueueRemovalBelongsToCurrentHead` to require a non-empty
|
||||
`last_queue_attempt_head_sha` that equals the current pull-request head and a
|
||||
non-empty `last_merge_signature`. A passive removal-only baseline must fail
|
||||
closed for auto-fix.
|
||||
|
||||
Keep `last_queue_fix_event_id` as the repair deduplication identity. Keep the
|
||||
existing same-head auto-merge guard and merge-attempt journal unchanged.
|
||||
|
||||
### Help and documentation
|
||||
|
||||
Update `autoFixRoundExplanation` and `autoFixPromptDescription` in all required
|
||||
GitHub locale catalogs. The text must name ordinary conflicts and actionable
|
||||
queue removals without changing the switch label.
|
||||
|
||||
Update the GitHub automation sections in `docs/public/integrations.md` and
|
||||
`docs/public/sessions-and-review.md`. These explanation pages must describe the
|
||||
same trigger and deduplication behavior.
|
||||
|
||||
### Responsive evidence
|
||||
|
||||
Reuse the existing PR status popover on desktop and PR status drawer on phones.
|
||||
No composition or geometry changes are planned.
|
||||
|
||||
Extend the existing mock GitHub E2E flow with a `dirty` mergeability update.
|
||||
Enable auto-fix through each responsive surface and assert one visible repair
|
||||
round. Repeat the same snapshot and assert that no duplicate round appears.
|
||||
|
||||
## Tests
|
||||
|
||||
- `AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.1` through `.8`: add
|
||||
orchestrator tests for conflict delta creation, prompt rendering, stable-state
|
||||
deduplication, prompt-free clearing, and re-arming.
|
||||
- `AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.1`, `.3`, `.7`, and `.9`:
|
||||
prove one failed-check repair round with durable queue-attempt evidence and
|
||||
reject a retained removal first observed after a head change.
|
||||
- `AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.9`: assert the shared help
|
||||
content in the existing desktop and mobile automation E2E files.
|
||||
- `AC-UI-CI-PR-AUTOMATION-001.8` and
|
||||
`AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.2`: prove that both responsive surfaces
|
||||
explain the complete trigger set and queue-recovery behavior.
|
||||
|
||||
The first failing backend regressions are
|
||||
`TestHandleTaskPRCIAutomationAutoFixesConflict` and
|
||||
`TestCIAutomationMergeQueueRecoveryRequiresAttemptEvidence`.
|
||||
|
||||
## E2E tests
|
||||
|
||||
- Desktop: extend `apps/web/e2e/tests/pr/ci-automation-options.spec.ts` with a
|
||||
merge-conflict auto-fix scenario in the existing hover popover.
|
||||
- Mobile: extend
|
||||
`apps/web/e2e/tests/pr/mobile-ci-automation-options.spec.ts` with the same
|
||||
outcome through the existing touch drawer.
|
||||
- Both scenarios use stable test IDs, assert the visible conflict snapshot and
|
||||
help text, and prove that one unchanged poll does not add a second round.
|
||||
|
||||
The mobile test uses the current PR status drawer as its nearest exemplar. The
|
||||
drawer remains the only scroll owner, and the existing 44-pixel switch rows are
|
||||
unchanged.
|
||||
|
||||
## Work orders
|
||||
|
||||
- [x] [Task 01: Correct auto-fix signals](task-01-correct-auto-fix-signals.md)
|
||||
- [x] [Task 02: Explain auto-fix signals](task-02-explain-auto-fix-signals.md)
|
||||
- [x] [Task 03: Prove responsive auto-fix](task-03-prove-responsive-auto-fix.md)
|
||||
|
||||
## Verification results
|
||||
|
||||
Passed:
|
||||
|
||||
- `go test -tags fts5 ./internal/orchestrator -run 'Test(CIAutomationConflictSignal|HandleTaskPRCIAutomationAutoFixesConflict|CIAutomationMergeQueueRecoveryRequiresAttemptEvidence|CIAutomationMergeQueueRecoveryQueuesOneRepairPerRemoval)$'` from `apps/backend`.
|
||||
- `go test -tags fts5 ./internal/orchestrator -run 'TestCIAutomation(ConflictSignal|RenderSnapshotIncludesConflict)|TestHandleTaskPRCIAutomationAutoFixesConflict' -count=1` from `apps/backend`.
|
||||
- `pnpm run i18n:check` from `apps/web`.
|
||||
- `node --test scripts/validate-public-docs.test.mjs` and `node scripts/validate-public-docs.mjs` from the repository root.
|
||||
- `python3 scripts/lint-spec-files.py --all` from the repository root.
|
||||
- `pnpm e2e:run --no-build tests/pr/ci-automation-options.spec.ts -- --grep 'merge conflict|merge queue recovery'` from `apps/web`: 2 passed.
|
||||
- `pnpm e2e:run --no-build --project mobile-chrome tests/pr/mobile-ci-automation-options.spec.ts -- --grep 'merge conflict|merge queue recovery'` from `apps/web`: 2 passed.
|
||||
|
||||
## Risks
|
||||
|
||||
- A stale removal event can appear after the active queue entry disappears.
|
||||
Auto-fix requires durable queue-attempt evidence and fails closed when the
|
||||
retained event cannot be associated with the current head.
|
||||
- GitHub can report mergeability as unknown during recomputation. Only `dirty`
|
||||
starts conflict repair, and unknown state must not clear prior deduplication.
|
||||
- A conflict and failed checks can appear in one evaluation. The combined
|
||||
checkpoint must consume one round, not two.
|
||||
- Localized help can grow inside the phone drawer. The focused mobile E2E test
|
||||
must confirm that the current scroll and overflow behavior remains valid.
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
id: "01-correct-auto-fix-signals"
|
||||
title: "Correct auto-fix signals"
|
||||
status: done
|
||||
wave: 1
|
||||
depends_on: []
|
||||
plan: "plan.md"
|
||||
requirements:
|
||||
- REQ-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001
|
||||
- REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002
|
||||
acceptance_criteria:
|
||||
- AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.1
|
||||
- AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.2
|
||||
- AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.3
|
||||
- AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.4
|
||||
- AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.5
|
||||
- AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.6
|
||||
- AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.7
|
||||
- AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.8
|
||||
- AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.1
|
||||
- AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.3
|
||||
- AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.7
|
||||
- AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.9
|
||||
system_design:
|
||||
- ../../specs/integrations/system-design/github-pr-auto-fix-conflicts.md
|
||||
- ../../specs/integrations/system-design/github-pr-merge-queue-recovery.md
|
||||
---
|
||||
# Task 01: Correct Auto-Fix Signals
|
||||
|
||||
## Summary
|
||||
|
||||
Add ordinary merge conflicts to the auto-fix checkpoint and prompt. Let an
|
||||
actionable failed-check queue removal use durable attempted or adopted queue
|
||||
evidence, and fail closed when its head provenance is unavailable.
|
||||
|
||||
## In scope
|
||||
|
||||
- Add an additive conflict snapshot to `ciAutomationCheckpoint`.
|
||||
- Build current and delta conflict state from `TaskPR`.
|
||||
- Render sanitized conflict context when `{{pr.feedback}}` is present.
|
||||
- Clear resolved conflict state without using a round.
|
||||
- Re-arm a changed or recurring conflict.
|
||||
- Require a matching queue-attempt head and merge signature for queue-removal
|
||||
auto-fix eligibility.
|
||||
- Add focused orchestrator regression tests with TDD.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- UI copy, public documentation, or E2E tests.
|
||||
- Queue-attempt persistence changes.
|
||||
- Auto-merge retry behavior.
|
||||
- Database schema changes.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- One settled `dirty` snapshot produces one auto-fix round with conflict
|
||||
context. An unchanged snapshot produces no duplicate.
|
||||
- An authoritative resolution clears the conflict checkpoint without a round.
|
||||
Unknown state preserves deduplication. A recurring or changed conflict can
|
||||
produce one new round.
|
||||
- One retained `checks_failed` removal with matching durable queue-attempt
|
||||
evidence produces one round. A removal first observed after a head change
|
||||
without that evidence produces no prompt or round. Its event ID still
|
||||
deduplicates later polls.
|
||||
|
||||
## Verification
|
||||
|
||||
Run from `apps/backend`:
|
||||
|
||||
```bash
|
||||
go test -tags fts5 ./internal/orchestrator -run 'Test(CIAutomationConflictSignal|HandleTaskPRCIAutomationAutoFixesConflict|CIAutomationMergeQueueRecoveryRequiresAttemptEvidence|CIAutomationMergeQueueRecoveryQueuesOneRepairPerRemoval)$'
|
||||
```
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/backend/internal/orchestrator/event_handlers_github_ci_automation.go`
|
||||
- `apps/backend/internal/orchestrator/event_handlers_github_ci_automation_test.go`
|
||||
- `apps/backend/internal/orchestrator/event_handlers_github_ci_merge_queue_recovery_test.go`
|
||||
|
||||
## Dependencies
|
||||
|
||||
None.
|
||||
|
||||
## Risks
|
||||
|
||||
- Checkpoint refresh must remove resolved conflict state without erasing active
|
||||
failed-check or comment state.
|
||||
- Combined conflict and queue-removal feedback must use one dispatch and round.
|
||||
|
||||
## Parallelism
|
||||
|
||||
`sequential`
|
||||
|
||||
## Inputs
|
||||
|
||||
- Both referenced integration designs.
|
||||
- Existing feedback delta, checkpoint refresh, prompt renderer, and
|
||||
queue-removal tests.
|
||||
|
||||
## Results
|
||||
|
||||
Implemented additive conflict checkpoint and prompt rendering, authoritative
|
||||
conflict clearing with unknown-state preservation, conflict deduplication and
|
||||
re-arming, durable session pinning across checkpoint refreshes, and fail-closed
|
||||
queue-removal provenance. The focused Go command passed with 8 tests, and the
|
||||
conflict rendering regression passed separately.
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
id: "02-explain-auto-fix-signals"
|
||||
title: "Explain auto-fix signals"
|
||||
status: done
|
||||
wave: 2
|
||||
depends_on:
|
||||
- "01-correct-auto-fix-signals"
|
||||
plan: "plan.md"
|
||||
requirements:
|
||||
- REQ-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001
|
||||
- REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002
|
||||
- REQ-UI-CI-PR-AUTOMATION-001
|
||||
- REQ-UI-CI-PR-MERGE-QUEUE-RECOVERY-001
|
||||
acceptance_criteria:
|
||||
- AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.6
|
||||
- AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.9
|
||||
- AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.1
|
||||
- AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.7
|
||||
- AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.9
|
||||
- AC-UI-CI-PR-AUTOMATION-001.8
|
||||
- AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.2
|
||||
system_design:
|
||||
- ../../specs/integrations/system-design/github-pr-auto-fix-conflicts.md
|
||||
- ../../specs/integrations/system-design/github-pr-merge-queue-recovery.md
|
||||
- ../../specs/ui/system-design/ci-pr-automation-01.md
|
||||
- ../../specs/ui/system-design/ci-pr-merge-queue-recovery-controls.md
|
||||
---
|
||||
# Task 02: Explain Auto-Fix Signals
|
||||
|
||||
## Summary
|
||||
|
||||
Update shared automation help and public GitHub documentation. Explain ordinary
|
||||
conflicts, actionable queue removals, settled checks, deduplication, and round
|
||||
limits without changing the switch label.
|
||||
|
||||
## In scope
|
||||
|
||||
- Update `autoFixRoundExplanation` and `autoFixPromptDescription`.
|
||||
- Update English, Portuguese, Simplified Chinese, Hong Kong Chinese, Taiwan
|
||||
Chinese, and pseudo-locale content through the repository workflow.
|
||||
- Update the public integration and session-review explanation sections.
|
||||
- Keep the desktop popover and mobile drawer on the same copy keys.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- New controls, layouts, routes, or translation keys.
|
||||
- Backend behavior and E2E tests.
|
||||
- GitLab auto-fix copy.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Shared help names ordinary merge conflicts and actionable failed-check queue
|
||||
removals.
|
||||
- The prompt description states that `{{pr.feedback}}` can include ordinary
|
||||
conflict context.
|
||||
- Public documentation matches the implemented trigger, checkpoint, and round
|
||||
behavior.
|
||||
|
||||
## Verification
|
||||
|
||||
Run the first commands from `apps/web`:
|
||||
|
||||
```bash
|
||||
pnpm run i18n:zh-hant
|
||||
pnpm run i18n:check
|
||||
```
|
||||
|
||||
Run the remaining commands from the repository root:
|
||||
|
||||
```bash
|
||||
node --test scripts/validate-public-docs.test.mjs
|
||||
node scripts/validate-public-docs.mjs
|
||||
```
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/web/src/locales/en/github.json`
|
||||
- `apps/web/src/locales/pt-pt/github.json`
|
||||
- `apps/web/src/locales/zh-cn/github.json`
|
||||
- `apps/web/src/locales/zh-hk/github.json`
|
||||
- `apps/web/src/locales/zh-tw/github.json`
|
||||
- `apps/web/src/locales/pseudo/github.json`
|
||||
- `docs/public/integrations.md`
|
||||
- `docs/public/sessions-and-review.md`
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Task 01 defines the final shipped trigger behavior.
|
||||
|
||||
## Risks
|
||||
|
||||
- Longer copy must remain clear in the existing phone drawer.
|
||||
- Every locale must preserve `{{placeholder}}`, `<0>`, and interpolation tokens.
|
||||
|
||||
## Parallelism
|
||||
|
||||
`sequential`
|
||||
|
||||
## Inputs
|
||||
|
||||
- The conflict design's control-help section.
|
||||
- Existing GitHub automation help and public explanation sections.
|
||||
|
||||
## Results
|
||||
|
||||
Updated the shared English, Portuguese, Simplified Chinese, Hong Kong Chinese,
|
||||
Taiwan Chinese, and pseudo-locale GitHub copy, plus both public integration
|
||||
explanation pages. `pnpm run i18n:check`, public-doc tests and validation, and
|
||||
specification lint all passed.
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
id: "03-prove-responsive-auto-fix"
|
||||
title: "Prove responsive auto-fix"
|
||||
status: done
|
||||
wave: 3
|
||||
depends_on:
|
||||
- "02-explain-auto-fix-signals"
|
||||
plan: "plan.md"
|
||||
requirements:
|
||||
- REQ-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001
|
||||
- REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002
|
||||
- REQ-UI-CI-PR-AUTOMATION-001
|
||||
- REQ-UI-CI-PR-MERGE-QUEUE-RECOVERY-001
|
||||
acceptance_criteria:
|
||||
- AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.1
|
||||
- AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.2
|
||||
- AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.3
|
||||
- AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.6
|
||||
- AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.9
|
||||
- AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.1
|
||||
- AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.3
|
||||
- AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.9
|
||||
- AC-UI-CI-PR-AUTOMATION-001.8
|
||||
- AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.2
|
||||
system_design:
|
||||
- ../../specs/integrations/system-design/github-pr-auto-fix-conflicts.md
|
||||
- ../../specs/integrations/system-design/github-pr-merge-queue-recovery.md
|
||||
- ../../specs/ui/system-design/ci-pr-automation-01.md
|
||||
- ../../specs/ui/system-design/ci-pr-merge-queue-recovery-controls.md
|
||||
---
|
||||
# Task 03: Prove Responsive Auto-Fix
|
||||
|
||||
## Summary
|
||||
|
||||
Extend the existing desktop and mobile GitHub automation scenarios. Prove one
|
||||
ordinary conflict repair prompt, stable-state deduplication, and the complete
|
||||
help text through the shipped responsive surfaces.
|
||||
|
||||
## In scope
|
||||
|
||||
- Add a desktop conflict auto-fix scenario to the existing PR automation spec.
|
||||
- Add the same user outcome to the existing mobile automation spec.
|
||||
- Use the mock GitHub mergeability update to produce `dirty` state.
|
||||
- Enable auto-fix through the visible popover or drawer.
|
||||
- Assert the visible conflict snapshot, one round, and no duplicate round.
|
||||
- Assert help mentions ordinary conflicts and actionable queue removals.
|
||||
- Re-run the existing merge-queue recovery scenarios with the new regressions.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Live GitHub writes.
|
||||
- New mock-controller endpoints.
|
||||
- New responsive composition or geometry.
|
||||
- Product screenshots or video.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Desktop and mobile users can enable auto-fix for an already conflicted pull
|
||||
request and observe one agent repair turn.
|
||||
- Repeating the same `dirty` snapshot does not produce another turn.
|
||||
- Both surfaces show the complete help text. Existing merge-queue recovery
|
||||
remains green.
|
||||
|
||||
## Verification
|
||||
|
||||
Run from `apps/web`:
|
||||
|
||||
```bash
|
||||
pnpm e2e:run tests/pr/ci-automation-options.spec.ts -- --grep 'merge conflict|merge queue recovery'
|
||||
pnpm e2e:run --no-build --project mobile-chrome tests/pr/mobile-ci-automation-options.spec.ts -- --grep 'merge conflict|merge queue recovery'
|
||||
```
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/web/e2e/tests/pr/ci-automation-options.spec.ts`
|
||||
- `apps/web/e2e/tests/pr/mobile-ci-automation-options.spec.ts`
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Task 01 supplies conflict dispatch and corrected queue-removal eligibility.
|
||||
- Task 02 supplies the final localized help content.
|
||||
|
||||
## Risks
|
||||
|
||||
- Automation delivery is asynchronous. Tests must wait on the provider or
|
||||
session state that causes the visible transcript change.
|
||||
- Desktop hover and mobile tap can mount separate responsive surfaces. Each
|
||||
assertion must scope to the active surface.
|
||||
|
||||
## Parallelism
|
||||
|
||||
`sequential`
|
||||
|
||||
## Inputs
|
||||
|
||||
- Existing desktop and mobile CI automation E2E fixtures.
|
||||
- Existing merge-queue recovery scenarios and causal wait helpers.
|
||||
|
||||
## Results
|
||||
|
||||
Added desktop popover and mobile drawer conflict scenarios. Each proves one
|
||||
repair message and round, stable-state deduplication, visible conflict
|
||||
context, and complete help copy. The final desktop and mobile targeted E2E
|
||||
commands each passed 2 tests, including the existing merge-queue recovery
|
||||
scenario.
|
||||
@@ -326,9 +326,11 @@ For a task with linked GitHub pull requests, open the PR status control above th
|
||||
|
||||
This is a GitHub-only lifecycle feature. Kandev reuses the existing lightweight task PR poller, which checks watched linked PRs roughly once per minute; it does not add a separate scheduler. Saving enabled options also evaluates the task's current linked PRs without waiting for the next poll.
|
||||
|
||||
Auto-fix waits for the pull request checks to settle, then sends one repair round for newly failed checks, review comments, an ordinary merge conflict, or an actionable merge-queue removal. It snapshots each check, comment, conflict, and removal state, so repeated observations do not create duplicate rounds. An authoritative conflict resolution clears its checkpoint without using a round; an unknown mergeability state preserves the checkpoint. Updating an already queued auto-fix message does not use another round, and auto-fix pauses after 10 rounds unless the user disables and re-enables it.
|
||||
|
||||
When GitHub puts a linked pull request in a merge queue, the PR status control shows its queue state. It also shows the queue position and estimated merge time when GitHub provides them. The same status appears in the task summary, the mobile PR chip, and Review.
|
||||
|
||||
The existing two automation switches also control merge-queue recovery. Auto-fix sends one actionable queue removal to the linked task agent and counts it as one auto-fix round. Auto-merge submits an eligible pull request through GitHub's queue-aware merge action. If GitHub removes a queue attempt, Kandev records the reason and waits for a new pull-request head before it submits another attempt; it never retries the same head automatically. An active queue entry is adopted when auto-merge is enabled, so enabling the option does not submit a duplicate request. Unknown, manual, and branch-protection removals remain visible but do not start automatic repair.
|
||||
The existing two automation switches also control merge-queue recovery. Auto-fix sends one actionable queue removal to the linked task agent and counts it as one auto-fix round when durable evidence from an attempted or adopted queue entry matches the current pull-request head. If Kandev sees only a retained removal and cannot prove its head, it fails closed and does not spend a repair round. Auto-merge submits an eligible pull request through GitHub's queue-aware merge action. If GitHub removes a queue attempt, Kandev records the reason and waits for a new pull-request head before it submits another attempt; it never retries the same head automatically. An active queue entry is adopted when auto-merge is enabled, so enabling the option does not submit a duplicate request. Unknown, manual, and branch-protection removals remain visible but do not start automatic repair.
|
||||
|
||||
Each automatic merge request uses the pull-request head that passed the readiness checks. If GitHub reports a different head, Kandev stops the request. After a failed request, Kandev does not automatically repeat the same readiness state.
|
||||
|
||||
|
||||
@@ -279,6 +279,8 @@ The PR panel has two action controls:
|
||||
- **Auto-fix CI and address comments** waits for a check run to finish, then sends newly failed checks or review comments to the agent. It refreshes about once a minute, coalesces queued updates, and stops after 10 repair rounds for that PR. Disable and re-enable it after manual review to reset the limit.
|
||||
- **Auto-merge when ready** merges only after CI, required reviews, and mergeability are all ready.
|
||||
|
||||
Auto-fix also sends one repair round for an ordinary merge conflict or an actionable merge-queue removal after the PR checks settle. It snapshots each feedback state, so the same conflict, check, comment, or removal does not create duplicate rounds. A resolved conflict clears its checkpoint without using a round, while an unknown mergeability state keeps the prior checkpoint. A retained queue removal starts repair only when durable evidence from an attempted or adopted queue entry matches the current pull-request head. If that provenance is unavailable, Kandev fails closed without using a round. Updating an already queued message does not use another round, and the 10-round limit still applies.
|
||||
|
||||
Open **PR events** for three notification controls:
|
||||
|
||||
- **Your review is requested** wakes the agent for any new request, including re-review after changes.
|
||||
|
||||
@@ -43,6 +43,7 @@ outcomes that expose those contracts.
|
||||
- [External MCP Endpoint](requirements/external-mcp.md)
|
||||
- [External Question Answering — authorized, discoverable, idempotent clarification resolution](requirements/external-question-answering.md)
|
||||
- [Workspace GitHub Authentication](requirements/github-authentication.md)
|
||||
- [GitHub PR Auto-Fix Conflicts](requirements/github-pr-auto-fix-conflicts.md)
|
||||
- [GitHub PR Merge Queue](requirements/github-pr-merge-queue.md)
|
||||
- [GitHub PR Merge Queue Recovery](requirements/github-pr-merge-queue-recovery.md)
|
||||
- [GitHub task pull request sync coordination](requirements/github-task-pr-sync-coordination.md)
|
||||
@@ -75,6 +76,7 @@ outcomes that expose those contracts.
|
||||
- [Workspace GitHub Authentication System Design Part 1](system-design/github-authentication-01.md)
|
||||
- [Workspace GitHub Authentication System Design Part 2](system-design/github-authentication-02.md)
|
||||
- [Workspace GitHub Authentication System Design Part 3](system-design/github-authentication-03.md)
|
||||
- [GitHub PR Auto-Fix Conflicts](system-design/github-pr-auto-fix-conflicts.md)
|
||||
- [GitHub PR Merge Queue](system-design/github-pr-merge-queue.md)
|
||||
- [GitHub PR Merge Queue Recovery](system-design/github-pr-merge-queue-recovery.md)
|
||||
- [GitHub task pull request sync coordination](system-design/github-task-pr-sync-coordination.md)
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
status: draft
|
||||
system: integrations
|
||||
created: 2026-09-05
|
||||
owners:
|
||||
- Kandev
|
||||
---
|
||||
# GitHub PR Auto-Fix Conflicts Requirements
|
||||
|
||||
## Overview
|
||||
|
||||
GitHub reports a pull request with unresolved merge conflicts as `dirty`.
|
||||
Kandev blocks auto-merge for this state, but auto-fix does not send the state
|
||||
to the linked task agent.
|
||||
|
||||
The existing auto-fix option must treat a merge conflict as actionable pull
|
||||
request feedback. Merge-queue removals remain under the merge-queue recovery
|
||||
contract.
|
||||
|
||||
## Requirements
|
||||
|
||||
### REQ-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001: Conflict repair prompt
|
||||
|
||||
**Intent:** Auto-fix sends a current merge conflict to the linked task agent
|
||||
without sending the same conflict state on every poll.
|
||||
|
||||
#### Acceptance criteria
|
||||
|
||||
- **AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.1:** When auto-fix is
|
||||
enabled, checks are settled, and an open linked pull request is `dirty`,
|
||||
Kandev shall send or queue one auto-fix prompt.
|
||||
- **AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.2:** When a user enables
|
||||
auto-fix while the linked pull request is already `dirty`, the next eligible
|
||||
evaluation shall send or queue the conflict prompt.
|
||||
- **AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.3:** The conflict state,
|
||||
head commit, head branch, and base branch shall participate in the auto-fix
|
||||
checkpoint. Repeated observations of the same snapshot shall not send a
|
||||
duplicate prompt.
|
||||
- **AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.4:** When an authoritative
|
||||
mergeability state shows that the conflict cleared, Kandev shall clear it
|
||||
from the checkpoint without using another auto-fix round.
|
||||
- **AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.5:** When a conflict
|
||||
reappears after it clears, or the head commit or target branch changes while
|
||||
the pull request remains `dirty`, Kandev shall allow one new auto-fix round.
|
||||
- **AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.6:** When the auto-fix
|
||||
prompt includes `{{pr.feedback}}`, the visible snapshot shall identify the
|
||||
merge conflict and its head and base branches.
|
||||
- **AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.7:** Conflict prompts shall
|
||||
use the existing session selection, durable queue, coalescing, and 10-round
|
||||
limit for that linked pull request.
|
||||
- **AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.8:** Mergeability states
|
||||
other than `dirty` shall not start a conflict repair prompt.
|
||||
- **AC-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001.9:** The desktop popover
|
||||
and mobile drawer help shall state that auto-fix handles ordinary merge
|
||||
conflicts and actionable merge-queue removals.
|
||||
|
||||
## Related requirements
|
||||
|
||||
- [GitHub PR Merge Queue Recovery](github-pr-merge-queue-recovery.md)
|
||||
- [Task PR Automation Controls](../../ui/requirements/ci-pr-automation.md)
|
||||
- [Merge Queue Recovery Controls](../../ui/requirements/ci-pr-merge-queue-recovery-controls.md)
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Changing the auto-fix switch label.
|
||||
- Starting a conflict repair round while pull-request checks are unsettled.
|
||||
- Selecting a merge strategy for the agent.
|
||||
- Editing branch protection, rulesets, or workflow files.
|
||||
- GitLab merge-request behavior.
|
||||
@@ -71,11 +71,18 @@ task agent without sending the same removal more than once.
|
||||
busy, the queue-recovery prompt shall use the existing durable auto-fix queue
|
||||
and coalescing rules.
|
||||
- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.7:** When a user enables
|
||||
auto-fix after Kandev retained an unhandled actionable removal for the
|
||||
current head, the next evaluation shall send or queue one repair prompt.
|
||||
auto-fix after Kandev retained an unhandled actionable removal with durable
|
||||
queue-attempt evidence for the current head, the next evaluation shall send
|
||||
or queue one repair prompt.
|
||||
- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.8:** Kandev shall not
|
||||
treat the removal event's commit as a merge-group commit unless GitHub
|
||||
identifies it as such.
|
||||
- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.9:** When Kandev first
|
||||
observes an actionable removal after the active queue entry disappears, it
|
||||
shall start auto-fix only when durable evidence from an attempted or adopted
|
||||
queue entry matches the current pull-request head. A passive removal-only
|
||||
baseline is not sufficient provenance, so Kandev shall fail closed when that
|
||||
evidence is unavailable.
|
||||
|
||||
### REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003: Safe automatic requeue
|
||||
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
---
|
||||
status: draft
|
||||
system: integrations
|
||||
requirements:
|
||||
- REQ-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001
|
||||
created: 2026-09-05
|
||||
owners:
|
||||
- Kandev
|
||||
---
|
||||
# GitHub PR Auto-Fix Conflicts System Design
|
||||
|
||||
## Purpose and boundaries
|
||||
|
||||
The integration system owns GitHub mergeability state and auto-fix signal
|
||||
classification. This design adds ordinary merge conflicts to the existing
|
||||
per-PR auto-fix evaluator.
|
||||
|
||||
The design reuses the current poller, prompt renderer, session dispatcher,
|
||||
round limit, and automation controls. Merge-queue removal behavior remains in
|
||||
[GitHub PR Merge Queue Recovery](github-pr-merge-queue-recovery.md).
|
||||
|
||||
## Requirement mapping
|
||||
|
||||
| Requirement | Design section |
|
||||
| --- | --- |
|
||||
| `REQ-INTEGRATIONS-GITHUB-PR-AUTO-FIX-CONFLICTS-001` | [Conflict classification](#conflict-classification), [Checkpoint and prompt](#checkpoint-and-prompt), [Control help](#control-help) |
|
||||
|
||||
## Components and responsibilities
|
||||
|
||||
`TaskPR.MergeableState` carries the normalized GitHub mergeability state from
|
||||
the latest lightweight pull-request sync.
|
||||
|
||||
`handleTaskPRCIAutoFix` in
|
||||
`apps/backend/internal/orchestrator/event_handlers_github_ci_automation.go`
|
||||
combines full feedback with pull-request state. It then builds the actionable
|
||||
delta and current checkpoint.
|
||||
|
||||
The existing auto-fix dispatcher owns session selection, direct delivery,
|
||||
durable queueing, coalescing, and round accounting. This change does not add a
|
||||
second dispatch path.
|
||||
|
||||
## Conflict classification
|
||||
|
||||
The evaluator classifies only normalized `dirty` mergeability as an ordinary
|
||||
merge conflict. It does not classify `blocked`, `behind`, `draft`, `unknown`,
|
||||
or an empty state as a conflict.
|
||||
|
||||
The existing settled-check gate applies before conflict evaluation. This gate
|
||||
keeps one auto-fix round focused on the final check, review, and conflict
|
||||
snapshot.
|
||||
|
||||
## Checkpoint and prompt
|
||||
|
||||
`ciAutomationCheckpoint` adds an optional conflict item. The item contains:
|
||||
|
||||
- normalized mergeability state.
|
||||
- head commit.
|
||||
- head branch.
|
||||
- base branch.
|
||||
|
||||
The JSON field is additive. Existing checkpoint rows decode with no conflict
|
||||
item, so the change needs no database migration.
|
||||
|
||||
The current checkpoint contains one conflict item while the pull request is
|
||||
`dirty`. The delta contains that item when no identical item exists in the
|
||||
previous checkpoint.
|
||||
|
||||
An authoritative non-conflict observation removes the item during the existing
|
||||
prompt-free checkpoint refresh. An empty or `unknown` state preserves the
|
||||
prior item because GitHub can report these states during recomputation. A later
|
||||
`dirty` observation is actionable after an authoritative clear. A changed head
|
||||
commit or base branch also creates a new snapshot while the state remains
|
||||
`dirty`.
|
||||
|
||||
When `{{pr.feedback}}` is present, `ciAutomationRenderSnapshot` adds a merge
|
||||
conflict section. It names the sanitized head and base branches and the head
|
||||
commit. The saved prompt remains hidden system context.
|
||||
|
||||
## Queue-removal compatibility
|
||||
|
||||
The same evaluator can include an ordinary conflict item and a queue-removal
|
||||
item in one delta. The dispatcher sends one combined round for that evaluation.
|
||||
|
||||
For failed-check queue removals, current-head ownership requires both a
|
||||
matching durable `last_queue_attempt_head_sha` and a non-empty
|
||||
`last_merge_signature` written by an attempted or adopted queue entry. A
|
||||
passive removal-only baseline does not prove which pull-request head produced
|
||||
the removal, so it cannot start an auto-fix round. The removal event ID remains
|
||||
the deduplication identity, and same-head automatic requeue protection remains
|
||||
unchanged.
|
||||
|
||||
## Control help
|
||||
|
||||
`PRCIAutomationControls` keeps the existing switch label and responsive
|
||||
surfaces. English and translated help state that auto-fix handles these inputs:
|
||||
|
||||
- settled failed checks.
|
||||
- review comments.
|
||||
- ordinary merge conflicts.
|
||||
- actionable merge-queue removals.
|
||||
|
||||
The existing desktop popover and mobile drawer use the same localized copy.
|
||||
No layout, navigation, touch target, or scroll behavior changes.
|
||||
|
||||
## Failure and recovery
|
||||
|
||||
- If mergeability is unknown, Kandev preserves prior conflict checkpoint state
|
||||
and waits for a later authoritative state.
|
||||
- If checks are unsettled, Kandev waits and does not use a round.
|
||||
- If prompt delivery fails, the existing per-PR error and retry behavior apply.
|
||||
- If a conflict clears, the checkpoint refresh is prompt-free.
|
||||
- If Kandev restarts, the persisted JSON checkpoint preserves deduplication.
|
||||
|
||||
## Security
|
||||
|
||||
Branch names and commit identities are untrusted provider data. Snapshot
|
||||
rendering uses the existing field sanitizer and `sysprompt.Wrap` boundary.
|
||||
|
||||
The change does not expand GitHub write permissions. The agent uses its current
|
||||
task and repository permissions to repair the branch.
|
||||
|
||||
## Observability
|
||||
|
||||
Existing CI automation logs identify the task, repository, pull request, and
|
||||
dispatch result. Focused tests cover conflict classification, checkpoint
|
||||
transitions, queue-removal eligibility, and prompt rendering.
|
||||
|
||||
## Related decisions and designs
|
||||
|
||||
- [PR agent notifications](../../../decisions/0051-pr-agent-notifications-extend-task-pr-automation.md)
|
||||
- [Bind automatic merge attempts to the reviewed head](../../../decisions/2026-08-28-bind-github-auto-merge-attempts-to-reviewed-head.md)
|
||||
- [Task PR automation design](../../ui/system-design/ci-pr-automation-02.md)
|
||||
- [GitHub PR Merge Queue Recovery](github-pr-merge-queue-recovery.md)
|
||||
@@ -134,6 +134,14 @@ actionable, belongs to the current head, and has not been checkpointed, the
|
||||
evaluator accepts one repair round. Enabling the option while the pull request
|
||||
is still queued only arms this behavior for a later removal.
|
||||
|
||||
Auto-fix current-head ownership requires durable evidence from an observed
|
||||
active attempt or a submitted merge attempt. Both the recorded
|
||||
`last_queue_attempt_head_sha` and a non-empty `last_merge_signature` must be
|
||||
present before a retained removal can start a repair round. A removal-only
|
||||
baseline is still useful for the same-head automatic requeue guard, but it does
|
||||
not prove which head produced the removal and therefore fails closed for
|
||||
auto-fix.
|
||||
|
||||
## Requeue flow
|
||||
|
||||
The merge signature adds `TaskPR.HeadSHA`. This change makes a new commit a
|
||||
@@ -155,7 +163,8 @@ reconciliation rule.
|
||||
|
||||
When Kandev first observes a removal without an earlier active observation, it
|
||||
records a conservative baseline for the current head. This fallback prevents
|
||||
an immediate same-head requeue.
|
||||
an immediate same-head requeue. The same baseline lets auto-fix handle the
|
||||
actionable removal that produced it.
|
||||
|
||||
The normal readiness gates remain unchanged. Auto-merge submits a new request
|
||||
only when the head SHA changes and all gates pass. The existing
|
||||
|
||||
@@ -67,7 +67,11 @@ scoping; see that ADR's Consequences section).
|
||||
after changes.`, and the two terminal switches share an explanation that they
|
||||
wake the agent when review work ends while remaining independently
|
||||
configurable.
|
||||
- `Auto-fix CI & address comments` causes Kandev to send or queue an agent prompt when a linked PR gets actionable CI or review feedback.
|
||||
- `Auto-fix CI & address comments` causes Kandev to send or queue an agent
|
||||
prompt for provider-owned actionable feedback. GitHub ordinary conflicts
|
||||
follow the [integration contract](../../integrations/requirements/github-pr-auto-fix-conflicts.md),
|
||||
and merge-queue removals follow the
|
||||
[recovery contract](../../integrations/requirements/github-pr-merge-queue-recovery.md).
|
||||
- `Auto-merge when ready` causes Kandev to merge a linked PR only when the PR is open and not a draft, checks are passing, review requirements are satisfied, unresolved review threads are cleared, and the PR is cleanly mergeable.
|
||||
- `Your review is requested` follows the GitHub account connected to the task's
|
||||
workspace. It silently baselines that account's current request state, then
|
||||
@@ -153,7 +157,10 @@ scoping; see that ADR's Consequences section).
|
||||
- The auto-fix prompt is customizable per task from the PR CI popover.
|
||||
- The per-task prompt editor is opened from an edit button in the automation section.
|
||||
- The per-task prompt editor links to Settings > Prompts so the user can edit the default `ci-auto-fix` prompt.
|
||||
- The per-task prompt editor explains that `{{pr.feedback}}` is the placeholder that inserts Kandev's PR feedback snapshot. The explanation lists the included data: PR identifier, new or changed failing checks with job links, and new or changed review comments with file, line, and body text.
|
||||
- The per-task prompt editor explains that `{{pr.feedback}}` inserts Kandev's
|
||||
PR feedback snapshot. The explanation lists the PR identifier, changed
|
||||
failing checks, review comments, ordinary conflicts, and actionable
|
||||
merge-queue removal context.
|
||||
- Omitting `{{pr.feedback}}` from the prompt means Kandev still evaluates PR feedback for dedupe and trigger decisions, but it does not include the PR snapshot in the agent message. This supports prompts that tell the agent to pull/fetch the branch and inspect GitHub itself.
|
||||
- If a task has no custom auto-fix prompt, Kandev uses a built-in default prompt named `ci-auto-fix`.
|
||||
- The default `ci-auto-fix` prompt is editable from Settings > Prompts like other built-in prompts.
|
||||
@@ -168,7 +175,14 @@ scoping; see that ADR's Consequences section).
|
||||
task-level — one prompt override and one resolved reviewer identity apply
|
||||
to every linked PR. Dedupe, last-attempt, review-request, and terminal
|
||||
state are tracked per linked PR, as before.
|
||||
- Kandev checks watched PRs through the existing lightweight PR watch poller, which runs once per minute. Automation wakeups sync the latest lightweight PR state before evaluating gates. When auto-fix is enabled, Kandev fetches full PR feedback so failing checks, requested changes, unresolved threads, and human PR conversation comments can trigger deduped prompts even when the persisted lightweight row was stale. Auto-fix waits until all PR checks have finished before sending or queueing a prompt, so the agent receives the final check set and current comments in one pass. Bot-authored PR conversation comments without failed checks or unresolved review threads are treated as non-actionable status chatter and do not send an agent prompt.
|
||||
- Kandev checks watched PRs through the existing lightweight PR watch poller,
|
||||
which runs once per minute. Automation wakeups sync the latest lightweight
|
||||
PR state before evaluating gates. When auto-fix is enabled, Kandev fetches
|
||||
full PR feedback. Failing checks, requested changes, unresolved threads,
|
||||
human comments, and provider-owned conflict signals can trigger deduplicated
|
||||
prompts. Auto-fix waits until all PR checks finish, so one prompt contains
|
||||
the final actionable snapshot. Bot-authored status comments without another
|
||||
actionable signal do not send an agent prompt.
|
||||
- Lightweight PR status sync counts unresolved review threads across every page
|
||||
returned by GitHub. A connection's `totalCount` indicates that more threads
|
||||
exist; it never classifies omitted threads as unresolved. The CI popover,
|
||||
@@ -186,9 +200,13 @@ scoping; see that ADR's Consequences section).
|
||||
- Saving PR automation options while any option is enabled immediately
|
||||
evaluates the task's current linked PRs instead of waiting for the next PR
|
||||
watch poll. Prompt edits do not reset unchanged checkpoints.
|
||||
- Every auto-fix attempt records the latest actionable feedback snapshot it used. Later fix rounds include only new or materially changed CI/review feedback since the last recorded round, with enough summary context for the agent to understand the PR. If a previously recorded feedback snapshot becomes non-actionable after checks pass or review threads are cleared, Kandev can refresh the checkpoint without sending a prompt or counting another round.
|
||||
- Every auto-fix attempt records the latest actionable feedback snapshot.
|
||||
Later rounds include only new or materially changed checks, comments, or
|
||||
provider-owned conflict signals. When recorded feedback becomes
|
||||
non-actionable, Kandev refreshes the checkpoint without a prompt or round.
|
||||
- The first auto-fix round targets the task's active primary session when one exists. Once a PR has an accepted auto-fix round, later auto-fix prompts for that task/repository/PR continue targeting the recorded `last_fix_session_id`. A newer active agent session for the same task must not steal auto-fix messages. Disabling and re-enabling auto-fix resets this binding with the rest of the per-PR auto-fix state.
|
||||
- Automation must not repeatedly prompt for the same failure/comment snapshot or repeatedly retry the same failed merge attempt on every poll.
|
||||
- Automation must not repeatedly prompt for the same actionable snapshot or
|
||||
repeatedly retry the same failed merge attempt on every poll.
|
||||
- When auto-fix is enabled and the task session is busy, Kandev keeps at most one pending CI auto-fix queue entry per task/repository/PR. Newer feedback replaces that pending entry instead of appending another queued `@ci-auto-fix` message.
|
||||
- Auto-fix is capped at 10 accepted rounds per task/repository/PR. A round is counted when Kandev sends a prompt directly or inserts a new queued auto-fix prompt. Replacing an already queued auto-fix prompt does not count as another round.
|
||||
- The auto-fix enabled chip above the chat input shows round progress as `Auto-fix N/10`; PRs paused by the backend after the cap is reached show `Auto-fix 10/10` with warning/paused styling.
|
||||
|
||||
@@ -89,8 +89,12 @@ Auto-fix cycle for one task/PR:
|
||||
2. Kandev syncs the latest lightweight PR state for the task's linked PRs, including linked PR rows that do not currently have an active watch.
|
||||
3. Kandev fetches full PR feedback.
|
||||
4. If the latest lightweight PR state or fetched check list shows any queued, pending, or in-progress check, the cycle ends without prompting or counting a round.
|
||||
5. Kandev filters feedback down to prompt-worthy signals: failed, timed-out, cancelled, or action-required completed checks, unresolved review-thread comments, and human PR conversation comments. Bot-authored PR conversation comments without a failed check or unresolved thread are ignored before delta computation.
|
||||
6. Kandev compares the current feedback snapshot to `last_fix_checkpoint_json` and `last_fix_signature`.
|
||||
5. Kandev filters feedback down to prompt-worthy signals. These signals include
|
||||
failed completed checks, review comments, and provider-owned conflict state.
|
||||
Bot-authored status comments without another actionable signal are ignored.
|
||||
6. Kandev compares the current actionable snapshot to
|
||||
`last_fix_checkpoint_json` and `last_fix_signature`. GitHub conflict and
|
||||
queue-removal rules come from the integration system designs.
|
||||
7. If there is no material change, the cycle ends without prompting.
|
||||
8. If there is new or materially changed prompt-worthy feedback, Kandev renders the task override or default `ci-auto-fix` prompt and sends or queues it for the task session. If `last_fix_session_id` is already set for this task/repository/PR, Kandev targets that same session instead of the newest active session for the task. Otherwise, Kandev targets the active primary session when one exists, falling back to the newest active session only when there is no primary active session. The saved/shared `ci-auto-fix` instructions are hidden system context. If the rendered prompt contains `{{pr.feedback}}`, Kandev replaces it with visible PR snapshot details after `@ci-auto-fix`, before the agent output for that automation turn. If the placeholder is absent, no PR snapshot is included in the chat message.
|
||||
9. The default prompt instructs the agent to classify the new feedback before editing. If the
|
||||
@@ -131,7 +135,7 @@ Auto-merge cycle for one task/PR:
|
||||
| Task session is busy | Auto-fix queues the rendered prompt with workflow/automation metadata for later delivery; the visible `@ci-auto-fix` chat message, including PR snapshot details, is created when the queued prompt is delivered and before the agent's response for that turn. |
|
||||
| Task session is busy during a lifecycle event | Kandev queues one visible automation message per task/repository/PR/event and does not interrupt the running turn. Duplicate observations of that same event coalesce. |
|
||||
| Task session is busy and a pending auto-fix already exists for that PR | Kandev replaces the pending queued prompt with the latest feedback snapshot; it does not append a second queued message or increment the round count. |
|
||||
| Same feedback snapshot repeats | Auto-fix does not send another prompt. |
|
||||
| Same actionable snapshot repeats | Auto-fix does not send another prompt. |
|
||||
| Auto-fix reaches 10 rounds for a PR | Kandev pauses auto-fix for that task/repository/PR, records a visible error, and does not create an 11th round. Already exhausted PRs skip full feedback fetching on later watcher wakes. |
|
||||
| GitHub merge fails | Auto-merge records the error and does not retry until the readiness signature changes. |
|
||||
| Default prompt row is missing | Backend falls back to the embedded `ci-auto-fix.md` content. |
|
||||
|
||||
@@ -47,6 +47,9 @@ This design preserves the technical source detail for `REQ-UI-CI-PR-AUTOMATION-0
|
||||
- **GIVEN** a task with a custom auto-fix prompt, **WHEN** the user resets the prompt override, **THEN** the task uses the current default `ci-auto-fix` prompt.
|
||||
- **GIVEN** the default `ci-auto-fix` prompt is edited in Settings > Prompts, **WHEN** a task without an override later auto-fixes a PR, **THEN** the rendered prompt uses the edited default content.
|
||||
- **GIVEN** auto-fix is enabled and a watched PR transitions from passing to failing CI, **WHEN** the 1-minute PR watch poll observes the failure, **THEN** Kandev fetches full PR feedback and sends or queues one auto-fix prompt for that failure snapshot.
|
||||
- **GIVEN** auto-fix is enabled and checks are settled, **WHEN** a linked PR is
|
||||
`dirty`, **THEN** Kandev sends or queues one conflict repair prompt under the
|
||||
[GitHub integration contract](../../integrations/requirements/github-pr-auto-fix-conflicts.md).
|
||||
- **GIVEN** auto-fix is enabled and a PR still has queued, pending, or in-progress checks, **WHEN** automation evaluates the PR, **THEN** Kandev does not send or queue an `@ci-auto-fix` prompt and does not count a round, even if some checks have already failed or comments are present.
|
||||
- **GIVEN** auto-fix already prompted for a failure snapshot, **WHEN** the same failure is observed again on a later poll, **THEN** no duplicate prompt is sent.
|
||||
- **GIVEN** auto-fix already prompted for a failure snapshot, **WHEN** a new failed check or new unresolved review comment appears, **THEN** Kandev sends or queues a new prompt containing the new or materially changed feedback.
|
||||
|
||||
Reference in New Issue
Block a user