Imported from kunchenguid/no-mistakes (
.agents/skills/ci-monitor/SKILL.md). Install upstream withnpx skills add kunchenguid/no-mistakes --skill ci-monitor. Copyright stays with the author.
CI Monitor Lifecycle
- The green observation only clears the failure signal by default: the step keeps polling until the PR is merged, closed, or its idle timeout elapses, which is what every run has always done. Under the opt-in
ci.release_after_checks_pass(sctx.Config.CI.ReleaseAfterChecksPass, resolved from the operator's globalciblock and overridable by a trusted repository config)CIStep.Executereturns readiness inStepOutcome.CIReadyNoCIinstead of polling again;Executor.executeStepcommits it with CI completion throughdb.CompleteCIStep, which also stampsruns.ci_released_at, then emits readiness after the step-completed event. Release readiness must never appear with a running CI step, which would make AXI promise a live monitor. Readiness-write and terminal-verdict read failures fail the run. The run ends there and releases its worktree.Executor.terminalRunStatusrecordstypes.RunChecksPassedonly when that durable release marker is set (withpr_stateopen and the CI stepcompleted); it deliberately does not infer the verdict fromci_ready_at, because a monitoring run also records readiness and can end with an open PR after a timeout gate is approved.db.RecoverStaleRunsExceptfinalizes the durable state before generic stale-run failure if the daemon crashed before run completion, preserving the CI duration (TestRecoverStaleRunsCompletedCI,TestRecoverOnStartup_FinalizesCommittedCI);aximaps the status tooutcome: checks-passed(outcomeFor) and renders the released contract,types.RunStatus.Terminal()includes it, and a merge observed afterwards writes onlypr_state- never the run's recorded outcome, and never the CI step's status or duration (which is why a released run's recorded duration covers CI only). Regressions:TestCIStep_DefaultKeepsMonitoringAfterGreenChecks(the default),TestCIStep_GreenChecksReleaseWithoutWaitingForTheMerge,TestCIStep_TrustedNoCIReleasesWithoutWaiting,TestExecutor_GreenCIRunRecordsChecksPassedAndReleases,TestExecutor_MergedCIRunStillRecordsCompleted,TestRenderDriveResult_TerminalChecksPassed,TestDriveRun_ReleaseAndWatchGuidance,TestExecutor_CIReleaseReadinessFollowsStepCompletion,TestExecutor_CIReadinessWriteFailureFailsRun,TestExecutor_TerminalVerdictReadErrors,TestReleasedChecksPassedGuidance_SyncedAcrossSurfaces,TestCIReleaseAfterChecksPassDefaultsOffAndResolvesFromTrustedConfig. - The CI step produces findings, not fixes: a settled observation returns one finding per issue with an
action(ciObservationFindings,internal/pipeline/steps/ci_findings.go) and the executor's shared auto-fix loop drives fix rounds (repairFromFindingsinci_fix.goconsumessctx.PreviousFindings), soauto_fix.ciis counted by the executor from round history and a review-bot check (scm.ReviewBots, identified byscm.ReviewBotForCheck) parks asask-usercomment findings without spending a round. A published repair keeps monitoring inside the fix round and must callsctx.MarkRunning()so status returns torunningfor the readiness consumers. "CI Step Findings Model" below owns the contract and regression list. ci_timeoutis an idle timeout, not an absolute deadline: onlytimeoutAnchorre-arms when the upstream default-branch tip advances,startedstays fixed for poll pacing, and re-arm only ever extends the deadline (fail-safe on transient base-tip failures). It bounds the wait for checks to finish, and the merge watch after that unlessci.release_after_checks_passends the step at the verdict. Value semantics (0unset, negative unlimited sentinel, keyword parsing) live inconfig.go; keepconfig.DefaultCITimeoutanddefaultConfigYAMLin sync (TestDefaultConfigYAML_MatchesGoDefaults). User-facing semantics are owned bydocs/src/content/docs/reference/global-config.md.- GitHub readiness is the union of the exact current PR head commit's check rollup and every Actions workflow run returned by the Actions API for that same SHA. A workflow rejected before creating jobs/check-runs is absent from the commit rollup but still present in that API; run discovery errors and unknown run states fail closed instead of certifying a green rollup. Regressions:
TestGetChecksIncludesFailedWorkflowRunMissingFromPRRollup,TestGetChecksBindsRollupAcrossABAHeadMovement,TestCIStep_FailedHeadWorkflowRunPreventsChecksPassed. - GitHub's raw commit
statusCheckRollupreturns every check run a commit ever had, including a same-named run a later run at that same head has already superseded (e.g. a required-check job re-triggered bysynchronizeafter a pipeline auto-fix push).Host.GetChecks(internal/scm/github/github.go) collapses same-name reruns of one workflow to the neweststartedAt(using Actions run identity to break timestamp ties, comparing against every retained duplicate, and preserving unordered records, including ambiguous pending replacements, before acompletedAtfallback) while preserving independent workflows, unidentified external checks, and same-named commit status contexts, restoring the semanticsgh pr checksalready applies and this repo's own gateverify.pyalready assumes (last-wins). Collapse must run AFTERappendUnrepresentedWorkflowRuns, never before: that call dedupes the Actions-run union against checks by run ID over the FULL uncollapsed rollup, so collapsing first drops a superseded run's ID out of the "represented" set and the union re-adds the same stale run under its own workflow run name. Without this collapse, a monitor that keepssynchronizein its gate trigger set can loop forever: each auto-fix push legitimately fails the gate at the old head, a later same-head rebind turns it green, but the stale FAILURE stays visible and re-triggers another auto-fix round. Regressions:TestGetChecksCollapsesSupersededSameNameCheckToLatestAtOneHead,TestGetChecksCollapseOrderingDoesNotLetWorkflowRunUnionResurrectSupersededCheck,TestGetChecksPreservesIndependentSameNameWorkflows,TestGetChecksPreservesIndependentSameNameExternalCheckRuns,TestGetChecksCollapseComparesNewestRunWithEverySameNameCandidate,TestGetChecksKeepsQueuedReplacementWithEqualStartTime,TestGetChecksPreservesUnorderedExternalPendingReplacement,TestGetChecksPreservesSameNameStatusContextAndCheckRun. - A cancelled Actions run with zero jobs surfaces only as its run-level summary named after the workflow, while its same-head replacement surfaces only as job checks, so name-based collapse can never match them.
appendUnrepresentedWorkflowRunstherefore omits that cancelled summary when a later-started run with the sameWorkflowID(never name) is represented by job (CheckKindRun) checks; a replacement with no job checks, whatever its conclusion, an unknown workflow ID, or an unorderable start leaves the cancellation visible (fail closed). Regression:TestGetChecksSupersedesJoblessCancelledWorkflowOnlyWithALaterReplacement. - A published CI repair rebinds an existing pipeline attestation in the PR body to the new head (
restampPublishedAttestationafterpublishRepair) only when the host has an existing attestation and aPRContentReader. All supported providers implement raw reads; Bitbucket owned templates carry the same marker as visible text. Owned restamps enforce provider budgets before the adapter can clamp (especially Azure). Hosts without readers skip with a warning. A restamp that cannot settle after retries is unsettled, not success. The CI fixer prompt may conclude that a stale/attestation/infrastructure red is not a code defect; a genuine failing test or build must still be fixed. Regressions:TestCIStep_PublishRepairRebindsAttestationAcrossRepairPushes,TestCIStep_PublishRepairDoesNotMintAttestation,TestCIStep_PublishRepairSkipsRestampWithoutReader,TestRestampPRAttestation_MissingReaderIsSkipped,TestRebindPipelineAttestationHead_VerifyPyRoundTrip,TestCIStep_FixPromptPrefersSimplificationOverMachinery,TestCIStep_FixPromptDistinguishesCodeDefectFromExternalFailure. - CI readiness never treats an unproven empty forge check list as green. Ready requires observed all-green checks, or trusted default-branch
no_ci: truewith zero registered checks (internal/pipeline/steps/ci.godecides whether the declaration applies;internal/cimonitorowns the agent-facing log vocabulary and Ready/DeclaredNoCI parse). Delayed registration, pending checks, failures, errors, unknowns, and stale-head evidence stay not-ready; registered checks on a declared no-CI repo are still honored. Regressions:TestChecksPassed_PR607RealLogSequence,TestCIStep_EmptyChecksWithoutNoCIStaysNotReadyPastOldGracePeriod,TestCIStep_EmptyChecksWithTrustedNoCIBecomesReady,TestCIStep_DelayedCheckRegistrationStaysNotReadyUntilGreen,TestCIStep_DeclaredNoCIWithUnexpectedChecksHonorsThem,TestEffectiveRepoConfig_NoCITrustedOnly. - Persistent provider check-read failures are not an invisible spin: 6 consecutive
GetCheckserrors while the PR is still open park the CI step at an ask-user gate whose provider-neutral finding names the provider CLI/credentials support and includes the underlying error (with a GitHub-specific clause requiringgh>= 2.50 for thegh pr checks --jsoncall), and the streak resets on any successful read. Regression:TestCIStep_PersistentCheckReadFailureParksAtAskUser. - Reap an orphaned monitor from outside its worktree with
no-mistakes axi abort --run <id>; it needs onlyNM_HOMEand never starts a stopped daemon. A known run succeeds only with durable terminal truth, a recorded nonterminal run fails unconfirmed, and only an unknown id is an idempotent no-op. Bareaxi abortstays worktree/branch-scoped. - A merged or closed PR observation transactionally completes an active run and its CI step; PR lifecycle state is monotonic, so duplicate or delayed observations cannot reactivate or regress a terminal run. Startup reconciles legacy
pendingorrunningrows that already hold terminal PR state before parked-run planning and generic crash recovery. Regressions:TestUpdateRunPRStateFinalizesActiveTerminalOutcomes,TestUpdateRunPRStateIgnoresDuplicateAndDelayedRegressions,TestReconcileTerminalPRRunsFinalizesLegacyActiveRows,TestRecoverOnStartup_FinalizesLegacyTerminalPRRun, e2eTestTerminalPRRunDisappearsFromActiveListing. - A provider-reported
cancelledcheck is never a job verdict, so the deterministic rerun runs strictly before any CI fix round: cancellation and a GitHub pre-run infrastructure failure (a job that failed in setup/action resolution before any repository step ran, flaggedCheck.PreRunFailureby the opt-inscm.PreRunFailureDetectorand re-bucketed to cancel so it never masks a real test/lint failure, which cleared setup and failed a later step) are the outcomes that earn a rerun, either outcome after its budget parks asask-userinstead of entering theauto_fix.ciloop, and any genuine or unrecognized failure or merge conflict in the same poll suppresses reruns so real failures still escalate on their first observation. The budget is per check name per run and spent on request rather than on success, and a rerun is never issued once the published branch head no longer equalsruns.head_sha, because it would certify a commit this run never delivered. Each outstanding rerun records its verified pipeline head and the same-name provider links visible when it was requested; it retires durably when the run head advances or a new conclusive non-cancel link appears. Retirement keeps the spent budget and never changes check buckets. A delayed same-named green sibling can satisfy the link trigger on the same head, matching the default branch's existing name-keyed masking; removing that limitation requires provider truth outside this policy. Classification, the deliberateTIMED_OUT/STALEexclusions, rollup-lag grace, and retirement live ininternal/pipeline/steps/ci_transient.go; provider support is the optionalscm.CheckRerunner(GitHub only), pre-run infrastructure detection is the optionalscm.PreRunFailureDetector(GitHub only, folded in bymarkPreRunInfraFailures), and user-facing semantics are owned bydocs/src/content/docs/reference/repo-config.md. Regressions:TestCIStep_CancelledCheckIsRerunBeforeEscalating,TestCIStep_CancelledCheckStaysUnresolvedAfterItsBudget,TestCIStep_LaggingRerunRollupKeepsWaitingForTheRepublishedCheck,TestCIStep_SameHeadGreenRerunEmitsChecksPassed,TestCIStep_DelayedSameNameCheckRetainsLegacyNameBehavior,TestCIStep_ResolvedRerunDoesNotParkALaterGreenHead,TestRetireResolvedReruns,TestRetireResolvedRerunsRetriesAfterPersistenceFailure,TestCIStep_MovedPublishedHeadTerminatesInsteadOfRerunning,TestCIStep_MovedPublishedHeadClearsCIReadiness,TestClassifyCheckFailure,TestMarkPreRunInfraFailures_RetriesInfraButNotGenuine,TestMarkPreRunInfraFailures_OptInGated,TestPreRunFailures_FlagsSetupFailureNotGenuine,TestPreRunFailures_FailsClosedOnUnreadableRun. - Terminal is not pending. Readiness must reject every non pass/fail/skip bucket (
hasUnresolvedChecks), but only checks that can still finish on their own (hasExecutingPendingChecks) may defer escalation. A GitHub run held for maintainer approval (scm.Check.AwaitingApproval:action_requiredwith no jobs) is pending for readiness but never defers another check's failure (internal/pipeline/steps/ci_awaiting_approval_test.go). A GitLab blocking manual job (scm.Check.AwaitingManualJob, set bygitlabJob.awaitingManualJobininternal/scm/gitlab/gitlab.go:when: manualwithallow_failure: false) is the same kind of hold, and both answerscm.Check.HeldForHuman(): readiness waits on it, but it never defers escalation of a failure it is observed alongside (TestManualJobHoldIsAWaitNotAFailure,TestCIWaitingMessage_NamesTheManualJobHold,TestCIStep_GitLabBlockingManualJobNeverReportsChecksPassed). Acancelbucket - GitHubCANCELLED, GitLabcanceled, BitbucketSTOPPED, and how GitHub reports a job killed by its owntimeout-minutes- is a published conclusion that nothing will replace, so with no rerun outstanding it is reported as anask-usertransient finding (unresolvedTransientFindings, selected bycancelledWithoutRerun) instead of waiting. Conflating the two is the #628 regression that hung real runs for their wholeci_timeout; an unrecognized bucket is deliberately still treated as waiting, because unknown is not evidence of terminal. Regressions:TestCIStep_CancelledCheckAmongPassingChecksEscalatesInsteadOfPollingForever,TestCIStep_ZeroRerunBudgetEscalatesCancelledCheckWithoutMakingItReady,TestCIStep_BitbucketStoppedCheckParksForADecision. - A green GitHub PR reporting
mergeStateStatus: BEHIND(scm.MergeableBehind, parsed byparseMergeableStateininternal/scm/github/github.go; GitHub reports it only when the base requires up-to-date branches) reads as MERGEABLE and reportschecks-passedunless trustedci.rebase_behindopts in (off by default: each rebase is another full pass). Opted in, it is neverchecks-passed: once a second consecutive poll sees it green and BEHIND at the same resolved base tip (behindBaseTipinci.go), it becomes oneauto-fixci-behind-basefinding thatparseCIFixTargetsroutes into the merge-conflict rebase repair (ciFixTargets.BehindBase), so it revalidates from Review like a conflict. Other providers never report BEHIND. Regressions:TestGetMergeableStateReadsMergeStateStatusBehind,internal/pipeline/steps/ci_behind_base_test.go(default off:TestCIStep_GreenBehindPRReportsChecksPassedWhenRebaseBehindIsOff),TestCIRebaseBehind_OffByDefaultAndTrustedOnly. - A rebase repair (merge conflict or
BehindBase) normally revalidates from Review because ancestry cannot prove continuity. Trusted opt-inci.carry_review_on_replayletsrecordRepairtryrecordReplayedRepairfirst:pureReplayGap(ci_replay.go) must prove the repaired head is the reviewed chain replayed onto exactlyCIStep.replayBase(the resolved rebase target, bound only for that repair's recording) - equal-length linear chains, each reviewed commitgit merge-tree-replaying conflict-free onto its new parent to the repaired commit's exact tree, andgit patch-id --verbatimover the full binary diff equal. Any shape mismatch or git error fails closed to today's Review restart; the flag is ignored underci.revalidate_repairs: trueand when a gate is anchoredafter: review(Test restart would skip it). On success the approval moves viadb.UpdateRunHeadSHAWithCarriedReview, a compare-and-swap on the approval the proof used (never resurrects a revoked one), and the outcome isRestartFrom: types.StepTest(ciRepairResult.restartFrom). Nothing publishes early. That outcome does not survive a parked CI gate (a Fix response replaces it, a daemon restart drops it), so the same write sets the durableruns.ci_test_restart_pending, which onlyUpdateRunPublicationclears (CI never publishes while it is set, so only Push can). While set,recordRepairrestarts the unchanged head at Test and sends a commit on top torecordLocalRepair, a fix round whose fixer failed records its worktree head throughrecordRepairinstead of monitoring (a commit it left on top revalidates from Review, the unchanged head restarts at Test), andResumerestarts a recovered CI gate's approval at Test. Regressions:internal/pipeline/steps/ci_carry_review_on_replay_test.go,TestCICarryReviewOnReplay_OffByDefaultAndTrustedOnly,TestExecutor_RestartFromTestKeepsTheCompletedReview,TestExecutor_RecoveredCIGateApprovalRestartsAPendingCarryAtTest. - CI readiness is read from the provider's live PR head check rollup on every poll, so it always describes the head the forge currently has for that PR; no recorded SHA gates it, and a run whose row still names a pre-advance commit must still recognize green at the head the pipeline last pushed. Regression:
TestCIStep_GreenChecksAtAdvancedHeadAreRecognizedWhileRunTracksOlderHead. getCIChecks(ci_checks.go) is the shared observation boundary for monitoring, repair, and approval verification: it clearspr.ObservedHeadSHAbefore discovery, keeps it separate from requestedpr.HeadSHA, and normalizes read errors after observed movement toErrHeadChangedwithout hiding plugin protocol violations. Monitoring compares actual observations withlastCheckHeadSHAto clear timeout failures even on a later read error; a failed head lookup supplies no observation.repairFromFindingsrefreshes onlyFindingCategoryCIChecktargets, replacesobservedCompletedAtonly after all selected checks remain terminal failures, and rebuilds both targets and findings without rejected checks so independent rebase or bot-comment repairs cannot carry them into the prompt. User-facing semantics belong todocs/src/content/docs/reference/pipeline-steps.md; plugin wire rules belong todocs/src/content/docs/reference/provider-plugin-protocol.md. Regressions:TestCIResumedRepairEmptySnapshotSkipsRepairAndPreservesCompletionMap,TestCIResumedRepairRejectedChecksStillRepairsRebase,TestCIResumedRepairGreenReviewBotCommentReachesFixer,TestCIResumedRepairGitHubMovedHeadReadErrorWaits,TestCIStep_GitHubObservedHeadOnReadErrorResetsTimeoutFailures,TestCIStep_GitHubHeadObservationAcrossLookupFailures,TestGetChecksHeadSHAOptIn.
CI Step Findings Model
CIStep.Executehas the review step's shape: a fix round (sctx.Fixing) first repairs the findings the executor selected (repairFromFindings,internal/pipeline/steps/ci_fix.go), then the monitor runs; every settled observation (all checks finished, every authorized transient rerun spent, at least one issue) returns one finding per issue with its action (ciObservationFindings,ci_findings.go) and the executor's shared loop decides what follows. The step never counts or limits fix rounds;auto_fix.ciis enforced by the executor from the round history, which survivesResetStepsFromand daemon restarts.step_results.ci_fix_attemptsis retired (column kept, never read).- Classification reads provider structure only: a job verdict (fail bucket) and a merge conflict are
auto-fixerrors; an unresolved transient isask-user(unresolvedTransientFindings); a check authenticated byscm.ReviewBotForCheckagainstscm.ReviewBotsFor(ci.required_reviews)(an opt-in bot such as CodeRabbit,ReviewBot.RequiredReview, is absent unless trustedci.required_reviewsnames it, so its checks stay ordinary checks and its comments never match;ciIssues.requiredReviewscarries the list) becomes oneask-userwarning per unresolved bot comment, bounded (maxReviewBotCommentFindings), so it never spends a round.scm.ReviewBotForCheckowns publisher matching, including CodeRabbit's immutable identity and Greptile's legacy slug matching. The comment half uses provider-scopedscm.ReviewBotForLoginplus the registry'sAuthorIDwhen set; only GitHub can attribute a check to a bot, so a GitLab run reaches the comment read through the green/not-yet-registered path below andon_failurereads nothing there.scm.Capabilities.ReviewCommentsis the opt-in (GetReviewComments), true on GitHub (unresolved review threads) and GitLab (unresolved discussion notes:resolvable && !resolved, not a system note, bot-authored, diff position for the anchor); a GitLab host without the project path answersscm.ErrUnsupportedrather than erroring, so it degrades instead of parking. Under opt-inci.review_bot_comments: always(defaulton_failure), a bot check that completed green, or a registered bot with no check on the head yet (its earlier threads can still be unresolved), goes through the same path (greenReviewBotFindings->ciObservationFindingswithgreenBots): its unresolved comments are the same ask-user findings and blockchecks-passed, a green bot with none produces nothing, an unreadable comment list withholds readiness and parks afterconsecutiveCheckErrorLimit, andVerifyApprovalOverrideapplies the refusal or override contract owned bypipeline.ApprovalOverrideVerifier. The fix agent, not metadata, decides whether an unknown red is about the code: itscode_change_needed=falseconclusion parks the selected findings asask-user(ciRepairParkOutcome). types.Finding.Check(display name),types.Finding.CheckID(provider identity), and theFindingCategoryCI*constants are the routing keys the fix half reads (parseCIFixTargets);Finding.UnmarshalJSONandfindingWirecopy fields explicitly, so a new field must be added there or it drops on every parse. Findings ride the IPC event stream, so per-comment findings stay bounded.- A published repair keeps monitoring inside the same fix round and must call
sctx.MarkRunning()(executor-owned: durable status back torunningplus astep_startedevent), or the step staysfixingand neither the TUI noraxi'schecks-passed(ciReadyToMergerequiresrunning) can surface. The post-repair stale-evidence guard islastRepairStillUnverified: selected checks are snapshotted before a resumed repair, and targets that remain red with neither a newer completion/execution discriminator nor an observed pending state keep the monitor waiting instead of re-escalating. A merge conflict seen against a different base tip than the one the last conflict repair rebased onto (lastFixedIssues.ConflictBaseSHA, fromciRepairResult.ConflictBaseSHA) is a new issue and is repaired withinauto_fix.ci; an empty SHA on either side keeps the old same-conflict dedup. Regressions:internal/pipeline/steps/citest/ci_conflict_dedup_test.go. Findings deferred by a fix round survive pending observations and terminal monitor errors until a fresh settled issue set or an established clean result supersedes them. - Regressions:
TestCIStep_GreptileOnlyRedParksAsAskUserAndSpendsNoAutoFixRound,TestCIStep_MixedGreptileAndTestFailureRoutesOnlyTheTestToAutoFix,TestCIStep_PublishedRepairResumesMonitoringAndWaitsForTheRerun,TestCIResumedRepairSnapshotsSelectedCheckCompletion,TestTerminalFailureFreshnessUsesExecutionIDWithoutCompletionTime,TestCIStep_PersistentCheckReadFailureParksAtAskUser,TestCIStep_UserFixOfTransientFindingIsHonored,TestCIObservationFindings_ClassifiesEachIssueByProviderStructure,TestExecutor_MarkRunningReturnsAFixingStepToRunning,TestExecutor_RestartsValidationFromRequestedStep,TestGetChecksCarriesCheckSuiteAppSlug,TestParseFindingsJSON_RoundTripsCICheckFields,TestCIStep_GreenReviewBotCommentsParkUnderAlways,TestCIStep_GreenReviewBotCommentsIgnoredByDefault,TestCIStep_GreenReviewBotUnreadableCommentsParkUnderAlways,TestCIStep_JobFailureWithGreenReviewBotFollowsThePolicy,TestCIStep_VerifyApprovalOverride_GreenReviewBotComments,TestCIStep_UnregisteredReviewBotCommentsFollowThePolicy,TestReviewBotCommentsOnGitLabFollowThePolicy,TestCIStep_GitLabReviewBotDiscussionCommentsParkUnderAlways,TestGetReviewCommentsReadsUnresolvedBotDiscussions,TestReviewBotRegistry_ResolvesAppAndLoginToTheSameBot. Step tests drive the loop withstepstest.ExecuteWithAutoFix(the executor's auto-fix loop in miniature); a test that callsExecuteonce sees only the observation.
