Skip to content

fix(herdr): match bare composer prompt glyphs by alternation, not a bracket class#647

Open
zachlandes wants to merge 3 commits into
kunchenguid:mainfrom
zachlandes:fm/herdr-composer-locale
Open

fix(herdr): match bare composer prompt glyphs by alternation, not a bracket class#647
zachlandes wants to merge 3 commits into
kunchenguid:mainfrom
zachlandes:fm/herdr-composer-locale

Conversation

@zachlandes

Copy link
Copy Markdown

What Changed

  • FM_BACKEND_HERDR_BARE_PROMPT_RE now defaults to the alternation '^(❯|›)' rather than the bracket class '^[❯›]'.
    Under a byte-oriented locale (LC_CTYPE=C, which the supervise daemon inherits) grep walks a bracket class byte-wise, so [❯›] matched any row whose first byte was 0xE2 — including the box-drawing glyphs ╭ ╰ │.
    Because fm_backend_herdr_composer_state lets the lowest matching row win, a bordered composer's own closing border overwrote the correct shape=bordered verdict and was carried in as content: an empty composer read pending (away-mode defers delivery), and a dark-themed composer holding real typed text read empty (away-mode injects over an unsubmitted draft).
    The alternation matches each full glyph sequence, so it is correct in any locale and matches a strict subset of the old pattern's matches.
  • tests/fm-backend-herdr.test.sh adds three composer_state cases that pin LC_ALL=C explicitly, which no composer case did before.
    Two are the regression pins and cover both failure directions (empty bordered composer reads empty; a dark-themed composer with real text reads pending); the third pins that the alternation still matches each full unbordered / prompt glyph, so a pattern matching nothing could not satisfy the other two.
  • docs/herdr-backend.md records the 2026-07-16 incident and a known-gap note, and docs/configuration.md warns that an override of this knob must be spelled as an alternation.
    The gap: the composer's two grep call sites (fm_backend_herdr_composer_state and fm_composer_idle_matches in bin/fm-composer-lib.sh) still inherit the ambient locale, unlike fm_composer_strip_ansi and fm_composer_strip_ghost which pin LC_ALL=C.
    The shipped defaults are locale-invariant, so fleet behavior is correct, but an operator override of FM_BACKEND_HERDR_BARE_PROMPT_RE or FM_BACKEND_HERDR_IDLE_RE still carries locale-dependent semantics; pinning those call sites is deferred to a separate change against the shared lib.

Risk Assessment

✅ Low: The one-line fix is verified correct and matches a strict subset of the old pattern's matches (removing only false positives, so no previously-correct read can change), and the follow-up commit adds locale-pinned regression tests that I traced as genuinely failing on the old regex and passing on the new one, with scope held exactly to the captain's deferral decision.

Testing

Completed 1 recorded test check.

Pipeline

Updates from git push no-mistakes

⏭️ **intent** - skipped

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed ✅
  • ⚠️ bin/backends/herdr.sh:746 - The fix ships no regression test, and the existing suite cannot distinguish it from the bug. No herdr/composer test pins a locale, so everything runs under ambient UTF-8 where the old '^[❯›]' and new '^(❯|›)' behave identically — which is exactly how this regression shipped. The coverage is already written but runs in the wrong locale: the fixture at tests/fm-backend-herdr.test.sh:818 (' ╭───╮ / │ ❯ │ / ╰──── Composer ───╯') is the precise triggering shape, and under LC_ALL=C with /usr/bin/grep the old regex makes it return 'pending' where the test asserts 'empty'. Adding a composer_state case that runs under LC_ALL=C would fail on the old regex and pass on the new one. This also departs from the repo's own convention for this area: every prior composer fix (0eaf293, a955a05, 1811b89) shipped a regression test in tests/ plus a docs/herdr-backend.md entry.
  • ℹ️ bin/backends/herdr.sh:839 - This grep is the only text-processing step in the composer pipeline that does not pin a locale — fm_composer_strip_ansi pins 'LC_ALL=C sed' (fm-composer-lib.sh:61) and fm_composer_strip_ghost pins 'LC_ALL=C awk' (fm-composer-lib.sh:87), both with comments explaining why. Inheriting the ambient locale here is what made this bug a heisenbug: UTF-8 in an interactive shell (works), C under the daemon (broken). The alternation makes the default pattern locale-invariant, but FM_BACKEND_HERDR_BARE_PROMPT_RE is an advertised override knob, so a user-supplied pattern still has locale-dependent semantics; the same applies to FM_BACKEND_HERDR_IDLE_RE via the unpinned grep in fm_composer_idle_matches (fm-composer-lib.sh:177). Pinning LC_ALL=C at the call site would make override behavior deterministic and fail visibly in dev rather than only in the daemon. Noting rather than pushing: the author's comment ('correct in any locale') plus the docs warning is a defensible alternative design.

🔧 Fix: add C-locale composer regression tests and incident entry
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

* Under a byte-oriented locale (LC_CTYPE=C) grep walks a [❯›] bracket
  class byte-wise, so it matches any row whose first byte is 0xE2 -
  including the box-drawing composer borders ╭ ╰ │ - and misreads a
  bordered composer as a bare prompt, classifying it 'pending' instead
  of 'empty'. The (❯|›) alternation matches each full glyph sequence and
  is correct in any locale.
* Surfaced deterministically in the no-mistakes gate's C-locale test run,
  where it failed the bare-prompt composer-state assertion in
  tests/fm-backend-herdr.test.sh. cmux is unaffected (it classifies via
  locale-safe shell case globs, not this grep).
* Keep docs/configuration.md's documented default in sync and note the
  alternation-not-bracket-class requirement for any override.
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.

1 participant