Skip to content
This repository was archived by the owner on Aug 25, 2026. It is now read-only.

feat(backend): deepen experimental Herdr adapter - #73

Merged
JTInventory merged 11 commits into
mainfrom
fm/herdr-depth-0719
Jul 19, 2026
Merged

JTInventory merged 11 commits into
mainfrom
fm/herdr-depth-0719

Conversation

@JTInventory

Copy link
Copy Markdown
Owner

Intent

Deepen experimental Herdr adapter: husk/respawn, prune-safety, eventwait, composer-lib, transition helpers, unit tests. Preserve JT locks and lab refuse-default. Do not port zellij/orca/cmux.

What Changed

  • Deepened the experimental Herdr adapter with husk detection/respawn, prune-safe workspace and tab cleanup, explicit session routing, and fail-closed rollback verification.
  • Added native Herdr event waiting with shared transition records and policy helpers, plus composer parsing that defers injection for pending or unknown input.
  • Expanded Herdr, composer, daemon, transition, watch, and eventwait coverage, and updated the related architecture and backend documentation.

Risk Assessment

⚠️ Medium: The Herdr adapter is a broad experimental change spanning lifecycle, cleanup, event waiting, and injection paths, but the reviewed failure cases are now fail-closed and no material issue remains.

Testing

The configured baseline command had already passed and was rerun successfully on the target. Focused automated tests covered husk/respawn, exact prune-safety, eventwait, composer and transition helpers, JT locks, lab refuse-default behavior, and backend routing. The full behavior suite passed. A read-only live Herdr smoke verified protocol 16; no mutating real-Herdr e2e was run because it would create external lab state. Reviewer-visible CLI transcripts are saved in the evidence artifacts above.

Evidence: Focused Herdr and safety tests
=== focused Herdr depth and safety tests ===
$ bash tests/fm-backend-herdr.test.sh
NOTICE: auto-detected herdr runtime (HERDR_ENV=1) - spawning into the EXPERIMENTAL herdr backend. Set config/backend or pass --backend tmux to opt out.
ok - Herdr selection honors explicit config/env and nested TMUX precedence
ok - bootstrap backend tool gating keeps Herdr dependencies opt-in
ok - Herdr version gate enforces 0.7.x and protocol 14+
ok - Herdr container ensure is version-gated and workspace-per-home
ok - Herdr workspace bind failures verify cleanup
ok - Herdr malformed workspace creates clean up new workspaces
ok - Herdr task creation, duplicate protection, target parsing, and key mapping work
ok - Herdr kill propagates close failures and avoids server creation
ok - Herdr capture uses safe over-fetch and native busy state
ok - Herdr text submit uses native confirmation and safe composer fallback
ok - Herdr wait_for_working samples native status and classifies blocked submits
ok - Herdr replaces a confirmed husk only after creating the replacement
ok - Herdr create cleanup closes tabs after post-create failures
ok - Herdr missing-id cleanup fails closed when unobserved
ok - Herdr pre-create failures preserve existing tabs
ok - Herdr seed pruning closes and verifies the exact seeded tab
ok - Herdr event capability probes use the explicit session
ok - Herdr eventwait returns and records a fresh blocked transition
ok - backend dispatch accepts Herdr and preserves opaque session:pane targets
$ python3 tests/fm-backend-herdr-eventwait.test.py
.....
----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK
$ bash tests/fm-composer-lib.test.sh
# fm-composer-lib.test.sh: all assertions passed
$ bash tests/fm-transition-lib.test.sh
# fm-transition-lib.test.sh: all assertions passed
$ bash tests/fm-watch-herdr-event.test.sh
PASS: watch event helper routes explicit Herdr transitions
$ bash tests/fm-herdr-lab.test.sh
ok - Herdr lab names are scoped to fm-lab-* and reject default
ok - Herdr lab lifecycle uses trailing session scoping and guarded teardown
ok - Herdr lab refuses destructive operations without ownership evidence
ok - Herdr lab never stops or deletes the default session
$ bash tests/fm-gate-refuse.test.sh
ok - gate refusal helper covers marker, empty marker, path backstop, and normal session
ok - spawn, send, and teardown refuse both gate signals before lifecycle work
ok - tracked gate-refusal wiring and trusted no-mistakes config are present
# all fm-gate-refuse tests passed
Evidence: Full behavior suite
ok: PR target repo jtinventory/firstmate verified
tmux 3.4
ok - gate refusal helper covers marker, empty marker, path backstop, and normal session
ok - spawn, send, and teardown refuse both gate signals before lifecycle work
ok - tracked gate-refusal wiring and trusted no-mistakes config are present
# all fm-gate-refuse tests passed
Running 64 behavior tests with 4 parallel job(s)
START: tests/fm-afk-inject-e2e.test.sh (TMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-afk-inject-e2e/tmp GOTMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-afk-inject-e2e/gotmp)
START: tests/fm-afk-inject-herdr-e2e.test.sh (TMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-afk-inject-herdr-e2e/tmp GOTMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-afk-inject-herdr-e2e/gotmp)
START: tests/fm-afk-launch.test.sh (TMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-afk-launch/tmp GOTMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-afk-launch/gotmp)
START: tests/fm-backend-herdr-smoke.test.sh (TMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-backend-herdr-smoke/tmp GOTMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-backend-herdr-smoke/gotmp)
PASS: tests/fm-afk-inject-e2e.test.sh
ok - Scenario A: partial input defers injection; digest arrives clean after idle
ok - Scenario B: swallowed Enter produces exactly one clean digest
ok - Scenario C: a normal captain status injects exactly one clean single-line sentinel digest
all e2e injection tests passed
PASS: tests/fm-afk-inject-herdr-e2e.test.sh
skip: set FM_HERDR_SMOKE=1 to opt into the real Herdr AFK e2e
PASS: tests/fm-afk-launch.test.sh
ok - AFK daemon survives harness process-group reap and stops on return
ok - AFK launch fails closed when the durable away flag cannot be created
ok - AFK launch is idempotent and return clears the away flag
ok - AFK launch forwards the resolved Herdr supervisor target and backend
ok - AFK refresh fails closed when the durable away flag cannot be written
ok - AFK return retains the away flag when identity verification fails
ok - AFK return retains the away flag when a live daemon record is missing
ok - AFK return retains the away flag when a stale daemon record remains live
ok - AFK status keeps a live unverified daemon visible
ok - AFK return fails closed when the durable away flag cannot be removed
ok - AFK transition lock rejects an invalid state path promptly
ok - AFK transition lock distinguishes I/O failure from contention
ok - AFK transition lock handoff preserves contention return code
ok - AFK transition lock bounds contention retries
ok - AFK transition lock preserves a replacement daemon and away flag
ok - AFK return clears the away flag after confirmed daemon absence
ok - AFK return retains the away flag when TERM does not stop the daemon
all fm-afk-launch tests passed
PASS: tests/fm-backend-herdr-smoke.test.sh
skip: set FM_HERDR_SMOKE=1 to opt into the real Herdr smoke
START: tests/fm-backend-herdr.test.sh (TMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-backend-herdr/tmp GOTMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-backend-herdr/gotmp)
START: tests/fm-backend.test.sh (TMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-backend/tmp GOTMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-backend/gotmp)
START: tests/fm-backlog-audit.test.sh (TMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-backlog-audit/tmp GOTMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-backlog-audit/gotmp)
START: tests/fm-bearings-snapshot.test.sh (TMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-bearings-snapshot/tmp GOTMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-bearings-snapshot/gotmp)
PASS: tests/fm-backend-herdr.test.sh
NOTICE: auto-detected herdr runtime (HERDR_ENV=1) - spawning into the EXPERIMENTAL herdr backend. Set config/backend or pass --backend tmux to opt out.
ok - Herdr selection honors explicit config/env and nested TMUX precedence
ok - bootstrap backend tool gating keeps Herdr dependencies opt-in
ok - Herdr version gate enforces 0.7.x and protocol 14+
ok - Herdr container ensure is version-gated and workspace-per-home
ok - Herdr workspace bind failures verify cleanup
ok - Herdr malformed workspace creates clean up new workspaces
ok - Herdr task creation, duplicate protection, target parsing, and key mapping work
ok - Herdr kill propagates close failures and avoids server creation
ok - Herdr capture uses safe over-fetch and native busy state
ok - Herdr text submit uses native confirmation and safe composer fallback
ok - Herdr wait_for_working samples native status and classifies blocked submits
ok - Herdr replaces a confirmed husk only after creating the replacement
ok - Herdr create cleanup closes tabs after post-create failures
ok - Herdr missing-id cleanup fails closed when unobserved
ok - Herdr pre-create failures preserve existing tabs
ok - Herdr seed pruning closes and verifies the exact seeded tab
ok - Herdr event capability probes use the explicit session
ok - Herdr eventwait returns and records a fresh blocked transition
ok - backend dispatch accepts Herdr and preserves opaque session:pane targets
PASS: tests/fm-backend.test.sh
ok - backend selection precedence, metadata default, and known/unknown validation
ok - fm-spawn refuses unknown backends from FM_BACKEND, config/backend, and --backend
ok - selector resolution and capture/key/readability/kill dispatch use tmux adapter
ok - tmux adapter propagates kill and container-creation failures
ok - teardown preserves task state when backend kill fails
PASS: tests/fm-backlog-audit.test.sh
ok - clean backlog/state audit passes
ok - required backlog/state drift cases are reported
ok - backlog audit is read-only
ok - registered secondmate meta is accepted outside main In flight
ok - ordinary and unregistered secondmate meta without In flight are reported
PASS: tests/fm-bearings-snapshot.test.sh
ok - bearings defaults to a compact local-only TOON view
ok - bearings soft-fails unavailable remote PR data
ok - bearings discovers gh-axi through HOME-local PATH normalization
START: tests/fm-bootstrap.test.sh (TMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-bootstrap/tmp GOTMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-bootstrap/gotmp)
START: tests/fm-cbm.test.sh (TMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-cbm/tmp GOTMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-cbm/gotmp)
START: tests/fm-cd-pretool-check.test.sh (TMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-cd-pretool-check/tmp GOTMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-cd-pretool-check/gotmp)
START: tests/fm-cognee-brief-rules.test.sh (TMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-cognee-brief-rules/tmp GOTMPDIR=/tmp/fm-behavior-tests.4bIcE9/fm-cognee-brief-rules/gotmp)
PASS: tests/fm-bootstrap.test.sh
ok - bootstrap reports treehouse lease + tasks-axi default/backend contracts
ok - bootstrap requires gh pr checks --json for no-mistakes CI monitoring
ok - bootstrap enforces no-mistakes minimum version
ok - bootstrap surfaces active crew-dispatch rules and default
ok - bootstrap validates crew-dispatch.json and reports malformed or unverified configs
ok - bootstrap validates secondmate-profile.json and reports malformed or invalid configs
ok - bootstrap discovers HOME NVM tasks-axi in a clean non-interactive PATH
PASS: tests/fm-cbm.test.sh
ok - fm-cbm-lib: FM_CBM_ENABLED=0 disables CBM even if binary exists
ok - fm-cbm-lib: auto enables with binary and builds launch prefix
ok - fm-cbm-lib: unsafe resource caps are rejected
ok - fm-cbm-lib: rejects relative explicit binary
ok - fm-cbm-lib: default project allowlist
ok - fm-cbm-lib: config/cbm-projects overrides defaults
ok - fm-cbm-lib: append brief policy is idempotent for eligible projects
ok - fm-cbm-lib: skips brief policy for ineligible projects
ok - fm-spawn: CBM integration contract lines present
ok - fm-cbm-index.sh: help works
ok - fm-cbm-index: escapes paths and surfaces index failures
ok - fm-cbm-index: rejects unallowlisted absolute paths
ok - fm-cbm-index: index all skips ineligible without hard abort mid-list
ok - fm-cbm-index: rejects the .openclaw monorepo root
ok - fm-cbm-index: list surfaces CLI failures
ok - fm-cbm-cli: appends durable usage.jsonl and summary works
ok - fm-cbm-lib: no-jq fallback writes valid escaped JSONL
ok - fm-cbm-lib: omitted detail is logged as null
ok - fm-cbm-lib: brief points at fm-cbm-cli.sh
PASS: tests/fm-cd-pretool-check.test.sh
ok - primary persistent cd into projects is

... [49480 bytes truncated] ...

parked run is absorbed as an external wait, not a wedge
ok - provably-working non-terminal stale is absorbed on first sight, then wedge-escalated past the threshold
ok - a not-provably-working non-terminal stale is surfaced immediately (never left to wait out the timer)
ok - matching non-terminal stale suppressors repair missing or corrupt stale-since timers
ok - triage log capping handles wc byte counts with leading spaces
ok - a heartbeat with no captain-relevant change is absorbed and backs off the cadence
ok - heartbeat backstop fail-safe surfaces a captain-relevant status the per-wake path missed
ok - the liveness beacon stays fresh while the watcher absorbs benign wakes (fm-guard never false-alarms)
ok - with .afk present the watcher reverts to one-shot so the daemon owns triage (no double-triage)
PASS: tests/fm-watcher-lock.test.sh
ok - simultaneous watcher starts leave exactly one live process
ok - killed watcher stale lock is reclaimed
ok - live watcher lock with stale heartbeat is actionable
ok - guard banner leads when down with pending wakes (re-arm-after-drain) and stays silent when fresh+live
ok - guard requires a fresh beacon plus a live matching watcher lock
ok - concurrent fm_lock_try_acquire yields exactly one winner
ok - dead-pid stale lock is reclaimed by a single acquirer
ok - concurrent stale-lock steal yields exactly one winner
ok - live steal mutex is not reclaimed
ok - live-held lock is not stolen
ok - live-held lock with matching pid identity is not stolen
ok - live-held lock with mismatched pid identity is reclaimed
ok - live-held legacy identity remains protected during migration
ok - expired live-held legacy identity is reclaimed
ok - matching expired legacy watcher identity is migrated before expiry recovery
ok - live-held lock without pid identity remains live-held
ok - zombie follower lock is reclaimed using its stored process identity
ok - legacy zombie follower lock is reclaimed without new metadata
ok - fallback process identity includes stable process-group and command data
ok - fallback start identity accepts and distinguishes prior formats
ok - detach cleanup rejects legacy start tokens
ok - detached spawn waits for the target after exec
ok - detached spawn cleans the launcher after pid-file timeout
ok - detached spawn cleans the target after exec timeout
ok - arm reclaims a legacy follower lock whose pid was reused
ok - legacy follower locks without home scope fail closed
ok - watcher health rejects a zombie lock owner
ok - watcher health rejects an unpinned legacy lock
ok - Grok treats an existing follower as a live cycle
ok - empty mid-acquire lock keeps a minimum grace
ok - late original claimant cannot claim a recreated lock
ok - paused mid-acquire claimant backs off to active stealer
ok - watch restart refuses to signal a reused pid
ok - plain arm recovers from a reused-pid stale watcher lock
ok - watcher self-evicts when the lock pid no longer names it
ok - arm attaches to a live fresh watcher and exits only when that cycle ends
ok - arm migrates and attaches to a live legacy watcher lock
ok - arm rejects an unverified legacy watcher lock
ok - arm starts+confirms a fresh watcher on a clean lock and self-heals a dead-pid lock (never healthy off a dead pid)
ok - arm stands down on HUP while the detached watcher keeps its lock and beacon
Terminated
ok - watcher survives SIGTERM of the arm's entire process group
ok - a healthy cycle keeps one attach waiter and duplicate arms exit without stacking
ok - restart hands off the follower slot without stacking waiters
ok - process start identity distinguishes same-second processes
ok - arm propagates an immediate watcher wake before confirmation
ok - arm attaches to a peer watcher after child stands down and exits when peer dies
ok - arm reports FAILED and exits non-zero when no fresh watcher can be confirmed
PASS: tests/fm-x-mode.test.sh
ok - fm-x-poll is a hard no-op without a token (inert default)
ok - fm-x-poll treats an explicitly empty env token as configured
ok - fm-x-poll stays silent on HTTP 204 (the common case)
ok - fm-x-poll lets an explicitly empty relay env override .env
ok - fm-x-poll surfaces auth/config errors once and clears on recovery
ok - fm-x-poll stashes the question and prints the compact marker
ok - fm-x-poll preserves in_reply_to conversation context in the inbox
ok - fm-x-poll reports inbox commit failures without emitting a mention wake
ok - fm-x-poll requires a non-empty question before waking
ok - fm-x-poll rejects an unsafe request_id (path-traversal guard)
ok - fm-x-reply posts a request-bound answer and echoes only the request_id
ok - fm-x-reply accepts the reply via --text-file and stdin (safe, unexpanded)
ok - fm-x-reply exits non-zero on a non-2xx relay response
ok - fm-x-reply cleans up auth header temp files on interrupted posts
ok - fm-x-reply rejects missing arguments with a usage error
ok - fm-x-reply --help makes image support discoverable
ok - fm-x-reply rejects whitespace-only reply text
ok - fm-x-reply dry-run records the would-be reply and never posts
ok - fm-x-reply dry-run works without a token
ok - fm-x-reply honors FMX_DRY_RUN from .env
ok - fm-x-reply lets an explicitly empty dry-run env override .env
ok - fm-x-reply dry-run fails when it cannot record the preview
ok - fmx_split_thread: word-boundary, within-limit, numbered, lossless, capped
ok - fm-x-reply keeps a concise reply as a single unnumbered tweet
ok - fm-x-reply auto-splits a long reply into a numbered thread (texts[])
ok - fm-x-reply clamps a below-floor max to 50 characters
ok - fm-x-reply posts a thread payload (texts[]) to the relay
ok - fm-x-reply --image posts an image object on answer
ok - fm-x-reply streams large image payloads outside curl argv
ok - fm-x-reply dry-run records compact image metadata for threaded replies
ok - fm-x-reply cleans image and payload temp files
ok - fm-x-reply --image rejects missing and unsupported image paths clearly
ok - fm-x-reply --image rejects oversized files before encoding
ok - fm-x-reply --followup posts to /connector/followup with the same request-bound body
ok - fm-x-reply --followup --image posts an image object
ok - fm-x-reply --followup is accepted in any position and leaves the answer path default
ok - fm-x-reply --followup dry-run marks the endpoint without changing the answer path
ok - fm-x-reply --followup auto-splits a long follow-up into a marked thread
ok - fm-x-reply followup dry-run keeps endpoint marker and compact image metadata
ok - fm-x-dismiss posts a request-bound dismiss and echoes only the request_id
ok - fm-x-dismiss dry-run records the would-be body and never posts
ok - fm-x-dismiss dry-run works without a token
ok - fm-x-dismiss exits non-zero on a non-2xx relay response
ok - fm-x-dismiss exits non-zero on a transport failure
ok - fm-x-dismiss rejects an unsafe request_id (path-traversal guard)
ok - fm-x-dismiss rejects missing or extra arguments with a usage error
ok - fm-x-link records and refreshes the X-request link without disturbing meta
ok - meta rewrites are independent of TMPDIR
ok - fm-x-link rejects unsafe ids, missing meta, and missing arguments
ok - fm-x-followup --check reports postable / not-linked correctly
ok - fm-x-followup --check prunes a link past the 24h window
ok - fm-x-followup posts the follow-up and clears the link on success
ok - fm-x-followup --image forwards the attachment through fm-x-reply --followup
ok - fm-x-followup keeps the link when the post fails
ok - fm-x-followup skips silently and clears the link past the 24h window
ok - fm-x-followup is a no-op for a task with no X link
ok - fm-x-followup dry-run records the follow-up and clears the link
ok - fm-x-followup rejects malformed invocations
ok - bootstrap activates X mode from an .env token, idempotently
ok - bootstrap reports missing X-mode dependencies before arming
ok - bootstrap does not report X mode on when activation artifacts cannot be written
ok - bootstrap is inert without a non-empty .env token (non-X users unaffected)
ok - bootstrap cleans up X artifacts on opt-out and is silent once off
ok - bootstrap reports failed X artifact cleanup on opt-out
All 64 behavior tests passed
Evidence: Real Herdr smoke

ok - real Herdr smoke: client protocol 16 and live server verified (read-only)

$ FM_HERDR_SMOKE=1 bash tests/fm-backend-herdr-smoke.test.sh
ok - real Herdr smoke: client protocol 16 and live server verified (read-only)
- Outcome: 🔧 1 issue found → auto-fixed ✅ across 2 runs (24m56s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 4 issues found → auto-fixed (7) ✅
  • 🚨 bin/fm-composer-lib.sh:199 - The classifier returns unknown for a bare shell prompt, but the daemon's injection guard only defers on pending; inject_msg therefore proceeds to send text to a dead shell, where Enter can execute it. Make unknown a blocking pre-injection result.
  • 🚨 bin/fm-backend.sh:287 - The authoritative intent requires “Deepen experimental Herdr adapter: ... eventwait ...”. The diff adds fm_backend_wait_transition, but no production caller invokes it; matches are limited to definitions and unit tests, so runtime supervision remains poll-only. Please confirm whether eventwait was intentionally left unwired.
  • ⚠️ bin/backends/herdr.sh:367 - Seed pruning now closes only the pane. For a dead pane, fm_backend_herdr_pane_for_tab can return no pane and pruning exits; even for no-agent, closing the pane may leave the seeded tab behind. Close and verify the exact seeded tab id after the safety checks.
  • ⚠️ bin/backends/herdr.sh:790 - The named session argument is ignored by the capability probe: herdr status and herdr api schema use ambient routing instead of fm_backend_herdr_cli, despite the adapter's explicit-session contract. A named Herdr session can therefore be probed against the wrong server/schema.

🔧 Fix: Fixed all four Herdr review findings
1 warning still open:

  • ⚠️ bin/backends/herdr.sh:781 - fm_backend_herdr_socket_path filters for the requested session but invokes ambient herdr session list --json without routing through fm_backend_herdr_cli "$session". For named sessions this can resolve the wrong server or no socket, causing eventwait to fail and silently fall back to polling.

🔧 Fix: Route Herdr socket discovery through explicit sessions
2 warnings still open:

  • ⚠️ bin/backends/herdr.sh:515 - After creating the replacement tab, any later failure exits without closing the new tab. The spawn cleanup trap is installed only after fm_backend_create_task returns, so seed-prune or husk-verification failures can leave an unowned Herdr tab behind. Add failure cleanup for the created tab id before returning.
  • ⚠️ bin/backends/herdr.sh:522 - Husk cleanup verification pipes the response directly through .result.tabs[]?; a successful but malformed tab-list response produces no output and is treated as proof that no duplicate husk remains. Validate the response shape before accepting cleanup, otherwise stale duplicate-label tabs can survive and block later spawns.

🔧 Fix: Close created tabs on Herdr post-create failures
1 warning still open:

  • ⚠️ bin/backends/herdr.sh:488 - The new EXIT cleanup only closes a tab when tab_id was parsed, and it suppresses any close failure. If Herdr successfully creates a tab but returns a malformed response without tab_id, execution exits at the parse check with no fallback cleanup; a failed close is also treated as handled. Preserve a safe fallback for malformed responses and verify the created tab is absent.

🔧 Fix: Fail closed on malformed Herdr tab cleanup
1 warning still open:

  • ⚠️ bin/backends/herdr.sh:453 - When the create response lacks a tab ID, cleanup treats zero newly observed tabs as success. A stale or incomplete label query can therefore leave the created tab behind while the EXIT trap reports cleanup succeeded; fail closed unless the created tab's absence is authoritatively verified.

🔧 Fix: Fail closed on unobserved malformed Herdr tabs
2 issues (1 error, 1 warning) still open:

  • 🚨 bin/backends/herdr.sh:511 - The EXIT trap is active before the initial duplicate-tab lookup. If that lookup fails before any create, cleanup re-queries with an empty baseline and can mistake an existing live tab for the newly created tab, then close it. Gate fallback cleanup on a create-attempted flag and never close tabs after pre-create failures.
  • ⚠️ bin/backends/herdr.sh:330 - When workspace metadata binding fails, workspace close errors are discarded. A workspace-create call may already have succeeded, leaving an orphaned Herdr workspace and seeded tab after the spawn returns failure; verify cleanup and surface failure if the workspace cannot be closed.

🔧 Fix: Protect live tabs and verify Herdr workspace rollback
1 warning still open:

  • ⚠️ bin/backends/herdr.sh:346 - If workspace create succeeds but returns malformed output without workspace_id, this exits at the parse check without attempting cleanup, leaving the newly created workspace and seeded tab orphaned. Preserve a pre-create workspace baseline and identify/close the unique new workspace, or fail with a verified cleanup path.

🔧 Fix: Rollback malformed Herdr workspace creations safely
✅ Re-checked - no issues remain.

🔧 **Test** - 1 issue found → auto-fixed ✅
  • 🚨 tests failed with exit code 1
  • bash bin/fm-run-behavior-tests.sh

🔧 Fix: Update stale AFK defer assertion
✅ Re-checked - no issues remain.

  • bash bin/fm-run-behavior-tests.sh
  • bash tests/fm-backend-herdr.test.sh
  • python3 tests/fm-backend-herdr-eventwait.test.py
  • bash tests/fm-composer-lib.test.sh
  • bash tests/fm-transition-lib.test.sh
  • bash tests/fm-watch-herdr-event.test.sh
  • bash tests/fm-herdr-lab.test.sh
  • bash tests/fm-gate-refuse.test.sh
  • bash bin/fm-run-behavior-tests.sh
  • FM_HERDR_SMOKE=1 bash tests/fm-backend-herdr-smoke.test.sh
  • Removed generated bin/backends/__pycache__/ test residue and verified git status --short is clean.
✅ **Document** - passed

✅ No issues found.

🔧 **Lint** - 1 issue found → auto-fixed ✅
  • ⚠️ linter found issues (exit code 1)

🔧 Fix: Captain: export Herdr event capability marker
✅ Re-checked - no issues remain.

✅ **Push** - passed

✅ No issues found.

@JTInventory
JTInventory merged commit a5ec062 into main Jul 19, 2026
4 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant