Originally created by: theBGuy
Originally owned by: theBGuy
Three unrelated defects fixed together: automated PR re-reviews fired twice after a push, branch-name suggestions compared against a stale local branch in non-origin repos, and a set of MCP tools that can silently discard state carried no destructive annotation. Each ships its own changelog fragment.
The old dedup only remembered the last head fired for, and the runner only compared against the latest persisted review — so an eventually-consistent forge poll re-serving the previous head after a push (an A→B→A flap) was read as new work and paid for a second review.
src/lib/automations/sync.ts: replaces the lastFiredHead single-value map with firedHeads, keeping every head fired for per (kind, repo, ref); membership is tested with sameSha rather than set equality so a short SHA from one provider and a full SHA from another still match.src/lib/automations/runner.ts: swaps getLatestReview for listReviews, filtered to the mode, and skips the run when any retained record covers the head — the history store's MAX_PER_GROUP window is what absorbs the flap.src/lib/automations/runner.ts (rule-outcome docs, cancel/dismiss notes, rerunAutomation toast comment), sync.ts and src/features/repository/RepositoryView.tsx, since the mechanism is now "heads this mode already covered".origin remotescommitted_base_ref only ever tried origin/<default> before falling back to the local branch — the exact copy that goes stale — so a clone -o upstream repo diffed against local main.
src-tauri/src/mcp_server/generate.rs: after the origin probe, lists remotes and checks refs/remotes/<remote>/<default> in git remote order, mirroring git_default_branch's own precedence; doc comment updated to state the new order.committed_base_ref_resolves_a_non_origin_remote, a temp-dir repo with no origin, a hand-added upstream remote, and a same-named local branch, asserting upstream/main wins.src-tauri/src/git/remote.rs: promotes remote_head_branch here as pub(crate) and narrows read_symbolic_ref to private; git_remote_default_branch now calls the shared helper on both its pre- and post-fetch paths.src-tauri/src/git/branches.rs: drops its local duplicate of remote_head_branch in favor of the git::remote one.destructive_hint annotationsFlips the hint on the tools that can drop state the caller never named, or whose outcome isn't trivially recoverable, so a connected agent can prompt first.
src-tauri/src/mcp_server/write_forge.rs: request_reviewers, set_issue_assignees, set_pull_request_assignees, set_issue_milestone — all replace-a-collection / clear-by-omission tools; module doc now explains the two classes (payload-replaces-collection vs. not-trivially-recoverable, the latter covering merge_pull_request and update_release).src-tauri/src/mcp_server/write_jira.rs: assign_jira_issue (omitted account_id unassigns) and update_jira_issue (labels replaces the whole set).src-tauri/src/mcp_server/write_git.rs: merge_branch, whose ours/theirs strategy drops one side of every conflicting hunk; module doc records that destructive_hint and the ensure_destructive capability ladder answer different questions and don't track 1:1.src-tauri/src/mcp_server/write_local.rs: set_review_notes, since an empty body clears hand-written text with no history or forge copy behind it.remotes tool: one git remote -vsrc-tauri/src/mcp_server/read_git.rs: the remotes tool now shells out once and parses the listing via a new parse_remote_v helper instead of a listing plus a get-url per remote; names come from every row (a URL-less remote emits only a bare name\t row) and URLs from the (fetch) row, with the old git_remote_url path kept as the fallback arm so git's "answers with the name itself" behavior is preserved.get-url reports), and the ordinary fetch+push pair — measured against git 2.51.1.windows.1.src-tauri/src/github/release.rs: softens the gh_release_sync_updater_notes doc comment to say a failed upload parks the patched copy on disk when it can.
Originally posted by: cloudflare-workers-and-pages[bot]
Deploying gitdesktop with
Cloudflare Pages
cf4dcffView logs
Originally posted by: theBGuy
Context for reviewers — deliberate calls and disclosures on the record before the ready-flip. The PR description covers what/why; these are the calls a review would otherwise re-derive.
Deliberate calls
committed_base_refmirrorsgit_default_branch's probe order, and claims only that. In a corner (origin's tracking ref exists but its HEAD symref is unset while another remote resolved the default), the two functions can pick different remotes — the doc deliberately states order-mirroring, not same-remote; a same-remote claim was caught false in review and removed.destructive_hintnow states the tool's semantic, not its permission tier. The write_git doc's 1:1 hint↔ladder claim is gone — they answer different questions.merge_branchis the proof case: hint-destructive (itsours/theirsstrategies drop one side of every conflicting hunk, across files the caller never named) without being ladder-destructive.close_pull_request(a static annotation can't express "irreversible on Bitbucket only"; its description carries the caveat) · the in-progress-conflict family (rebase_branch,cherry_pick,revert_commit, rebase-modepull) — the real gap is that the MCP surface has no abort/continue tools, filed in the backlog as its own decision · the remaining write_git ladder tools, each with a recorded reason.set_review_notesflipped because it whole-value-replaces hand-written, unversioned notes with no history and no forge copy — strictly worse recoverability than the milestone/label flips it matches.remotestool is onegit remote -vspawn, with the parse extracted and pinned by measured fixtures (git 2.51.1.windows.1): a URL-less remote emits a barename<TAB>row (andget-urlanswers the name, exit 0 — the fallback preserves that degenerate answer exactly); a multi-URL remote emits exactly ONE(fetch)row, which equalsget-url's answer. Side effect on record: this path no longer warmsgit_remote_url's TTL cache.MAX_PER_GROUPrather than duplicating the literal 3 — a copied literal goes stale; the symbol doesn't.Disclosures
firedHeadsis per-process; a second app instance keeps its own map. Cross-instance duplicate protection rests on the runner's existing claim system, unchanged here.runner.tsis the standing Windows checkout artifact (HEAD blob is LF; content verified clean via LF-normalized format checks; CI checks out LF).Verification at open: full
cargo test934/934 (6 new tests) · CI-form clippy clean ·pnpm buildclean ·tsc -bclean ·changelog:check14 fragments valid · live MCP stdio probe: 119 tools, 18 destructive,remotesoutput byte-identical pre/post refactor.Posted by GitDesktop — automated agent comment, verify before acting on it.
Related
Tickets:
#137Originally posted by: theBGuy
🤖 GitDesktop AI review ·
opus· automatedNo blockers — the pr-sync covered-set rewrite, the
remote_head_branchconsolidation, thecommitted_base_refnon-origin sweep, and thedestructive_hintflips all hold up under tracing. I verified the module-doc claims against the actual annotation set (update_releaseis alreadydestructive_hint = trueatwrite_forge.rs:1114;edit_labelsis add/remove so correctly left false;update_issue/update_pull_requesttake only title/body so correctly left false;merge_branchatwrite_git.rs:598is gated onensure_git_write, so the new module doc's hint-vs-ladder claim is accurate), and I independently confirmed note [#11]'s doc-surface call —README.md:577-579,site/src/data/capabilities.ts, andcontent.ts:1683-1685carry no claim this change contradicts. Recorded decisions #1/#2/#3/#5/#7/#9 stand as noted; none is contradicted by code I can see. Findings below are one should-fix and a handful of nits.Tests
should-fix —
write_forge.rs:758,885,909,1306,write_jira.rs:256,289,write_git.rs:596,write_local.rs:252: the eightdestructive_hintflips are the entire payload ofchangelog.d/fixed-mcp-destructive-hints.md, and nothing in-repo pins them — verification was a manual stdio probe only. A future edit that flipsmerge_branchorset_pull_request_assigneesback tofalsecompiles, passescargo test, and silently removes the confirm prompt a connected agent shows before auto-resolving a merge or clearing an assignee set.mcp_server/mod.rs:904-916already pins the tool count viaToolRouter::list_all(), so the sibling test is a few lines: in the samemod tests, buildhandler(false, false, false, false), collecthandler.tool_router.list_all()into the sorted set of names whose annotations reportdestructive_hint == Some(true), andassert_eq!it against an explicit literal list of the 18 (merge_pull_request,request_reviewers,set_issue_assignees,set_pull_request_assignees,update_release,set_issue_milestone,assign_jira_issue,update_jira_issue,set_review_notes,merge_branch,delete_branch,discard_changes,discard_all_changes,reset_to_commit,force_push,delete_remote_branch,drop_stash,delete_tag). Give it the same doc-comment framing the count test uses — that the list is updated only when a change intends to move the destructive set — so adding a destructive tool carries the obligation explicitly rather than tripping a mystery failure.nit —
generate.rs,committed_base_ref_resolves_a_non_origin_remote: the comment calls it "Aclone -o upstreamrepo", but the fixture never writesrefs/remotes/upstream/HEAD, sogit_default_branch(branches.rs:396) resolvesmainthrough its localrefs/heads/mainfallback, not through upstream's HEAD — the opposite of a realclone -o upstream. Either addgit symbolic-ref refs/remotes/upstream/HEAD refs/remotes/upstream/mainafter theupdate-ref(the assertion is unchanged), or reword the comment to describe the local-fallback path it actually builds.Readability
nit — the renamed "watermark" vocabulary survives in two spots the sweep missed:
src/features/activity/ActivityDock.tsx:372("a claim/watermark still held") is the direct mirror of thererunAutomationcomment reworded atrunner.ts:624, andsrc-tauri/src/automation_claims.rs:7describes the frontend dedup as "an in-memory debounce map plus a tauri-store watermark" — the map now holds a head list per PR and the store side is a covered set; reword both to "an already-covered head" / "a per-PR fired-head list plus a tauri-store covered-set".nit —
src/lib/pulls/reviews-history.ts:159-160:listReviews's doc scopes it to "the 'Previous reviews' disclosure", butrunner.ts:282-284now makes it the load-bearing pr-sync covered-set read; name both consumers so a future change to its filtering knows it isn't UI-only.nit —
src/features/repository/RepositoryView.tsx:222: the line above the one you edited still claimsuseWatchPrHeadscovers "(local + remote)", butuseWatchPrHeads.ts:8-11watches open local PRs only (remote heads come fromusePrNotifications/useBackgroundPrSync) — drop "+ remote" while you're in the comment.nit —
read_git.rs,parse_remote_v: names are collected first-wins whilefetch_urls.insertis last-wins, andgit remote get-urlreports the first url;fetch_urls.entry(name).or_insert(url);makes both halves agree with git's answer for free.Posted by GitDesktop — AI output, verify before acting on it.
Related
Tickets:
#11Originally posted by: theBGuy
🤖 GitDesktop AI security audit ·
opus· automatedNo genuinely exploitable security issues in these changes.
What I checked and why each changed sink is safe:
remotes(read_git.rs) — the newgit remote -vparse replaces per-remoteget-url, but both arms still pass the URL through the existingredact_url_credentialsbefore it reaches the agent, so the credential-redaction guard is unchanged. The remote names taken from git's stdout are only emitted as JSON values; the one place a name is fed back into a spawn is the fallbackgit_remote_url, which still runsvalidate_remote_arg.destructive_hintflips (write_forge/write_git/write_jira/write_local) — advisory MCP annotations only. Authorization isensure_write/ensure_remote_write/ensure_git_write/ensure_destructiveinmcp_server/mod.rs, all untouched; every flipped tool still calls its same gate (e.g.merge_branch→ensure_git_write). Flippingfalse → truecannot widen capability, and the module doc'supdate_releaseclaim matches the code (destructive_hint = trueat write_forge.rs:1114).committed_base_ref(generate.rs) — the new{remote}/{default}base is built from local.git/configremote names, and its diff consumergit_branch_diffruns the project'svalidate_refonbasebefore interpolating thebase...comparerange, so the option/refspec-injection shape is covered by the established validator.remote_head_branch/read_symbolic_ref(branches.rs, remote.rs) — pure move plus visibility narrowing (read_symbolic_refbecame private); no behavior or argument construction changed.runner.ts,sync.ts) — thefiredHeads/covered-set change gates whether a paid AI re-review fires; it is not an auth, trust, or output-sanitization boundary, and the cross-instanceclaim_automation_runpath is unchanged. UnboundedfiredHeadsgrowth is resource-use, which is out of scope.Copilot flagged the
parse_remote_vloop for missingtrim_end()on CRLF; that's not a defect — Rust'sstr::lines()already strips a trailing\r, and even if a row did miss the" (fetch)"match the fallback arm redacts identically, so there's no credential-exposure path either way. Its other two comments are style/perf, not security.Posted by GitDesktop — AI output, verify before acting on it.
Originally posted by: theBGuy
Round-1 dispositions — the should-fix and all five nits verified and accepted; the batch is applied and rides the next push.
destructive_tools_are_exactly_this_setsits beside the tool-count test, filterslist_all()ondestructive_hint == Some(true), and pins the sorted 18-name literal with the intends-to-move doc framing. Negative-controlled: temporarily flipping an unrelated tool's annotation made the test fail with the 19-name diff, then reverted — the pin is not vacuous. A fresh stdio probe independently measured the same 18.refs/remotes/upstream/HEAD, so it exercises the remote-HEAD resolution path end-to-end and the "clone -o upstream" description is true (the symref command itself was probe-verified in a scratch repo, not assumed from the still-passing assertion).ActivityDock.tsx,automation_claims.rs) to the covered-set vocabulary.listReviewsdoc (nit) — fixed: names both consumers (the "Previous reviews" disclosure and the runner's pr-sync covered-set read).useWatchPrHeadsscope claim (nit) — fixed: "(local + remote)" corrected to local-only, with the remote-head sources named.parse_remote_vfirst-wins (nit) — fixed viaentry(...).or_insert(...); the existing fixtures pass unchanged (measured: git emits a single(fetch)row, so this is agreement-by-construction withget-url, not a behavior change).Copilot's two comment-tightening nits are also in the batch (both threads replied + resolved); its
parse_remote_vCRLF/O(n²) finding is declined with evidence on the thread.Verification on the batch: full
cargo test935/935 (the pin test included) · CI-form clippy clean ·tsc -bclean · scoped biome clean (the standingrunner.tsCRLF artifact aside) ·changelog:check14 valid.Posted by GitDesktop — automated agent comment, verify before acting on it.
Originally posted by: theBGuy
🤖 GitDesktop AI review ·
opus· automatedThe three fixes and their doc/test collateral hold up: I re-traced the destructive-hint pin against the actual annotations (
grepfinds exactly 18destructive_hint = truesites acrosswrite_forge.rs,write_git.rs,write_jira.rs,write_local.rs— matching the test's literal list name-for-name), thecommitted_base_refupstream fixture now really exercises the remote-HEAD path, and the reworded comments all check out against the code they describe (useWatchPrHeads.ts:7-13is local-only;reviews-history.ts:41,129-144prunesMAX_PER_GROUPper(kind, ref, mode), so the runner's flap-window claim is exact;release.rs:528-537really does have a "could not be saved" arm behind the new "when it can"). Nothing blocking.Readability
src/features/activity/ActivityDock.tsx:372-373: "a claim or already-covered head still held by the canceled run unwinding" reads as a doubleorand attaches "held by the canceled run unwinding" to the covered head, which isn't something a run holds;…, or a claim still held by the canceled run unwinding, or a head this mode already covered)keeps both causes distinct.Copilot's
parse_remote_vcomment (CRLF-unsafe suffix match / O(n²) name scan) is not a real issue on the current code:str::lines()strips a trailing\rbeforestrip_suffix(" (fetch)")sees it, andnamesholds one entry per remote (single digits), so notrim_end()or set is warranted.Resolved since last review
mcp_server/mod.rs:918-961), filterslist_all()ondestructive_hint == Some(true), and its 18-name literal matches the annotations in the tree exactly; the doc comment carries the "moves only when a change INTENDS to move it" framing.generate.rs:3029-3037now writesrefs/remotes/upstream/HEAD, so the fixture matches its "what aclone -o upstreamwrites" comment and resolves through the remote-HEAD path rather than the local fallback.ActivityDock.tsx:372andautomation_claims.rs:6-8(now "per-PR fired-head list plus a tauri-store covered set").reviews-history.ts:159-162names both consumers oflistReviews.RepositoryView.tsx:222-224corrects the scope to open local PRs and names where remote heads come from.read_git.rs:325usesfetch_urls.entry(name).or_insert(url), so the url map is first-wins likenamesand agrees withgit remote get-url.Recorded decisions #1/#2/#3/#5/#7/#9/#11 remain as noted; nothing I can see in the current code contradicts them.
Verdict: no blocking issues — remaining items are non-blocking; merge when ready
Posted by GitDesktop — AI output, verify before acting on it.
Originally posted by: theBGuy
Round-2 disposition — the ActivityDock double-
ornit is accepted and deferred with a home: the suggested phrasing is right (a covered head isn't something a run holds), but a comment-only push would spend a full review round on two lines. It rides the next automations-area PR (the backlog's MCP abort/continue item is the likely vehicle) — recorded there.With that on record: two rounds, all findings closed against on-record dispositions or deferred with homes, every inline thread resolved, CI green on this head (all seven checks, full matrix). Merge-ready.
Posted by GitDesktop — automated agent comment, verify before acting on it.
Ticket changed by: theBGuy