Skip to content

fix: repair legacy Herdr task bindings - #10

Merged
rega10 merged 7 commits into
mainfrom
fm/firstmate-legacy-herdr-cleanup-repair
Aug 15, 2026
Merged

fix: repair legacy Herdr task bindings#10
rega10 merged 7 commits into
mainfrom
fm/firstmate-legacy-herdr-cleanup-repair

Conversation

@rega10

@rega10 rega10 commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Intent

Implement a small, guarded Firstmate maintenance fix for legacy Herdr task records that predate endpoint_task_id= and therefore can never be cleaned after their work has landed. Observed symptom: repeated monitoring alerts from the inactive firstmate-source-reconciliation session even though PR 9 is merged, its no-mistakes run completed 13/13 checks, and its branch is clean - normal cleanup correctly refuses because the opaque Herdr endpoint lacks an exact task binding. The captain explicitly authorized a safe reconciliation/cleanup fix for this Firstmate case.

Requirements and accepted decisions:

  • One narrow authoritative repair path, NOT a weakening of fm_backend_validate_task_endpoint and NOT teaching ordinary teardown/control to trust unbound opaque endpoints. Ordinary cleanup must keep refusing unbound records implicitly and never repair metadata as a side effect.
  • The chosen design is bind-only: bin/fm-herdr-legacy-repair.sh restores the single missing endpoint_task_id= line, and only when independent evidence all agrees: exact record shape proven by running the shared validator on the candidate record; recorded worktree/project existence, isolation, and origin agreement; clean worktree (same dirty allowances as teardown); landed committed work (reachable from a remote-tracking ref, or proven landed by the shared merged-PR/content-in-default predicates); and live read-only Herdr evidence against the recorded named session only (structured pane_not_found counts as endpoint gone; a present pane must match recorded workspace/tab/pane topology, carry the fm- tab label, hold either no agent or a finished idle/done agent, and have a foreground cwd in the recorded worktree - or the recorded project for an agent-less restored shell). Filename/task-id agreement alone, labels alone, cwd alone, or a merged PR alone are insufficient. Any missing, duplicate, stale, ambiguous, mismatched, unreadable, live-working, blocked, unlanded, or dirty evidence refuses with a concrete diagnostic and preserves everything.
  • The repair is explicit, deterministic, idempotent, safe to rerun: an already-bound record is a validated no-op; the single mutation is an atomic temp+rename metadata write under the task's control and metadata locks; it never issues a mutating Herdr command, never closes panes, never deletes records. Cleanup afterwards remains bin/fm-teardown.sh, which re-runs its own complete landed-work and confirmed-close safety.
  • Scope limits: primary-home Herdr records only; kind=secondmate, remote routes, secondmate homes, tmux (self-identifying fm- window names need no repair), and the other opaque backends (zellij/orca/cmux, which share the masking condition but are out of authorized scope) all refuse by name. No general runtime migration machinery, no automatic startup cleanup, no changes to secondmate/remote ownership. The live firstmate-source-reconciliation record was not touched; Firstmate will invoke the public repair path separately after this lands.
  • To honor the one-owner rule, teardown's landed-work predicates (default_branch, PR-number resolution, ensure_commit_object, patch-id containment, pr_is_merged, content_in_default, work_is_landed) were extracted verbatim into bin/fm-landed-lib.sh with explicit parameters; fm-teardown.sh keeps thin wrappers with its historical names and identical messages, and remains the owner of the complete landed-work decision. The now-unused content_in_default wrapper was dropped.
  • The validator's legacy-Herdr refusal message gained one pointer line naming the guarded repair path so a future session can discover it; the refusal itself is unchanged.
  • Public-interface regression tests (tests/fm-herdr-legacy-repair.test.sh) cover: proven modern bound task untouched; fully provable synthetic legacy landed task repaired exactly once; idempotent rerun; mismatched task/worktree/project/session/workspace/tab/pane; dirty and unlanded work; active (working), blocked, and unreadable agents; missing and ambiguous fields; tmux/zellij/secondmate/remote scope refusals; unrelated-record and default-session preservation; teardown refuses before repair and accepts after; lock-contention refusal; and refusal before any lifecycle mutation (no mutating Herdr subcommand ever appears in the fake-CLI log). tests/fm-herdr-legacy-repair-e2e.test.sh proves mismatch refusal, exactly-once repair, idempotent rerun, and pane preservation against real Herdr 0.8.0 through the guarded fm-herdr-lab.sh isolation (named non-default session, default-session byte-identical tripwire).
  • tests/fm-gotmp.test.sh was repaired: its partial-bin teardown fixture was already broken on main by upstream reconciles (duplicate fm-wake-lib.sh link causing an ln error, and missing fm-session-lock-lib.sh which the tmux adapter now sources, aborting teardown mid-kill under set -e); it also needed the new fm-landed-lib.sh link. All three gotmp assertions pass again.
  • Docs updated proportionately: the script's own header/--help owns the mechanics; docs/herdr-backend.md gained a short Legacy record repair section and the two regression entry points; docs/configuration.md gained one pointer line at the binding-contract paragraph; docs/scripts.md gained rows for the two new files; docs/verification/runtime-backends.md gained a dated Legacy Herdr binding repair record (herdr 0.8.0, 2026-08-14) including the harness-neutrality statement. Test-runner classification maps (bin/fm-test-run.sh families, changed-file map, weight hints; bin/fm-test-isolation-proof.sh exclusion reasons) cover the new tests and scripts.
  • Validation already performed: bin/fm-lint.sh (pinned shellcheck) clean; bin/fm-doc-audience-check.sh clean; focused suites green (legacy-repair portable and e2e, gotmp, teardown, teardown-endpoint-safety, backend, backend-herdr, control, control-relaunch, herdr-lab, herdr-session-cleanup, test-run/isolation-proof/nm-contract/doc-audience meta suites). A local full --all sweep was recorded indeterminate after wedging in tests/fm-watch-arm.test.sh and being stopped; known failures observed there were verified pre-existing on pristine main and unrelated to this change (fm-calm-pi-extension /export, fm-muse-harness identity probe, fm-teardown.test.sh herdr-preflight-missing-adapter) or flaky-environmental (fm-remote-doctor passes standalone). Broad validation is delegated to this pipeline and CI.
  • Delivery: open a PR, never merge it; merge authority remains the captain's.

What Changed

  • Add an explicit, idempotent repair command that atomically binds legacy primary-home Herdr records only after metadata, clean/landed Git state, and read-only endpoint evidence agree; ordinary cleanup continues refusing unbound endpoints.
  • Extract shared landed-work predicates into fm-landed-lib.sh for teardown and repair while preserving teardown’s complete landed-work decision.
  • Add portable and real-Herdr regression coverage, update test classification and teardown fixtures, and document the guarded repair workflow.

Risk Assessment

✅ Low: The guarded repair is well-bounded, the prior review findings are resolved, and no remaining source-verifiable defect or intent contradiction was substantiated.

Testing

The author-reported prior validation remained baseline context; this phase independently exercised the guarded repair’s full portable safety matrix, the narrow gotmp cleanup regression, and real Herdr 0.8.0 behavior. After applying the repository’s documented test-only validation-agent bypass, real-runtime mismatch refusal, exactly-once binding, idempotency, pane preservation, and isolated-session protection all passed; a reviewer-visible CLI transcript was captured, and the worktree remained clean.

Evidence: Real Herdr legacy-repair E2E evidence

Real Herdr 0.8.0 transcript showing mismatch refusal, exactly-once repair, idempotent rerun, and pane preservation.

ok - a live-topology mismatch refuses before any change against real Herdr
ok - a fully provable legacy record repairs exactly once against real Herdr evidence
ok - the repair reruns as a no-op with the pane untouched
all fm-herdr-legacy-repair-e2e tests passed

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed (4) ✅
  • 🚨 bin/fm-herdr-legacy-repair.sh:142 - Intent requires “kind=secondmate ... refuse by name” and “Any missing, duplicate ... evidence refuses,” but this hunk reads the last kind= value and only compares it with secondmate. An ambiguous record containing kind=secondmate followed by kind=ship passes this scope gate and can be bound when the remaining evidence agrees. Require exactly one unambiguous primary-task kind= value before candidate validation, with an executable regression covering this duplicate ordering.

🔧 Fix: Reject ambiguous legacy Herdr task kinds
1 error still open:

  • 🚨 bin/fm-herdr-legacy-repair.sh:198 - The requirement says “Any missing, duplicate ... ambiguous ... evidence refuses,” but this line silently selects the last pr= value. A legacy record with duplicate PR fields can therefore use the last merged PR to prove unpushed work landed and receive a binding while its evidence remains ambiguous. This repeats the same causal theme as the fixed duplicate-kind bypass; require zero or one unambiguous pr= record before landed-work validation and add an executable duplicate-PR refusal regression.

🔧 Fix: Reject ambiguous legacy Herdr PR evidence
4 errors still open:

  • 🚨 bin/fm-herdr-legacy-repair.sh:79 - Intent requires the script’s “header/--help [to own] the mechanics,” but usage prints only lines 2–15, omitting even the Usage line plus all evidence, refusal, locking, and mutation mechanics. Expand --help to expose the owned operational contract.
  • 🚨 bin/fm-herdr-legacy-repair.sh:70 - Intent requires “the single mutation” to be the atomic metadata write and refusals to preserve everything, but sourcing fm-wake-lib.sh runs mkdir -p "$STATE" before help, argument validation, gate refusal, or metadata checks. Thus --help or a refused invocation against a missing state directory creates durable state. Avoid the side-effecting source path until an existing valid state directory is proven, or use side-effect-free lock support.
  • 🚨 bin/fm-herdr-legacy-repair.sh:123 - Intent requires secondmate homes to “refuse by name,” but -e follows symlinks, so a secondmate home with a dangling .fm-secondmate-home marker is treated as primary and its ordinary ship/scout record can be bound. Treat any marker path, including a symlink or otherwise unreadable marker, as refusal evidence.
  • 🚨 tests/fm-herdr-legacy-repair.test.sh:351 - Intent explicitly requires public-interface coverage for a mismatched pane, but this topology test covers tab, workspace membership, label, cwd, and unreadable responses without ever returning a different pane_id or staging a window/herdr_pane_id mismatch. Add an executable refusal-and-preservation case for the pane mismatch.

🔧 Fix: Harden legacy Herdr repair preflight and topology
1 error still open:

  • 🚨 bin/fm-herdr-legacy-repair.sh:300 - Intent requires “a present pane must ... hold either no agent or a finished idle/done agent” and says unreadable evidence must refuse, but this path ignores the agent-name field: {"result":{"agent":{"agent_status":"idle"}}} is classified as live, parsed as an empty name plus idle, and authorizes the binding. Require a nonempty registered-agent identity before accepting idle/done, with an executable malformed-agent refusal case.

🔧 Fix: Reject malformed legacy Herdr agent identities
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-herdr-legacy-repair.test.sh
  • bash tests/fm-herdr-legacy-repair-e2e.test.sh — initial standalone attempt was correctly stopped by the validation-agent lifecycle guard; no product logic ran
  • FM_GATE_REFUSE_BYPASS=1 bash tests/fm-herdr-legacy-repair-e2e.test.sh
  • bash tests/fm-gotmp.test.sh
  • FM_GATE_REFUSE_BYPASS=1 bash tests/fm-herdr-legacy-repair-e2e.test.sh 2>&1 | tee /var/folders/pl/kn_kjwr11lv6jtr8pt8j93y00000gn/T/no-mistakes-evidence/01M017ZV9SB2MW9EP28RTNQMVF/herdr-legacy-repair-real-e2e.txt
  • git rev-parse HEAD and git status --short — confirmed target commit 166bb99f35599d3577955b7109495fd2d7985b13 and no testing residue
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Task records written before endpoint_task_id= existed can never pass
fm_backend_validate_task_endpoint on an opaque backend, so teardown and
control refuse them forever while the watcher keeps alerting on their
finished endpoints.

Add bin/fm-herdr-legacy-repair.sh: an explicit, deterministic, idempotent
bind-only repair for a legacy primary-home Herdr record. It restores the
single missing endpoint_task_id= line only when independent evidence all
agrees - exact record shape via the shared validator, worktree/project
identity and origin agreement, clean worktree, landed work through the
shared landed predicates, and live read-only Herdr evidence (structured
pane-gone, or exact workspace/tab/pane topology with an fm-<id> tab label,
an absent or finished agent, and a matching foreground cwd). Any missing,
ambiguous, mismatched, dirty, unlanded, live-working, or unreadable
evidence refuses with a concrete diagnostic and preserves everything.
It never closes a pane, deletes a record, or issues a mutating Herdr
command; cleanup remains fm-teardown.sh, which re-runs its own complete
landed-work and confirmed-close safety against the repaired record.

Extract teardown's landed-work predicates verbatim into
bin/fm-landed-lib.sh so both callers share one owner; teardown keeps its
historical wrapper names and messages. The validator's legacy-Herdr
refusal now names the repair path. tmux legacy records still self-identify
by window name and need no repair; zellij/orca/cmux legacy records share
the masking condition but stay out of this narrow path and refuse by name.

Regression coverage: tests/fm-herdr-legacy-repair.test.sh (modern bound
no-op, repair-exactly-once, idempotent rerun, every mismatch class, dirty/
unlanded, active/nonterminal, missing/ambiguous fields, unrelated-record
and default-session preservation, refusal before any lifecycle mutation,
lock contention) and tests/fm-herdr-legacy-repair-e2e.test.sh against real
Herdr through the guarded lab with the default-session tripwire.
The partial-bin teardown fixture was broken by two upstream reconciles it
never followed: it linked fm-wake-lib.sh twice (an ln 'File exists' error
on every run), and it lacked fm-session-lock-lib.sh, which the tmux
adapter now sources - a missing sourced sibling aborts teardown mid-kill
under set -e even inside a guarded call, so the suite failed on main
before this branch. Replace the duplicate link with fm-landed-lib.sh
(teardown now sources the shared landed-work predicates) and add the
missing session-lock lib, restoring all three gotmp assertions.

Also restructure the legacy-repair suite's default-branch fallback so the
checkout retry is an explicit if rather than an A && B || C chain
(shellcheck SC2015).
@rega10
rega10 merged commit bae3180 into main Aug 15, 2026
13 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.

2 participants