Skip to content

fix(bin): prove steer delivery with payload deltas - #2814

Closed
kunchenguid wants to merge 28 commits into
mainfrom
fm/remote-send-delivery-delta-r1
Closed

fix(bin): prove steer delivery with payload deltas#2814
kunchenguid wants to merge 28 commits into
mainfrom
fm/remote-send-delivery-delta-r1

Conversation

@kunchenguid

Copy link
Copy Markdown
Owner

Intent

Re-implement fm-send steer delivery verification using the captain-APPROVED payload-delta proof, replacing the shape-parsing read-back that was abandoned. This is firstmate's SHARED, TRACKED transport core - the steer channel the whole fleet relies on.

APPROVED DESIGN (captain decision 1): implement the delivery proof in the ONE owner bin/fm-composer-lib.sh. Normalize both captures and the payload identically (strip ANSI/box-drawing/whitespace, keep row structure), anchor on the last min(24,len) normalized chars of the payload TAIL, and accept ONLY when the anchor is both (A) newly ADDED in the before->after row diff AND (B) its whole-screen occurrence COUNT increased. This excludes the round-1 scrollback false-pass by TIME not location, so that false-pass is impossible by construction. Keep it refusing rather than proceeding (non-zero + clear diagnostic, never a silent partial or false success) and backend-independent.

CAPTAIN DECISION 2 (fresh PR): branch off main; carry over ONLY the cleanly-separable KEEP set from the now-CLOSED branch fm/remote-send-confirm-readback-fix-r1 - the pending-reply durability half (full-payload durable recovery), the launch-readiness gate (claude_wait_for_ready in bin/fm-spawn.sh, fm_composer_screen_is_gated, fm_composer_pre_type_ok's GATED branch only), the locking, and the behavior-through-fm-send tests. Do NOT carry the shape-parsing read-back. PR #2761 is closed as superseded and must not be reopened.

CAPTAIN DECISION 3 (no bypass): do NOT add an FM_SEND_UNPROVEN escape hatch or any equivalent caller-declared exemption. A harness whose composer cannot be proven is refused, not steered blind. The existing exit-3 "unconfirmed" already covers the post-Enter case.

REPLACE (not keep): fm_composer_pre_enter_verdict was replaced wholesale by the delta verdict - its caps/cursor-row/identity params, the styled=1 vs styled=0 fork, the fm_composer_classify_screen precondition, and both fm_composer_extract_selected_content calls are gone. The caps/cursor_row/identity fetches were stripped from the send path in bin/fm-tmux-lib.sh fm_tmux_submit_core and from every per-backend copy (including zellij's private acceptance rule) so every backend uses the ONE shared verdict. The whole visible pane is captured identically before and after, replacing FM_COMPOSER_ACCEPT_LINES cropping.

CAPTAIN'S LATER OPTION C (accepted, and the substance of this branch's redesign): an earlier round of this work refused every payload shorter than the 24-character anchor minimum, which broke real fleet traffic - a decision answer like "2", a "yes", and every fm-control lifecycle exit command (/exit, /quit). The captain rejected both ways out that were tried. Short steers must WORK, not be refused; and there must be NO weaker-proof path for them, because a weaker test reopens the false-accept this whole design exists to close. The captain named three candidate approaches - a verifiable envelope/marker wrapping the payload, a confirmation handshake, or an occurrence count robust to coincidental modal text - and asked for the SOUNDEST of them.

CHOSEN APPROACH AND WHY THE ALTERNATIVES WERE REJECTED. A previous attempt on this branch tried "occurrence count plus a positively classified empty composer before the write, and a count rising by exactly one". That was analyzed and found UNSOUND and has been removed: a live agent pane redraws rows containing a single character constantly (a token counter ticking from 1.9k to 2.1k is a newly added row, containing "2", with the count risen by exactly one), so an empty-composer precondition does not exclude the coincidence. A confirmation handshake was rejected as not generically available across harnesses. The envelope was chosen: for a payload shorter than the anchor minimum, fm-send now MANUFACTURES the missing entropy. It types the payload followed by a fresh random suffix sized so the combined string is exactly the anchor length, proves that combined anchor through the same unchanged delta verdict, then erases exactly the suffix and proves the erase. The agent still receives precisely the payload the caller wrote, and because the proven anchor contains the payload's own characters, this is a proof ABOUT the payload rather than a proxy for it. The suffix goes AFTER the payload because erasing consumes from the tail; a leading suffix would need cursor movement no backend agrees on.

THE ERASE PROOF, and a hole found and closed during implementation. The first erase check compared the full combined anchor's count back to its before value. A test drove the hardest under-erase case and showed that check is wrong: dropping even one character from the tail destroys the full-length anchor, so its absence proves nothing and a composer still holding suffix characters would have been accepted, sending noise to the agent. The check was replaced with a boundary probe. Erasing consumes from the tail, so an incomplete erase can only ever leave payload+suffix-prefix, and every such leftover down to a single character still has the suffix's FIRST character immediately after the payload - so the test is whether that two-character boundary string gained an occurrence. The second check catches the opposite direction: an over-erase eats the payload's tail, so the payload's own occurrence count must be unchanged from the just-typed capture. The suffix alphabet deliberately EXCLUDES every character the payload contains, which is what makes that second count decidable - the payload cannot occur inside the suffix or across the boundary, so removing the suffix cannot move the count on its own.

FINDING 2 FROM THE PRIOR REVIEW ROUND (typed-unproven had no operator resolution path) is fixed: bin/fm-send.sh gained --typed-confirm and --typed-abandon, which settle a typed-unproven pending-reply record by correlation id, the diagnostic that creates that state now names both commands with the concrete id, and the watcher escalates a typed_unproven record on the ordinary cadence so it can never silently block teardown. A stranded suffix additionally gets a named stderr warning carrying the exact text and suffix length, so "inspect the pane" is an actionable instruction.

fm-control lifecycle exit now works with NO exemption. The earlier attempt added an FM_COMPOSER_DELIVERY_POSTCONDITION_EXTERNAL environment variable letting fm-control declare its own postcondition; that has been deleted as exactly the bypass shape decision 3 forbids. A short exit command needs no exemption once it carries a full-length anchor. The agent-state wait remains the authoritative post-exit proof, as it always was.

ARCHITECTURE: the type-and-prove sequence moved into one shared owner, fm_composer_typed_delivery_core, which every adapter calls with three primitives (capture, literal, erase), replacing five copies of the ordering - the same pattern fm_composer_submit_retry_core already uses.

BACKEND ERASE KEY: added per backend, because the key name belongs to each tool's own vocabulary and the byte it resolves to belongs to the pane. tmux BSpace and zellij Backspace were both verified live against real panes to remove exactly one character per press (zellij rejects tmux's BSpace spelling outright). herdr and cmux carry the lowercase backspace mapping consistent with their own vocabularies but are NOT live-verified on this machine, and the evidence table says so explicitly rather than implying otherwise. Orca's send API exposes only text, Enter, and interrupt, so a character cannot be taken back off its composer at all - the shared core therefore refuses a short payload on Orca BEFORE typing anything, rather than stranding a suffix it has no way to remove. Sending a raw backspace byte through the literal-text channel instead of a named key was measured and rejected: 0x08 erased nothing against a real tmux pane and 0x7f did, which is precisely the convention a named key delegates to the backend.

TESTS: the design's benches are regressions in existing tests/ files, with no new runner. tests/fm-composer-lib.test.sh covers the delta proof accepting a shape-free echo, refusing a gated trust dialog, refusing a swallowed send, and the round-1 scrollback copy yielding no new occurrence; plus the envelope's freshness, suffix-alphabet exclusion, exact erase count, verbatim treatment of a self-proving payload, refusal of a short multi-row payload, and both erase-proof directions. A pane simulator drives fm_composer_typed_delivery_core through its real interface for the end-to-end cases including "a short steer is proven and leaves ONLY the payload in the composer". Per firstmate-coding-guidelines' harness-dependent-checks rule, each backend's erase key also has a live guard in its real smoke test that fails naming the backend and version, and docs/verification/runtime-backends.md records the dated results and the commands that refresh them. The behavior-through-fm-send tests from the KEEP set are retained.

Also in this change: the composer-delta fixtures README landed unclassified earlier on this branch and failed bin/fm-doc-audience-check.sh, so it is classified as maintainer-verification; and two probe-absent sentinel assignments are quoted because shellcheck reads them as arithmetic once the shared library grows past its analysis limit. Both were pre-existing branch defects that lint and the doc check caught.

DELIVERY: mode=no-mistakes, yolo OFF - the captain owns the merge, so do not merge. This changes shared steer/transport core the whole fleet relies on; the PR should note that homes pick it up after merge plus a self-update, and that landing timing is coordinated with the main firstmate. Escalate any ask-user finding rather than deciding it; no --yes.

ADDENDUM - what happened in the previous run and what is already settled. A first validation run on this branch reached the lint step with review, test, and document all COMPLETED and review reporting ZERO findings, after thirteen rounds of review fixes that are all present as commits on this branch. Its lint fixer then wedged - the agent process sat at 0% CPU for seventeen minutes and emitted no output at all - so that worker was terminated by exact pid and the run went terminal with "step lint failed". Custody was recovered from the local gate: the pipeline had rebased onto a newer main, and the two original commits were verified patch-identical through that rebase before the branch was moved, with the pre-recovery head anchored first. Nothing was lost.

The lint findings that run never got to fix have since been fixed directly and committed, since fixing them is the documented action after a failed run: the hyphenated string sentinels shellcheck misreads as arithmetic are now quoted, the three test stubs that shadow indirectly-invoked functions carry narrow per-site directives with reasons rather than a file-wide disable, and a dead SIM_WIRE variable was removed rather than silenced because the assertions read the wire from its file instead. bin/fm-lint.sh now exits clean, bin/fm-doc-audience-check.sh is clean, and tests/fm-composer-lib.test.sh and tests/fm-pending-reply.test.sh both pass with 34 and 40 checks and no failures.

Decisions already made and NOT open for relitigation, each ruled on explicitly by the captain during the previous run: after a successful write, EVERY unproven outcome is typed-unproven and there is no retryable post-write shape, because no screen delta can distinguish a delivered-but-unrendered write from a swallowed one; only the pre-write refusals stay retryable, being a gated modal refused before typing and a write that did not go out. The flattened row matching in the delta proof is deliberate and load-bearing because it is what makes a WRAPPED payload provable. Shape-parsing must not be reopened in any form. The low-level stranded-suffix warning stays purely descriptive and the request-owning layer owns the settlement instruction, because the shared transport core has no concept of a pending request and the lifecycle commands call it with no correlation id. The erase proof's payload-retention barrier compares for EQUALITY against the stage-one capture. And a bounded, mechanism-less residual is knowingly accepted and documented: there is no shape-free positive proof that the payload survived the final erase keystroke, because once the erase is correct nothing high-entropy remains to anchor on.

What Changed

  • Replace shape-parsed composer read-back with one shared, backend-independent payload-delta proof that gates Enter and rejects gated or unprovable panes.
  • Prove short steers using a randomized erasable suffix, with backend-specific erase keys, erase verification, and pre-write refusal where erasure is unsupported.
  • Persist complete pending-request payloads for durable recovery and add locked typed-unproven tracking, escalation, and explicit --typed-confirm / --typed-abandon settlement commands.

Risk Assessment

⚠️ Medium: No material source defects were found, but this is a substantial shared transport rewrite with an explicitly accepted erase-proof residual on backends not yet live-verified.

Testing

Inspected the target diff, ran focused shared-core, fm-send, lifecycle, pending-reply, daemon, remote-delivery, and backend adapter tests, then manually exercised a short steer through the real fm-send executable; all checks passed, and the transcript confirms exit 0, exact suffix erasure, Enter submission, and payload-only delivery. No screenshot was applicable because the changed user surface is terminal transport rather than graphical UI.

Evidence: Short-steer fm-send end-to-end transcript

Source: Short-steer fm-send end-to-end transcript

$ fm-send.sh demo "2"
exit=0
--- stderr (end-user surface) ---
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the requested message WILL still be sent.
●  repair a missing or failed watcher cycle with the Pi tool fm_watch_arm_pi, or restart Pi with -e /tmp/fm-send-evidence.BN3qID/home/.pi/extensions/fm-primary-turnend-guard.ts -e /tmp/fm-send-evidence.BN3qID/home/.pi/extensions/fm-primary-pi-watch.ts if the extensions are not loaded.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
--- terminal transport ---
TYPE literal payload=2twddftaknl8rah03rzkfoti
KEY BSpace
KEY BSpace
KEY BSpace
KEY BSpace
KEY BSpace
KEY BSpace
KEY BSpace
KEY BSpace
KEY BSpace
KEY BSpace
KEY BSpace
KEY BSpace
KEY BSpace
KEY BSpace
KEY BSpace
KEY BSpace
KEY BSpace
KEY BSpace
KEY BSpace
KEY BSpace
KEY BSpace
KEY BSpace
KEY BSpace
KEY Enter
--- payload received by agent at Enter ---
2
--- final composer state ---
typed-buffer=2
enter-sent=yes
Evidence: Shared composer delivery proof tests

Source: Shared composer delivery proof tests

ok - fm_composer_queued_enter_verdict: pending + busy returns empty (queued Enter)
ok - fm_composer_queued_enter_verdict: pending + idle/unknown stays pending
ok - fm_composer_queued_enter_verdict: only proven pending is converted
ok - fm_composer_classify_screen: pre-type gate verdicts do not leak post-Enter
ok - fm_composer_screen_is_gated: only current modal evidence is combined
ok - fm_composer_delivery_delta_verdict: 11 real-harness scenarios, plain and ANSI captures agreeing (22 verdicts)
ok - fm_composer_delivery_delta_verdict: a mid-word wrap, a hint row, and a non-bottom-anchored composer do not block acceptance
ok - fm_composer_delivery_delta_verdict: refuses a swallowing modal the gate scorer does not recognise
ok - fm_composer_delivery_delta_verdict: a trust dialog refuses before typing and again after, both with a reason
ok - fm_composer_delivery_delta_verdict: a scrollback copy plus a ticking pane is not delivery
ok - fm_composer_delivery_delta_verdict: when a redrawing pane defeats novelty, the occurrence count still refuses
ok - fm_composer_delivery_delta_verdict: overlapping anchors cannot manufacture an occurrence increase
ok - fm_composer_delivery_delta_verdict: a partial write that lost the payload tail is refused
ok - fm_composer_delivery_delta_verdict: a payload echoed with no container at all is a healthy send
ok - fm_composer_delivery_delta_verdict: an unusable payload is refused with a named reason
ok - fm_composer_delivery_delta_verdict: short collision anchors are refused
ok - fm_composer_envelope_prepare: a short payload gets a fresh full-length anchor and an exact erase count
ok - fm_composer_envelope_prepare: a single-character suffix is selected afresh
ok - fm_composer_envelope_prepare: a self-proving payload is typed verbatim with no envelope
ok - fm_composer_envelope_prepare: an unerasable short payload is refused, not guessed at
ok - fm_composer_envelope_boundary_verdict: opposite checkpoints pin both erase directions
ok - fm_composer_typed_delivery_core: a short steer is proven and leaves only the payload in the composer
ok - fm_composer_typed_delivery_core: a self-proving steer is delivered with no envelope
ok - fm_composer_typed_delivery_core: every unproven successful write is typed-unproven
ok - fm_composer_typed_delivery_core: unchanged visible tail never invites a duplicate write
ok - fm_composer_typed_delivery_core: a rising count prevents ambiguous row diff from inviting retry
ok - fm_composer_typed_delivery_core: an earlier copy of the payload on screen proves nothing
ok - fm_composer_typed_delivery_core: a gated modal is refused before anything is typed
ok - fm_composer_typed_delivery_core: unavailable boundary probes refuse before typing
ok - fm_composer_typed_delivery_core: no erase primitive refuses short steers before typing, long steers still work
ok - fm_composer_typed_delivery_core: erase checkpoints and final payload retention refuse every measured direction
ok - fm_composer_typed_delivery_core: every way the erase can fail reports typed-unproven, never a bare refusal
ok - fm_composer_typed_delivery_core: stranded warnings are exact transport evidence only
ok - fm_composer_delivery_delta_verdict: box glyphs in the payload vanish on both sides, not just one
ok - fm_composer_delivery_delta_verdict: a narrow single-row composer that scrolls its buffer still proves delivery
Evidence: fm-send strict behavior tests

Source: fm-send strict behavior tests

ok - fm-send strict: exact task/lane ids resolve through home metadata
ok - fm-send --key: exit status follows delivery, and an undelivered key never reports success
ok - fm-send strict: unset FM_HOME fails before target resolution
ok - fm-send strict: unresolvable selectors do not fall back to tmux
ok - fm-send strict: prefixless herdr pane ids are rejected before tmux fallback
ok - fm-send strict: unmatched single-colon explicit targets must verify live before sending
ok - fm-send strict: fm-prefixed Herdr sessions remain explicit backend targets
ok - fm-send strict: healthy fm-<id> sends still type once and submit
ok - fm-send: a Claude trust-dialog pane is refused loudly, with no Enter and no delivered marker
ok - fm-send: after-type capture failure preserves unresolved durable recovery
ok - fm-send: typed resolution accepts only exact correlation ids
ok - fm-send: missing payload tail is not-accepted, sends no Enter, and closes no resolve-key
ok - fm-send: a healthy send is confirmed with no composer container to parse at all
Evidence: Remote delivery and settlement tests

Source: Remote delivery and settlement tests

ok - fm-send remote: delivered-unconfirmed reports delivered, exits 0, keeps the expectation armed
ok - fm-send remote: typed-unproven settlement guidance stays exact and suffix-aware
ok - fm-send remote: a real remote failure still fails loudly with the remote diagnostics
ok - fm-send remote: ssh 255 still refuses loudly and preserves the expectation as delivery_unknown
ok - fm-send remote: a delivered-unconfirmed answer closes its --resolve-key decision
ok - fm-send local: an unconfirmed submit exits 3 with an honest non-error report
ok - fm-send local: an unconfirmed submit still never closes a --resolve-key decision
ok - fm-send local: an unconfirmed secondmate send keeps its reply expectation armed
all fm-send-remote-delivery tests passed
Evidence: Lifecycle control tests

Source: Lifecycle control tests

ok - fm-control exit: every verified harness gets its own verified exit command
ok - fm-control interrupt: every verified harness gets its own verified key and repeat count
ok - fm-control interrupt: opencode needs a double Escape, claude a single one
ok - fm-control: a harness with no verified control mechanics is refused, not guessed at
ok - fm-control-lib: a recorded harness resolves to its verified adapter without guessing
ok - fm-control: prefixed recorded harnesses reach interrupt and exit mechanics
ok - fm-control-lib: the backend key matrix matches each adapter's real send-key surface
ok - fm-control-lib: adapter capability is per task kind, not per adapter alone
ok - fm-control interrupt: a backend that cannot deliver the harness's key refuses instead of sending another
ok - fm-control: a backend that cannot prove an agent stopped refuses exit and relaunch
ok - fm-control-lib: stop-proving verbs are gated on the backends that really classify agent state
ok - fm-control: a legacy window label is refused and the exact task id is named
ok - fm-control: an explicit backend endpoint is never a control target
ok - fm-control: an unrecorded task id is refused
ok - fm-control: a record whose endpoint identity names another task is refused
ok - fm-control: a remotely placed secondmate is refused by placement, not by a metadata complaint
ok - fm-control: interrupt and exit lock before task-state resolution
ok - fm-control: the verb list is closed - no raw keys, arbitrary text, or clear verb
ok - fm-control: resume is refused with the determinism reason and the alternative
ok - fm-control: profile and note flags belong to relaunch only
ok - fm-control exit: an already-stopped agent is idempotent success with no bytes sent
ok - fm-control exit: a vanished endpoint refuses instead of silently succeeding
ok - fm-control interrupt: refuses when no agent is running rather than keying a shell
ok - fm-control exit: an endpoint whose process cannot be attributed refuses
ok - fm-control exit: a busy agent receives interrupt delivery before the exit command
ok - fm-control exit: an idle agent goes straight to its exit command
ok - fm-control interrupt: unconfirmed delivery preserves observed busy state
ok - fm-control interrupt: muse confirms cancellation from its session log
ok - fm-control interrupt: postconditions are revalidated after acknowledgement polling
ok - fm-control exit: an interrupt-stopped agent satisfies the gone-state postcondition
ok - fm-control exit: a stubborn agent reports delivered input and an unconfirmed exit
ok - fm-control exit: pre-Enter refusals fail immediately
ok - fm-control interrupt: grok reports delivery without claiming cancellation
ok - fm-control interrupt: grok's idle footer does not confirm cancellation
ok - fm-control: a lifecycle command to a secondmate is unmarked and opens no reply expectation
ok - fm-control's arrival leaves fm-send's from-firstmate marking untouched
Evidence: Pending-reply durability tests

Source: Pending-reply durability tests

ok - pending request bodies are never scanned as records
ok - pending reply creation removes partial serialization artifacts
ok - pending reply creation falls back to sha256sum
ok - normal correlated reply resolves once (idempotent)
ok - completed turn with no report triggers exactly one recovery
ok - recovery transport releases and reacquires its correlation lock around delivery
ok - recovery validates durable bodies and preserves legacy summary fallback
ok - recovery exit 4 remains distinct, visible, and operator-settleable
ok - recovery attempts reconcile without reinjection
ok - recovery reply resolves the original expectation
ok - second missed turn escalates once and remains durable
ok - escalations and replies wake; the home's own escalation close stays quiet
ok - failed escalation publication remains retryable and publishes once
ok - legacy escalation closes under the shared default key
pending-reply: request body missing for fd373b99922e67f5
ok - legacy escalation cannot close an unrelated default-key decision
pending-reply: request body missing for 39bad4bbe33f2269
ok - foreign correlated blocker cannot impersonate a pending-reply escalation
ok - concurrent resolution closes one keyed escalation exactly once
ok - concurrent escalation yields to a late correlated reply
ok - transport success cannot masquerade as reply success
ok - undelivered records remain immutable across scan paths
ok - delivery confirmation fallback reconciles durably
ok - unrelated events and stale correlation ids cannot resolve
ok - restart preserves expectation and exact parent destination
ok - wrong-home reports are detected but do not silently acknowledge
ok - direct unmarked captain input creates no expectation
ok - fm-send marked secondmate path creates pending and embeds corr
ok - status-pointed document resolves the expectation
ok - optional helper report resolves without being required for correctness
ok - backend busy/idle observation covers Pi/Claude paths without conversation scrape
ok - tmux and zellij unknown states use bounded capture fallback
ok - pending replies scope Kimi capture fallback by recorded harness
ok - tick skips terminal records and reuses target observations
ok - correlations are reused only for matching open task records
ok - tick end-to-end: miss -> one recovery -> escalate -> durable
pending-reply: request body missing for 42b6ba96864e74b0
ok - typed-unproven records escalate once with exact suffix-aware settlement guidance
ok - typed-unproven supersedes only unconfirmed delivery uncertainty and stays settleable
ok - typed abandonment serializes with watcher escalation
ok - failed transport discards undelivered expectation only
ok - a remote repost waits for the reply channel and still fires on a real miss
ok - a mirrored correlated remote reply resolves without any repost
ok - all pending-reply tests passed
Evidence: Daemon typed-unproven retry containment tests

Source: Daemon typed-unproven retry containment tests

ok - fm-afk-start.sh fails before daemon startup when the afk flag cannot be written
ok - fm-afk-start.sh ignores stale pidfile-only live pids
ok - fm-afk-start.sh reclaims stale daemon locks whose live pid identity no longer matches
ok - supervise daemon state root is scoped by FM_HOME
ok - routine signal self-handles
ok - captain-relevant status verbs escalate
ok - check + unknown escalate; heartbeat self-handles
ok - transient stale self-handles and records a persistence marker
ok - enriched stale wedges bypass status absorption without disturbing busy workers
ok - stale + terminal status escalates immediately
ok - paused reasons with captain phrases remain pause-classified
ok - a captain-held transfer classifies as pause, not as a wedge candidate
ok - handle_wake on a paused stale records a pause marker, drops the wedge marker, and does not escalate
ok - handle_wake records a declared pause from a routine signal for long-cadence rechecks
ok - a terminal signal clears pause and stale tracking across both supervisors
ok - housekeeping migrates a normal-watcher's declared pause into daemon tracking
ok - housekeeping clears an already-resumed watcher pause across both supervisors
ok - housekeeping seeds pause tracking from status without a watcher marker
ok - persistent stale escalates after threshold and clears its marker
ok - resumed (busy) stale clears its marker without escalating
ok - housekeeping re-surfaces a stale declared pause on the long cadence and resets its window
ok - housekeeping re-surfaces a forgotten captain hold on the long cadence and resets its window
ok - housekeeping clears a paused marker whose pane became busy again, without escalating
ok - housekeeping clears a paused marker once the crew is no longer declaring the pause
ok - housekeeping clears the pause marker once a captain hold is answered
ok - housekeeping moves an existing stale marker to pause before wedge escalation
ok - housekeeping moves a captain hold's existing stale marker to pause before wedge escalation
ok - housekeeping clears tracking when a crew leaves pause
ok - persistent herdr stale resolves the target from metadata and escalates
ok - herdr idle busy-footer stale clears through capture corroboration
ok - resumed herdr stale clears through backend-aware busy state
ok - persistent Orca stale resolves the terminal from metadata
ok - multiple escalations flush as a single batched digest
ok - batch flush measures max-delay from the first append, not the last
ok - catch-all scan escalates a missed terminal once, not twice
ok - handle_wake routes routine->self and captain->escalate
ok - INJECT_SKIP forces self-handle, bypassing captain-relevant classification
ok - is_wake_reason distinguishes watcher wake reasons from singleton-status stdout
ok - terminal-stale escalate removes its marker so housekeeping does not re-escalate
ok - captain signal escalate marks seen so the catch-all scan does not re-fire
ok - _collapse_newlines replaces newlines with literal separator
ok - afk flag absent: daemon does not inject, buffer preserved
ok - busy-guard defers injection when supervisor pane is busy
ok - marker detection: marker -> stay afk, no marker -> exit afk
ok - /afk invocation is exempt from afk exit (no self-cancel)
ok - should_exit_afk returns false when afk is not active
ok - strip_injection_marker removes the sentinel marker cleanly
ok - pane_input_pending detects partial input on the cursor line
ok - pane_input_pending: a blank unidentified cursor row defers (strict container-proof rule)
ok - pane_input_pending: only proven empty agent prompts pass
ok - fm_tmux_composer_state: a bare shell prompt ($/%/#/>) reads unknown, never empty (dead-shell injection safety)
ok - fm_tmux_composer_state: a bordered composer box and bare agent glyphs (❯/›) still read empty
ok - fm_tmux_composer_state: only matching edge borders form a composer box
ok - pane_input_pending preserves bright placeholder-like drafts in styled captures
ok - classify_signal dedupes against the catch-all scan seen marker
ok - classify_stale dedupes against the signal path seen marker
ok - AFK nonterminal working:+merged keeps wedge aging and re-escalates at bound
ok - genuine done: and merge-check events still escalate
ok - pane_input_pending: an idle bordered composer is NOT pending (afk-invx-i5)
ok - pane_input_pending: text inside a bordered composer is still pending
ok - submit-ACK confirms a submit when the composer returns to a bordered-empty box
ok - submit-ACK reports pending on a persistently swallowed Enter (type-once)
ok - max-defer on an empty stuck pane types once, alarms, and preserves the buffer
ok - max-defer flushes and clears the buffer on an empty bordered pane
ok - max-defer on a pending composer alarms without typing
ok - normal flush clears a stale wedge marker
ok - below MAX_DEFER: no inject, no alarm, buffer preserved
ok - max-defer does not flush or alarm while afk is inactive
ok - library mode: sourcing the daemon defaults FM_WEDGE_ALARM_EXEC to discard (no test can fire a real notification)
ok - wake helpers replace inherited notifier overrides with the safe recorder
ok - the discard seam suppresses every notifier, including command: (fires nothing)
ok - direct notifier helpers honor the discard seam, including command:
ok - osascript channel routes through the notifier seam with the summary (never a real notification)
ok - herdr channel routes through the notifier seam with the summary (never a real notification)
ok - command channel runs the captain command with the summary on $1 and on stdin
ok - command channel failures redact configured commands while logging their exit status
ok - unknown channel directives are redacted while the alarm keeps running
ok - off disables every active alert regardless of directive position (marker and tmux flash are unaffected)
ok - auto resolves to the macOS osascript notifier on Darwin (default-on)
ok - auto on a non-macOS platform selects no built-in OS channel (the marker or a configured command carries it)
ok - config/wedge-alarm selects every configured channel and skips comment and blank lines
ok - a failing channel logs and falls back to the next channel, never crashing the alarm
ok - a hung notifier is bounded, logged, and falls through to the next channel
ok - a backgrounded command notifier remains bounded until its process group is reaped
ok - a hung notifier override is bounded, logged, and proceeds to the next channel
ok - daemon shutdown stops and reaps the active notifier process group
ok - inject_wedge_alarm writes the marker AND emits the active alert even with no tmux status-line (herdr backend)
ok - in-process wedge throttle prevents alert spam when the marker cannot persist
ok - fm-send returns 3 with a non-error no-resend warning when confirmation stays pending
ok - fm-send exits non-zero when initial text send fails
ok - fm-send exits non-zero unless delivery is proven empty
ok - discover_supervisor_backend: override > TMUX_PANE > HERDR_ENV+HERDR_PANE_ID > tmux fallback
ok - discover_supervisor_target: override > TMUX_PANE > herdr '<session>:<pane-id>' composition > firstmate:0 fallback
ok - pane_is_busy: herdr native busy_state='busy' short-circuits without a capture fallback
ok - primary busy guard isolates rendered signatures by detected harness
ok - pane_is_busy: omitted backend defaults to tmux for Grok's isolated fallback
ok - pane_input_pending: dispatches through fm_backend_composer_state for backend=herdr
ok - inject_msg: herdr busy-guard defers before ever attempting a submit
ok - inject_msg: herdr composer-guard defers before ever attempting a submit
ok - inject_msg: herdr pane-gone check defers before any busy/composer/submit call
ok - inject_msg: typed-unproven never retries even when wedge persistence fails
ok - inject_msg: dispatches busy-guard/composer-guard/submit through the herdr backend and succeeds on a confirmed empty composer
ok - inject_msg: defers on a dead-shell/unreadable composer (unknown), never typing the escalation into a shell
ok - inject_msg: unrecognized composer states defer by default
Evidence: Backend abstraction tests

Source: Backend abstraction tests

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: auto-detect selects herdr or cmux (loud notice) or tmux (silent, including nested tmux-in-herdr/tmux-in-cmux)
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_validate_spawn: all implemented lifecycle backends are spawn-supported
ok - fm_meta_get / fm_backend_of_meta: read key=value, 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
Evidence: cmux backend tests

Source: cmux backend tests

ok - fm_backend_cmux_version_check: accepts the verified minimum (0.64.17)
ok - fm_backend_cmux_version_check: accepts a newer version (0.70.0)
ok - fm_backend_cmux_version_check: refuses an old version loudly
ok - fm_backend_cmux_version_check: refuses loudly when cmux is not found on PATH or at the bundle path
ok - fm_backend_cmux_password: reads the first non-empty line of config/cmux-socket-password
ok - fm_backend_cmux_password: preserves spaces and tabs in config/cmux-socket-password
ok - fm_backend_cmux_password: respects FM_CONFIG_OVERRIDE
ok - fm_backend_cmux_password: empty when config/cmux-socket-password is absent
ok - fm_backend_cmux_cli: exports CMUX_SOCKET_PASSWORD when config/cmux-socket-password is set
ok - fm_backend_cmux_parse_target: splits '<workspace_uuid>:<surface_uuid>' on the first colon
ok - fm_backend_cmux_normalize_key: Enter/Escape/C-c map to cmux's verified enter/escape/ctrl-c
ok - fm_backend_cmux_scoped_title: scopes a primary task title with firstmate plus root hash
ok - fm_backend_cmux_scoped_title: scopes a secondmate task title with the home marker plus root hash
ok - fm_backend_cmux_scoped_title: includes the resolved FM_ROOT hash in the home label
ok - fm_backend_validate: cmux is a known backend
ok - fm_backend_busy_state: cmux (no native primitive) always reports unknown, same as tmux/zellij/orca
ok - fm_backend_composer_state: routes cmux to the cmux composer classifier
ok - fm_backend_cmux_ping_state: reports 'ok' on PONG
ok - fm_backend_cmux_ping_state: reports 'denied' when socketControlMode=cmuxOnly rejects the connection
ok - fm_backend_cmux_ping_state: reports 'unauth' when password mode rejects a missing/wrong password
ok - fm_backend_cmux_ping_state: reports 'unauth' when password mode rejects a wrong password (Invalid password)
ok - fm_backend_cmux_ping_state: reports 'down' when the app is not running yet
ok - fm_backend_cmux_ensure_running: returns immediately when cmux is already reachable
ok - fm_backend_cmux_ensure_running: fails fast on a denied socket without attempting to launch, naming every viable mode
ok - fm_backend_cmux_ensure_running: fails fast on an unauthenticated socket, naming the password config and the Automation mode alternative
ok - fm_backend_cmux_create_task: refuses a duplicate workspace title (cmux's own new-workspace has no uniqueness check)
ok - fm_backend_cmux_create_task: creates a workspace and parses workspace_id/surface_id from list responses
ok - fm_backend_cmux_target_ready: fails when the workspace/surface is not found (list-panes structural check)
ok - fm_backend_cmux_target_ready: verifies the workspace title against the expected label first
ok - fm_backend_cmux_target_ready: rejects a workspace id reused under a different title
ok - fm_backend_cmux_capture: fetches generously and trims to N lines locally
ok - fm_backend_cmux_capture: propagates a read-screen failure even when stdout is empty
ok - fm_backend_cmux_capture: fails when the target surface is absent
ok - fm_backend_cmux_send_key: normalizes the key (Escape -> escape) and targets the explicit workspace/surface
ok - fm_backend_cmux_send_key: recovers stale workspace/surface ids by expected label
ok - fm_backend_cmux_send_literal: calls send with an explicit workspace/surface and a -- separator
ok - fm_backend_cmux_send_text_line: clears partial input when Enter fails
ok - fm_backend_cmux_send_text_line: reports unsafe input when cleanup also fails
ok - fm_backend_cmux_current_path: actively probes with marked begin/end lines (zellij-shape frozen cwd)
ok - fm_backend_cmux_composer_state: a bare '❯' composer row reads empty
ok - fm_backend_cmux_composer_state: a borderless Claude '❯' composer row reads empty
ok - fm_backend_cmux_composer_state: a borderless Claude row outranks stale bordered scrollback
ok - fm_backend_cmux_composer_state: a borderless Claude '❯'+NBSP composer row reads empty under LC_ALL=C
ok - fm_backend_cmux_composer_state: plain-capture text after a bare glyph degrades to unknown (never false pending)
ok - fm_backend_cmux_composer_state: the ghost placeholder text reads empty, not pending
ok - fm_backend_cmux_composer_state: real composer text reads pending
ok - fm_backend_cmux_composer_state: a slash-command popup's argument-hint placeholder still reads pending (the incident fix)
ok - fm_backend_cmux_composer_state: reports unknown when the surface cannot be captured
ok - fm_backend_cmux_composer_state: reports unknown when no border-delimited composer row is found
ok - fm_backend_cmux_send_text_submit: reports 'empty' once the composer row reads empty after one Enter
ok - fm_backend_cmux_send_text_submit: reports 'pending' when the composer never clears after retried Enters (swallowed)
ok - fm_backend_cmux_send_text_submit: retries past a popup-placeholder-fill Enter and lands the real second Enter (the incident fix)
ok - fm_backend_cmux_send_text_submit: reports 'send-failed' when the target workspace/surface is absent
ok - fm_backend_cmux_window_of_workspace: walks windows and counts the membership-confirming workspace list
ok - fm_backend_cmux_window_of_workspace: echoes nothing when no window holds the workspace
ok - fm_backend_cmux_kill: closes the task workspace directly when it is not the last in its window
ok - fm_backend_cmux_kill: adds a throwaway sibling then closes the target when it is the last workspace in its window
ok - fm_backend_cmux_kill: never fails even when close-workspace fails
ok - fm_backend_cmux_kill: recovers stale workspace/surface ids by expected label
ok - fm_backend_cmux_list_live: lists only this home's scoped task workspaces using plain fm-<id> labels
ok - fm-spawn.sh: refuses backend=cmux for --secondmate spawns (mirrors Orca's refusal; no secondmate launch design exists yet)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 3 issues found → auto-fixed (2) ✅
  • 🚨 bin/fm-pending-reply-lib.sh:1022 - The required “full-payload durable recovery” is not lossless for requests ending in newlines. body=$(fm_pending_reply_request_body ...) strips all trailing newlines, and line 1103 performs another command substitution on the assembled recovery message. Thus a persisted request such as command\n\n is recovered as command without error. Preserve the body through result variables or another trailing-newline-safe boundary all the way into prepared_message.
  • 🚨 bin/fm-composer-lib.sh:1477 - The checkpoint accepts any occurrence of payload + first suffix character, although this can be only two characters (for example 2k). On an unverified backend whose backspace over-erases during the first N−1 keys, ordinary pane churn can add that short probe at the checkpoint and remove it after the final key while preserving the payload occurrence count; both erase verdicts then pass and Enter submits a damaged or empty composer. This exceeds the explicitly accepted residual limited to the final erase. Revisit whether the checkpoint needs stronger send-specific evidence or explicitly authorize this additional probabilistic containment.
  • ⚠️ bin/fm-send.sh:744 - Every typed-unproven result is reported as a pane capture failure, but the shared core also emits it after a successful capture when the delta proof fails, an erase key fails, or either erase proof fails. The preceding warning may name the real cause, leaving operators with contradictory diagnostics. Use a neutral “delivery or suffix removal could not be proven” summary while retaining the detailed transport warning.

🔧 Fix: Clarify unproven delivery and erase residuals
2 errors still open:

  • 🚨 bin/fm-composer-lib.sh:1241 - The whole-screen COUNT proof uses non-overlapping matches, so it can increase even when the actual occurrence count does not. For a 24-character anchor of all as, a before capture containing 25 as has two overlapping occurrences but reports 1; an unrelated redraw to 24*a + b + 24*a still has two actual occurrences, reports 2, and supplies an added row containing the anchor. The delta verdict therefore accepts although the payload was swallowed. Count overlapping occurrences at this shared boundary so the required occurrence increase is real.
  • 🚨 bin/fm-pending-reply-lib.sh:353 - The heredoc write that constructs the durable pending-reply record is unchecked. If it fails partway, such as under ENOSPC, execution still chmods and renames the truncated temporary file and returns a correlation id, allowing delivery to proceed without a valid durable recovery record. Check the write result and remove both temporary/body files before returning failure.

🔧 Fix: Harden occurrence counting and pending record writes
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-composer-lib.test.sh
  • bash tests/fm-send-strict.test.sh
  • bash tests/fm-send-remote-delivery.test.sh
  • bash tests/fm-control.test.sh
  • bash tests/fm-pending-reply.test.sh
  • bash tests/fm-daemon.test.sh
  • bash tests/fm-backend.test.sh
  • bash tests/fm-backend-cmux.test.sh
  • bash tests/fm-backend-herdr.test.sh
  • bash tests/fm-backend-orca.test.sh
  • bash tests/fm-backend-zellij.test.sh
  • Hermetic end-to-end invocation of ./bin/fm-send.sh demo &#39;2&#39; against an echoing tmux pane, verifying the random 23-character suffix was erased before Enter and the agent received exactly 2.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…omposer

A steer that the harness never took could be recorded as delivered. The
pre-Enter check that was supposed to catch that located the composer inside a
pane capture and read its contents - but the composer region is not a fact the
terminal carries. Locating it means inferring it from one vendor's current
drawing conventions, across harness x version x backend x capture fidelity x
cursor availability x pane width x transient overlays. Two successive attempts
to hold that inference spent twenty review rounds on narrow shape fixes without
converging, and one cosmetic keybinding-hint row under opencode's composer was
enough to make the region parser answer "no composer here" for a healthy steer.

Ask a different question. fm_composer_delivery_delta_verdict takes two whole
captures and the payload and asks only whether the payload became newly present
between them - never where the composer is. Acceptance needs both a novelty
test (the payload's normalized tail appears among the rows a diff reports as
added) and an occurrence-count test (its whole-screen count strictly rose).
They fail in different directions, so their conjunction can only move toward
refusal: a pane that redraws every row defeats novelty, and the count still
refuses. The earlier scrollback false pass - a stale copy of the payload
already on screen while the real send was swallowed - is excluded by time
rather than by location, so it cannot arise at all: a copy that was already
there is in the before capture, so it is neither an addition nor a new
occurrence. Location is a shape question; time is not.

Every adapter's obligation on the send path collapses to capture, type,
capture. The capability descriptor, cursor row, and identity probe are gone
from it, and with them the styled/plain fork that made the same real screen
accept under tmux and refuse under cmux, orca, and herdr's plain fallback.
zellij also loses its own private acceptance rule and now proves delivery the
way every other backend does; fm_composer_extract_selected_content had no
remaining caller and is deleted with it, while the shape engine it used stays
in place for the classifier.

There is deliberately no bypass. A harness whose composer cannot be proven is
refused rather than steered blind, and the existing exit-3 "unconfirmed" status
already covers the post-Enter side. The pre-Enter proof only gates pressing
Enter: the shape-aware post-Enter confirmation is untouched, so the two proofs
bracket the Enter and the pre-Enter half never has to be airtight alone.

Also kept from the abandoned attempt, because they were sound and separable:
durable full-payload pending-reply recovery with its per-correlation lock, and
the Claude launch-readiness gate. That gate is deliberately demoted - a modal
that swallows produces no delta, so the delivery proof refuses whatever the
gate scorer thinks. Its remaining jobs are refusing to type into a modal at all
(codex's numbered dialog treats typed digits as control input, and typing the
probe into it exited the pane), giving a specific diagnostic, and answering the
spawn-time question where there is no payload to diff against.

Two deviations from the approved design, both from evidence measured here:

- The anchor is capped at 24 normalized characters, not 48. A single-row
  composer that horizontally scrolls shows only a window of its buffer: the
  away-mode end-to-end reference leaves 33 characters visible, so 48 turns that
  healthy injection into a refusal (bisected: 32 passes, 34/37/48 fail). This
  does not trade safety for reach, because acceptance still requires the anchor
  to be both newly added and newly counted in the same instant it was typed.
- fm_composer_extract_selected_content is deleted rather than kept. The design
  kept it only because zellij still consumed it, and this change removes that
  consumer.

Verified live against every installed harness in isolated tmux servers: claude
2.1.239, codex 0.147.0, opencode 1.14.46, pi and pi-signed 0.84.1, grok 1.0.5,
muse 0.2.1, and cursor 2026.08.11 all accept a really-typed payload and refuse
an untyped pane on both capture fidelities. Cursor is covered for the first
time, since the proof reads no cursor. Grok passes the delivery proof in the
same run in which it fails the empty-composer shape matrix - a vendor drift
that predates this change and needs its own work item, and the sharpest
statement of what the new shape buys.

Portable regressions run everywhere from real captures checked in under
tests/fixtures/composer-delta/: eleven scenarios across eight harnesses,
asserted on plain and ANSI captures and required to agree, including three real
swallows. One of those - opencode's vendor update modal - is not recognised by
the gate scorer and is refused anyway, which is the recorded reason that scorer
must never be load-bearing for send safety.
The pre-Enter delivery proof needs 24 characters of anchor before an
appearance on screen means anything. Real steers often have fewer - `2`,
`y`, `approved`, `/exit` - and both ways out of that were wrong: refusing
them breaks ordinary fleet traffic including every lifecycle exit, and
giving them a weaker test reopens the false delivery the proof exists to
close, because a live agent pane redraws rows containing `2` constantly.

So neither happens. A short payload is now typed with a fresh random
suffix that lifts it to a full-length anchor, proven at that anchor, and
then the suffix alone is erased and the erase is proven. The agent still
receives exactly the payload the caller wrote, and what was proven to be
in the composer contains the payload's own characters.

The erase proof reads two counts. An incomplete erase can only leave
`<payload><suffix-prefix>` behind, so the nonce's first character still
sits immediately after the payload - that boundary string must not have
gained an occurrence. An over-erase eats the payload's tail instead, so
the payload's own count must be unchanged; the suffix shares no
character with the payload, which is what makes that count decidable.

Also:
- Drop the pre-type composer classification the short-payload attempt
  had reintroduced, and the environment-variable exemption that let a
  caller declare its own postcondition. A short exit command needs no
  exemption now that it carries a full-length anchor.
- Move the type-and-prove sequence into one shared owner every adapter
  calls with three primitives, replacing five copies of the ordering.
- Add the erase key per backend: BSpace on tmux and Backspace on zellij,
  both verified against a real pane; lowercase `backspace` on herdr and
  cmux. Orca's send API exposes no such key, so it refuses a short
  payload before typing rather than stranding a suffix it cannot remove.
- Give a stranded suffix a named warning carrying the exact text and
  suffix length, so a typed-unproven request can actually be settled.
The verification envelope only delivers the caller's exact payload if the
backend's erase key removes exactly one character per press, and that is
a vendor fact per backend: the key name belongs to each tool's own
vocabulary and the byte it resolves to belongs to the pane. A stub can
only confirm the spelling its author already assumed.

- Add a live guard to each backend's real smoke test, failing with the
  backend and version named.
- Record the dated results: tmux BSpace and zellij Backspace both remove
  exactly one character, verified against real panes. herdr and cmux
  carry the mapping but are not live-verified here, and the evidence
  table says so rather than implying they are. Orca has no such key at
  all, so it refuses a short payload before typing.
- Record that a raw backspace byte through the literal-text channel was
  measured and rejected: 0x08 erased nothing against a real tmux pane
  and 0x7f did, which is exactly the convention a named key delegates to
  the backend.
- Teach the shared fake-pane helper the erase key and a submitted log, so
  a stub models a real composer instead of the raw type log. A stub that
  ignores the erase still refuses, which stays the intended proof.
- Classify the composer-delta fixtures README, which landed unclassified
  earlier on this branch and failed the documentation audience check.
- Quote two probe-absent sentinel assignments that shellcheck reads as
  arithmetic once the shared library grows.
The lint step's own fixer wedged before it ran, so these are applied
directly. Each is fixed at the source rather than blanket-suppressed.

- Quote the hyphenated string sentinels shellcheck reads as arithmetic
  subtraction: the envelope's error sentinel and the pending-reply
  escalation kinds. Quoting is the real fix and removes an ambiguity a
  human reader shares.
- Annotate the three test stubs that shadow a function the code under
  test calls indirectly, which shellcheck cannot see. Narrow per-site
  directives with a reason, never a file-wide disable.
- Drop SIM_WIRE. It was assigned and never read: the assertions read the
  wire from its file instead, because a variable set inside the
  command-substitution subshell never reaches the caller. The file stays;
  only the dead variable goes.
@greptile-apps

greptile-apps Bot commented Aug 22, 2026

Copy link
Copy Markdown

Confidence Score: 4/5

The trailing-newline recovery defect should be fixed before merging because it violates the promised lossless durable payload recovery.

Automatic recovery captures the persisted request through shell command substitution, so valid trailing newline bytes are removed before the repost is sent.

Files Needing Attention: bin/fm-pending-reply-lib.sh, tests/fm-pending-reply.test.sh

Reviews (1): Last reviewed commit: "no-mistakes(document): Document unproven..." | Re-trigger Greptile

local rec=$1 corr body token msg
corr=$(fm_pending_reply_get "$rec" corr_id)
summary=$(fm_pending_reply_get "$rec" request_summary)
body=$(fm_pending_reply_request_body "$rec") || return 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Recovery strips trailing newlines

When a persisted request ends with newline characters, body=$(fm_pending_reply_request_body "$rec") removes them before constructing the recovery message, causing automatic recovery to repost a payload that differs from the original request.

@kunchenguid

Copy link
Copy Markdown
Owner Author

Superseded and dropped by captain decision 2026-08-22: the payload-delta/three-state composer-verification approach is being replaced wholesale by the inbox+doorbell steering design (data/fm-send-reliability-reframe-s1/report.md), which removes the composer-verification problem entirely rather than solving it. Not landing this branch. Follow-up implementation tracked as fm-send-inbox-doorbell-r1.

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