Twinned from https://github.com/cfsh/ninefold/issues/433 by tools/beads/import_github.py's reconcile pass.
Filed unclaimed and unlabelled by the /design round that produced it, for a future /triage to activate. Do not start this piecemeal — the work breakdown lives in the doc, attached to the argument that produced it.
Doc: [design_docs/001-no-logic-in-shell.md](https://github.com/cfsh/ninefold/blob/main/design_docs/001-no-logic-in-shell.md) · design PR #429
No logic in checked-in shell. The Tier-2 tooling tier (~2,478 lines across 15 scripts) moves to Python — directly executable ./tools/pr/poll.py with #!/usr/bin/env python, subprocess wrappers (nflib) with hand-written fakes, and a single polyglot ./presubmit.py that lands before any script migrates. No .sh wrappers, no Bazel, no compiled artifact.
Why: 92% of the tier's code lines sit in scripts past the Google Shell Style Guide's own rewrite threshold, and the failure mode is documented in this repo — #352 killed the Tier-0 reviewer for ~38 hours from a set -euo pipefail wrinkle while the log kept looking healthy; #382 reintroduced the same class *in the fix for it*; the gojq anchor bug shipped twice in one file. Cost is zero new dependencies: stdlib unittest, and python is already on the critical path because this box has no jq.
~11 PRs, honestly 2 chunks, not 1. The doc marks the split after piece 5.
nflib + presubmit.py (incl. --doctor) + the gate wired into merge_pr. Nothing migrates in it.poll.sh — the dispatcher heartbeat + STALE-DISPATCH. This is the *detection* half of #352, which the language change does not fix. Neither piece 2 nor 3 merges without it.poll.py (2) → dispatch.py (3) → merge_pr.py (4) → probe_gate.py (5) → rebuild_f5 (6) → playtest cluster (7) → f5 (8) → probes (9) → release (10) → CLAUDE.md (11).Migration is characterization-first: a throwaway bash harness pins the existing script's golden output, port, diff byte-identical, delete both.
nflib/__init__.py must sys.stdout.reconfigure(encoding="utf-8") on day one. Python's stdout here is cp1252 and nine of the characters this tier prints are outside it — it passes interactively and crashes under redirection, which is every cron tick. probe_gate.sh:161's sentinel is eight U+2500, so a probe_all.py literally cannot emit the string the UI gate matches on. §2.4.os.environ.get(X, "0") == "1", never bare truthiness. Every escape hatch in the tier is an exact-string test; Python truthiness inverts three of them, and REVIEWER_SELF_SYNC=0 would run git reset --hard in a working checkout. §6.#414's prescription (a permanent bash test harness). Its *diagnosis* is excellent, reproduced and credited throughout the doc, and its mkrepo/stub/assert_unchanged harness is adopted wholesale — but as a throwaway differential oracle for the migration, not as the destination. #414 should close when piece 1 lands.
Also absorbs the Shell tooling backlog chunk (#414, #411, #412) and answers part of CI / presubmit (#416, #417, #409) — triage should probably collapse those rather than run three chunks.
None.
No comments.