Skip to content

fix: refuse away supervision without an operator pane handle - #5591

Open
coreldh wants to merge 7 commits into
kunchenguid:mainfrom
coreldh:fix/supervisor-target-refuse-fallback
Open

coreldh wants to merge 7 commits into
kunchenguid:mainfrom
coreldh:fix/supervisor-target-refuse-fallback

Conversation

@coreldh

@coreldh coreldh commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Intent

Fix defect A of #1506: away mode must not arm pane escalation against the constant firstmate:0 when no operator pane can be identified.

Today discover_supervisor_target returns the default firstmate:0 when there is no FM_SUPERVISOR_TARGET, no TMUX_PANE and no herdr pane, and the supervise daemon logs target_source=FALLBACK(firstmate:0) and arms anyway. On a machine where the primary runs outside tmux, that target is a crew or login shell, so escalations are deferred forever while away mode looks armed.

Following the triage ("identity-or-refuse is simpler than a new discovery heuristic"), this change refuses instead of guessing:

  • Discovery prints no target and returns 1 when no handle exists.
  • The daemon then exits 1 with target_source=UNAVAILABLE on stderr and in state/.supervise-daemon.log, releases its lock and pidfile, and never logs a successful start.
  • fm-afk-launch.sh start already refused on this path; its message now names the same verdict, and it appends the same refusal line to state/.supervise-daemon.log so the home keeps a durable record.
  • An explicit FM_SUPERVISOR_TARGET, TMUX_PANE and herdr pane resolution are unchanged. The launcher's existing guards still run first.

Out of scope, by design: no terminal-specific (Ghostty/TTY) identity resolver, no change to the composer guard (defect B), and no new independent alarm channel (defect C). Known limitation: on the native Claude/Grok start path, state/.afk is written before the daemon refuses; the turn-end guard then blocks the away turn loudly because no daemon owns supervision, so the failure is visible, not silent.

Supersedes #2217, which no longer rebases onto main and bundled the terminal-specific resolver.

What Changed

  • Supervisor target discovery now returns no target when there is no explicit override, tmux pane, or Herdr pane; the daemon refuses to arm with target_source=UNAVAILABLE and releases its lock and pidfile.
  • fm-afk-launch.sh start reports and logs the same refusal before launch. Explicit target, tmux, and Herdr pane resolution remain supported.
  • Update AFK and configuration guidance for the refusal and native Claude/Grok startup behavior, and add regression coverage for missing and valid pane handles.

Risk Assessment

✅ Low: Captain, low risk: the change adds a bounded refusal when no operator pane handle exists and preserves the explicit, tmux, and herdr target paths.

Testing

Prior focused daemon and launcher runs were reviewed; an initial private tmux socket path exceeded the socket pathname limit, so the live checks were rerun in the runbook’s short disposable lab path and demonstrated the no-handle refusals, guard order, explicit/tmux controls, and Herdr identity composition, while a successful Herdr-pane start remained untestable because lab preparation found no running default session; transcript saved as supervisor-target-live.txt.

  • Live validation: ✅ go - 6 of 7 scenarios driven live against the product
Scenario Result Live Evidence
Start the daemon without an operator handle while an unrelated firstmate:0 pane exists; it exits with target_source=UNAVAILABLE, releases its pidfile and lock, and never logs a successful start. ✅ pass live Live supervisor target validation transcript — daemon-no-handle-with-live-firstmate-decoy
Run launcher start before entering away mode; the required-record guard refuses before target discovery. ✅ pass live Live supervisor target validation transcript — launcher-existing-guard-runs-before-target-discovery
Enter away mode, then start without a pane handle; the refusal appears on stderr and in the durable log, while no active flag or terminal record is created. ✅ pass live Live supervisor target validation transcript — launcher-no-handle-refusal-is-durable
Start the daemon with an explicit target; it arms against firstmate:0 and releases its lock and pidfile on shutdown. ✅ pass live Live supervisor target validation transcript — explicit-target
Start the daemon with the real isolated pane ID in TMUX_PANE; it arms against that pane and releases its lock and pidfile on shutdown. ✅ pass live Live supervisor target validation transcript — tmux-pane-handle
Start with Herdr session and pane markers; the daemon checks the composed session:pane identity and refuses the nonexistent pane instead of guessing a fallback target. ✅ pass live Live supervisor target validation transcript — herdr-marker-composes-explicit-session-and-pane
Start the daemon from a real pane in a named Herdr lab session; it arms against that pane. ⏸️ untested no I tried bin/fm-herdr-lab.sh prepare fm-lab-supervisor-target with its state directory inside the worktree; it refused because exactly one running default session is required. The live daemon did com…
Evidence: Live supervisor target validation transcript

Source: Live supervisor target validation transcript

scenario=daemon-no-handle-with-live-firstmate-decoy
decoy-pane:
UNRELATED: decoy firstmate pane























daemon_exit=1
daemon_output:
error: away-mode pane escalation unavailable: no operator pane handle (target_source=UNAVAILABLE; no FM_SUPERVISOR_TARGET, TMUX_PANE, or HERDR_ENV+HERDR_PANE_ID); refusing to arm - set FM_SUPERVISOR_TARGET and FM_SUPERVISOR_BACKEND to firstmate's own pane
daemon_log:
[2026-09-29T09:03:15-0600] startup refused: away-mode pane escalation unavailable; target_source=UNAVAILABLE; backend_source=FALLBACK(tmux)
pidfile=absent lock=absent

scenario=launcher-existing-guard-runs-before-target-discovery
launcher_exit=1
fm-afk-launch: an away-posture record is required; run enter before starting the daemon

scenario=launcher-no-handle-refusal-is-durable
enter_exit=0
Away posture recorded at 2026-09-29T15:03:16Z: hold-for-return only. No phone channel is configured; anything that needs you waits for your return. Your away instructions are recorded verbatim; the away session will carry them out where it can, and anything it is unsure of, or that needs you, waits for your return. Destructive, irreversible, and security-sensitive actions are never pre-authorizable, whatever the words say. Expected return: not given. Spend cap: 4 concurrent workers.
Away posture (recorded):
  entered: 2026-09-29T15:03:16Z
  expected return: not given
  spend cap: 4 concurrent workers
  reach: hold-for-return only. No phone channel is configured; anything that needs you waits for your return.
  your words (verbatim):
    live no-handle refusal validation
launcher_exit=1
launcher_output:
fm-afk-launch: away-mode pane escalation unavailable: no operator pane handle (target_source=UNAVAILABLE; no FM_SUPERVISOR_TARGET, TMUX_PANE, or HERDR_ENV+HERDR_PANE_ID); refusing to launch the daemon
durable_log:
[2026-09-29T09:03:16-0600] startup refused: away-mode pane escalation unavailable; target_source=UNAVAILABLE; refused_by=fm-afk-launch start
contract=present afk_flag=absent terminal_record=absent

scenario=explicit-target
[2026-09-29T09:03:16-0600] daemon starting (pid 52776); target=firstmate:0; target_source=FM_SUPERVISOR_TARGET; backend=tmux; backend_source=FM_SUPERVISOR_BACKEND; afk=off; inject_skip='heartbeat'; stale_escalate=240s; batch=90s
[2026-09-29T09:03:16-0600] daemon shutting down

scenario=tmux-pane-handle
[2026-09-29T09:03:16-0600] daemon starting (pid 53164); target=%0; target_source=TMUX_PANE; backend=tmux; backend_source=TMUX_PANE; afk=off; inject_skip='heartbeat'; stale_escalate=240s; batch=90s
[2026-09-29T09:03:18-0600] daemon shutting down

scenario=herdr-marker-composes-explicit-session-and-pane
daemon_exit=1
output:
error: supervisor target 'fm-lab-absent-validation:w1:p9' does not resolve to a herdr pane; set FM_SUPERVISOR_TARGET
daemon_log:
[2026-09-29T09:03:18-0600] startup failed: target 'fm-lab-absent-validation:w1:p9' not found (backend=herdr)

cleanup=private-tmux-server-killed; lab-home-removed

scenario=herdr-lab-preflight
command=FM_HERDR_LAB_STATE_DIR=.validation-supervisor-target/herdr/current-state bin/fm-herdr-lab.sh prepare fm-lab-supervisor-target
fm-herdr-lab: fleet-state tripwire requires exactly one running default session
prepare_exit=1
- Outcome: 🔧 2 issues found → no changes applied ✅ across 2 runs (42m7s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

🔧 **Test** - 2 issues found → no changes applied ✅

🔧 No changes applied.
✅ Re-checked - no issues remain.

  • Live validation: ✅ go - 6 of 7 scenarios driven live against the product
Scenario Result Live Evidence
Start the daemon without an operator handle while an unrelated firstmate:0 pane exists; it exits with target_source=UNAVAILABLE, releases its pidfile and lock, and never logs a successful start. ✅ pass live Live supervisor target validation transcript — daemon-no-handle-with-live-firstmate-decoy
Run launcher start before entering away mode; the required-record guard refuses before target discovery. ✅ pass live Live supervisor target validation transcript — launcher-existing-guard-runs-before-target-discovery
Enter away mode, then start without a pane handle; the refusal appears on stderr and in the durable log, while no active flag or terminal record is created. ✅ pass live Live supervisor target validation transcript — launcher-no-handle-refusal-is-durable
Start the daemon with an explicit target; it arms against firstmate:0 and releases its lock and pidfile on shutdown. ✅ pass live Live supervisor target validation transcript — explicit-target
Start the daemon with the real isolated pane ID in TMUX_PANE; it arms against that pane and releases its lock and pidfile on shutdown. ✅ pass live Live supervisor target validation transcript — tmux-pane-handle
Start with Herdr session and pane markers; the daemon checks the composed session:pane identity and refuses the nonexistent pane instead of guessing a fallback target. ✅ pass live Live supervisor target validation transcript — herdr-marker-composes-explicit-session-and-pane
Start the daemon from a real pane in a named Herdr lab session; it arms against that pane. ⏸️ untested no I tried bin/fm-herdr-lab.sh prepare fm-lab-supervisor-target with its state directory inside the worktree; it refused because exactly one running default session is required. The live daemon did com…
  • bin/fm-lab-home.sh create "$LAB" and a private tmux server with an unrelated firstmate:0 pane
  • Live bin/fm-supervise-daemon.sh no-handle startup with TMUX, TMUX_PANE, Herdr markers, and target overrides unset
  • Live bin/fm-afk-launch.sh start before and after enter with all pane handles unset
  • Live daemon startup with FM_SUPERVISOR_TARGET=firstmate:0 and with the isolated pane ID in TMUX_PANE, followed by signal shutdown
  • Live daemon startup with HERDR_ENV=1, HERDR_SESSION=fm-lab-absent-validation, and HERDR_PANE_ID=w1:p9
  • bin/fm-herdr-lab.sh prepare fm-lab-supervisor-target (preflight refused because no running default session was available)
  • Reviewed prior focused runs recorded for tests/fm-daemon.test.sh and tests/fm-afk-launch.test.sh; the launcher run was repeated after an isolated tmux setup failure
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@coreldh
coreldh force-pushed the fix/supervisor-target-refuse-fallback branch from 6dc5c9f to 74653e1 Compare September 26, 2026 03:35
@coreldh coreldh changed the title fix: refuse away supervision without an operator pane fix: refuse away-mode escalation without an operator pane Sep 26, 2026
…andle

With no FM_SUPERVISOR_TARGET, no $TMUX_PANE, and no herdr pane, supervisor
target discovery printed the constant firstmate:0 and the away daemon armed
pane escalation against it whenever a pane of that name existed, which can be
an unrelated crew or login shell. Discovery now returns no target in that
case, and the daemon refuses to arm, naming target_source=UNAVAILABLE on
stderr and in its durable log, instead of reporting a guessed target. The
script-owned launcher's existing refusal names the same verdict.

Explicit override, tmux pane, and herdr pane resolution are unchanged.

Refs kunchenguid#1506 (defect A)
`bin/fm-afk-launch.sh start` refused without an operator pane handle only on
stderr, so a later look at the home could not tell that refusal from a launch
that was never attempted. It now appends the same `startup refused ...
target_source=UNAVAILABLE` line the daemon writes to state/.supervise-daemon.log,
keeping the stderr refusal.

Refs kunchenguid#1506 (defect A)
FM_SUPERVISOR_TARGET_DEFAULT is no longer printed by discovery, but the
daemon's inject and wedge-alarm helpers still read it after sourcing this
library as the unset fallback for FM_SUPERVISOR_TARGET. Full cross-file
ShellCheck therefore reported it as unused (SC2034). Mark it with the
repository's sourced-global directive instead of changing behavior.

Also restore the paragraph break before the unsupported-backend sentence
in docs/configuration.md.

Refs kunchenguid#1506 (defect A)
@coreldh
coreldh force-pushed the fix/supervisor-target-refuse-fallback branch from bc4b779 to 1ae07a1 Compare September 29, 2026 15:18
@coreldh coreldh changed the title fix: refuse away-mode escalation without an operator pane fix: refuse away supervision without an operator pane handle Sep 29, 2026
@greptile-apps

greptile-apps Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Away-mode daemon now refuses to start without an operator pane.

The PR appears safe to merge; no actionable issue was identified in the changed behavior.

Reviews (1) · Last reviewed commit: "no-mistakes(document): Document supervis..."

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