Skip to content

Commit

Merge branch 'worktree-agent-a0796e9a7a41f51e5'

syntaqxcommitted Parentsf964b03fae8c33Browse files
11 files+307−350/11 viewed
+4−2
28722872 "suspects": [
28732873 12
28742874 ],
2875− "bisected": true
2875+ "bisected": true,
2876+ "landed": []
28762877 },
28772878 "created_at": "2026-10-12T15:04:11.204Z"
28782879 },
29572958 "suspects": [
29582959 12
29592960 ],
2960− "bisected": true
2961+ "bisected": true,
2962+ "landed": []
29612963 },
29622964 "created_at": "2026-10-12T15:04:11.204Z"
29632965 },
+40−4
149149
150150 | What happened | Who it failed with (`suspects`) |
151151 | --- | --- |
152−| Tested on `main` alone | Nobody: the change is the cause. |
152+| Tested on `main` alone, and its own checks fail on its head too | Nobody: the change is the cause. |
153+| Tested on `main` alone, though its own checks pass on its head | Nobody: the change answers for it, and the note names what landed on `main` since it branched (`landed`), [below](#when-main-moved-under-it). |
153154 | Its own checks fail on its head too | Nobody: the change is the cause. |
154155 | It conflicts with an entry ahead | That entry, and the conflicting files. |
155156 | A required check did not report, or the state could not be built | Nobody: the note says what to fix. |
156157 | Its own checks pass, one entry ahead | That entry. |
157158 | Its own checks pass, several entries ahead | Found by building smaller combinations, below. |
158159
160+### When `main` moved under it
161+
162+An entry with nothing ahead of it is tested on `main` as it is now. When
163+its own checks pass on its head but a check fails once it is merged into
164+`main`, something that landed on `main` after the pull request branched is
165+what it now breaks with: #1 changed a function's signature, say, and this
166+pull request, written before #1 landed, still calls the old one.
167+
168+The entry still answers for the failure, since it is the one that has to
169+adapt, and nothing extra is built. g1t looks at the 50 pull requests most
170+recently merged into `main` before the entry was tested, keeps those whose
171+commits the entry's history (its latest 1,000 commits) does not hold, and
172+reports up to ten of them, newest first, as `landed`. A pull request that
173+was brought up to date with `main` since it branched holds what it merged,
174+so only what landed after that is named. The note on the pull request then
175+reads, for example:
176+
177+> g1t traced its merge queue failure to the default branch as it is now:
178+> `CI / merge_group` failed with this change merged into it, though this
179+> passes its checks on its own. Since this branched, #1 landed and is the
180+> likely cause. Bring it up to date with the default branch, fix what
181+> breaks, and push. Push a fix: once its checks pass on the new commit, it
182+> rejoins the merge queue by itself.
183+
184+When only one pull request landed it is named as the likely cause; when
185+several did, they are listed as what it may conflict with. When none is
186+found (`main` moved by a push rather than a pull request, say), the note
187+says the change is the cause, as before. A conflict, a required check that
188+did not report and a state that could not be built never look this up.
189+
159190 ### Narrowing it down
160191
161192 An entry whose own checks pass but which fails combined with several
186217 conflict, the pull request ahead it collided with and the files.
187218 2. Its conversation records that it was taken out of the queue, then what
188219 g1t found: the pull requests it fails with, whether that was narrowed
189− down, and the check that failed.
220+ down, and the check that failed; or, with nothing ahead, the pull
221+ requests that landed on `main` since it branched.
190222 3. Its issue's conversation says the same in one line, and so does the
191223 pull request's [session](/guides/why-blame/#sessions), so the agent
192224 working on it reads it there.
193225 4. A [decision record](/guides/why-blame/#decision-records) of kind
194226 `integration_failure`, confidence `tested`, keeps it, with the
195− combination, the check and the suspects as evidence.
227+ combination, the check, the suspects, `bisected` and `landed` as
228+ evidence. [`get_work`](/guides/sessions/)'s `last_integration_failure`
229+ is the latest of these.
196230 5. The `integration.failed` [event](/guides/webhooks/#events) carries the
197231 same to webhooks and to anyone following the repository's events:
198232 `pull` (the entry that failed, also `number`), `issue`, `combination`
199233 (the pull requests tested together, in order), `check` (`name`,
200− `conclusion`, `url`), `bisected`, `suspects`, `other_pulls` and `commit`.
234+ `conclusion`, `url`), `bisected`, `suspects`, `other_pulls`, `landed`
235+ (with nothing ahead, the pull requests merged into `main` since it
236+ branched, newest first; otherwise empty) and `commit`.
201237 6. The entries that were tested on top of it are tested again without it.
202238
203239 A combined state that passes is recorded too: an `integration_passed`
+1−1
4343 | `blocked_by` | Each issue it waits on, with that issue's claims and pull requests. |
4444 | `blocks` | The issues waiting on it. |
4545 | `decisions` | [Decision records](/guides/why-blame/#decision-records) on it and its pull requests, newest first. |
46−| `last_integration_failure` | The latest `integration_failure` decision among them, if any: the combination that failed in the merge queue, and the check. |
46+| `last_integration_failure` | The latest `integration_failure` decision among them, if any: the combination that failed in the merge queue, the check, the `suspects` and, when it failed on `main` alone, the pull requests that `landed` on `main` since it branched. |
4747 | `recent` | Its latest 20 coordination events, newest first. |
4848
4949 Then claim it, and start (or attach) your session.
+1−1
111111 | `deployment_status.created` | A deployment got a status, from any source. `data.repo_id`, `data.deployment` without its `payload`, and `data.deployment_status`. |
112112 | `release.created`, `.published`, `.released`, `.prereleased`, `.edited`, `.unpublished`, `.deleted` | A [release](/guides/releases/) changed; one change can send several, as the [release activity types](/guides/actions/#releases) say. `data.release_id`, `data.tag_name`, and `data.release` (`tag_name`, `target`, `name`, `body`, `draft`, `prerelease`, `author`, `created_at`, `published_at`), as it was for `release.deleted`. On `release.edited`, `data.changes` holds the old `name` and `body`. |
113113 | `queue.changed` | The merge queue gained, lost or settled an entry. |
114−| `integration.failed` | A pull request failed in the [merge queue](/guides/merge-queue/#when-an-entry-fails) combined with the ones ahead of it. `pull` (also `number`) is the one that answers for it, `issue` its issue, `combination` the pull requests tested together, `check` the check that failed (`name`, `conclusion`, `url`), `suspects` the pull requests ahead it fails with, `bisected` whether smaller combinations were built to find them, and `other_pulls` the rest of the combination. |
114+| `integration.failed` | A pull request failed in the [merge queue](/guides/merge-queue/#when-an-entry-fails) combined with the ones ahead of it. `pull` (also `number`) is the one that answers for it, `issue` its issue, `combination` the pull requests tested together, `check` the check that failed (`name`, `conclusion`, `url`), `suspects` the pull requests ahead it fails with, `bisected` whether smaller combinations were built to find them, `other_pulls` the rest of the combination, and `landed`, when nothing was ahead of it though it passes its checks on its own, the pull requests merged into the default branch since it branched, newest first and at most ten (empty otherwise). |
115115 | `integration.passed` | A pull request's combined state passed in the merge queue: `pull` and `combination`. |
116116 | `decision.recorded` | A [decision record](/guides/why-blame/#decision-records) was kept on an issue or pull request: `decision`, with `number` and `issue`. |
117117 | `session.appended` | A pull request's [session](/guides/why-blame/#sessions) grew: a coding agent recorded entries with `record_session`, a session on it recorded some, or g1t noted a merge queue failure. Busy: choose it only if you need it. |
+0−0

Binary or large file; its contents are not shown.

+15−1
100100 pub summary: String,
101101 /// What it rests on, as an object: the commit, the check (`name`,
102102 /// `conclusion`, `url`), the `combination` of pull requests tested
103− /// together, `other_pulls`, `suspects`.
103+ /// together, `other_pulls`, `suspects`, `bisected`, and for a failure
104+ /// with nothing ahead, `landed`.
104105 pub evidence: serde_json::Value,
105106 /// RFC 3339.
106107 pub created_at: String,
173174 pub suspects: Vec<u32>,
174175 /// The other pull requests in `combination`.
175176 pub other_pulls: Vec<u32>,
177+ /// With nothing ahead of it, though its own checks pass on its head:
178+ /// the pull requests merged into the default branch since it branched,
179+ /// newest first and at most ten, any of which may be what it now fails
180+ /// with. It still answers for the failure. Empty otherwise.
181+ #[serde(default)]
182+ pub landed: Vec<u32>,
176183 /// The combined state that failed.
177184 pub commit: Option<String>,
178185 }
277284 bisected: true,
278285 suspects: vec![12],
279286 other_pulls: vec![11, 12],
287+ landed: Vec::new(),
280288 commit: Some("abc".into()),
281289 };
282290 let sent = serde_json::to_value(&event).unwrap();
284292 assert_eq!(sent["issue"], 9);
285293 assert_eq!(sent["otherPulls"], serde_json::json!([11, 12]));
286294 assert_eq!(sent["check"]["conclusion"], "failure");
295+ assert_eq!(sent["landed"], serde_json::json!([]));
296+ // An event from before `landed` was sent still reads.
297+ let mut older = sent.clone();
298+ older.as_object_mut().unwrap().remove("landed");
299+ let read: IntegrationFailed = serde_json::from_value(older).unwrap();
300+ assert!(read.landed.is_empty());
287301 }
288302 }
+4−0
466466 if !combination.is_empty() {
467467 line.push_str(&format!(" with {combination}"));
468468 }
469+ let landed = nums("landed");
470+ if !landed.is_empty() {
471+ line.push_str(&format!(" (since it branched: {landed} landed)"));
472+ }
469473 return line.trim().to_owned();
470474 }
471475 if let Some(other) = data["other_pull"].as_u64() {
+5−1
351351 /**
352352 * A combined state in the merge queue failed. `pull` (also `number`)
353353 * answers for it; `suspects` are the pull requests ahead it fails with,
354− * narrowed by building smaller combinations when `bisected`.
354+ * narrowed by building smaller combinations when `bisected`. With
355+ * nothing ahead, though it passes its checks on its own, `landed` lists
356+ * the pull requests merged into the default branch since it branched,
357+ * newest first.
355358 */
356359 "integration.failed": {
357360 repoId: string;
364367 bisected: boolean;
365368 suspects: number[];
366369 otherPulls: number[];
370+ landed: number[];
367371 commit: string | null;
368372 };
369373 /** A combined state in the merge queue passed. */
+8−4
1313 request it holds responsible, the combination it tested (the pull requests
1414 merged together, in order), the failing check and its link, whether it
1515 bisected the combination to find the culprit, and the suspects when it
16−could not tell.
16+could not tell. When nothing was ahead of it, the default branch itself
17+moved: `landed` lists the pull requests merged into it since your branch
18+left it, newest first, and one alone is the likely cause.
1719
1820 Pull request: $ARGUMENTS (if empty, use the current branch's pull request).
1921
2325 on it and the decisions so far.
2426 2. **Open the failing check's log** from the event's check link, or with
2527 `g1t run logs --failed` on that run. Find the first real error.
26−3. **Read the other pull requests in the combination.**
27− `g1t pr diff OTHER` for each one tested with yours, and look for what
28+3. **Read the other pull requests in the combination**, or in `landed`
29+ when the combination is yours alone.
30+ `g1t pr diff OTHER` for each one, and look for what
2831 meets your change: the same function changed two ways, a renamed or
2932 removed name you still use, a migration, a fixture or a test that
3033 assumes the old behaviour.
3134 4. **Reproduce it locally.** Merge the default branch and the other pull
3235 requests' branches into a scratch branch in the same order, and run the
33− failing check's command.
36+ failing check's command. For a `landed` failure, merging the default
37+ branch as it is now into yours is enough.
3438 5. **Fix it on your branch.** Change only what the failure needs. If the
3539 right fix belongs in the other pull request, say so on it
3640 (`g1t pr comment OTHER`) instead of working around it, and tell the
+228−20
1717 //! are, by the `merge_group` workflows, and never landed). What is left is
1818 //! reported as `suspects`.
1919 //!
20+//! **A default branch that moved.** An entry that fails with nothing ahead
21+//! of it, though its own checks pass on its head, fails on the default
22+//! branch as it is now: something landed there since it branched. g1t looks
23+//! up the pull requests merged into the default branch whose commits the
24+//! entry's history does not hold, newest first and at most [`MAX_LANDED`],
25+//! and reports them as `landed`. The entry still answers for the failure
26+//! (it is the one that must adapt); one landed pull request is named as the
27+//! likely cause.
28+//!
2029 //! **Telling the work.** The answer goes on the pull request's and its
2130 //! issue's timelines, into the pull request's session, as a decision
2231 //! record (`integration_failure`, tested) and as `integration.failed`. A
2837
2938 use g1t_contracts::decisions::{DecisionConfidence, DecisionKind, FailedCheck, IntegrationFailed, IntegrationPassed};
3039 use g1t_contracts::events::SessionAppended;
31−use g1t_contracts::repos::{GetByIdArgs, PathByIdArgs, Repo, RepoPath};
40+use g1t_contracts::repos::{Commit, GetByIdArgs, LogArgs, PathByIdArgs, Repo, RepoPath};
3241 use g1t_contracts::time::rfc3339;
3342 use g1t_contracts::work::*;
3443 use g1t_contracts::{FailureCode, Outcome, new_id};
4756 /// The most extra combinations built to narrow down one failure.
4857 pub(crate) const PROBE_BUDGET: u32 = 3;
4958
59+/// The most pull requests reported as landed since an entry branched.
60+pub(crate) const MAX_LANDED: usize = 10;
61+
62+/// How far back the entry's own history is read to tell what it holds.
63+const LANDED_HISTORY: u32 = 1000;
64+
65+/// How many of the latest merged pull requests are looked at.
66+const LANDED_CANDIDATES: u32 = 50;
67+
5068 /// What kind of failure a combined state had.
5169 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
5270 #[serde(rename_all = "snake_case")]
174192 Blame(Vec<u32>),
175193 /// It fails with some of the entries ahead: narrow them down.
176194 Bisect,
195+ /// Nothing is ahead and it passes its checks on its own, yet it fails
196+ /// on the default branch as it is now: it answers for it, and what
197+ /// landed there since it branched is looked up.
198+ BaseMoved,
177199 }
178200
179201 /// Decides who answers for a failure of the entry tested with `ahead`
180202 /// merged in first. `passed_alone`: its own checks pass on its head.
181203 pub(crate) fn plan(ahead: &[u32], failure: &Failure, passed_alone: bool) -> Plan {
182204 if ahead.is_empty() {
183− return Plan::Blame(Vec::new());
205+ return if failure.kind == FailureKind::Checks && passed_alone {
206+ Plan::BaseMoved
207+ } else {
208+ Plan::Blame(Vec::new())
209+ };
184210 }
185211 match failure.kind {
186212 // The sandbox names the entry ahead that changed the same files.
193219 }
194220 }
195221
222+/// A pull request merged into the default branch, as looked at for what
223+/// landed since an entry branched.
224+#[derive(Clone, Debug, Deserialize)]
225+pub(crate) struct MergedRow {
226+ pub(crate) number: u32,
227+ /// The commit it landed as.
228+ pub(crate) head_commit: Option<String>,
229+ pub(crate) merged_at: Option<String>,
230+}
231+
232+/// The pull requests among `merged` (newest first) that landed since the
233+/// entry branched: those whose commit `history`, the entry's own, does not
234+/// hold. When that history was cut short, `oldest` is the time of its
235+/// oldest commit, and a pull request merged before it is not counted, as
236+/// its commit may be further back. Newest first, at most [`MAX_LANDED`].
237+pub(crate) fn landed_since(merged: &[MergedRow], history: &[Commit], oldest: Option<&str>) -> Vec<u32> {
238+ merged
239+ .iter()
240+ .filter(|pull| {
241+ pull.head_commit
242+ .as_deref()
243+ .is_some_and(|commit| !history.iter().any(|known| known.hash == commit))
244+ })
245+ .filter(|pull| match (oldest, pull.merged_at.as_deref()) {
246+ (Some(oldest), Some(at)) => at >= oldest,
247+ (Some(_), None) => false,
248+ (None, _) => true,
249+ })
250+ .map(|pull| pull.number)
251+ .take(MAX_LANDED)
252+ .collect()
253+}
254+
196255 /// The suspects to build with the entry next, the first half of them, or
197256 /// none when one is left or the budget is spent.
198257 pub(crate) fn next_probe(suspects: &[u32], builds: u32) -> Option<Vec<u32>> {
234293
235294 /// The note on the pull request, as the rest of a sentence starting with
236295 /// `g1t`.
237−pub(crate) fn pull_note(ahead: &[u32], suspects: &[u32], bisected: bool, check: &FailedCheck) -> String {
296+pub(crate) fn pull_note(ahead: &[u32], suspects: &[u32], bisected: bool, landed: &[u32], check: &FailedCheck) -> String {
238297 let what = what_failed(check);
239298 let rejoin = "Push a fix: once its checks pass on the new commit, it rejoins the merge queue by itself.";
299+ if suspects.is_empty() && ahead.is_empty() && !landed.is_empty() {
300+ return format!(
301+ "traced its merge queue failure to the default branch as it is now: {what} with this change merged into it, though this passes its checks on its own. Since this branched, {}. Bring it up to date with the default branch, fix what breaks, and push. {rejoin}",
302+ landed_clause(landed),
303+ );
304+ }
240305 if suspects.is_empty() {
241306 let on = if ahead.is_empty() {
242307 "on the default branch as it is now".to_owned()
258323 )
259324 }
260325
326+/// What landed on the default branch since the entry branched, as a
327+/// clause: one is named as the likely cause.
328+fn landed_clause(landed: &[u32]) -> String {
329+ match landed {
330+ [one] => format!("#{one} landed and is the likely cause"),
331+ _ => format!("{} landed and may be what it conflicts with", numbers(landed)),
332+ }
333+}
334+
261335 /// The note on its issue, and the one-line summary of the decision.
262−pub(crate) fn summary(number: u32, suspects: &[u32], check: &FailedCheck) -> String {
336+pub(crate) fn summary(number: u32, suspects: &[u32], landed: &[u32], check: &FailedCheck) -> String {
263337 let what = what_failed(check);
264− if suspects.is_empty() {
338+ if suspects.is_empty() && !landed.is_empty() {
339+ format!(
340+ "#{number} failed in the merge queue on the default branch as it is now: {what}, though #{number} passes its checks on its own; since it branched, {}.",
341+ landed_clause(landed),
342+ )
343+ } else if suspects.is_empty() {
265344 format!("#{number} failed in the merge queue: {what}, and #{number} itself is the cause.")
266345 } else {
267346 format!("#{number} failed in the merge queue together with {}: {what} when they were combined.", numbers(suspects))
376455 return Ok(());
377456 };
378457 let ahead = row.ahead();
379− // Only a check failure with entries ahead asks whether it fails alone.
380− let passed_alone = failure.kind != FailureKind::Checks || ahead.is_empty() || self.passed_alone(row).await?;
458+ // Only a check failure asks whether it fails alone.
459+ let passed_alone = failure.kind != FailureKind::Checks || self.passed_alone(row).await?;
381460 match plan(&ahead, &failure, passed_alone) {
382− Plan::Blame(suspects) => self.integration_failed(row, &pull, &failure, false, &suspects).await,
461+ Plan::Blame(suspects) => self.integration_failed(row, &pull, &failure, false, &suspects, &[]).await,
462+ Plan::BaseMoved => {
463+ // What landed is context: without it, the failure is still told.
464+ let landed = self.landed_since_branched(row, &pull).await.unwrap_or_default();
465+ self.integration_failed(row, &pull, &failure, false, &[], &landed).await
466+ }
383467 Plan::Bisect => {
384468 let Some(stack) = self.tested_stack(row, &pull, &ahead).await? else {
385469 // What was tested can no longer be found: say what is known.
386− return self.integration_failed(row, &pull, &failure, false, &ahead).await;
470+ return self.integration_failed(row, &pull, &failure, false, &ahead, &[]).await;
387471 };
388472 let diagnosis = Diagnosis {
389473 base_commit: row.base_commit.clone().unwrap_or_default(),
398482 }
399483 }
400484
485+ /// The pull requests merged into the default branch since `pull`'s
486+ /// entry branched from it and before it was tested, newest first: the
487+ /// latest merged ones whose commits the entry's history does not hold.
488+ async fn landed_since_branched(&self, row: &EntryRow, pull: &Pull) -> Result<Vec<u32>> {
489+ let (Some(head), Some(tested_at)) = (row.head_commit.clone(), row.tested_at.clone()) else {
490+ return Ok(Vec::new());
491+ };
492+ let viewer = Some(pull.owner().clone());
493+ let repo: Outcome<Repo> = g1t_kit::call(
494+ &self.repos,
495+ "get_by_id",
496+ &GetByIdArgs { id: row.repo_id.clone(), viewer: viewer.clone() },
497+ )
498+ .await?;
499+ let Outcome::Ok(repo) = repo else { return Ok(Vec::new()) };
500+ let path = pull.fork.clone().unwrap_or_else(|| RepoPath {
501+ namespace: repo.namespace.clone(),
502+ name: repo.name.clone(),
503+ });
504+ let merged = async {
505+ self.db
506+ .prepare(
507+ "SELECT number, head_commit, merged_at FROM pulls
508+ WHERE repo_id = ? AND status = 'merged' AND id != ? AND merged_at <= ?
509+ AND (base_branch IS NULL OR base_branch = '' OR base_branch = ?)
510+ ORDER BY merged_at DESC LIMIT ?",
511+ )
512+ .bind(&[
513+ row.repo_id.as_str().into(),
514+ pull.id.as_str().into(),
515+ tested_at.as_str().into(),
516+ repo.default_branch.as_str().into(),
517+ LANDED_CANDIDATES.into(),
518+ ])?
519+ .all()
520+ .await?
521+ .results::<MergedRow>()
522+ };
523+ let log = LogArgs { path, viewer, git_ref: Some(head), limit: LANDED_HISTORY };
524+ let history = g1t_kit::call::<_, Outcome<Vec<Commit>>>(&self.repos, "log", &log);
525+ let (merged, history) = futures_util::future::try_join(merged, history).await?;
526+ let Outcome::Ok(history) = history else { return Ok(Vec::new()) };
527+ // A history cut short holds only what is newer than its oldest commit.
528+ let oldest = (history.len() >= LANDED_HISTORY as usize)
529+ .then(|| history.iter().map(|commit| commit.authored_at.as_str()).min())
530+ .flatten();
531+ Ok(landed_since(&merged, &history, oldest))
532+ }
533+
401534 /// Each entry ahead and then `row`'s, as they were tested together.
402535 async fn tested_stack(&self, row: &EntryRow, pull: &Pull, ahead: &[u32]) -> Result<Option<Vec<QueueStackItem>>> {
403536 let path: Option<RepoPath> = g1t_kit::call(
450583 return Ok(());
451584 };
452585 return self
453− .integration_failed(row, &pull, &diagnosis.failure, narrowed(&diagnosis), &diagnosis.suspects)
586+ .integration_failed(row, &pull, &diagnosis.failure, narrowed(&diagnosis), &diagnosis.suspects, &[])
454587 .await;
455588 };
456589 let now = now_ms();
721854 let Some(pull) = self.pull_by_id(&row.pull_id).await? else {
722855 return Ok(());
723856 };
724− self.integration_failed(&row, &pull, &diagnosis.failure, narrowed(&diagnosis), &diagnosis.suspects)
857+ self.integration_failed(&row, &pull, &diagnosis.failure, narrowed(&diagnosis), &diagnosis.suspects, &[])
725858 .await
726859 }
727860 ProbeResult::Broke | ProbeResult::Passed => {
735868 // --- Telling the work -------------------------------------------------------
736869
737870 /// Tells the pull request, its issue, its session and anyone following
738− /// that its combined state failed, and who it failed with.
871+ /// that its combined state failed, and who it failed with. `landed`:
872+ /// with nothing ahead, the pull requests merged into the default branch
873+ /// since it branched.
739874 pub(crate) async fn integration_failed(
740875 &self,
741876 row: &EntryRow,
743878 failure: &Failure,
744879 bisected: bool,
745880 suspects: &[u32],
881+ landed: &[u32],
746882 ) -> Result<()> {
747883 let ahead = row.ahead();
748884 let mut combination = ahead.clone();
749885 combination.push(pull.number);
750886 let g1t = ("g1t", "g1t");
751− let note = pull_note(&ahead, suspects, bisected, &failure.check);
887+ let note = pull_note(&ahead, suspects, bisected, landed, &failure.check);
752888 self.note(&row.repo_id, pull.number, g1t, &note).await?;
753− let summary = summary(pull.number, suspects, &failure.check);
889+ let summary = summary(pull.number, suspects, landed, &failure.check);
754890 if let Some(issue) = pull.issue {
755891 self.note(&row.repo_id, issue, g1t, &format!("noted: {summary}")).await?;
756892 }
772908 "other_pulls": ahead,
773909 "suspects": suspects,
774910 "bisected": bisected,
911+ "landed": landed,
775912 }),
776913 },
777914 )
791928 bisected,
792929 suspects: suspects.to_vec(),
793930 other_pulls: ahead,
931+ landed: landed.to_vec(),
794932 commit: failure.combined_commit.clone(),
795933 },
796934 )
9811119 #[test]
9821120 fn who_answers_for_a_failure() {
9831121 let checks = failure(FailureKind::Checks);
984− // Tested alone on the default branch: it is the cause.
985− assert_eq!(plan(&[], &checks, true), Plan::Blame(vec![]));
1122+ // Tested alone on the default branch, failing there though it
1123+ // passes on its own: the default branch moved under it.
1124+ assert_eq!(plan(&[], &checks, true), Plan::BaseMoved);
1125+ // Alone, and failing on its own too: it is the cause.
1126+ assert_eq!(plan(&[], &checks, false), Plan::Blame(vec![]));
1127+ // Alone, but not a check failure: nothing to look up.
1128+ for kind in [FailureKind::Conflict, FailureKind::Missing, FailureKind::Build] {
1129+ assert_eq!(plan(&[], &failure(kind), true), Plan::Blame(vec![]), "{kind:?}");
1130+ }
9861131 // Its own checks fail too: still the cause.
9871132 assert_eq!(plan(&[11, 12], &checks, false), Plan::Blame(vec![]));
9881133 // One entry ahead: that one, without building anything.
10351180 #[test]
10361181 fn the_notes_name_the_other_changes_and_the_check() {
10371182 let check = failure(FailureKind::Checks).check;
1038− let note = pull_note(&[11, 12, 13], &[12], true, &check);
1183+ let note = pull_note(&[11, 12, 13], &[12], true, &[], &check);
10391184 assert!(note.starts_with("traced its merge queue failure to how it combines with #12 (narrowed down from #11, #12 and #13"), "{note}");
10401185 assert!(note.contains("`CI / merge_group` failed"));
10411186 assert!(note.contains("once #12 has landed"));
1042− let own = pull_note(&[], &[], false, &check);
1187+ let own = pull_note(&[], &[], false, &[], &check);
10431188 assert!(own.contains("on the default branch as it is now, and the change is the cause"), "{own}");
1044− let conflict = pull_note(&[11], &[11], false, &FailedCheck { name: "merge".into(), conclusion: "conflict".into(), url: None });
1189+ let conflict = pull_note(&[11], &[11], false, &[], &FailedCheck { name: "merge".into(), conclusion: "conflict".into(), url: None });
10451190 assert!(conflict.contains("it did not merge cleanly"), "{conflict}");
10461191 assert!(!conflict.contains("narrowed"));
1047− assert_eq!(summary(14, &[11, 12], &check), "#14 failed in the merge queue together with #11 and #12: `CI / merge_group` failed when they were combined.");
1192+ assert_eq!(summary(14, &[11, 12], &[], &check), "#14 failed in the merge queue together with #11 and #12: `CI / merge_group` failed when they were combined.");
10481193 assert_eq!(numbers(&[]), "");
10491194 }
10501195
10511196 #[test]
1197+ fn the_notes_name_what_landed_since_it_branched() {
1198+ let check = failure(FailureKind::Checks).check;
1199+ // One landed: named as the likely cause.
1200+ let one = pull_note(&[], &[], false, &[1], &check);
1201+ assert_eq!(
1202+ one,
1203+ "traced its merge queue failure to the default branch as it is now: `CI / merge_group` failed with this change merged into it, though this passes its checks on its own. Since this branched, #1 landed and is the likely cause. Bring it up to date with the default branch, fix what breaks, and push. Push a fix: once its checks pass on the new commit, it rejoins the merge queue by itself."
1204+ );
1205+ assert_eq!(
1206+ summary(14, &[], &[1], &check),
1207+ "#14 failed in the merge queue on the default branch as it is now: `CI / merge_group` failed, though #14 passes its checks on its own; since it branched, #1 landed and is the likely cause."
1208+ );
1209+ // Several: listed, newest first, as possible causes.
1210+ let several = pull_note(&[], &[], false, &[7, 5, 1], &check);
1211+ assert!(several.contains("Since this branched, #7, #5 and #1 landed and may be what it conflicts with."), "{several}");
1212+ assert!(!several.contains("the change is the cause"));
1213+ assert!(summary(14, &[], &[7, 5], &check).ends_with("since it branched, #7 and #5 landed and may be what it conflicts with."));
1214+ // None found: the change is the cause, as before.
1215+ assert_eq!(summary(14, &[], &[], &check), "#14 failed in the merge queue: `CI / merge_group` failed, and #14 itself is the cause.");
1216+ }
1217+
1218+ fn commit(hash: &str, at: &str) -> Commit {
1219+ Commit {
1220+ hash: hash.into(),
1221+ tree_hash: format!("t{hash}"),
1222+ message: String::new(),
1223+ author: g1t_contracts::repos::Signature { name: "a".into(), email: "a@example.com".into() },
1224+ parents: Vec::new(),
1225+ authored_at: at.into(),
1226+ }
1227+ }
1228+
1229+ fn merged(number: u32, commit: &str, at: &str) -> MergedRow {
1230+ MergedRow { number, head_commit: Some(commit.into()), merged_at: Some(at.into()) }
1231+ }
1232+
1233+ #[test]
1234+ fn what_landed_is_what_its_history_does_not_hold() {
1235+ // #3 and #2 landed after it branched from #1's commit.
1236+ let pulls = [
1237+ merged(3, "c3", "2026-10-12T12:00:00.000Z"),
1238+ merged(2, "c2", "2026-10-12T11:00:00.000Z"),
1239+ merged(1, "c1", "2026-10-12T10:00:00.000Z"),
1240+ ];
1241+ let history = [commit("mine", "2026-10-12T10:30:00Z"), commit("c1", "2026-10-12T10:00:00Z"), commit("c0", "2026-10-11T00:00:00Z")];
1242+ assert_eq!(landed_since(&pulls, &history, None), vec![3, 2]);
1243+ // Brought up to date with #2 since: only #3 is new to it.
1244+ let updated = [commit("merge", "2026-10-12T11:30:00Z"), commit("c2", "2026-10-12T11:00:00Z"), commit("c1", "2026-10-12T10:00:00Z")];
1245+ assert_eq!(landed_since(&pulls, &updated, None), vec![3]);
1246+ // A history cut short does not count what is older than it.
1247+ let short = [commit("mine", "2026-10-12T11:30:00Z"), commit("x", "2026-10-12T10:30:00Z")];
1248+ assert_eq!(landed_since(&pulls, &short, Some("2026-10-12T10:30:00Z")), vec![3, 2]);
1249+ // A merged pull request with no recorded commit is not counted.
1250+ let unknown = [MergedRow { number: 9, head_commit: None, merged_at: None }];
1251+ assert!(landed_since(&unknown, &history, None).is_empty());
1252+ // At most MAX_LANDED, newest first.
1253+ let many: Vec<MergedRow> = (1..=15).rev().map(|n| merged(n, &format!("m{n}"), "2026-10-12T12:00:00.000Z")).collect();
1254+ let found = landed_since(&many, &history, None);
1255+ assert_eq!(found.len(), MAX_LANDED);
1256+ assert_eq!(found.first(), Some(&15));
1257+ }
1258+
1259+ #[test]
10521260 fn failures_are_read_from_what_was_reported() {
10531261 let conflict = ReportQueueArgs {
10541262 entry_id: "qen_1".into(),
+1−1
4747 combined_commit: Option<String>,
4848 results: Option<String>,
4949 error: Option<String>,
50− tested_at: Option<String>,
50+ pub(crate) tested_at: Option<String>,
5151 created_at: String,
5252 finished_at: Option<String>,
5353 /// What failed its combined state while entries ahead were still