diff --git a/docs/herdr-backend.md b/docs/herdr-backend.md index 4c75fd8bc5..fce3cee031 100644 --- a/docs/herdr-backend.md +++ b/docs/herdr-backend.md @@ -226,6 +226,8 @@ The poll density bounds the residual possibility of an extremely fast complete t `pane read --lines N` can return empty output when N is below the viewport height. The capture owner requests at least 200 lines from Herdr and trims locally to the caller's bound. This generous floor is required for small composer and peek reads. +Alternative-screen harnesses, including Claude Code, expose no scrollback history to Herdr at all. +Any capture of such a pane is therefore bounded to the visible window regardless of the requested `--lines`. Herdr's native agent state can read idle while a harness waits on its own long foreground tool. The shared crew-state path therefore accepts a native `busy` as evidence of activity but never a native `idle` as evidence that a worker has stopped; the task's own semantic busy state (`bin/fm-busy-lib.sh`) decides that. diff --git a/tests/fm-backend-herdr-smoke.test.sh b/tests/fm-backend-herdr-smoke.test.sh index 98f1db2e97..3b1c2f1e23 100755 --- a/tests/fm-backend-herdr-smoke.test.sh +++ b/tests/fm-backend-herdr-smoke.test.sh @@ -285,6 +285,39 @@ case "$p" in esac pass "real herdr: current_path reads the pane's live cwd" +# --- viewport stability: a firstmate capture never moves the pane scroll ----- +# (fm-herdr-scroll-scout, section 8) Alternative-screen harnesses expose no +# scrollback history to Herdr, so every capture of an agent pane is bounded to +# the visible window. The one behavior of OUR code worth pinning is that a +# capture never MOVES the viewport: exactly like tmux capture-pane, `herdr +# pane read` must leave the pane's scroll untouched. This fails the moment a +# Herdr version or an adapter change introduces a capture that scrolls the +# view. Uses its own throwaway pane so the 500 lines of seeded history cannot +# interfere with the rest of the suite. +SCROLL_LABEL="fm-smoke-scroll" +SCROLL_IDS=$(fm_backend_herdr_create_task "$CONTAINER" "$SCROLL_LABEL" /tmp) || fail "could not create the viewport-stability scenario's tab" +read -r _SCROLL_TAB_ID SCROLL_PANE_ID </dev/null \ + || fail "capture failed during the viewport-stability check" +fm_backend_herdr_capture_ansi "$SCROLL_TARGET" 20 >/dev/null \ + || fail "capture_ansi failed during the viewport-stability check" +fm_backend_herdr_composer_state "$SCROLL_TARGET" >/dev/null +scroll_after=$(herdr pane get "$SCROLL_PANE_ID" --session "$SESSION" | jq -c '.result.pane.scroll') +[ "$scroll_before" = "$scroll_after" ] || fail "a firstmate capture moved the pane viewport: $scroll_before -> $scroll_after" +pass "real herdr: a firstmate capture never moves the pane viewport (no scroll for alternative-screen harnesses)" +fm_backend_herdr_kill "$SESSION:$SCROLL_PANE_ID" + # --- busy_state on a real claude harness (verified in herdr-verification-p2.md) --- if [ "${FM_HERDR_SMOKE_REAL_CLAUDE:-0}" = 1 ] && command -v claude >/dev/null 2>&1; then