Skip to content

Avoid duplicate PR sous-chef nudges and reduce finisher costs - #65411

Merged
pelikhan merged 3 commits into
mainfrom
pelikhan-pr-sous-chef-deduplication
Oct 3, 2026
Merged

pelikhan merged 3 commits into
mainfrom
pelikhan-pr-sous-chef-deduplication

Conversation

@pelikhan

@pelikhan pelikhan commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator

Why

PR sous-chef repeatedly woke Copilot for unchanged or already-addressed feedback. A sample of 30 recent runs consumed 364.95 AI credits and 5.76M tokens across six model executions; one model run only created a report issue. Finisher replies on #65165 confirmed repeat passes on feedback that was already resolved.

The existing prefilter read the oldest ten issue comments because that endpoint does not support its sort parameters. Its hidden nudge marker was also stripped from posted comments by safe-output sanitization.

Approach

  • Run deterministic eligibility checks before model startup. Paginate comments, fingerprint the PR head and actionable blockers, and persist the fingerprint visibly in nudge comments. Unchanged states remain suppressed indefinitely, including conflicts; trusted bot replies and sous-chef metadata churn do not count as progress.
  • Exclude Dependabot/Renovate, PRs with no substantive activity for 14 days, and active coding/review agents without an age-based escape hatch. Preserve the pending-CI gate and a 30-minute startup cooldown for trusted Copilot requests.
  • Select at most four PRs, prioritizing conflicts, failed checks, and unresolved feedback. Recheck eligibility before writes, perform eligible approvals/review cleanup/branch refresh directly, and delegate only concrete remaining implementation work. Remove routine formatting, generic sub-agent passes, and report-only no-ops.
  • Correct pr-finisher's unsupported gh pr view --json reviewThreads query with a paginated GraphQL snapshot. Batch validation and reuse prior results only when their recorded SHA matches the current head; retain broader coverage when affected code or prior CI failures require it.

Slash-command acknowledgements remain mandatory but do not bypass eligibility. Workflow runs are serialized, writes remain safe-output-only, and the CJS/CGO/CWI approval allowlist is preserved. The large lock-file diff is generated; centralized slash-command metadata is regenerated as well.

Validation

  • node --test scripts/pr-sous-chef.test.mjs: 30 passing regression tests covering deduplication, pagination, stale/bot/active-agent gates, trusted feedback, queue limits, compact output, and explicit read failures.
  • make recompile: all 304 workflows compiled.
  • ./gh-aw compile pr-sous-chef --strict --schedule-seed github/gh-aw.
  • make agent-report-progress: passed using installed Bash 5; generated workflows are in sync.
  • Read-only live queue checks confirmed that active-agent PRs produce an empty eligible queue.

No live workflow runs were triggered. Post-change production cost savings are not yet measured.

Prefilter meaningful PR state before model execution, skip stale/dependency/active-agent work, and use sanitizer-safe fingerprints. Prioritize concrete blockers and direct cleanup over generic nudges, branch churn, or formatting. Correct finisher review snapshots and reuse validation on unchanged heads.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@pelikhan
pelikhan marked this pull request as ready for review October 3, 2026 19:42
Copilot AI balanced review requested due to automatic review settings October 3, 2026 19:42
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

✅ Ponytail Reviewer completed successfully!

Lean already. Ship.

Generated by Ponytail Reviewer for #65411

@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

✅ Design Decision Gate 🏗️ completed the design decision gate check. See the comment below for the result and any generated ADR draft.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

✅ Test Quality Sentinel completed test quality analysis.

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

✅ PR Code Quality Reviewer completed the code quality review.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /tdd — found one concrete runtime bug in the new deterministic prefilter script; the rest of the refactor (shell logic → scripts/pr-sous-chef.mjs + pr-sous-chef.test.mjs, 30 passing tests) is well-structured and a clear improvement over the inline-shell approach.

📋 Key Themes & Highlights

Key Themes

  • Untested code path has a bug: main()'s GITHUB_STEP_SUMMARY branch references a free variable skipped that doesn't exist (should be output.skipped), which will throw ReferenceError whenever this script runs in real CI with a step summary enabled. This logic lives inline in main() and isn't covered by the otherwise-thorough pr-sous-chef.test.mjs suite, since those tests only exercise the exported classify/buildQueue/activeAgent functions.

Positive Highlights

  • ✅ Moving deduplication/eligibility logic from inline YAML shell into a real, unit-testable Node module (scripts/pr-sous-chef.mjs) is a strong /codebase-design win — much easier to reason about and extend than the prior shell-heavy implementation.
  • ✅ 30 regression tests cover a good spread of edge cases: fingerprint stability across comment reordering/marker stripping, bot/human activity distinction, staleness, active-agent detection across status values, and dependency-bot exclusion.
  • ✅ pr-finisher SKILL.md updates clearly document the GraphQL reviewThreads workaround and the rationale for reusing prior validation by SHA — good traceability for future readers.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · copilot · sonnet50 · 36.5 AIC · ⌖ 14.4 AIC · ⊞ 10K
Comment /matt to run again

Comment thread scripts/pr-sous-chef.mjs Outdated
}
if (process.env.GITHUB_STEP_SUMMARY) {
const counts = new Map();
for (const item of skipped) counts.set(item.skip_reason, (counts.get(item.skip_reason) ?? 0) + 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] ReferenceError: skipped is not defined — this references a free variable that does not exist in main(); it should be output.skipped. This path only runs when GITHUB_STEP_SUMMARY is set (i.e., in real CI), so it will throw at runtime and is not covered by any existing unit test.

💡 Fix
if (process.env.GITHUB_STEP_SUMMARY) {
    const counts = new Map();
    for (const item of output.skipped) counts.set(item.skip_reason, (counts.get(item.skip_reason) ?? 0) + 1);

Also consider adding a unit test (or extracting this summary-building logic into an exported, testable function) so regressions like this are caught before make test — this logic currently has zero test coverage because it lives inline in main(), which is only exercised as a CLI entrypoint.

@copilot please address this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the summary crash by using the queue output and extracted the summary formatter with regression coverage. Commit: 8b964c2.

@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

Comment Memory

reviewed_at: 2026-10-03T19:45:03.004+00:00
review_event: REQUEST_CHANGES
top_themes:
  - legacy deduplication can suppress fresh blockers
  - repo-wide active-run sweep is still too expensive for a prefilter
files_reviewed:
  - .github/skills/pr-finisher/SKILL.md
  - .github/workflows/agentic_commands.yml
  - .github/workflows/pr-sous-chef.md
  - .github/workflows/pr-sous-chef.lock.yml
  - Makefile
  - scripts/pr-sous-chef.mjs
  - scripts/pr-sous-chef.test.mjs
comment_count: 3

Note

This comment is managed by comment memory.

It stores persistent context for this thread in the code block at the top of this comment.
Edit only the text inside the backtick fences; workflow metadata and the footer are regenerated automatically.

Learn more about comment memory

🔎 Code quality review by PR Code Quality Reviewer · copilot · gpt54 · 59.3 AIC · ⌖ 7.47 AIC · ⊞ 20.2K · ◷
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes

The duplicate-suppression rewrite is headed in the right direction, but the legacy fallback is still broad enough to hide new actionable blockers after an old markerless sous-chef comment. The prefilter also still performs a repo-wide active-run sweep up front, which weakens the cost-saving goal of this change.

Blocking themes
  • unchanged_legacy can suppress fresh BEHIND / CONFLICTING states or changed failing checks instead of producing a new targeted nudge.
  • The new prefilter still does five paginated Actions queries before it knows which PRs matter, so it is not actually lightweight on busy repositories.

🔎 Code quality review by PR Code Quality Reviewer · copilot · gpt54 · 59.3 AIC · ⌖ 7.47 AIC · ⊞ 20.2K
Comment /review to run again

Comment thread scripts/pr-sous-chef.mjs
return { skip_reason: "unchanged" };
}
if (latest && !statePattern.test(latest.body) && activity <= epoch(latest.created_at)) {
return { skip_reason: "unchanged_legacy" };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probe

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment did not include an actionable concern, so no code change was warranted. Commit 8b964c2 addresses the substantive prefilter feedback.

Comment thread scripts/pr-sous-chef.mjs Outdated
? [gh(["pr", "view", checkNumber, "--repo", repo, "--json", prFields])]
: gh(["pr", "list", "--repo", repo, "--state", "open", "--search", "is:pr is:open -is:draft -author:app/dependabot -author:app/renovate -label:broccoli sort:updated-desc", "--limit", "200", "--json", prFields]);
if (!Array.isArray(candidates)) throw new Error("PR queue must be an array");
const runs = candidates.length ? fetchActiveRuns(repo) : [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main() still fan-outs five repository-wide workflow-run queries before it knows whether any PR is eligible, so the new "cheap prefilter" can spend most of its time scanning Actions history instead of reducing cost.

💡 Why this is risky

fetchActiveRuns() paginates queued, in_progress, pending, requested, and waiting runs for the entire repository, and main() does that on every scheduled run whenever the candidate list is non-empty. On a busy repo, that is a lot of extra API traffic and latency before you even inspect the first PR, which undercuts the stated goal of reducing repeated PR-sous-chef cost and makes rate-limit failures more likely.

Please defer the run lookup until after the cheap per-PR filters, or narrow it to the branches / PR numbers you are actually considering in this pass. At minimum, the single-PR slash-command path should not need a repo-wide active-run sweep.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Active-run lookup now occurs only after dependency/staleness filters pass, and explicit single-PR checks filter runs by the PR branch. Commit: 8b964c2.

Comment thread scripts/pr-sous-chef.mjs
return { skip_reason: "unchanged" };
}
if (latest && !statePattern.test(latest.body) && activity <= epoch(latest.created_at)) {
return { skip_reason: "unchanged_legacy" };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unchanged_legacy still suppresses follow-up nudges after the actionable blocker changes, so a PR with an old markerless sous-chef comment can stay muted forever once it becomes BEHIND/CONFLICTING or picks up different failing checks.

💡 Why this blocks merge

This fallback only checks activity <= latest.created_at, but activity is derived from commits, trusted review feedback, and human comments. It does not move when the same SHA becomes BEHIND, when the merge state flips to CONFLICTING, or when failed checks change because of a rerun. For any PR that still has a legacy markerless nudge, those state changes will keep hitting unchanged_legacy, which means the workflow never emits the new targeted nudge even though the reason to wake Copilot changed.

A safer fallback is to apply unchanged_legacy only when the newly computed actionable state is still effectively empty/no-op, instead of using it as a blanket suppression path for every later state transition.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Legacy nudges now suppress only an otherwise no-op state; new branch conflicts, behind states, and failed checks can proceed. Added regression coverage. Commit: 8b964c2.

@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

🧪 Test quality analysis by Test Quality Sentinel · copilot · sonnet50 · 70.5 AIC · ⌖ 18.3 AIC · ⊞ 11.1K · ◷
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧪 Test Quality Sentinel Report

✅ Test Quality Score: 96/100 — Excellent

Analyzed 30 test(s): 30 design, 0 implementation, 0 violation(s).
Note: Pre-fetch scoping only matched *.test.cjs/*.test.js; this PR's new test file uses the .mjs extension with Node's built-in node:test runner (scripts/pr-sous-chef.test.mjs), so it was analyzed manually from the PR diff instead of the prefetch artifacts.

📊 Metrics (30 tests)
Metric Value
Analyzed 30 (Go: 0, JS: 30)
✅ Design 30 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 27 (90%)
Duplicate clusters 0
Inflation No (test 319 lines : prod 257 lines ≈ 1.24:1)
🚨 Violations 0
Test File Classification Issues
unchanged conflicts never get a repeat poke, even after cooldown pr-sous-chef.test.mjs design_test / high_value none
visible fingerprint survives HTML marker removal and arbitrary comment order pr-sous-chef.test.mjs design_test / high_value none
new HEAD and changed blockers can advance again pr-sous-chef.test.mjs design_test / high_value none
bot replies and updatedAt churn are not progress pr-sous-chef.test.mjs design_test / high_value none
legacy provenance without a hidden marker suppresses unchanged PRs pr-sous-chef.test.mjs design_test / high_value none
latest legacy nudge is selected by timestamp, not REST array order pr-sous-chef.test.mjs design_test / high_value none
dependency bots are ignored using all known login formats pr-sous-chef.test.mjs design_test / high_value none
bot comments cannot revive a completely stale PR pr-sous-chef.test.mjs design_test / high_value none
fresh human feedback revives an older PR pr-sous-chef.test.mjs design_test / high_value none
an active coding agent blocks even an old run on a previous SHA pr-sous-chef.test.mjs design_test / high_value none
active agents match PR number even without a branch match pr-sous-chef.test.mjs design_test / high_value none
long-running agent checks never age out pr-sous-chef.test.mjs design_test / high_value none
short pending CI gates, WAITING CI remains directly approvable pr-sous-chef.test.mjs design_test / high_value none
maintainer Copilot requests also enforce startup cooldown pr-sous-chef.test.mjs design_test / high_value none
DIRTY or BLOCKED alone is not an implementation blocker pr-sous-chef.test.mjs design_test / high_value none
check reruns with identical failures do not change the fingerprint pr-sous-chef.test.mjs design_test / high_value none
author replies alone do not re-poke an unresolved thread pr-sous-chef.test.mjs design_test / high_value none
truncated review data fails closed pr-sous-chef.test.mjs design_test / high_value none
resolved feedback permits direct dismissal of stale automation reviews pr-sous-chef.test.mjs design_test / high_value none
GraphQL bot logins without REST suffixes permit review cleanup pr-sous-chef.test.mjs design_test / high_value none
external discussion does not change the trusted-state fingerprint pr-sous-chef.test.mjs design_test / high_value none
informational sous-chef acknowledgements made with a maintainer token are not progress pr-sous-chef.test.mjs design_test / high_value none
external replies cannot change a trusted review fingerprint pr-sous-chef.test.mjs design_test / high_value none
out-of-scope unresolved feedback does not wake an implementation agent or dismiss reviews pr-sous-chef.test.mjs design_test / high_value none
active-run lookup paginates every nonterminal status pr-sous-chef.test.mjs design_test / high_value uses a plain injected fake for the gh call boundary (external I/O), not a mocking library — acceptable
deduplication finds a nudge beyond the first hundred comments pr-sous-chef.test.mjs design_test / high_value none
queue selects exactly four PRs with deterministic blocker priority pr-sous-chef.test.mjs design_test / high_value none
dependency bots and clearly stale queue entries require no detail reads pr-sous-chef.test.mjs design_test / high_value none
invalid metadata and failed reads are errors, not an empty success queue pr-sous-chef.test.mjs design_test / high_value none
eligible context excludes raw successful checks and comment bodies pr-sous-chef.test.mjs design_test / high_value none

Verdict

✅ Passed. 0% implementation tests (threshold: 30%). All 30 tests in scripts/pr-sous-chef.test.mjs exercise exported public functions (classify, buildQueue, activeAgent, fetchActiveRuns) against observable return values (skip reasons, fingerprints, queue contents, thrown errors) rather than internal call counts. Edge/error paths (stale PRs, truncated data, invalid timestamps, bot noise, pagination boundaries) are well covered. No mock libraries, no missing build tags (N/A for JS), and the test file closely tracks the new scripts/pr-sous-chef.mjs production module (1.24:1 line ratio, no inflation). Other changed files (SKILL.md, workflow .md/.lock.yml, Makefile) are non-test and out of scope.

Verdict Summary

✅ Test Quality Sentinel: 96/100. 0% implementation tests (threshold: 30%).

🧪 Test quality analysis by Test Quality Sentinel · copilot · sonnet50 · 70.5 AIC · ⌖ 18.3 AIC · ⊞ 11.1K
Comment /review to run again

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The prefilter currently crashes while writing its step summary and misses some active reviewer workflows.

Review effort: Balanced
Findings: 1 High severity · 3 Medium severity

Open (4)
What changed in this PR

Adds deterministic PR sous-chef filtering to prevent duplicate agent work and reduce finisher costs.

Changes:

  • Adds fingerprint-based eligibility, pagination, prioritization, and active-agent checks.
  • Streamlines the sous-chef workflow and generated configuration.
  • Updates pr-finisher review collection and validation guidance.
File Description
scripts/​pr-sous-chef.mjs Implements queue filtering and deduplication.
scripts/​pr-sous-chef.test.mjs Adds regression coverage.
Makefile Runs the new tests.
.github/​workflows/​pr-sous-chef.md Redesigns workflow behavior.
.github/​workflows/​pr-sous-chef.lock.yml Regenerates the compiled workflow.
.github/​workflows/​agentic_commands.yml Updates command metadata.
.github/​skills/​pr-finisher/​SKILL.md Fixes review retrieval and validation guidance.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread scripts/pr-sous-chef.mjs Outdated
}
if (process.env.GITHUB_STEP_SUMMARY) {
const counts = new Map();
for (const item of skipped) counts.set(item.skip_reason, (counts.get(item.skip_reason) ?? 0) + 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the undefined summary variable and added a formatter regression test. Commit: 8b964c2.

Comment thread scripts/pr-sous-chef.mjs Outdated
const marker = "<!-- gh-aw-pr-sous-chef-nudge -->";
const statePattern = /Sous-chef state: ([a-f0-9]{64})/;
const activeStatuses = new Set(["queued", "in_progress", "pending", "requested", "waiting"]);
const agentPattern = /copilot|coding.agent|claude|codex|gemini|opencode|(^|[/ -])agent([/ -]|$)|^Q$/i;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added reviewer workflow names to active-run detection and regression cases for both PR Code Quality Reviewer and Ponytail Reviewer. Commit: 8b964c2.

Comment thread scripts/pr-sous-chef.mjs Outdated
Comment thread scripts/pr-sous-chef.mjs Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed with Impeccable harden + audit modes (new deterministic script replacing inline bash — classified as bug_fix/refactor_cleanup).

One blocking issue found: scripts/pr-sous-chef.mjs's main() references an undefined skipped variable when writing the step summary (should be output.skipped), which throws a ReferenceError on every real Actions run and crashes the prefilter job before it can write pr-sous-chef-candidates-compact.json or the job outputs. See inline comment for details and fix.

Everything else reviewed (prefilter/classify logic, workflow YAML restructuring, SKILL.md doc updates, and the 30 new regression tests) looks solid and well-tested.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · copilot · sonnet50 · 56.6 AIC · ⌖ 13 AIC · ⊞ 8.1K

Comment thread scripts/pr-sous-chef.mjs Outdated
}
if (process.env.GITHUB_STEP_SUMMARY) {
const counts = new Map();
for (const item of skipped) counts.set(item.skip_reason, (counts.get(item.skip_reason) ?? 0) + 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

skipped is never defined in main()'s scope — only output.skipped exists (as returned by buildQueue). This ReferenceError: skipped is not defined will throw on every real GitHub Actions run, since GITHUB_STEP_SUMMARY is always set, crashing the prefilter job before the queue/output files are written. None of the 30 tests in pr-sous-chef.test.mjs exercise this main() path, so the regression wasn't caught.

Fix: iterate output.skipped instead of the undefined skipped.

for (const item of output.skipped) counts.set(item.skip_reason, (counts.get(item.skip_reason) ?? 0) + 1);

@copilot please address this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the undefined summary variable by iterating the queue result and added formatter regression coverage. Commit: 8b964c2.

@pelikhan

pelikhan commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

@copilot run the PR finisher skill.

Copilot AI and others added 2 commits October 3, 2026 20:08
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

@copilot run the PR finisher skill.

Ran the PR finisher pass. Fixes are in commit 8b964c2; local format, lint, focused tests, and the final validation gate passed. CI includes an unrelated unified-session test failure and needs a maintainer-triggered rerun after these commits.

@pelikhan
pelikhan merged commit 2db976f into main Oct 3, 2026
@pelikhan
pelikhan deleted the pelikhan-pr-sous-chef-deduplication branch October 3, 2026 20:26
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.91.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants