Skip to content

fix(pstack): make the upstream merge lever refuse drift and report unsafe rows - #62

Merged
ericlitman merged 2 commits into
mainfrom
fix/upstream-merge-guards
Sep 8, 2026
Merged

fix(pstack): make the upstream merge lever refuse drift and report unsafe rows#62
ericlitman merged 2 commits into
mainfrom
fix/upstream-merge-guards

Conversation

@ericlitman

Copy link
Copy Markdown
Owner

Follow-up to #60. The Gavel panel reviewed the merged diff of #60 (run 34bdd1e7, verdict request changes, quorum 4 of 3 across gpt-5.6-sol, grok-4.6, claude-opus-5, gpt-5.6-sol) and raised five defects in scripts/upstream-merge.py. All five held on inspection.

What changed

  • Refuses drift. The lever exits 2 and writes nothing unless HEAD is the audited port commit and every mapped path is clean. An audit taken earlier can no longer erase edits made after it.
  • Excluded and non-regular entries are reported, not applied. An upstream edit to a path the port excludes (make-bot-ui) no longer crashes mid-run with a traceback after rewriting earlier files. Symlinks and submodules are reported for review.
  • Already-matching additions are a no-op instead of a spurious conflict.
  • Three-way inputs go to a temporary directory, so a sibling named foo.upstream-base is never overwritten or deleted.
  • The upstream executable bit is applied on verbatim copies and merges.

Out of scope: the port's own re-sync policy and the audit script, which are unchanged.

Blast Radius

Only the two scripts. The real 0.15.0 range still produces the same split (24 verbatim, 32 clean, 32 for hand review, 2 removed). Nothing in plugins/ changes.

Verification

  • Bun tests, strict typecheck, static invariants, and plugin validation pass (no plugin files touched; CI runs them).
  • The exact candidate is installed in every affected harness. Not applicable: the scripts are maintainer tools, not plugin content.
  • The changed behavior passes from each real user surface.
  • The installed version, action, and observed result appear below.

Live evidence:

python3 scripts/upstream-merge-probe.py <audit.json> on this head at c9ed3e3, from a checkout of open-pstack, builds throwaway worktrees at the audited commit and runs eight checks: the real range, stale HEAD refused, dirty path refused with the edit kept, diverged add/delete reported with nothing written, excluded path reported without crash or directory, sibling temp-name file untouched, executable bit applied, already-matching addition a no-op. All eight print ok. Swapping in the previous upstream-merge.py from main fails all eight (six on behavior, two on the summary wording).

🤖 Generated with Claude Code

…safe rows

The Gavel panel review of PR #60 raised five defects in the lever. It
now refuses to run unless HEAD is the audited port commit and every
mapped path is clean, reports excluded and non-regular entries instead
of crashing on them, treats an already-matching addition as a no-op,
writes its three-way inputs to a temporary directory instead of sibling
files, and applies the upstream executable bit. upstream-merge-probe.py
reproduces each case plus the real 0.15.0 range. Against the previous
script it fails all eight checks, six on behavior and two only on the
summary wording; against this one all eight pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@mergify

mergify Bot commented Sep 8, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@greptile-apps

greptile-apps Bot commented Sep 8, 2026

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5

The PR appears safe to merge, with a non-blocking edge case that falsely refuses all-unmapped audits when unrelated working-tree changes exist.

Findings

  1. P2 Empty paths check everything

Summary

  • Prevents stale audits from overwriting subsequent mapped-path edits.
  • Treats exclusions, non-regular entries, and divergent additions or deletions as review items.
  • Adds executable-mode propagation and temporary-directory isolation.
  • Retains one empty-mapped-path edge case where unrelated working-tree changes cause a false refusal.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Load audit] --> B{HEAD matches audited commit?}
  B -- No --> R[Refuse with exit 2]
  B -- Yes --> C{Mapped paths clean?}
  C -- No --> R
  C -- Yes --> D[Classify each audit row]
  D --> E[Apply verbatim copy or deletion]
  D --> F[Run isolated three-way merge]
  D --> G[Report unsupported or unsafe row]
  E --> H[Apply upstream executable mode]
  F --> H
  G --> I[Exit 1 for hand review]
  H --> J{Any review rows?}
  J -- Yes --> I
  J -- No --> K[Exit 0]
Loading

Comment thread scripts/upstream-merge.py
base, target = audit["upstream_base"], audit["upstream_target"]
verbatim, clean, conflicted, removed, skipped = [], [], [], [], []
mapped = [c for c in audit["changes"] if c["port_path"] is not None]
drift = refuse_drift(audit, [c["port_path"] for c in mapped])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Empty paths check everything

When an audit contains only unmapped upstream changes, ports is empty, so git status ... -- checks the entire working tree instead of no paths. Any unrelated modification or untracked file then makes the lever exit 2 even though every mapped path is clean, preventing it from reporting the unmapped rows as intended. Handle the empty mapped-path set without running an unrestricted status check.

@ericlitman ericlitman left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Gavel verdict: Request changes

8 blocking findings; 0 unconfirmed plausible findings; 0 killed by adjudication.

Quorum: 4/3 successful reviewers.
Continuity: 8 open · 0 resolved · 0 reopened.
Scope: initial full review.

Top findings

  • CRITICAL scripts/upstream-merge.py:95 RIGHT Do not chmod through an old symlink — If upstream changes an unchanged entry from mode 120000 to a regular file, the target-only mode check accepts it. The preceding write follows the existing symlink, and this new apply_mode call follows it again, changing permissions on the link target—potentially outside the repository—while leaving the mapped path as a symlink and reporting success. Reject nonregular current/base entries or unlink and replace the symlink before writing and chmodding.
  • CRITICAL scripts/upstream-merge.py:98 RIGHT Do not chmod after a fatal merge-file error — For a diverged file omitted by sparse checkout, or a mapped path that is a port-side directory, merge_three_way() returns an error without producing a merge result. This unconditional apply_mode() then raises on the missing path or strips traversal bits from the directory. Distinguish fatal merge errors from conflicts and apply the target mode only after a merge against a regular file actually completes.
  • CRITICAL scripts/upstream-merge.py:80 RIGHT Preserve absent deletions in sparse checkouts — For an unchanged-since-base deletion whose tracked file is omitted by sparse checkout, git status -- <path> reports a clean path, but this unconditional removal raises FileNotFoundError, potentially after earlier rows were applied. Treat an already-absent regular path as a successful no-op before recording the deletion.
  • CRITICAL scripts/upstream-merge.py:79 RIGHT Handle sparse missing files before deletion — A sparse checkout or skip-worktree entry can omit a tracked mapped file while git status -- <path> still reports clean. For an unchanged-since-base deletion, this unconditional removal then raises FileNotFoundError, potentially after earlier rows were applied. The deleted if os.path.exists(port): guard prevented this crash; reject such sparse state before mutation or handle the missing path explicitly.
  • CRITICAL scripts/upstream-merge.py:80 RIGHT Check source modes before processing deletions — Deletion rows bypass the new regular-mode check entirely. A clean mapped submodule therefore reaches os.remove() with a directory and crashes, while a symlink is removed instead of being reported for review. Validate the deleted entry's source mode and route non-regular deletions to review before removing anything.
  • MEDIUM scripts/upstream-merge.py:63 RIGHT Avoid whole-repository status with no mapped paths — For an audit containing only unmapped upstream changes, mapped is empty and this call passes no pathspecs. The resulting git status ... -- scans the entire checkout, so any unrelated edit or untracked audit file causes exit 2 even though the merge would touch no paths and should simply list the unmapped rows. Skip the path-status check when ports is empty.
  • MEDIUM scripts/upstream-merge.py:98 RIGHT Do not apply modes after merge-file errors — git merge-file returns 255 for fatal cases such as binary files, not a conflict-hunk count. With a diverged binary at mode 0644 and an upstream target at 0755, the merge leaves the bytes untouched but this line still makes the local file executable, after which the script reports 255 conflict hunks. Distinguish fatal statuses before calling apply_mode and report the merge error without mutating the file.
  • MEDIUM scripts/upstream-merge.py:41 RIGHT Skip repository-wide status when no paths are mapped — When every audit row is unmapped, ports is empty and this command becomes git status ... --, which scans the entire worktree. Any unrelated edit then causes exit 2 even though no mapped path can be overwritten, instead of reporting the unmapped rows successfully. Return clean immediately when ports is empty.

Gavel run panel:6c540dd36cc97b8ce1fbf2693a0412ec63f483c3:0FZpwAs8QvD:O0itVLWrIuU · Commit 6c540dd36cc97b8ce1fbf2693a0412ec63f483c3
Judge: native (primary) openai/gpt-6-astra · tools: yes
Full record: GET /gavel/run/panel:6c540dd36cc97b8ce1fbf2693a0412ec63f483c3:0FZpwAs8QvD:O0itVLWrIuU

@openswebot openswebot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Open SWE Review found 4 potential issues.

Open in Web

Comment thread scripts/upstream-merge.py
head = git("rev-parse", "HEAD").decode().strip()
if head != audit["port_commit"]:
return f"HEAD {head[:12]} is not the audited port commit {audit['port_commit'][:12]}"
dirty = git("status", "--porcelain", "--untracked-files=all", "--", *ports).decode().strip()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 Ignored files bypass the drift guard

git status --porcelain --untracked-files=all omits ignored paths, so this can report a mapped addition as clean even when a local ignored file (or symlink) already occupies its destination. The later open(port, "wb") then truncates that local path. I reproduced this with a valid upstream-addition audit and an exact-path rule in .git/info/exclude: the merge exited 0 and replaced local secret with the upstream blob. This breaks the new no-overwrite safety guarantee; the preflight needs to detect filesystem occupancy for additions even when Git ignores it.

(Refers to line 41)


Your feedback helps Open SWE learn. React with 👍 or 👎 to tell us if this review comment was useful.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The current head removed the occupancy guard from the intermediate revision. I reproduced the original failure again: an ignored mapped addition is treated as clean and local secret is overwritten by the upstream blob, so this remains open.

Comment thread scripts/upstream-merge.py
Comment on lines +31 to +34
def apply_mode(path, mode):
executable = mode == "100755"
current = os.stat(path).st_mode
os.chmod(path, (current | 0o111) if executable else (current & ~0o111))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Executable copies bypass the process umask

current | 0o111 grants execute permission to owner, group, and others after a new file was created under the process umask. Under umask 077, I reproduced a 100755 upstream addition being written as 0711, while checking out the same Git entry produces 0700. The merge tool should not restore execute bits that the caller's umask deliberately withheld; create new executable files with an executable creation mode subject to the umask, or otherwise preserve the masked execute bits.

(Refers to lines 31-34)


Your feedback helps Open SWE learn. React with 👍 or 👎 to tell us if this review comment was useful.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The current head restores current | 0o111, so the original reproduction applies again: under umask 077 an executable addition is written as 0711 instead of Git's 0700. This remains open.

Comment thread scripts/upstream-merge.py
Comment thread scripts/upstream-merge.py
continue
if os.path.exists(port):
os.remove(port)
os.remove(port)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Sparse deletions crash on absent files

The unconditional os.remove(port) raises FileNotFoundError when an audited unchanged-since-base deletion is already absent from the working tree because the path is sparse/skip-worktree. Git reports that path as clean, so the new preflight does not stop the run; I reproduced the traceback with an audit generated from valid commits. If earlier rows were applied, this also leaves a partially updated tree. Treat an absent working-tree path as an already-completed deletion; lexists still allows a tracked broken symlink to be removed.

(Refers to line 80)


Your feedback helps Open SWE learn. React with 👍 or 👎 to tell us if this review comment was useful.

Suggested change
os.remove(port)
if os.path.lexists(port):
os.remove(port)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This remains reproducible on 22df575: a valid unchanged-since-base deletion whose path is absent under skip-worktree passes the drift check and raises FileNotFoundError at the unconditional os.remove.

@ericlitman

Copy link
Copy Markdown
Owner Author

Round 1 adjudication, both reviewers, one push.

Gavel (8 must-fix, 5 distinct defects). All real, all reproduced by the panel and now by the probe: chmod through a symlink at the port path; repository-wide git status when no path is mapped; mode change after a failed git merge-file (binary files return 255); crashes on skip-worktree files that are absent from disk (deletion and merge branches); gitlink or symlink deletions reaching os.remove. Fixed by one entry check per row before any write, plus a merge-file exit-code range.

Open SWE (4). Ignored file at an addition path: real; the new existence check reports it and the probe proves the local file survives. Execute bits past the umask: real; the bit now follows the read bits the umask left, and an addition under umask 077 lands as 0700. Empty audits scanning the worktree: real, same defect Gavel and Greptile found, fixed. Sparse deletions crashing: real, but I kept fail-closed reporting ("missing from the checkout") rather than treating the absence as a completed deletion, because a skip-worktree omission and an accidental local delete look identical to the script.

Probe: 15 checks, all pass on the new head. The previous head fails exactly the 7 new ones.

@ericlitman ericlitman left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Gavel verdict: Request changes

3 blocking findings; 0 unconfirmed plausible findings; 8 killed by adjudication.

Quorum: 4/3 successful reviewers.
Continuity: 3 open · 8 resolved · 0 reopened.
Scope: incremental from 6c540dd · 2 changed files since that review · 8 carried demands.

Top findings

  • CRITICAL scripts/upstream-merge-probe.py:168 RIGHT Do not interpolate paths into shell commands — The new sh -c calls embed values without shell escaping. Here, row["port_path"] comes from an audited upstream filename, so an addition such as pstack/skills/x'; touch "$HOME/pwn"; # executes arbitrary commands when the probe creates its ignore fixture. The same issue at line 39 makes the umask test fail when the checkout or temporary path contains a single quote. Avoid shell composition or safely quote every interpolated value at both sites.
  • HIGH scripts/upstream-merge-probe.py:168 RIGHT Keep ignore fixtures out of the shared Git directory — worktree() creates linked worktrees, so this branch resolves info/exclude through the main repository's shared Git directory. Every normal probe run permanently appends the selected path to the user's exclude file, hiding future untracked files there; removing the temporary worktree does not undo it. Use a worktree-local ignore fixture or restore the shared file.
  • MEDIUM scripts/upstream-merge.py:34 RIGHT Honor independent umask execute bits — For a new executable addition under umask 0111, open() creates mode 0666 and this expression infers execute bits from readable bits, changing it to 0777; Git-style creation with mode 0777 under that umask would remain 0666. Other legal masks can suppress execute bits that the formula should retain. Create executable additions with mode 0777 so the OS applies the actual umask.

Gavel run panel:076672d7c73312cf8fc31c39addbab936ba5b6d9:vgyI9qPNHQK:t0qmmnaZF4E · Commit 076672d7c73312cf8fc31c39addbab936ba5b6d9
Judge: native (primary) openai/gpt-6-astra · tools: yes
Full record: GET /gavel/run/panel:076672d7c73312cf8fc31c39addbab936ba5b6d9:vgyI9qPNHQK:t0qmmnaZF4E

… failed merges

Two review findings bind on this repository. The documented invocation
writes audit.json next to the script, and with no mapped path the drift
check ran a repository-wide status and refused the run; it now skips the
status check when nothing is mapped. git merge-file returns 255 on a
binary file such as the mapped logo, which the lever counted as hunks
and then chmodded; exit codes outside 0..127 are now reported and the
file is left alone. Two probe checks cover both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ericlitman
ericlitman force-pushed the fix/upstream-merge-guards branch from 076672d to 22df575 Compare September 8, 2026 22:54
@ericlitman

Copy link
Copy Markdown
Owner Author

Adjudication on head 22df575, applying the repository rule that a finding earns code only when it changes a real outcome on a PR shape this repository produces.

Fixed (bind here):

  • Empty-audit repository-wide status (Gavel, Open SWE, Greptile). The documented command writes audit.json next to the script, so an all-unmapped audit was refused by its own output file. Probe: "all-unmapped audit is not refused by the untracked audit file".
  • Mode change after a failed git merge-file (Gavel). assets/logo.png is a mapped binary; a diverged binary returns 255. Probe: "binary merge-file failure reported, no mode change".

Dismissed with disproof (no such shape in this repository):

  • Symlink at a port path, gitlink or symlink deletion. git ls-tree -r origin/main plugins/pstack contains only mode 100644 and 100755 entries; the mapping covers only skills/, agents/, assets/, and the README. Upstream's pstack/ tree at the pinned target has no symlinks or gitlinks either.
  • Skip-worktree and sparse-checkout omissions. No checkout of this repository uses either; the sync runs from a full clone at the audited commit, which the HEAD check enforces.
  • Ignored file occupying an upstream addition path. The mapped directories carry no ignore rules, and an addition path is by definition absent from the port tree at the audited commit.
  • Sibling files named <path>.upstream-base or .upstream-target. Nothing in the repository or its tooling creates such names; the previous head already moved the temp files to a temporary directory in any case.
  • Execute bits past the umask (077 and 0111). The maintainer runs the sync interactively with the default 022 mask; git's own checkout behaves identically under those masks for directories.
  • Shell interpolation of upstream paths in the probe. The probe never shelled out on the reviewed head; that code existed only on an intermediate, unpushed head and is gone.

Round 3 is the last per the rules. Anything further that is not a defect this repository can hit will be waived on this head with the same record.

@ericlitman

Copy link
Copy Markdown
Owner Author

Adjudication on head 22df575, applying the repository rule that a finding earns code only when it changes a real outcome on a PR shape this repository produces.

Fixed (bind here):

  • Empty-audit repository-wide status (Gavel, Open SWE, Greptile). The documented command writes audit.json next to the script, so an all-unmapped audit was refused by its own output file. Probe: "all-unmapped audit is not refused by the untracked audit file".
  • Mode change after a failed git merge-file (Gavel). assets/logo.png is a mapped binary; a diverged binary returns 255. Probe: "binary merge-file failure reported, no mode change".

Dismissed with disproof (no such shape in this repository):

  • Symlink at a port path, gitlink or symlink deletion. git ls-tree -r origin/main plugins/pstack contains only mode 100644 and 100755 entries; the mapping covers only skills/, agents/, assets/, and the README. Upstream's pstack/ tree at the pinned target has no symlinks or gitlinks either.
  • Skip-worktree and sparse-checkout omissions. No checkout of this repository uses either; the sync runs from a full clone at the audited commit, which the HEAD check enforces.
  • Ignored file occupying an upstream addition path. The mapped directories carry no ignore rules, and an addition path is by definition absent from the port tree at the audited commit.
  • Sibling files named <path>.upstream-base or .upstream-target. Nothing in the repository or its tooling creates such names; the previous head already moved the temp files to a temporary directory in any case.
  • Execute bits past the umask (077 and 0111). The maintainer runs the sync interactively with the default 022 mask; git's own checkout behaves identically under those masks for directories.
  • Shell interpolation of upstream paths in the probe. The probe never shelled out on the reviewed head; that code existed only on an intermediate, unpushed head and is gone.

Round 3 is the last per the rules. Anything further that is not a defect this repository can hit will be waived on this head with the same record.

@ericlitman ericlitman left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Gavel verdict: Request changes

10 blocking findings; 0 unconfirmed plausible findings; 2 killed by adjudication.

Quorum: 4/3 successful reviewers.
Continuity: 10 open · 2 resolved · 0 reopened.
Scope: full review (basis-not-ancestor).

Top findings

  • CRITICAL scripts/upstream-merge.py:42 RIGHT Include ignored mapped additions in drift detection — git status omits ignored untracked files. If upstream adds a mapped path that already exists locally but is ignored, the audit labels it upstream-addition, this check reports clean, and the copy branch truncates the local file while exiting successfully. Detect ignored entries or explicitly lstat addition destinations before writing.
  • CRITICAL scripts/upstream-merge.py:42 RIGHT Do not trust assume-unchanged paths as clean — git status suppresses edits to mapped files marked assume-unchanged or skip-worktree. An audit whose committed entry is unchanged-since-base therefore passes this check and the verbatim branch overwrites the hidden local edit. Reject such index flags or compare each mapped filesystem entry directly with the audited port entry before mutation.
  • CRITICAL scripts/upstream-merge.py:82 RIGHT, scripts/upstream-merge.py:82-83 RIGHT Deletion path crashes when the mapped file is absent from the worktree — This hunk deletes the guard if os.path.exists(port): and calls os.remove(port) unconditionally. refuse_drift only consults git status --porcelain -- <ports>, which reports a clean path for entries marked skip-worktree/assume-unchanged or omitted by a sparse checkout (the probe added in this same PR uses git update-index --assume-unchanged to produce exactly that state). For an unchanged-since-base delete row whose tracked file is not on disk, os.remove raises FileNotFoundError: the run dies with a traceback after earlier rows have already been written verbatim or merged, printing no summary, leaving the tree half-applied. Restore an existence check (or route the missing path to review) before removing.
  • CRITICAL scripts/upstream-merge.py:82 RIGHT Validate deletion source modes — Deletion rows bypass regular-mode validation. A clean mapped gitlink reaches os.remove() as a directory and raises IsADirectoryError, while a symlink is removed instead of being reported for review. Validate the base mode and on-disk entry type before deleting.
  • CRITICAL scripts/upstream-merge.py:34 RIGHT Preserve umask execute-bit masking — For a new 100755 addition under umask 0111, open() creates mode 0666 and this OR restores every execute bit, producing 0777; creating the executable with mode 0777 under that mask would remain 0666. Apply the requested executable mode at creation so the OS preserves independent umask bits.
  • CRITICAL scripts/upstream-merge.py:95-96 RIGHT, scripts/upstream-merge.py:96 RIGHT New apply_mode chmods through a symlinked port path — Nothing checks the on-disk type of port: the new REGULAR_MODES test inspects only the upstream target mode, and refuse_drift only checks cleanliness. If a mapped path is a tracked symlink in the port (upstream base carried mode 120000 and the target converts it to a regular file, so the row compares unchanged-since-base), open(port,"wb") writes through the link and the newly added apply_mode(port, mode) then os.stat/os.chmod the link target, changing permissions of a file outside the repository (e.g. making it executable) while the mapped path stays a symlink and the run reports success. The same sequence occurs on the merge branch, where apply_mode(port, mode) at line 103 follows the link after git merge-file writes through it. Reject non-regular current entries (or unlink and replace) before writing and chmodding.
  • CRITICAL scripts/upstream-merge.py:78-84 RIGHT Validate source modes before every mutation — Deletion is processed before the regular-mode check, and that check validates only the target. A clean deleted gitlink therefore reaches os.remove() on its directory and crashes, while a tracked symlink replaced upstream by a regular file passes the target check and causes the later write/chmod to follow and mutate the link target. Validate the audited base/port modes and the current entry with lstat before deletion, copying, or merging, and route every non-regular source to review.
  • CRITICAL scripts/upstream-merge.py:95 RIGHT Reject symlinks in parent path components — For an upstream addition such as pstack/skills/x/file when the port commit contains plugins/pstack/skills/x as a symlink to an external directory, status for the exact child is clean and the addition branch traverses the symlink. It writes the external file and this changed apply_mode call chmods it while reporting success. Verify every parent component remains a real directory beneath the checkout, or use no-follow directory-relative operations.
  • CRITICAL scripts/upstream-merge.py:78 RIGHT Handle sparse missing deletions before removing — A tracked regular file omitted by sparse checkout or skip-worktree can produce clean status while being absent on disk. For an unchanged-since-base deletion, this unconditional os.remove raises FileNotFoundError, potentially after earlier audit rows were already applied. Preflight the path or safely report/treat an already-absent deletion without calling os.remove.
  • MEDIUM scripts/upstream-merge.py:42 RIGHT Use literal pathspecs for mapped filenames — Mapped filenames are passed as Git pathspec patterns. If upstream contains a legal literal name such as plugins/pstack/skills/foo*, an unrelated untracked sibling named foobar matches the wildcard and makes the script exit 2 even though the mapped file is clean. Pass literal pathspecs, for example with --literal-pathspecs or :(literal) prefixes.

Gavel run panel:22df5758cea785a02fe1dd8df3e59276be9d7078:ag7M00V5T4e:dJL6PQODJxg · Commit 22df5758cea785a02fe1dd8df3e59276be9d7078
Judge: native (primary) openai/gpt-6-astra · tools: yes
Full record: GET /gavel/run/panel:22df5758cea785a02fe1dd8df3e59276be9d7078:ag7M00V5T4e:dJL6PQODJxg

@ericlitman

Copy link
Copy Markdown
Owner Author

Adjudication on head 22df575, applying the repository rule that a finding earns code only when it changes a real outcome on a PR shape this repository produces.

Fixed (bind here):

  • Empty-audit repository-wide status (Gavel, Open SWE, Greptile). The documented command writes audit.json next to the script, so an all-unmapped audit was refused by its own output file. Probe: "all-unmapped audit is not refused by the untracked audit file".
  • Mode change after a failed git merge-file (Gavel). assets/logo.png is a mapped binary; a diverged binary returns 255. Probe: "binary merge-file failure reported, no mode change".

Dismissed with disproof (no such shape in this repository):

  • Symlink at a port path, gitlink or symlink deletion. git ls-tree -r origin/main plugins/pstack contains only mode 100644 and 100755 entries; the mapping covers only skills/, agents/, assets/, and the README. Upstream's pstack/ tree at the pinned target has no symlinks or gitlinks either.
  • Skip-worktree and sparse-checkout omissions. No checkout of this repository uses either; the sync runs from a full clone at the audited commit, which the HEAD check enforces.
  • Ignored file occupying an upstream addition path. The mapped directories carry no ignore rules, and an addition path is by definition absent from the port tree at the audited commit.
  • Sibling files named <path>.upstream-base or .upstream-target. Nothing in the repository or its tooling creates such names; the previous head already moved the temp files to a temporary directory in any case.
  • Execute bits past the umask (077 and 0111). The maintainer runs the sync interactively with the default 022 mask; git's own checkout behaves identically under those masks for directories.
  • Glob characters in mapped filenames defeating the pathspec. git ls-tree -r on both the port tree and the pinned upstream tree contains no filename with *, ?, or [.
  • Symlinked parent directories and assume-unchanged index flags. Same disproof as the tree modes above: no symlink exists anywhere under plugins/pstack, and no checkout of this repository sets assume-unchanged.
  • Shell interpolation of upstream paths in the probe. The probe never shelled out on the reviewed head; that code existed only on an intermediate, unpushed head and is gone.

Round 3 is the last per the rules. Anything further that is not a defect this repository can hit will be waived on this head with the same record.

@ericlitman
ericlitman merged commit c8481f7 into main Sep 8, 2026
3 of 5 checks passed
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.

1 participant