Repository navigation
feat(retro): add flapping detection to retro analysis skill #834
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -119,6 +119,149 @@ If you are unsure whether a failure is a genuine flake or a real bug, | |
| say so explicitly in your proposal and recommend investigation before | ||
| applying either fix category. | ||
|
|
||
| ## Flapping detection | ||
|
|
||
| Check whether the workflow exhibits fix-break oscillation: the agent | ||
| repeatedly applies and reverses the same change, or the review-fix loop | ||
| never converges. Flapping wastes agent cycles and often signals a deeper | ||
| problem (conflicting instructions or an approach the agent cannot settle | ||
| on). | ||
|
|
||
| Flapping is distinct from test flakiness (above): flakiness is the *same | ||
| commit* passing and failing with no diff between runs; flapping is driven | ||
| by the agent's *own changes* across successive code/fix runs. | ||
|
|
||
| ### Applicability | ||
|
|
||
| Flapping detection applies to PR-based workflows with code/fix cycles. | ||
| Resolve the target PR before gathering data: | ||
|
|
||
| - If the retro originates from a PR, use it directly: `PR_NUMBER` is its | ||
| number and `PR_REPO` is its `owner/repo` (parse both from | ||
| `$ORIGINATING_URL`). | ||
| - If the retro originates from an issue, resolve linked PRs with the | ||
| `github-forge` / `gitlab-forge` skill's linked-PR/MR recipe | ||
| (`closedByPullRequestsReferences` / `closed_by`) — not the | ||
| forge-specific retro-analysis skill, which has no linkage recipe. | ||
| That query returns a list of up to 50 nodes, each with `state` and | ||
| `url`. Tie-break: prefer the single open linked PR; if none is open, | ||
| take the most recently updated; if several are open, either skip | ||
| flapping detection or analyse each separately and say which PR each | ||
| finding refers to. If no linked PR exists, skip flapping detection. | ||
| Derive `PR_REPO` by parsing the node's `url` — it may differ from | ||
| `$REPO_FULL_NAME`. | ||
|
|
||
| Once `PR_REPO` is resolved, re-derive `DISPATCH_REPO` from `PR_REPO`'s | ||
| org (`${PR_ORG}/.fullsend`). `$DISPATCH_REPO` built from | ||
| `$REPO_FULL_NAME` is the wrong dispatch repo when the PR lives under a | ||
| different org. Use `PR_NUMBER` and `PR_REPO` (not `$REPO_FULL_NAME`) for | ||
| all data gathering below. | ||
|
|
||
| ### Data gathering | ||
|
|
||
| Dispatch a subagent to identify the code/fix/review workflow runs for | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MEDIUM: Run discovery is unbounded, and every existing recipe caps at 10 runs "Find all code, fix, and review workflow runs related to PR # Verified at head, every run-listing recipe the retro agent has uses Suggestion: bound the search explicitly — derive a time window from the PR's
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. HIGH: Procedure is GitHub-only but lives in the shared skill both forges load, contradicting the file's own convention and the PR's forge-agnostic claim The whole Data gathering / Matching / Diffs block hard-codes GitHub-only surface: Verified at head: no other part of this shared file contains a Suggested fix: Keep the conceptual model in the shared file (match on source repo + number, anchor diffs on consecutive pre-run head SHAs, order by run creation time) and move the GitHub field names and the |
||
| this PR. Reuse the `finding-agent-runs` discovery recipe rather than | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MEDIUM: The Line 163 says "Reuse the Verified at head: (The round-2 thread on line 162 pointed at Suggested fix: Point the collector at |
||
| inventing a new listing; bound the search to the PR's | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. HIGH: The new Lines 164-166 bound run discovery to the PR's (This window was added in response to the round-2 comment on line 162 about unbounded discovery; that thread is addressed — this is a new defect in the fix itself, not a restatement.) Suggested fix: Anchor the code run separately rather than widening the window (widening would also admit older code runs for the same issue that produced earlier, closed PRs): search |
||
| `createdAt`..`updatedAt` window (pass `--created <start>..<end>` with a | ||
| raised `--limit` per workflow). Dispatch-repo runs are | ||
| `workflow_dispatch` with no PR in their ref, so identifying a candidate | ||
| requires downloading its log and parsing `event_payload` — an unbounded | ||
| "find all runs" will miss long-lived flaps (existing recipes cap at 10) | ||
| or blow the retro timeout. | ||
|
|
||
| **Matching.** `$DISPATCH_REPO` is org-wide; PR numbers are not unique | ||
| across repos. A run belongs to this PR only if `source_repo` (or | ||
| `pull_request.base.repo.full_name`) equals `PR_REPO` **and** a | ||
| three-way number match mirroring the platform concurrency key holds: | ||
|
|
||
| - `pull_request.number == PR_NUMBER`, **or** | ||
| - `issue.number == PR_NUMBER` (comment-triggered fix; the PR number | ||
| lands in `issue.number`), **or** | ||
| - `issue.number` equals the issue parsed from the `agent/{issue}-{slug}` | ||
| branch (code runs). | ||
|
|
||
| Code runs carry **no** `pull_request` object in `event_payload` (code | ||
| dispatches from issue events when `ready-to-code` is applied). Matching | ||
| only on `pull_request.number` drops the first file-changing run and | ||
| destroys Pattern 1's baseline. | ||
|
|
||
| **Diffs.** `event_payload` is built at dispatch time from the triggering | ||
| event, so `pull_request.head.sha` is the branch head *before* the run | ||
| does any work. Attributing "the commit's changed files" at that SHA | ||
| assigns each code/fix run its predecessor's diff. Define run N's | ||
| changes as the patch between `head.sha(run N)` and `head.sha(the next | ||
| run)` — equivalently, the head SHA of the review run that follows it | ||
| (a review is triggered on the pushed commit, so its `head.sha` *is* | ||
| the preceding code/fix output). Fetch that compare diff (patch/hunks), | ||
| not a file-path list: Pattern 1 needs line-level A→B→A. | ||
|
|
||
| **Ordering.** Order runs by workflow `createdAt`, not by commit. | ||
| Collapse same-SHA reruns into a single step. The fix agent rebases and | ||
| amends, so a `head.sha` recorded in an earlier payload is often no | ||
| longer reachable from the PR head — commit order is not a total order | ||
| on the PRs this section targets. | ||
|
|
||
| Collector prompt sketch: "Using `finding-agent-runs`, list code, fix, | ||
| and review runs in `$DISPATCH_REPO` whose `createdAt` falls in this | ||
| PR's lifetime. A run matches only if `source_repo == PR_REPO` and the | ||
| three-way number rule above holds. For each matched file-changing run, | ||
| fetch the compare patch from `head.sha(this run)` to `head.sha(next | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. HIGH: First file-changing run has no start SHA, so the compare-patch recipe is undefined for the run Pattern 1 depends on Lines 191-196 and the collector prompt at 207-209 define every file-changing run's changes as the compare patch from The section goes to some length to re-include the code run via the Suggested fix: State the code run's anchors explicitly: start = the PR base commit ( |
||
| run)` and the named CI check results from `PR_REPO`. Also fetch the | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MEDIUM: Pattern 2 needs check results per anchor SHA; the collector specifies no SHA and no per-commit check recipe exists Pattern 2 (lines 224-231) compares a named check's status across runs, which requires results at each anchor SHA. The collector prompt asks only for "the named CI check results from Verified at head: Suggested fix: Specify per-SHA retrieval at the run's output anchor (the following review run's |
||
| PR's review comments/findings across all pages." | ||
|
|
||
| ### Patterns to detect | ||
|
|
||
| Count only **file-changing** (code/fix) runs as steps after collapsing | ||
| same-SHA reruns; review runs typically touch no files and serve as | ||
| diff anchors and finding-text correlation. | ||
|
|
||
| 1. **File oscillation:** across three or more consecutive file-changing | ||
| runs, the same lines in a file are changed, reverted, then changed | ||
| again — a repeated A→B→A pattern (run N adds a line, run N+1 removes | ||
| it, run N+2 adds it back). Compare hunks from the consecutive-head | ||
| patches, not file-path lists. A single add-then-remove is the normal | ||
| review-fix cycle, not oscillation. | ||
| 2. **Check-status flipping:** the same named CI check (e.g. `unit-tests`) | ||
| flips *twice* (pass→fail→pass→fail, or two full oscillations) across | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MEDIUM: Pattern 2's "flips twice" read literally is the same shape the next sentence excludes as convergence Lines 224-228 give three descriptions of one rule: the check "flips twice", the parenthetical "pass→fail→pass→fail", and "two full oscillations". Counting flips literally, Suggested fix: Drop the flip-counting phrasing and commit to one concrete sequence — e.g. "the check must go |
||
| runs whose patches touch overlapping files. A single | ||
| pass→fail→pass is ordinary regress-then-recover: `pass` is the | ||
| desired attractor, so that shape is convergence, not oscillation. | ||
| CI results are reported at check/job level, not per test. A check | ||
| that flips with no overlap in the agent's changed files is likely | ||
| test flakiness (above), not agent-caused flapping. | ||
| 3. **Cycle count:** repeated review-fix cycles on the same PR without | ||
| convergence — the review keeps raising the same or alternating | ||
| findings (a fix for one issue reintroduces a previously resolved one). | ||
| As a starting heuristic, treat more than two cycles as a signal, but | ||
| this is a provisional default: the flapping budget should be | ||
| configurable per repo and per agent role. See | ||
| [`flapping-convergence.md`](https://github.com/fullsend-ai/fullsend/blob/main/docs/problems/flapping-convergence.md), | ||
| which lists the right default as an open question. A single rework | ||
| cycle that the review then approves is normal iteration. | ||
|
|
||
| ### When flapping is detected | ||
|
|
||
| Include a proposal with these specifics: | ||
|
|
||
| - **target_repo:** where the fix should land (see Localization guidance below) | ||
| - **title:** start with "Flapping detected:" followed by what oscillated | ||
| - **what_happened:** list each cycle with run IDs, which files changed, and how the changes reversed | ||
| - **what_could_go_better:** identify what might be driving the loop (conflicting review criteria, ambiguous instructions, an unconverging approach) | ||
| - **proposed_change:** suggest a concrete intervention (clarify the conflicting instruction, add a convergence guard) | ||
| - **validation_criteria:** a measurable outcome tied to the pattern, e.g. "The next 2 fix cycles touching `<file>` should not re-introduce the change reverted in run N+1." | ||
|
|
||
| ### When NOT to flag | ||
|
|
||
| - A single rework cycle (review requested changes, fix addressed them, review approved) is normal. | ||
| - Different files changing across runs is normal iteration, not oscillation. | ||
| - A CI check flipping with no overlap in the agent's changed files is flakiness, not flapping. | ||
| - A single pass→fail→pass on a named check that then stays green is recovery, not oscillation. | ||
| - If a recorded `head.sha` is no longer reachable from the PR head | ||
| (rebase / force-push), treat a reversal signal as unreliable rather | ||
| than as oscillation. | ||
| - Same-SHA workflow reruns are not additional steps. | ||
| - Only flag when the same change is applied and reversed repeatedly, or the review-fix loop clearly fails to converge. | ||
|
|
||
| ## Before proposing: check for existing issues | ||
|
|
||
| **This step is mandatory.** Before including any proposal in your output, verify that no open issue already covers the same improvement. The retro agent is the primary source of systemic proposals — without this check, repeated runs produce duplicate issues that waste human triage time. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MEDIUM: Linked-PR tie-break selects on
updatedAt, which the query it cites does not returnLines 142-150 say to resolve linked PRs with the
github-forgeskill'sclosedByPullRequestsReferencesrecipe and, if none is open, "take the most recently updated".Verified at head,
skills/github-forge/SKILL.md:41returnsnodes { number url author { login } state }— there is noupdatedAtfield, so the tie-break has no data to operate on and the agent must make an undocumented extra call per node.(Distinct from the round-2 thread, which established that the query returns a list of up to 50 rather than one PR; the tie-break added in response to it is the new, unexecutable part.)
Suggested fix: Add
updatedAtto theclosedByPullRequestsReferencesnode selection inskills/github-forge/SKILL.md(andupdated_atto the gitlab-forge equivalent), or state in this section that the field must be resolved per node withgh pr view <url> --json updatedAt.