Skip to content

fix(bin): remove fork-only Herdr HERDR_SOCKET_PATH container detection fallback - #26

Merged
cloud-practitioner merged 1 commit into
mainfrom
fm/fm-remove-herdr-container-detect
Oct 1, 2026
Merged

cloud-practitioner merged 1 commit into
mainfrom
fm/fm-remove-herdr-container-detect

Conversation

@cloud-practitioner

@cloud-practitioner cloud-practitioner commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Intent

Remove the Herdr container detection fix from the fork https://github.com/cloud-practitioner/firstmate main; the environment is set from devcontainer.json as upstream https://github.com/kunchenguid/firstmate's configuration documentation instructs. Context: this is the fork-only HERDR_SOCKET_PATH fallback detection added by fork PRs #2 and #7 to #10 and restored onto main by fork PR #25, including its follow-up test fix 4b2c1da if that only serves the fallback. After the change, Herdr detection behaves exactly as upstream and relies on the documented environment configuration. Every other fork fix stays (Bitbucket PR support, the zsh fix, the test fixes).

What Changed

  • fm_backend_detect in bin/fm-backend.sh no longer picks Herdr just because HERDR_SOCKET_PATH points to a socket, and it no longer reports FM_BACKEND_DETECT_SIGNAL=HERDR_SOCKET_PATH. Herdr is auto-detected only from HERDR_ENV=1 when $TMUX is not set, the same as upstream.
  • In bin/backends/herdr.sh, the session name no longer comes from the socket path (fm_backend_herdr_socket_session_name is removed). fm_backend_herdr_session now returns ${HERDR_SESSION:-default}.
  • Removed the tests that covered the fallback: the socket-fallback case in tests/fm-backend.test.sh, the socket-derived session tests in tests/fm-backend-herdr.test.sh, and the whole tests/fm-backend-herdr-container-session-e2e.test.sh file along with its entry in bin/fm-test-run.sh. The remaining detection tests no longer unset HERDR_SOCKET_PATH. docs/architecture.md, docs/configuration.md and docs/herdr-backend.md no longer mention the container fallback.

🤖 Generated with Claude Code

Provenance

Removed, by origin:

Deliberately kept from the same fork history: the zsh fixes (fm_backend_source positional sibling list from #17, the BASH_SOURCE[0]:-$0 root in bin/backends/herdr.sh, and the zsh all-backends sourcing test), Bitbucket PR support, the lint memory cap, the per-home task temp root, and the Herdr recovery lock wait.

Local note: tests/fm-test-run.test.sh failed locally both on this branch and on the unchanged base, with different failures (branch: a --jobs admission check and a background-session fixture timing out after a slow fm-session-lock-ancestry run; base: ruby missing to parse the CI workflow YAML), so they look environmental; CI passes.

Risk Assessment

✅ Low: The change only removes fork-only code. Against upstream aedb7bb, the HERDR_SOCKET_PATH detection fallback, the socket-derived session name and their docs and tests are all gone, and no references to them remain. The remaining diffs in the touched files belong to the fork fixes that are meant to stay: the zsh sourcing fix, Bitbucket support and the per-home temp root.

Testing

I provisioned a disposable fm-lab-* Herdr session and ran firstmate's detection code inside a real pane. Herdr injects HERDR_ENV=1, HERDR_SESSION and HERDR_SOCKET_PATH there, and firstmate detected herdr with signal HERDR_ENV and the lab session. With only HERDR_SOCKET_PATH set, pointing at a live socket, the change returns undetected and falls back to tmux with session "default", exactly as upstream does. Base 76a9f28 detected herdr through the removed HERDR_SOCKET_PATH signal in that same environment. With HERDR_ENV, HERDR_SESSION and HERDR_SOCKET_PATH passed into the container environment, as upstream's docs describe, a real container_ensure call reached the existing lab server without restarting it (same socket inode). Nested tmux still wins over Herdr. A file comparison found the three detection functions identical to upstream main; because that was a source diff rather than a live run, the scenario is marked untested. The targeted fm-backend, Herdr-adapter and Bitbucket test files pass, and the zsh sourcing fix still works. I tore down the lab session and removed every temp directory; the worktree is clean. This is a CLI change with no UI, so there are text transcripts and no screenshots.

  • Live validation: ✅ go - 6 of 7 scenarios driven live against the product
Scenario Result Live Evidence
Firstmate running in a real Herdr pane (HERDR_ENV=1 injected by Herdr) auto-detects herdr and targets the pane's own session ✅ pass live herdr-pane-detection-probes.txt S1: rc=0 detected=herdr signal=HERDR_ENV, session=fm-lab-detect-2347999-13979
Adversarial: a container-shaped env with only HERDR_SOCKET_PATH set (pointing at a live Herdr socket) no longer selects herdr; it falls back to tmux with session default, while base 76a9f28 still sele… ✅ pass live herdr-pane-detection-probes.txt S2: change rc=1, name=tmux, session=default; base rc=0 detected=herdr signal=HERDR_SOCKET_PATH
Container env set per upstream docs (HERDR_ENV=1, HERDR_SESSION, HERDR_SOCKET_PATH) detects herdr, and a real container_ensure call reaches the existing lab server without starting a new one ✅ pass live herdr-pane-detection-probes.txt S3, plus herdr-container-ensure.txt: backend=herdr, container_ensure rc=0 returned fm-lab-detect-2347999-13979:w1, socket inode 664106 unchanged, default session untouc…
Nested tmux inside a Herdr pane still resolves to tmux (innermost-first precedence unchanged) ✅ pass live herdr-pane-detection-probes.txt S4: detected=tmux signal=TMUX
Herdr detection and session resolution are identical to upstream kunchenguid/firstmate main ⏸️ untested no The earlier payload got this from a read-only source diff against upstream f593060 (upstream-parity.txt), not from running the product, so it did not establish a live result. Scenarios 1-4 exercise th…
Retained fork fix: Bitbucket PR support still works ✅ pass live bash tests/fm-pr-bitbucket.test.sh rc=0 (fm-pr-bitbucket.test.log); none of the Bitbucket files are touched by the change
Retained fork fix: fm-backend.sh sourced from zsh loads the herdr backend and detects correctly ✅ pass live zsh-sourcing.txt: in zsh, HERDR_ENV=1 gives herdr, socket-only gives session default and backend tmux; the zsh test in tests/fm-backend.test.sh passes
Evidence: Detection probes inside a real Herdr lab pane (change vs base contrast)

Source: Detection probes inside a real Herdr lab pane (change vs base contrast)

\### Probes run INSIDE a real Herdr pane of lab session fm-lab-detect-2347999-13979 (herdr herdr 0.9.3)
\### Raw pane env injected by herdr: HERDR_ENV=1 HERDR_SESSION=fm-lab-detect-2347999-13979 HERDR_SOCKET_PATH=~/.config/herdr/sessions/fm-lab-detect-2347999-13979/herdr.sock socket-is-live=yes

## S1 native Herdr pane (HERDR_ENV=1 injected by herdr), change under test
[change] root=~/.no-mistakes/worktrees/450411b3e67c/01M3VRW07TW96RM3MR0RDP8X13
  env: HERDR_ENV=1 HERDR_SESSION=fm-lab-detect-2347999-13979 HERDR_SOCKET_PATH=~/.config/herdr/sessions/fm-lab-detect-2347999-13979/herdr.sock TMUX=<unset>
  fm_backend_detect rc=0 detected=herdr signal=HERDR_ENV
  fm_backend_name=herdr
  fm_backend_herdr_session=fm-lab-detect-2347999-13979

## S2 container-shaped env: only HERDR_SOCKET_PATH forwarded (HERDR_ENV/HERDR_SESSION absent), live socket
[change] root=~/.no-mistakes/worktrees/450411b3e67c/01M3VRW07TW96RM3MR0RDP8X13
  env: HERDR_ENV=<unset> HERDR_SESSION=<unset> HERDR_SOCKET_PATH=~/.config/herdr/sessions/fm-lab-detect-2347999-13979/herdr.sock TMUX=<unset>
  fm_backend_detect rc=1 detected=<none> signal=<none>
  fm_backend_name=tmux
  fm_backend_herdr_session=default
   -- same env against BASE commit 76a9f28 (fork fallback present) for contrast:
[base-76a9f28] root=/tmp/fm-hdtest.JxaWcL/base
  env: HERDR_ENV=<unset> HERDR_SESSION=<unset> HERDR_SOCKET_PATH=~/.config/herdr/sessions/fm-lab-detect-2347999-13979/herdr.sock TMUX=<unset>
  fm_backend_detect rc=0 detected=herdr signal=HERDR_SOCKET_PATH
  fm_backend_name=herdr
  fm_backend_herdr_session=fm-lab-detect-2347999-13979

## S3 devcontainer configured per upstream docs: HERDR_ENV=1 + HERDR_SESSION + HERDR_SOCKET_PATH forwarded
[change] root=~/.no-mistakes/worktrees/450411b3e67c/01M3VRW07TW96RM3MR0RDP8X13
  env: HERDR_ENV=1 HERDR_SESSION=fm-lab-detect-2347999-13979 HERDR_SOCKET_PATH=~/.config/herdr/sessions/fm-lab-detect-2347999-13979/herdr.sock TMUX=<unset>
  fm_backend_detect rc=0 detected=herdr signal=HERDR_ENV
  fm_backend_name=herdr
  fm_backend_herdr_session=fm-lab-detect-2347999-13979

## S4 nested tmux inside Herdr still wins (innermost-first), change
[change] root=~/.no-mistakes/worktrees/450411b3e67c/01M3VRW07TW96RM3MR0RDP8X13
  env: HERDR_ENV=1 HERDR_SESSION=fm-lab-detect-2347999-13979 HERDR_SOCKET_PATH=~/.config/herdr/sessions/fm-lab-detect-2347999-13979/herdr.sock TMUX=fake,1,0
  fm_backend_detect rc=0 detected=tmux signal=TMUX
  fm_backend_name=tmux
  fm_backend_herdr_session=fm-lab-detect-2347999-13979
DONE
Evidence: Herdr pane read-back of the probe run

Source: Herdr pane read-back of the probe run

01M3VRW07TW96RM3MR0RDP8X13 on  HEAD
❯ bash /tmp/fm-hdtest.JxaWcL/driver.sh
\### Probes run INSIDE a real Herdr pane of lab session fm-lab-detect-2347999-13979 (herdr herdr 0.9.3)
\### Raw pane env injected by herdr: HERDR_ENV=1 HERDR_SESSION=fm-lab-detect-2347999-13979 HERDR_SOCKET_PATH=~/
.config/herdr/sessions/fm-lab-detect-2347999-13979/herdr.sock socket-is-live=yes

## S1 native Herdr pane (HERDR_ENV=1 injected by herdr), change under test
[change] root=~/.no-mistakes/worktrees/450411b3e67c/01M3VRW07TW96RM3MR0RDP8X13
  env: HERDR_ENV=1 HERDR_SESSION=fm-lab-detect-2347999-13979 HERDR_SOCKET_PATH=~/.config/herdr/sessions/fm-lab
-detect-2347999-13979/herdr.sock TMUX=<unset>
  fm_backend_detect rc=0 detected=herdr signal=HERDR_ENV
  fm_backend_name=herdr
  fm_backend_herdr_session=fm-lab-detect-2347999-13979

## S2 container-shaped env: only HERDR_SOCKET_PATH forwarded (HERDR_ENV/HERDR_SESSION absent), live socket
[change] root=~/.no-mistakes/worktrees/450411b3e67c/01M3VRW07TW96RM3MR0RDP8X13
  env: HERDR_ENV=<unset> HERDR_SESSION=<unset> HERDR_SOCKET_PATH=~/.config/herdr/sessions/fm-lab-detect-234799
9-13979/herdr.sock TMUX=<unset>
  fm_backend_detect rc=1 detected=<none> signal=<none>
  fm_backend_name=tmux
  fm_backend_herdr_session=default
   -- same env against BASE commit 76a9f28 (fork fallback present) for contrast:
[base-76a9f28] root=/tmp/fm-hdtest.JxaWcL/base
  env: HERDR_ENV=<unset> HERDR_SESSION=<unset> HERDR_SOCKET_PATH=~/.config/herdr/sessions/fm-lab-detect-234799
9-13979/herdr.sock TMUX=<unset>
  fm_backend_detect rc=0 detected=herdr signal=HERDR_SOCKET_PATH
  fm_backend_name=herdr
  fm_backend_herdr_session=fm-lab-detect-2347999-13979

## S3 devcontainer configured per upstream docs: HERDR_ENV=1 + HERDR_SESSION + HERDR_SOCKET_PATH forwarded
[change] root=~/.no-mistakes/worktrees/450411b3e67c/01M3VRW07TW96RM3MR0RDP8X13
  env: HERDR_ENV=1 HERDR_SESSION=fm-lab-detect-2347999-13979 HERDR_SOCKET_PATH=~/.config/herdr/sessions/fm-lab
-detect-2347999-13979/herdr.sock TMUX=<unset>
  fm_backend_detect rc=0 detected=herdr signal=HERDR_ENV
  fm_backend_name=herdr
  fm_backend_herdr_session=fm-lab-detect-2347999-13979

## S4 nested tmux inside Herdr still wins (innermost-first), change
[change] root=~/.no-mistakes/worktrees/450411b3e67c/01M3VRW07TW96RM3MR0RDP8X13
  env: HERDR_ENV=1 HERDR_SESSION=fm-lab-detect-2347999-13979 HERDR_SOCKET_PATH=~/.config/herdr/sessions/fm-lab
-detect-2347999-13979/herdr.sock TMUX=fake,1,0
  fm_backend_detect rc=0 detected=tmux signal=TMUX
  fm_backend_name=tmux
  fm_backend_herdr_session=fm-lab-detect-2347999-13979
DONE
01M3VRW07TW96RM3MR0RDP8X13 on  HEAD
❯
Evidence: container_ensure under documented env reaches the live lab server (same socket inode)

Source: container_ensure under documented env reaches the live lab server (same socket inode)

## S5 devcontainer-configured env drives a real adapter op (fm_backend_herdr_container_ensure)
before: socket inode=664106 workspaces=["fm-lab-probe"]
container_ensure rc=0 output: backend=herdr
fm-lab-detect-2347999-13979:w1	
after:  socket inode=664106 workspaces=["fm-lab-probe"]
default session socket untouched: {"name":"default","running":true}
DONE2
Evidence: Upstream parity of detection functions

Source: Upstream parity of detection functions

Upstream kunchenguid/firstmate main @ f593060312a48dab651138e6e705147358eba5ce
== fm_backend_detect: change 27 lines, upstream 27 lines
   IDENTICAL to upstream
== fm_backend_herdr_session: change 2 lines, upstream 2 lines
   IDENTICAL to upstream
== fm_backend_name: change 31 lines, upstream 31 lines
   IDENTICAL to upstream
== grep fm_backend_herdr_socket_session_name in change bin/: none
== full-file diff bin/fm-backend.sh vs upstream:
625c625
<   local name=$1 adapter rel lib_path
---
>   local name=$1 adapter rel sibling
627a628,629
>   # The sibling list rides in the positional parameters: zsh does not
>   # word-split an unquoted expansion, so a space-separated string is one path.
649,650d650
<   # The sibling list lives in the positional parameters because zsh does not
<   # word-split an unquoted variable the way bash does.
652,653c652,653
<     lib_path="$FM_BACKEND_LIB_DIR/$rel"
<     fm_backend_source_readable "$lib_path" || return 1
---
>     sibling="$FM_BACKEND_LIB_DIR/$rel"
>     fm_backend_source_readable "$sibling" || return 1
Evidence: zsh sourcing check

Source: zsh sourcing check

herdrdefault
default
tmux
Evidence: tests/fm-backend.test.sh log

Source: tests/fm-backend.test.sh log

ok - fm_backend_name: FM_BACKEND env > config/backend > default tmux
ok - fm_backend_detect: no markers -> undetected, HERDR_ENV=1 -> herdr, $TMUX -> tmux, CMUX_WORKSPACE_ID -> cmux, nested combinations resolve innermost-first
ok - fm_backend_detect: falls back to __CFBundleIdentifier=com.cmuxterm.app when CMUX_WORKSPACE_ID is absent (signal bundle-id; foreign bundle ids rejected)
ok - fm_backend_detect: the cmux fallback signals are macOS-only (inert on a non-Darwin uname)
ok - fm_backend_detect: an inherited cmux bundle id never outranks $TMUX or HERDR_ENV (tmux/herdr-inside-cmux false positive absorbed)
ok - fm_backend_detect: ancestry fallback matches the lsappinfo-resolved (bundle-id) cmux app pid in the parent chain
ok - fm_backend_detect: ancestry fallback matches a bundle-shaped cmux comm path at any install location when lsappinfo cannot resolve a pid
ok - fm_backend_detect: ancestry fallback stops undetected at launchd (a reparented tmux server never reaches cmux)
ok - fm_backend_name: a fallback-detected cmux prints a NOTICE naming the fallback signal; the primary-marker notice is unchanged
ok - fm_backend_name: verified Herdr and tmux stay silent while experimental cmux remains loud
ok - fm_backend_name: an explicit FM_BACKEND or config/backend setting always wins over runtime auto-detection, including an ambient cmux marker
ok - fm_backend_validate: implemented adapters accepted, unknown and blocked codex-app backends refused loudly
ok - zsh: fm_backend_source recognizes known backends and rejects unknown ones
ok - bash: fm_backend_source recognizes known backends and rejects unknown ones
ok - fm_backend_source: missing adapter fails before lifecycle continuation
ok - fm_backend_source: unreadable adapter fails before lifecycle continuation
ok - fm_backend_validate_spawn: all implemented lifecycle backends are spawn-supported
ok - fm_meta_get / fm_backend_of_meta: read last key=value and default backend to tmux
ok - fm_backend_resolve_selector: session:window literal, exact task id first, legacy fm-<id> label fallback, ad hoc bare name via tmux list-windows
ok - fm_backend_of_selector: exact task ids, legacy fm-<id> labels, and matching explicit targets inherit metadata backend
ok - fm-send.sh: explicit tmux targets are verified; text types once and submits with Enter
ok - fm-peek.sh: capture-pane invocation and output are byte-identical old vs new
ok - fm-spawn.sh: a project reached through a symlinked prefix (e.g. macOS /tmp -> /private/tmp) does not trip the isolation guard's false refusal
ok - fm-teardown.sh: treehouse return remains compatible while tmux cleanup uses exact selectors
ok - fm-spawn.sh --backend bogus is refused loudly
ok - fm-spawn.sh --backend codex-app is refused
ok - fm-spawn.sh honors FM_BACKEND and refuses an unimplemented value loudly
ok - fm-spawn.sh: an explicit --backend tmux resolves silently and writes no backend= (missing means tmux)
ok - fm-spawn.sh: explicit --backend tmux wins over an ambient HERDR_ENV=1 auto-detect marker
ok - fm-spawn.sh: auto-detect resolves nested tmux-in-herdr to tmux and stays silent end to end
bash tests/fm-backend.test.sh  18.90s user 15.41s system 41% cpu 1:22.72 total
Evidence: tests/fm-backend-herdr.test.sh log

Source: tests/fm-backend-herdr.test.sh log

ok - fm_backend_herdr_version_check: accepts the current protocol (14)
ok - fm_backend_herdr_version_check: refuses an old protocol loudly
ok - fm_backend_herdr_version_check: refuses loudly when herdr is not installed
ok - fm_backend_herdr_workspace_label: a primary home (no marker) resolves to 'firstmate'
ok - fm_backend_herdr_workspace_label: a secondmate home (.fm-secondmate-home) resolves to '2ndmate-<id>'
ok - fm_backend_herdr_workspace_label: trims whitespace around the marker's secondmate id
ok - fm_backend_herdr_workspace_label: an empty marker file falls back to the primary label 'firstmate'
ok - fm_backend_herdr_workspace_label: two different secondmate homes get two different, non-colliding labels
ok - fm_backend_herdr_cli: sets HERDR_SESSION AND appends a trailing --session flag on every call
ok - herdr client selection: a live pane behind a stale shadowing client reads alive
ok - herdr recovery-grade read: a stopped server means missing there, and nowhere else
ok - herdr stale registration: a shell-only pane with a lingering Pi record is agent-free with an explicit reason
ok - herdr stale registration: no registered status can outrank a shell-only process view
ok - herdr pane agent session: a resumable reference is reported with the agent label that reported it
ok - herdr pane agent session: anything unresumable degrades to a nonzero read with no output
ok - herdr stale registration: a registered agent with a live Pi foreground process still reads alive
ok - herdr stale registration: only a shell-only pane demotes a registration
ok - herdr stale registration: a transient prompt helper settles into stale-agent instead of reading live
ok - herdr stale registration: an exhausted settle window still reads a non-shell foreground as live
ok - herdr stale registration: an agent process outside the foreground group still counts as alive
Terminated
ok - herdr stale registration: the descendant walk reads a spaced executable path whole
ok - herdr stale registration: an unreadable process view refuses instead of guessing either way
ok - herdr stale registration: an empty foreground list over a real shell is not unreadable, it settles via the descendant walk
ok - herdr stale registration: presentation reclaim never closes a stale-registration pane
ok - herdr stale registration: busy_state proves a working record at process level before reporting busy
ok - herdr client selection: one selected client is reused per process
ok - herdr client selection: selected clients remain scoped to their session
ok - herdr client selection: only protocol_mismatch triggers reselection; other failures pass through untouched
ok - herdr client selection: the happy path makes no extra call
ok - herdr client status: .server.compatible, legacy protocol equality, unknown, and stopped shapes all normalize
ok - fm_backend_herdr_launcher_identity: a firstmate not running inside herdr has no launcher workspace to inherit
ok - fm_backend_herdr_launcher_identity: HERDR_ENV=1 without a pane id selects the backend but binds no parent
ok - fm_backend_herdr_launcher_identity: resolves the launcher's exact workspace even when a same-labeled workspace sorts first
ok - fm_backend_herdr_launcher_identity: refuses a launcher pane that names a different herdr session
ok - fm_backend_herdr_launcher_identity: refuses a claimed pane without exact server identity
ok - fm_backend_herdr_launcher_identity: refuses a launcher pane whose injected socket belongs to another herdr server
ok - fm_backend_herdr_launcher_identity: refuses when the launcher's own pane no longer resolves
ok - fm_backend_herdr_launcher_identity: refuses when the launcher's pane and tab disagree about their workspace
ok - fm_backend_herdr_launcher_identity: refuses when the launcher's workspace is gone from its own session
ok - fm_backend_herdr_workspace_ensure: places a worker in the launcher's exact workspace, not the first same-labeled one
ok - fm_backend_herdr_workspace_ensure: refuses to guess between two same-labeled home workspaces
ok - fm_backend_herdr_workspace_ensure: a --secondmate container resolves that home's own workspace, not the launcher's
ok - fm_backend_herdr_container_ensure: surfaces the exact ambiguous-placement refusal instead of a generic failure
ok - fm_backend_herdr_container_ensure: version-gates, starts the server, ensures the firstmate workspace, echoes session:workspace_id + the seeded default tab id
ok - fm_backend_herdr_server_ensure: scrubs home and harness identity without disturbing unrelated environment or session routing
ok - fm_backend_herdr_container_ensure: reuses an existing firstmate workspace without recreating it, and reports no seeded default tab (adopted, not created)
ok - fm_backend_herdr_container_ensure: workspace create passes --no-focus
ok - fm_backend_herdr_container_ensure: creates the workspace under the SECONDMATE home's own label, not 'firstmate'
ok - fm_backend_herdr_create_task: prunes exactly the seeded default tab container_ensure identified, once the first real task tab exists
ok - herdr repeated spawn/teardown: one persistent firstmate workspace reused, zero orphans, default tab pruned, create ran once
ok - fm_backend_herdr_create_task: an ADOPTED workspace's pre-existing tab is never pruned (the created-vs-adopted gate)
ok - fm_backend_herdr_create_task: the label-collision startup-workspace scenario (2026-07-02 incident) leaves the captain's live tab untouched
ok - fm_backend_herdr_workspace_prune_seeded_default_tab: refuses to close the seeded default tab when its pane reports a working agent (defense in depth)
ok - fm_backend_herdr_create_task: refuses a duplicate tab label (herdr's own tab create has no uniqueness check)
ok - fm_backend_herdr_create_task: a same-labeled tab with a live (even idle) registered agent still refuses exactly as before
ok - fm_backend_herdr_create_task: scans every same-labeled tab and refuses if any duplicate is live
ok - fm_backend_herdr_create_task: closes and replaces a same-labeled tab whose pane is dead (pane_not_found)
ok - fm_backend_herdr_create_task: closes and replaces a same-labeled tab whose pane is alive but hosts no registered agent (a restored plain shell)
ok - fm_backend_herdr_create_task: closes every confirmed same-labeled husk only after creating the replacement
ok - fm_backend_herdr_create_task: refuses success when a preexisting husk tab remains after replacement
ok - fm_backend_herdr_create_task: refuses (fail-safe) rather than guessing when the duplicate's agent state cannot be classified confidently
ok - fm_backend_herdr_create_task: creates the replacement tab BEFORE closing the husk tab, never the reverse
ok - fm_backend_herdr_create_task: creates a tab and parses tab_id/pane_id from the JSON response, prunes nothing when no seeded tab id is given
ok - fm_backend_herdr_create_task: tab create passes --no-focus
ok - herdr presentation: a home that set nothing gets the projection by default at or above the floor
ok - herdr presentation: an unconfigured home below the floor falls back flat with one naming warning
ok - herdr presentation: an unreadable client release falls back flat instead of guessing
ok - herdr presentation: a deliberate opt-in is never silently downgraded below the floor
ok - herdr presentation: an explicit off opts the home out
ok - herdr presentation: an unrecognized value warns and follows the default instead of failing a spawn
ok - herdr presentation: the below-floor warning is one per home per release, not one per spawn
ok - herdr presentation: warning marker publication is atomic, symlink-safe, and fails visible
ok - herdr presentation: client and selected server floors compose conservatively without overriding explicit opt-in
ok - herdr presentation floor: every measured release, and each signal alone, classifies correctly
ok - herdr presentation floor: either signal alone can carry an above verdict, and each divergence is real
ok - herdr presentation: config parsing separates a deliberate choice from an unconfigured default
ok - herdr presentation journal: atomically publishes one non-authoritative 128-bit correlator and refuses overwrite
ok - herdr presentation journ

... [8591 bytes truncated] ...

m_backend_herdr_composer_state: grok's real bright typed input still reads pending
ok - fm_backend_herdr_composer_state: a real-codex unbordered '›' prompt row reads empty
ok - fm_backend_herdr_composer_state: a faint real-codex ghost suggestion reads empty
ok - fm_backend_herdr_composer_state: non-faint codex prompt text still reads pending
ok - fm_backend_herdr_wait_for_working: reports 'busy' immediately on the first poll, without spending the rest of the budget
ok - fm_backend_herdr_wait_for_working: a slow transition landing on a later sample within one window is still caught (robust against the 'slow transition' failure direction)
ok - fm_backend_herdr_wait_for_working: spreads six samples across the full budget endpoint without a final trailing sleep
ok - fm_backend_herdr_send_text_submit: applies the herdr minimum confirmation budget before polling agent-state
ok - fm_backend_herdr_wait_for_working: reports 'idle' (readable, genuinely not yet working) when 'busy' never appears
ok - fm_backend_herdr_wait_for_working: reports 'unknown' (a hard read failure, not a timing race) only when EVERY poll in the window fails
ok - fm_backend_herdr_wait_for_working: treats blocked as submit-active for confirmation without changing watcher busy-state semantics
ok - fm_backend_herdr_send_text_submit: reports 'empty' once agent_status reports working after one Enter, without ever reading the composer
ok - fm_backend_herdr_send_text_submit: reports 'pending' when agent_status stays idle and the composer still holds unsent text after retried Enters (swallowed)
ok - fm_backend_herdr_send_text_submit: a failed literal send reports send-failed and replays the transport's stderr
ok - fm_backend_herdr_send_text_submit: a slash-command popup's placeholder fill on Enter #1 never flips agent_status to working, so it does not short-circuit as submitted; Enter #2 is retried and lands it
ok - fm_backend_herdr_send_text_submit: a post-Enter blocked state confirms delivery without retrying into the prompt
ok - fm_backend_herdr_send_text_submit: native working + proven pending after retries reports empty (queued Enter)
ok - fm_backend_herdr_send_text_submit: a failed Enter cannot borrow preexisting working state as queued-delivery proof
ok - fm_backend_herdr_send_text_submit: a failed Enter cannot borrow a later native transition as delivery proof
ok - fm_backend_herdr_send_text_submit: idle native agent-state plus empty composer reports empty (landed Claude turn)
ok - fm_backend_herdr_send_text_submit: idle native baseline uses a rendered busy footer to confirm a queued Enter
ok - fm_backend_herdr_composer_state: cursor's mid-turn placeholder-plus-busy-token row reads pending (why delivery needs a separate signal)
ok - fm_backend_herdr_rendered_busy_state: busy/idle/unknown from the rendered footer, with an unreadable pane never reading idle
ok - fm_backend_herdr_send_text_submit: a rendered-footer idle-to-busy transition confirms delivery when native agent-state never reports idle
ok - fm_backend_herdr_send_text_submit: an already-busy footer baseline is never accepted as proof that this Enter landed
ok - fm_backend_herdr_send_text_submit: confirms submission via native agent-state alone, immune to a codex-style dynamic idle-tip composer that would have misread as 'pending' under the old composer-based confirmation
ok - fm_backend_herdr_composer_state: a faint real-codex dynamic idle-tip composer row reads empty
ok - fm_backend_composer_state (herdr): the pre-injection empty-box guard still refuses a genuinely non-empty composer, unaffected by the submit-confirmation change
ok - fm_backend_herdr_send_text_submit: a slow transition landing on a later sample within one Enter's budget is confirmed WITHOUT sending a needless extra Enter
ok - fm_backend_herdr_send_text_submit: reports 'send-failed' when the literal send-text call itself errors
ok - fm_backend_herdr_send_text_submit: reports 'unknown' when the post-Enter agent-get read fails (never retries past an unreadable target)
ok - fm_backend_herdr_send_text_submit: an unreadable composer stops Enter retries after native status stays idle
ok - fm_backend_herdr_send_text_submit: a 1500-character payload a Claude composer still holds is submitted whole
ok - fm_backend_herdr_send_text_submit: a long payload whose Claude composer kept only the tail is not submitted, is cleared, and reports send-failed
ok - fm_backend_herdr_send_text_submit: a refused suffix whose clear cannot be verified reports unknown, not send-failed
ok - fm_backend_herdr_send_text_submit: a refused 480-character suffix wrapped over five rows is cleared one row per Ctrl+U and reports send-failed
ok - fm_backend_herdr_send_text_submit: after a refused suffix is cleared, a resend starts from an empty Claude composer and submits only the message
ok - fm_backend_herdr_send_text_submit: a Claude composer holding leftover text is refused before anything is typed
ok - fm_backend_herdr_send_text_submit: a matching head in the transcript does not prove the current composer
ok - fm_backend_herdr_send_text_submit: an away-mode digest and a marked steer are submitted when Claude's read-back only drops U+2063
ok - fm_backend_herdr_send_text_submit: dropping U+2063 does not let a marked digest missing its head be submitted
ok - fm_backend_herdr_composer_state: a slash-command popup cannot hide a typed composer
ok - fm_backend_herdr_send_text_submit: a typed slash command hidden behind its popup is still proven and submitted
ok - fm_backend_herdr_send_text_submit: a lone paste placeholder still submits the full long payload
ok - fm_backend_herdr_send_text_submit: a lone multi-line paste placeholder still submits the long multi-line payload
ok - fm_backend_herdr_send_text_submit: a paste placeholder followed by a literal remainder is not submitted and is cleared
ok - fm_backend_herdr_send_text_submit: three paste placeholders with no literal remainder submit the long payload
ok - fm_backend_herdr_send_text_submit: non-Claude and unidentified panes keep the pre-proof type-then-Enter behavior
ok - fm_backend_validate: herdr is a known backend (P2)
ok - fm_backend_busy_state: tmux (no native primitive) always reports unknown, preserving the P1 regex-only path
error: unknown backend 'bogus' (known: tmux herdr zellij orca cmux)
ok - fm_backend_composer_state dispatches every backend to its named thin classifier, unknown for unrecognized backends
ok - fm-peek/fm-send: explicit stale targets matching metadata use the recorded backend
ok - fm_backend_herdr_normalize_event routes through the shared record with an empty from_status
ok - fm_backend_herdr_escalation_marker keys the dedupe marker exactly like the watcher's .stale-<key>
ok - fm_backend_herdr_apply_transition: blocked dedupe starts only after explicit commit
ok - fm_backend_herdr_apply_transition: a working edge clears the marker so the next ->blocked re-escalates
ok - fm_backend_herdr_clear_transition removes task-owned dedupe state
ok - fm_backend_herdr_apply_transition: idle/done (defer) and unknown/empty (fallback) take no fast action
ok - fm_backend_herdr_wait_transition: a home with no herdr panes falls back to polling (rc 2)
ok - fm_backend_herdr_wait_transition: below-capability protocol/schema falls back to polling (rc 2)
ok - fm_backend_herdr_wait_transition: reconnect level-reconcile returns an uncommitted blocked pane
ok - fm_backend_herdr_wait_transition: subscribes before reconnect level-reconcile
ok - fm_backend_herdr_wait_transition: a still-blocked, already-escalated pane is not re-delivered on reconnect
ok - fm_backend_herdr_wait_transition: a streamed ->blocked edge returns the record sub-poll
ok - fm_backend_herdr_wait_transition: streamed working clears the marker, idle/done are deferred (clean timeout)
ok - fm_backend_herdr_wait_transition: a reader/subscribe failure falls back to polling (rc 2)
ok - fm_backend_herdr_wait_transition: Bash 3.2-safe bad-ack path closes fd 9 and removes its FIFO
ok - fm_backend_herdr_wait_transition: stock macOS Bash clean timeout closes fd 9 and returns 1
timeout 590 bash tests/fm-backend-herdr.test.sh  47.67s user 33.42s system 44% cpu 3:02.42 total
Evidence: tests/fm-pr-bitbucket.test.sh log

Source: tests/fm-pr-bitbucket.test.sh log

ok - canonical Bitbucket Cloud pull request URLs parse
ok - malformed Bitbucket URLs, other forges' shapes on bitbucket.org, and Data Center URLs are refused
ok - the Bitbucket record read reports state and merged, refuses another record, and keeps the token out of argv
ok - the status read follows pagination and refuses a next link that leaves the API base
ok - fm-pr-check records a Bitbucket pull request with its full head and arms the poll
ok - fm-pr-check refuses a draft Bitbucket pull request
ok - fm-pr-check refuses a Bitbucket watch without the credential and names it
ok - the Bitbucket merge poll wakes only on the exact merged record and stays silent otherwise
ok - fm-pr-state reports a Bitbucket pull request's state, draft, builds, and requested changes
ok - fm-pr-merge merges a green Bitbucket pull request, keeps its branch, and proves the merge by reading it back
ok - fm-pr-merge sends a requested Bitbucket strategy and refuses arguments it cannot translate
ok - fm-pr-merge refuses red builds, an unmet required build count, and an unreadable restriction set
ok - fm-pr-merge refuses a draft, a closed pull request, and a missing credential
ok - Bitbucket build waivers match the exact key, stay attended-only, and --allow-missing is refused
ok - a Bitbucket merge under away authority stays synchronous, records that authority, and keeps the green gate
ok - fm-pr-merge re-reads the Bitbucket head immediately before merging and refuses a moved head
ok - fm-pr-merge quotes Bitbucket's refusal, leaves an unconfirmed merge armed, and flags a landed head it did not verify
ok - fm-pr-merge refuses a Bitbucket pull request that misses an approval, default-reviewer, changes-requested, or task merge check
ok - fm-pr-merge reads a Bitbucket pull request back after its merge request got no response
Evidence: Detection probe excerpt
S1 native pane: detect rc=0 detected=herdr signal=HERDR_ENV session=fm-lab-detect-2347999-13979
S2 socket-only (change): detect rc=1 detected=<none> name=tmux session=default
S2 socket-only (base 76a9f28): detect rc=0 detected=herdr signal=HERDR_SOCKET_PATH session=fm-lab-detect-2347999-13979
S3 documented passthrough: detected=herdr signal=HERDR_ENV session=fm-lab-detect-2347999-13979
S4 nested tmux: detected=tmux signal=TMUX

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 6 of 7 scenarios driven live against the product
Scenario Result Live Evidence
Firstmate running in a real Herdr pane (HERDR_ENV=1 injected by Herdr) auto-detects herdr and targets the pane's own session ✅ pass live herdr-pane-detection-probes.txt S1: rc=0 detected=herdr signal=HERDR_ENV, session=fm-lab-detect-2347999-13979
Adversarial: a container-shaped env with only HERDR_SOCKET_PATH set (pointing at a live Herdr socket) no longer selects herdr; it falls back to tmux with session default, while base 76a9f28 still sele… ✅ pass live herdr-pane-detection-probes.txt S2: change rc=1, name=tmux, session=default; base rc=0 detected=herdr signal=HERDR_SOCKET_PATH
Container env set per upstream docs (HERDR_ENV=1, HERDR_SESSION, HERDR_SOCKET_PATH) detects herdr, and a real container_ensure call reaches the existing lab server without starting a new one ✅ pass live herdr-pane-detection-probes.txt S3, plus herdr-container-ensure.txt: backend=herdr, container_ensure rc=0 returned fm-lab-detect-2347999-13979:w1, socket inode 664106 unchanged, default session untouc…
Nested tmux inside a Herdr pane still resolves to tmux (innermost-first precedence unchanged) ✅ pass live herdr-pane-detection-probes.txt S4: detected=tmux signal=TMUX
Herdr detection and session resolution are identical to upstream kunchenguid/firstmate main ⏸️ untested no The earlier payload got this from a read-only source diff against upstream f593060 (upstream-parity.txt), not from running the product, so it did not establish a live result. Scenarios 1-4 exercise th…
Retained fork fix: Bitbucket PR support still works ✅ pass live bash tests/fm-pr-bitbucket.test.sh rc=0 (fm-pr-bitbucket.test.log); none of the Bitbucket files are touched by the change
Retained fork fix: fm-backend.sh sourced from zsh loads the herdr backend and detects correctly ✅ pass live zsh-sourcing.txt: in zsh, HERDR_ENV=1 gives herdr, socket-only gives session default and backend tmux; the zsh test in tests/fm-backend.test.sh passes
  • bin/fm-herdr-lab.sh name/provision/run/teardown on the throwaway session fm-lab-detect-2347999-13979 (default session left alone; tripwire check passed at teardown)
  • Detection probe (fm_backend_detect / fm_backend_name / fm_backend_herdr_session) run inside a real lab Herdr pane via herdr pane run, in four env shapes: native, socket-only container, documented passthrough, and nested tmux
  • Same socket-only probe against a git archive of base 76a9f28, to show the removed behavior
  • fm_backend_herdr_container_ensure /tmp run inside the lab pane with a disposable bin/fm-lab-home.sh create FM_HOME and the documented HERDR_ENV/HERDR_SESSION/HERDR_SOCKET_PATH env; socket inode compared before and after
  • Function-level diff of fm_backend_detect, fm_backend_name and fm_backend_herdr_session against upstream kunchenguid/firstmate main fetched with gh api
  • bash tests/fm-backend.test.sh
  • bash tests/fm-backend-herdr.test.sh
  • bash tests/fm-pr-bitbucket.test.sh
  • zsh: source bin/fm-backend.sh &amp;&amp; fm_backend_source herdr followed by detection and session calls
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…n fallback

Herdr detection now matches upstream: HERDR_ENV=1 (set through the
devcontainer environment as upstream's configuration docs describe) selects
Herdr, and fm_backend_herdr_session reads HERDR_SESSION with a default
fallback. Removes the socket-path detection branch, the socket-derived
session name, their docs, unit tests, and the real-Herdr container e2e test.
@cloud-practitioner
cloud-practitioner merged commit fc795e5 into main Oct 1, 2026
20 checks passed
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