Four entry points CLAUDE.md documents as ./tool.py are not executable in git, so they fail on Linux nf-7ca7 ← Beads

closed priority 2 task unassigned

Twinned from https://github.com/cfsh/ninefold/issues/592 by tools/beads/import_github.py's reconcile pass.

CLAUDE.md documents six entry points as ./tool.py. Four of them are mode 100644 in git, so on Linux the documented command is Permission denied:

``` $ git ls-files -s tools/pr/*.py presubmit.py f5.py 100755 presubmit.py ✅ 100755 tools/pr/poll.py ✅ 100644 tools/pr/merge_pr.py ❌ CLAUDE.md:184 "./tools/pr/merge_pr.py <n>" 100644 tools/pr/rebuild_f5.py ❌ CLAUDE.md:180 "./tools/pr/rebuild_f5.py, ~10s" 100644 f5.py ❌ CLAUDE.md:180,225 "./f5.py" 100644 tools/pr/probe_gate.py ❌ named in merge_pr.py's own SKIP_PROBE_GATE message

$ ./tools/pr/merge_pr.py --check 558 /bin/bash: line 1: ./tools/pr/merge_pr.py: Permission denied ```

python3 tools/pr/merge_pr.py works, so this is only the documented *form* — but the documented form is what a session follows, and merge_pr.py is the one where "never hand-rolled gh pr merge" makes following it non-optional.

Why it has not bitten before, and why it does now

The exec bit is not observable on the Windows sandbox box the way it is here — those four have been invoked as ./… there daily. This is the same class as #566: tooling that encodes a property of one box while claiming to be platform-neutral, discovered the moment the other box tried to do the thing.

⚠️ It became reachable on 2026-08-05, when dotnet was installed here and #567 made --all pass — the first moment a Linux box could actually run merge_pr.py. Before that the command was unreachable for a different reason and the missing bit was invisible.

The failure is loud (Permission denied, immediately, before anything happens), which is the good kind. But a session that hits it mid-merge has to work out on its own that the fix is python3 <path> rather than that something is broken — and the merge path is the worst place to spend that confusion.

The fix, and the thing to get right

```bash git update-index --chmod=+x tools/pr/merge_pr.py tools/pr/rebuild_f5.py \ tools/pr/probe_gate.py f5.py ```

⚠️ Use git update-index --chmod=+x, not chmod + git add. core.filemode is true on this box but is commonly false on Windows checkouts, so a plain chmod may not be recorded from there at all — the mode has to be set in the index explicitly for the change to survive.

Worth pairing with a test, since this is exactly the class the suite already guards for shell (tools/tests/test_launchers.py pins launcher properties): assert that every path CLAUDE.md documents as ./… is 100755 in git ls-files -s. That converts "someone remembers" into a gate, and it would have caught all four.

tools/office/spawn_dev.py is 100755 and presubmit.py/poll.py are already correct, so this is four files.

Filed unclaimed by nf-dev-office-spawn while working #557. Not on my chunk — hit it running merge_pr.py --check as merge pre-flight for #558. Related: #566 (platform-honest tooling), #567, #575, #584.

Dependencies

None.

Comments

No comments.

Add a comment