Skip to content

fix(bin): deterministically order remote tool paths - #2870

Merged
kunchenguid merged 2 commits into
kunchenguid:mainfrom
karotkriss:fm/fm-2843-path-order
Aug 23, 2026
Merged

fix(bin): deterministically order remote tool paths#2870
kunchenguid merged 2 commits into
kunchenguid:mainfrom
karotkriss:fm/fm-2843-path-order

Conversation

@karotkriss

@karotkriss karotkriss commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes #2843

Intent

Fix GitHub issue #2843: fm-on.sh's composed child PATH does not match the sorted portable-PATH contract tests/fm-on.test.sh expects. The reporter saw tests/fm-on.test.sh fail identically on pristine main on macOS: the child PATH a real fm-on remote dispatch composes grouped the mise/asdf install directories in filesystem-readdir order, while the documented portable-PATH contract - and the test's from-scratch reconstruction of it, which uses a literal bash glob - produce sorted order. The issue did not identify the composition site and asked whether the discrepancy is cosmetic or a real tool-version-resolution defect.

Root cause traced and reproduced: bin/fm-remote-job-lib.sh's fm_remote_job_compose_operator_path is the single composition owner for both the SSH entrypoint and the long-lived remote job worker, and it built the asdf and mise install directories through fm_remote_job_append_glob_dirs, which used compgen -G. Bash sorts glob matches only on the shell's own pathname-expansion path (pathexp.c calls strvec_sort after glob_filename); compgen -G reaches the same glob_filename through pcomplete.c, which sorts nothing. Verified in the bash 3.2 source (macOS /bin/bash) and reproduced live against a locally built bash 5.2: compgen -G returned raw readdir order while a literal shell glob on the identical pattern returned sorted order. Bash 5.3 moved sorting into the glob library, which is why the defect is invisible on a modern Linux dev box and on CI.

This was answered as version-resolution-relevant, not cosmetic: the mise/asdf path deliberately appends every install directory of every tool with no version selection of its own (unlike the nvm path, which has explicit selection), so PATH order alone decides which node/bun/python a remote job runs. Readdir order made that choice depend on the filesystem and change whenever directories are added or removed.

Fix decision: rather than adding a sort next to compgen, make the composition use the same mechanism the contract is written in. fm_remote_job_append_glob_dirs was renamed to fm_remote_job_append_dirs and now takes already-expanded matches, with the three call sites expanding the globs themselves with the account home quoted. The composition and the documented contract are now literally the same operation and cannot drift again on any bash, in any locale. Renaming rather than keeping the old name was deliberate: a caller passing a quoted pattern to the new signature would be silently dropped, so the name change makes the new contract explicit. Quoting the account home at the call site also fixes a latent second bug - a home directory whose name contains glob metacharacters was previously reinterpreted by compgen.

Regression coverage was deliberately placed in tests/fm-remote-job.test.sh, the colocated test of the composition owner, rather than in tests/fm-on.test.sh as the original triage suggested: fm-on.test.sh asserts the end-to-end contract against the real account home, so it can only exercise the glob path on a host that actually has mise or asdf installs and cannot be made deterministic. tests/fm-on.test.sh is deliberately left untouched and was NOT adjusted to accept readdir order. The new case asserts that the composed order equals the shell's own pathname expansion for a fixture home created in a deliberately unsorted order. Verified: that assertion fails on pre-fix code under bash 3.2 and bash 5.2, and passes on the fix.

Honest limitation, worth knowing before reading CI as proof: bash 5.3 moved glob sorting into the glob library, so on bash 5.3 and later compgen -G and shell pathname expansion agree and this assertion reads green even against the unfixed code. It detects the defect only where the defect exists - bash 3.2 (macOS /bin/bash) through bash 5.2. An earlier revision of this branch also carried a source-level guard asserting the library never composes PATH through compgen, which would have covered that blind spot on any runtime; it was removed during review because this repository's test-quality rule forbids tests that read implementation source, and the rule was judged to win. The removal is deliberate and the trade is recorded here rather than left implicit.

Vision alignment (VISION.md at current upstream main): "Logic that can be exact lives in deterministic scripts ... intelligence must never be spent on what a script can do exactly and repeatably" - remote tool resolution is exactly such logic, and it was not deterministic. Also "This repository ships through its own discipline: firstmate work is validated like any other project's, and field incidents become regression coverage." This is a strengthened determinism guarantee inside an existing owner, not a default-behavior change and not a new layer: no new files, no new dependency, one function signature narrowed, one mechanism removed.

Scope notes: no documentation file needed changing beyond the library header's own PATH paragraph, which now states the ordering guarantee. docs/remote-secondmates.md already names bin/fm-remote-job-lib.sh as the single owner of the worker PATH and says nothing that the fix contradicts. Two test failures observed locally (tests/fm-on.test.sh's doctor missing-tools case and tests/fm-remote-doctor.test.sh's stale-worker-identity case, plus a timing-sensitive queue case in tests/fm-remote-job.test.sh) were confirmed pre-existing on pristine HEAD in this environment and are unrelated to this change. The PR should close #2843 only.

What Changed

  • Compose asdf and mise install paths from shell-expanded globs, preserving deterministic pathname order and safely quoting the account home.
  • Document the ordering guarantee and add regression coverage comparing composed mise paths with the shell’s native expansion order.

Risk Assessment

✅ Low: The change is narrowly scoped, aligns PATH composition with the established shell-expansion contract, preserves existing filtering behavior, and removes the prohibited source-reading assertion.

Testing

The focused remote-job test passed under the host Bash 5.3 and an isolated Bash 5.2.37 built after a C17-compatible compiler retry. Before-and-after evidence reproduced the original version-resolution defect and proved the fix, while a queued-worker transcript confirmed sorted resolution and safe handling of glob metacharacters. No source changes or transient worktree artifacts were created.

Evidence: Bash 5.2 before-and-after PATH resolution

Source: Bash 5.2 before-and-after PATH resolution

Bash runtime: 5.2.37(1)-release
Fixture creation order: node/8.1, node/26, node/26.7.0
Contract order (literal shell glob):
  /tmp/fm2843-case2/home/.local/share/mise/installs/node/26.7.0/bin
  /tmp/fm2843-case2/home/.local/share/mise/installs/node/26/bin
  /tmp/fm2843-case2/home/.local/share/mise/installs/node/8.1/bin
Before fix composed mise order:
  /tmp/fm2843-case2/home/.local/share/mise/installs/node/8.1/bin
  /tmp/fm2843-case2/home/.local/share/mise/installs/node/26/bin
  /tmp/fm2843-case2/home/.local/share/mise/installs/node/26.7.0/bin
Before fix resolved executable: node-8.1
After fix composed mise order:
  /tmp/fm2843-case2/home/.local/share/mise/installs/node/26.7.0/bin
  /tmp/fm2843-case2/home/.local/share/mise/installs/node/26/bin
  /tmp/fm2843-case2/home/.local/share/mise/installs/node/8.1/bin
After fix resolved executable: node-26.7.0
Evidence: Queued remote worker tool resolution

Source: Queued remote worker tool resolution

Queued remote worker result for account home containing glob metacharacters:
worker-command=/tmp/fm2843-worker-e2e2/account[qa]/.local/share/mise/installs/node/26.7.0/bin/node
worker-resolved=node-26.7.0
worker-path-mise-order:
/tmp/fm2843-worker-e2e2/account[qa]/.local/share/mise/installs/node/26.7.0/bin
/tmp/fm2843-worker-e2e2/account[qa]/.local/share/mise/installs/node/26/bin
/tmp/fm2843-worker-e2e2/account[qa]/.local/share/mise/installs/node/8.1/bin
exit=0

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • 🚨 tests/fm-remote-job.test.sh:187 - The new grep reads implementation source and asserts that compgen is absent, which the supplied test-quality rule explicitly forbids. It can also fail if unrelated compgen usage is added anywhere in this library, while lines 178-182 already verify observable PATH ordering. Remove this assertion or replace it with behavioral execution under Bash 3.2 or 5.2. This conflicts with the accepted intent requiring that the test “pins ... the library must not compose PATH through compgen at all (a source-level guard),” so captain input is required.

🔧 Fix: Remove source-reading PATH regression guard
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-remote-job.test.sh
  • /tmp/fm2843-bash52/install/bin/bash tests/fm-remote-job.test.sh
  • Bash 5.2 before-and-after execution of fm_remote_job_compose_operator_path using the base and target libraries, followed by executable resolution through the resulting PATH
  • Real fm-remote-job-worker.sh queue flow using fm_remote_job_stage, fm_remote_job_wait, and a fake multi-version node installation under account[qa]
  • Verified the worktree remained clean and no test worker processes remained
✅ **Document** - passed

✅ No issues found.

⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 1)
✅ **Push** - passed

✅ No issues found.

fm_remote_job_compose_operator_path built the asdf and mise install
directories with `compgen -G`, which does not sort. Bash sorts glob
matches in pathexp.c, on the shell's own pathname-expansion path only;
`compgen -G` reaches the same glob_filename through pcomplete.c, which
sorts nothing. On bash 3.2 (macOS /bin/bash) and every bash before 5.3
that handed the composition raw readdir order, so which install of a
multi-version tool a remote job resolved was decided by directory order
on disk rather than by this composition.

Expand the globs at the call sites and let the function take the matches,
so the composition and the documented portable-PATH contract are the same
operation. Quoting the account home at the call site also stops a home
whose name contains glob metacharacters from being reinterpreted.

The colocated regression pins both the order and the mechanism: bash 5.3
moved sorting into the glob library, so an order-only assertion cannot
see the defect there.
@greptile-apps

greptile-apps Bot commented Aug 23, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge with no actionable defects identified.

The changed helper preserves unmatched-glob filtering and PATH deduplication, all repository call sites use the new contract, and focused coverage verifies deterministic install ordering.

Reviews (1): Last reviewed commit: "no-mistakes(review): Remove source-readi..." | Re-trigger Greptile

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

Scheduled 11:10am PT 8/23 pass. VISION.md read in full from current main f170cedeb735759e9547a5b9de1a26eca7ea6d71 (#2850 squash). Issue #2843 is ready-for-pr; that is a queue label, not a merge vote. No captain comment authorizing a merge. First inspection of this PR.

VISION (inspected fm_remote_job_append_dirs replacing compgen -G, quoted-home glob expansion at the three asdf/mise call sites, colocated order assertion in tests/fm-remote-job.test.sh). Per-rule: scripts own exact logic aligns (which install of a multi-version tool a remote job resolves was filesystem-readdir order on bash 3.2/5.2; the portable-PATH contract and tests/fm-on.test.sh already required the shell's own sorted pathname expansion); field incidents become coverage aligns; vendor-orthogonal aligns; not a new layer. This restores the documented composition rather than shipping a new PATH policy. Honest limitation recorded in the PR: the new assertion is blind on bash 5.3+ where compgen -G already sorts; CI's Stock macOS Bash snapshot job is the discriminating runtime and it is green.

Class: corrective. Auto-eligible class. Not default-behavior: production was required to match the existing sorted contract; locale-identical hosts already agreed.

Security: none. Quoting the account home at the call site also stops glob-metacharacter reinterpretation of the home path. No workflow-file / secret / injection risk.

Overlap / HOLD: none of the standing spawn/teardown/herdr/pool mechanism holds. Incidental same-file overlap with #2586 (teardown rewrite hold) on tests/fm-remote-job.test.sh — this adds a PATH-order case; it is not a teardown sibling. bin/fm-remote-job-lib.sh is otherwise not in a named hold pair.

CI / NM: HEAD 59c645a35b263581ab0f8be70e3d73285e2c48a5. MERGEABLE / CLEAN, ahead 2 / behind 3 vs current main (disjoint from this two-file diff). Matching no-mistakes-pipeline-attestation:v1 for THIS HEAD. Require no-mistakes SUCCESS (runs 32646112875, 32646981593). CI run 32646112872 all SUCCESS including Stock macOS Bash snapshot compatibility. Greptile SUCCESS — not a gate. Do not @greptileai.

Workflows: already approved (CI completed SUCCESS on this HEAD). Run IDs: 32646112872 (CI), 32646112875 / 32646981593 (Require no-mistakes). No pending first-time-fork approval.

Land-eligible rec: YES (corrective; matching NM attestation; green CI including macOS Bash snapshot; CLEAN despite behind 3). Captain-flag NOW: no.

@greptile-apps

greptile-apps Bot commented Aug 23, 2026

Copy link
Copy Markdown

The diff is clean and the triage reads correctly to me.

What I see in the code:

  1. Rename is intentional and right. fm_remote_job_append_glob_dirsfm_remote_job_append_dirs with "$@" instead of compgen -G "$pattern" — the name change is a deliberate contract signal: a caller passing a quoted pattern to the new function gets nothing silently dropped, so renaming forces the call sites to be explicit. All three call sites are updated correctly.

  2. Quoting at the call sites is correct. "$account_home"/.asdf/installs/*/*/bin — the home is double-quoted, the glob is unquoted. This is the right form: word-splitting and glob reinterpretation are blocked on the home component, and the shell expands the wildcard portion itself in sorted order.

  3. Test construction is sound. Fixtures created in deliberate unsorted order (26.7.0, 8.1, 26, 1.4, 1.3.14, 3.12.7), then MISE_COMPOSED is extracted from the composed PATH, and MISE_EXPECTED is printf '%s\n' "$MISE_INSTALLS"/*/*/bin — literally the same expansion mechanism as the portable-PATH contract. The cleanup rm -rf -- "$ACCOUNT_HOME/.local/share/mise" is scoped to the mise subtree, not the whole account home.

  4. Known limitation is correctly characterized. The assertion is blind on bash 5.3+ (glob sorting moved into the glob library, so compgen -G and shell expansion agree there). The macOS Bash snapshot CI job is the discriminating runtime and it's green. That's the right place to catch it given the PR's explicit constraint against source-reading guards.

No issues from my side. Aligns with triage: corrective, no new layer, quoting fix is a real second bug closed for free.

@kunchenguid
kunchenguid merged commit 197afbb into kunchenguid:main Aug 23, 2026
15 checks passed
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: this is merged. Thank you @karotkriss — really appreciate you taking the time on this.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fm-on.sh's composed child PATH does not match the sorted portable-PATH contract fm-on.test.sh expects

2 participants