Skip to content

fix(bin): require a live away pusher before an away turn can end - #34

Merged
Freudator86 merged 6 commits into
mainfrom
fm/fm-turnend-guard-afk-honesty
Jul 28, 2026
Merged

fix(bin): require a live away pusher before an away turn can end#34
Freudator86 merged 6 commits into
mainfrom
fm/fm-turnend-guard-afk-honesty

Conversation

@Freudator86

Copy link
Copy Markdown
Owner

Intent

Fix the away-mode turn-end guard so the mere presence of state/.afk never proves wake delivery coverage. Require a live, identity-matched away pusher using the supervise-daemon pid and portable lock identity, mirroring the existing watcher-health shape. Keep scope to this guard only: do not change the away daemon, watcher, or delivery architecture. Regress both directions: a dead pusher with queued wakes must block turn end, while a healthy identity-matched pusher must permit it. Review max-defer deliberately and state in the pull request body what an operator sees if the daemon dies at 3am: the next attempted turn end forces one bounded maintenance continuation; the loop guard then permits that turn to stop, later independent turns alert again, queued wakes remain durable, and the daemon-owned max-defer alarm cannot run after the daemon dies, so there is no autonomous endless forced-continuation loop.

What Changed

  • bin/fm-turnend-guard.sh no longer treats the mere presence of state/.afk as proof of wake delivery. The away branch now calls a new fm_pusher_healthy in bin/fm-wake-lib.sh, which requires state/.supervise-daemon.pid to name a live pid that matches the portable lock's pid and recorded pid-identity — the same shape as the existing watcher-health predicate, and exactly the three files bin/fm-supervise-daemon.sh publishes at startup. Watcher health stays independently mandatory; the away daemon, watcher, and delivery architecture are untouched.
  • The guard also stays active when a durable wake is still unread (FM_SUP_QUEUE_PENDING) even with no state/*.meta record left, passes --queue-pending to the repair-line renderer, and branches its banner on away mode: away turns are told to restore away delivery and are never told to drain (draining would consume records the daemon's cursor has not read), while session turns keep the drain-and-restore wording. The away repair line in bin/fm-supervision-instructions.sh is now harness-aware — start-native plus FM_AFK_STATE_PREPARED=1 bin/fm-afk-start.sh for harnesses with a native tracked-background tool, the terminal-backed bin/fm-afk-launch.sh start otherwise, with an in-place warning that the stop rollback exits away mode by clearing state/.afk.
  • Regressions cover both directions: a dead away pusher with queued wakes blocks turn end and a healthy identity-matched pusher permits it (tests/fm-turnend-guard.test.sh), with tests/fm-daemon.test.sh pinning fm_pusher_healthy against the real daemon's published pidfile and lock rather than a fixture, and tests/fm-supervision-instructions.test.sh covering the harness-aware repair text. docs/turnend-guard.md, docs/architecture.md, and docs/scripts.md are updated to match.

Max-defer was reviewed deliberately: it is daemon-owned, so it cannot run once the daemon is dead and is not counted as substitute coverage. If the away daemon dies at 3am, the operator sees the next attempted turn end forced into one bounded maintenance continuation naming the daemon relaunch; the hook loop guard then permits that same turn to stop, so there is no autonomous endless forced-continuation loop. Later independent turns alert again until delivery is repaired, and queued wakes stay durable in state/.wake-queue throughout.

Risk Assessment

✅ Low: The final commit is a two-branch wording fix plus strengthened regressions that closes the last open finding, leaving a tightly scoped change whose every path I traced against the away no-drain contract with no remaining inconsistencies.

Testing

Ran the three targeted suites covering the guard, the away repair-line renderer, and the daemon predicate (all pass, including the new both-directions regressions), then drove the real end-user path: a live bin/fm-supervise-daemon.sh away pusher in a hermetic away home, with the Stop hook invoked exactly as a harness invokes it. The captured terminal transcript shows a silent, permitted turn end while the pusher is alive; after the daemon is SIGKILLed (stale pidfile and state/.afk deliberately left behind) the next turn end is blocked with the away-specific banner and the concrete daemon-relaunch repair; the same turn's stop_hook_active=true retry is permitted, so there is no endless forced-continuation loop; a later turn alerts again; the queued wake is unchanged throughout; and the daemon process is confirmed gone, so its max-defer alarm cannot substitute for coverage. No browser screenshot was possible (no Chrome executable in this environment), but the end-user surface here is a terminal banner, captured verbatim as a transcript and also rendered as a styled HTML terminal view for reviewers.

Evidence: Operator terminal transcript: away guard with a real daemon killed at 3am

=== 3. Turn end while the away pusher is ALIVE (healthy, identity-matched) === exit status : 0 (0 = turn allowed to end) hook output : <silent> === 4. 3am: the away daemon dies (SIGKILL, no cleanup - pidfile and lock stay behind) === pidfile still on disk : 1293844 (pid is dead) state/.afk still present : yes === 5. Next attempted turn end -> ONE forced maintenance continuation === exit status : 2 (2 = turn blocked) ●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ● TURN WOULD END BLIND - SUPERVISION IS INCOMPLETE ● Away wake delivery missing: no live identity-matched pusher holds the supervise-daemon lock. ● Delivery repair: Away mode owns wake delivery and no live identity-matched away daemon is reading the durable queue: prepare the lifecycle with bin/fm-afk-launch.sh start-native, then run FM_AFK_STATE_PREPARED=1 bin/fm-afk-start.sh as its own Claude Code background task (never shell &); if that native launch fails, roll the preparation back with bin/fm-afk-launch.sh stop, which EXITS away mode by clearing state/.afk and therefore must be followed immediately by a fresh away entry so the captain is not left unattended without it. Then confirm state/.supervise-daemon.pid names a live pid matching the daemon lock identity; do not arm a session delivery wait instead. ● This forced continuation is internal maintenance; after restoring away delivery, end silently unless a queued wake is captain-relevant under AGENTS.md section 9. ●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ === 6. Same turn, the forced continuation tries to stop again (stop_hook_active=true) === exit status : 0 (0 = loop guard permits this turn to stop; no endless loop) hook output : <silent> === 7. A later, independent turn alerts again until delivery is repaired === exit status : 2 (2 = alerts again) banner headline : ● Away wake delivery missing: no live identity-matched pusher holds the supervise-daemon lock. === 8. Queued wake is still durable after all of it === 1^I1^Isignal^Itask1.status^Idone: refactor landed === 9. The max-defer alarm is daemon-owned, so it cannot run after the daemon dies === away daemon pid 1293844: gone - its loop, and the max-defer alarm inside it, are not running


=== 1. Captain goes away: state/.afk set, real away daemon started ===
away daemon pid        : 1293844
state/.supervise-daemon.pid          -> 1293844
state/.supervise-daemon.lock/pid     -> 1293844
state/.supervise-daemon.lock/pid-identity -> linux-starttime=200909007 cmdline-hex=62617368002f686f6d652f636f646974616e2f2e63616368652f6e6f2d6d697374616b65732d746d702f6177617933616d2e5a6c7236744a2f686f6d652f62696e2f666d2d7375706572766973652d6461656d6f6e2e736800

=== 2. Crewmate work in flight and a terminal wake lands in the durable queue ===
1^I1^Isignal^Itask1.status^Idone: refactor landed

=== 3. Turn end while the away pusher is ALIVE (healthy, identity-matched) ===
exit status : 0   (0 = turn allowed to end)
hook output : <silent>

=== 4. 3am: the away daemon dies (SIGKILL, no cleanup - pidfile and lock stay behind) ===
pidfile still on disk : 1293844  (pid is dead)
state/.afk still present : yes

=== 5. Next attempted turn end -> ONE forced maintenance continuation ===
exit status : 2   (2 = turn blocked)
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  TURN WOULD END BLIND - SUPERVISION IS INCOMPLETE
●  Away wake delivery missing: no live identity-matched pusher holds the supervise-daemon lock.
●  Delivery repair: Away mode owns wake delivery and no live identity-matched away daemon is reading the durable queue: prepare the lifecycle with bin/fm-afk-launch.sh start-native, then run FM_AFK_STATE_PREPARED=1 bin/fm-afk-start.sh as its own Claude Code background task (never shell &); if that native launch fails, roll the preparation back with bin/fm-afk-launch.sh stop, which EXITS away mode by clearing state/.afk and therefore must be followed immediately by a fresh away entry so the captain is not left unattended without it. Then confirm state/.supervise-daemon.pid names a live pid matching the daemon lock identity; do not arm a session delivery wait instead.
●  This forced continuation is internal maintenance; after restoring away delivery, end silently unless a queued wake is captain-relevant under AGENTS.md section 9.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

=== 6. Same turn, the forced continuation tries to stop again (stop_hook_active=true) ===
exit status : 0   (0 = loop guard permits this turn to stop; no endless loop)
hook output : <silent>

=== 7. A later, independent turn alerts again until delivery is repaired ===
exit status : 2   (2 = alerts again)
banner headline : ●  Away wake delivery missing: no live identity-matched pusher holds the supervise-daemon lock.

=== 8. Queued wake is still durable after all of it ===
1^I1^Isignal^Itask1.status^Idone: refactor landed

=== 9. The max-defer alarm is daemon-owned, so it cannot run after the daemon dies ===
away daemon pid 1293844: gone - its loop, and the max-defer alarm inside it, are not running
957:  max_defer=${FM_MAX_DEFER_SECS:-$MAX_DEFER_SECS_DEFAULT}
(both call sites live inside the daemon's own loop; killing the daemon kills the alarm)
Evidence: Rendered terminal view of the same scenario (HTML)
<!doctype html>
<meta charset="utf-8"><title>Away turn-end guard - operator terminal</title>
<style>
 body { background:#14161a; color:#d7dae0; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:13px; margin:0; padding:24px; }
 h1 { font-size:15px; color:#9aa4b2; font-weight:600; margin:0 0 16px; }
 .term { background:#0d0f12; border:1px solid #2a2f38; border-radius:8px; padding:18px 20px; white-space:pre-wrap; overflow-wrap:anywhere; }
 .step { color:#7cc4ff; font-weight:700; margin:14px 0 4px; }
 .banner { color:#ffb454; }
 .blocked { color:#ff6b6b; font-weight:700; }
 .allowed { color:#7ee787; font-weight:700; }
</style>
<h1>bin/fm-turnend-guard.sh — away mode, real fm-supervise-daemon.sh pusher, daemon killed mid-flight</h1>
<div class="term"><div>&nbsp;</div>
<div class="step">=== 1. Captain goes away: state/.afk set, real away daemon started ===</div>
<div>away daemon pid        : 1293844</div>
<div>state/.supervise-daemon.pid          -&gt; 1293844</div>
<div>state/.supervise-daemon.lock/pid     -&gt; 1293844</div>
<div>state/.supervise-daemon.lock/pid-identity -&gt; linux-starttime=200909007 cmdline-hex=62617368002f686f6d652f636f646974616e2f2e63616368652f6e6f2d6d697374616b65732d746d702f6177617933616d2e5a6c7236744a2f686f6d652f62696e2f666d2d7375706572766973652d6461656d6f6e2e736800</div>
<div>&nbsp;</div>
<div class="step">=== 2. Crewmate work in flight and a terminal wake lands in the durable queue ===</div>
<div>1^I1^Isignal^Itask1.status^Idone: refactor landed</div>
<div>&nbsp;</div>
<div class="step">=== 3. Turn end while the away pusher is ALIVE (healthy, identity-matched) ===</div>
<div class="allowed">exit status : 0   (0 = turn allowed to end)</div>
<div>hook output : &lt;silent&gt;</div>
<div>&nbsp;</div>
<div class="step">=== 4. 3am: the away daemon dies (SIGKILL, no cleanup - pidfile and lock stay behind) ===</div>
<div>pidfile still on disk : 1293844  (pid is dead)</div>
<div>state/.afk still present : yes</div>
<div>&nbsp;</div>
<div class="step">=== 5. Next attempted turn end -&gt; ONE forced maintenance continuation ===</div>
<div class="blocked">exit status : 2   (2 = turn blocked)</div>
<div class="banner">●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</div>
<div class="banner">●  TURN WOULD END BLIND - SUPERVISION IS INCOMPLETE</div>
<div class="banner">●  Away wake delivery missing: no live identity-matched pusher holds the supervise-daemon lock.</div>
<div class="banner">●  Delivery repair: Away mode owns wake delivery and no live identity-matched away daemon is reading the durable queue: prepare the lifecycle with bin/fm-afk-launch.sh start-native, then run FM_AFK_STATE_PREPARED=1 bin/fm-afk-start.sh as its own Claude Code background task (never shell &amp;); if that native launch fails, roll the preparation back with bin/fm-afk-launch.sh stop, which EXITS away mode by clearing state/.afk and therefore must be followed immediately by a fresh away entry so the captain is not left unattended without it. Then confirm state/.supervise-daemon.pid names a live pid matching the daemon lock identity; do not arm a session delivery wait instead.</div>
<div class="banner">●  This forced continuation is internal maintenance; after restoring away delivery, end silently unless a queued wake is captain-relevant under AGENTS.md section 9.</div>
<div class="banner">●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</div>
<div>&nbsp;</div>
<div class="step">=== 6. Same turn, the forced continuation tries to stop again (stop_hook_active=true) ===</div>
<div class="allowed">exit status : 0   (0 = loop guard permits this turn to stop; no endless loop)</div>
<div>hook output : &lt;silent&gt;</div>
<div>&nbsp;</div>
<div class="step">=== 7. A later, independent turn alerts again until delivery is repaired ===</div>
<div class="blocked">exit status : 2   (2 = alerts again)</div>
<div>banner headline : ●  Away wake delivery missing: no live identity-matched pusher holds the supervise-daemon lock.</div>
<div>&nbsp;</div>
<div class="step">=== 8. Queued wake is still durable after all of it ===</div>
<div>1^I1^Isignal^Itask1.status^Idone: refactor landed</div>
<div>&nbsp;</div>
<div class="step">=== 9. The max-defer alarm is daemon-owned, so it cannot run after the daemon dies ===</div>
<div>away daemon pid 1293844: gone - its loop, and the max-defer alarm inside it, are not running</div>
<div>957:  max_defer=${FM_MAX_DEFER_SECS:-$MAX_DEFER_SECS_DEFAULT}</div>
<div>(both call sites live inside the daemon&#x27;s own loop; killing the daemon kills the alarm)</div></div>

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 4 issues found → auto-fixed (4) ✅
  • ⚠️ tests/fm-turnend-guard.test.sh:277 - Both new away-mode cases assert only against hand-written fixtures (record_pusher_lock writes .supervise-daemon.pid, .lock/pid, and .lock/pid-identity itself), so nothing pins the predicate to what bin/fm-supervise-daemon.sh actually publishes. The guard now fails closed on three separate daemon-internal writes: echo &#34;$$&#34; &gt; &#34;$PIDFILE&#34; (fm-supervise-daemon.sh:1314, unchecked) and the best-effort fm_pid_identity ... &gt; &#34;$LOCK/pid-identity&#34; (:1315, || true, which leaves an EMPTY file when identity resolution fails — and fm_pusher_lock_matches_pid rejects an empty identity). If any of those drift or fail, a perfectly healthy away daemon reads as dead and every away turn end forces a maintenance continuation, while fm-afk-start.sh keeps reporting "daemon already running" (its daemon_pid_matches tolerates an empty identity via the ps-command fallback), so the printed repair is unsatisfiable. tests/fm-daemon.test.sh already starts the real daemon through bin/fm-afk-start.sh far enough to publish the pidfile+lock; add an assertion there (or in the lifecycle e2e) that fm_pusher_healthy returns 0 against that real published state.
  • ℹ️ bin/fm-guard.sh:176 - The pull-based guard still does [ -e &#34;$STATE/.afk&#34; ] &amp;&amp; delivery_armed=true — the exact false-coverage assumption this change removes from the turn-end guard. With a dead away pusher the two guards now disagree: fm-guard.sh stays silent about missing delivery (and even passes --afk 0 to the repair-line renderer at :229) while fm-turnend-guard.sh blocks the turn. The stated intent scopes this change to the turn-end guard only, so this is flagged as a deliberate follow-up rather than something to fix here — confirm whether fm-guard.sh should adopt fm_pusher_healthy in a separate change.
  • ℹ️ bin/fm-turnend-guard.sh:86 - The new away-pusher check sits behind the unchanged in-flight gate ([ &#34;$FM_SUP_IN_FLIGHT&#34; -gt 0 ] || exit 0), so "a dead pusher with queued wakes blocks turn end" only holds while state/*.meta records exist. A dead daemon plus a non-empty state/.wake-queue but zero in-flight tasks (e.g. terminal wakes queued after the metas were cleaned up) still ends the turn silently, even though fm_supervision_status already computes FM_SUP_QUEUE_PENDING. Low impact since nothing is being supervised at that point, but worth confirming this matches the intended contract.
  • ℹ️ bin/fm-turnend-guard.sh:120 - This change makes the away branch of fm-supervision-instructions.sh --repair-line reachable for the first time (previously delivery_armed was unconditionally 1 under .afk, and fm-guard.sh always passes --afk 0). That line reads "Away mode owns wake delivery; load /afk and ensure the daemon is reading the durable queue instead of arming a session delivery wait" — it never names a concrete restart (bin/fm-afk-launch.sh start), unlike every per-harness branch which names an exact command, and its "instead of arming a session delivery wait" phrasing describes the old contract rather than the dead-daemon case the agent is actually facing at 3am.

🔧 Fix: activate turn-end guard on queued wakes; name away restart
4 issues (1 warning, 3 infos) still open:

  • ⚠️ bin/fm-supervision-instructions.sh:113 - The new away repair line tells every harness to "restart it with bin/fm-afk-launch.sh start", but .agents/skills/afk/SKILL.md reserves that path for a harness WITHOUT a native background tool (e.g. pi): a harness WITH one (claude, codex background bash, grok — which includes the harness the rest of this guard's repair lines target) must instead run bin/fm-afk-launch.sh start-native followed by FM_AFK_STATE_PREPARED=1 bin/fm-afk-start.sh through that native tool, described there as "a deliberate no-separate-terminal exception". Following the printed line on a claude primary manufactures the detached tmux session / non-visible herdr workspace the native path exists to avoid, and it fails outright when the primary is not running under a discoverable tmux/herdr pane (fm_afk_launch_start aborts on could not resolve the captain supervisor pane). Unlike every other repair branch, the away branch returns before the per-harness case &#34;$HARNESS&#34; block, so it cannot be harness-aware. Flagged as ask-user because the captain's round-1 decision named this exact command — confirm whether the line should branch on $HARNESS (start-native + fm-afk-start.sh for native-background harnesses, fm-afk-launch.sh start otherwise) or stay as one command.
  • ℹ️ bin/fm-turnend-guard.sh:106 - protected_desc only reaches the daemon-down branch (:117), and the guard still calls the repair-line renderer without --queue-pending. In the activation path this commit adds — zero state/*.meta, queue pending, watcher healthy, no armed stub — the banner prints only "Wake delivery missing: no identity-matched delivery stub is armed for this session" plus the plain re-arm line, so nothing tells the agent that a queued wake is what kept the guard active. bin/fm-guard.sh:229-232 already passes --queue-pending &#34;$queue_arg&#34; to the same renderer, which prefixes "After draining queued wakes, ". Outcome is still correct (arming the stub does deliver the pending records), so this is a messaging gap rather than a wedge.
  • ℹ️ bin/fm-turnend-guard.sh:88 - Consequence worth recording: the away daemon reads state/.wake-queue through its own cursor and never drains it (docs/architecture.md, .agents/skills/afk/SKILL.md step 3), and bin/fm-watch.sh:1450 enqueues a heartbeat record every cadence while .afk is present. So FM_SUP_QUEUE_PENDING is effectively true for the whole away session and stays true until a post-return fm-wake-drain.sh/fm-afk-return.sh, which means the guard is now active at every away turn end regardless of in-flight metas — activation is no longer a proxy for "work in flight". This is benign (a healthy watcher plus a healthy pusher still exits 0, and the queue is emptied by the documented return flow), but docs/turnend-guard.md:30 describes the queue trigger generically without noting that away homes sit permanently in that state.
  • ℹ️ tests/fm-daemon.test.sh:123 - The new real-daemon case ends with kill -TERM &#34;$pid&#34; followed by a bare wait &#34;$pid&#34;. If the daemon misses or ignores the signal (its cleanup trap runs escalate_flush and the wedge-alarm notifier stop before exiting), the suite hangs indefinitely instead of failing, and the dead_rc assertion never runs. tests/wake-helpers.sh already provides the bounded wait_for_exit &#34;$pid&#34; 50 used elsewhere in this suite; use it here.

🔧 Fix: make away repair harness-aware; name drain; bound daemon wait
1 info still open:

  • ℹ️ bin/fm-supervision-instructions.sh:116 - The claude/grok away repair ends with "rolling the preparation back with bin/fm-afk-launch.sh stop if that native launch fails". .agents/skills/afk/SKILL.md states that rollback for a fresh ENTRY into away mode, where reverting to not-away is the right outcome; this line is printed in the opposite context — the daemon died mid-away-session and the captain is still gone. fm_afk_launch_stop clears state/.afk as its last step, so following the fallback silently exits away mode at 3am: the guard's away branch stops applying, the session falls back to arming a normal delivery stub, and the batched-escalation contract the captain asked for is abandoned with nothing instructing a re-entry. The failure branch is reachable — fm_afk_launch_start_native returns 1 when fm_afk_launch_reconcile cannot confirm teardown of a previous herdr/tmux daemon terminal, which is a plausible state after an unclean daemon death. Consider qualifying the clause (note that stop exits away mode and must be followed by a fresh away entry) or dropping it from the recovery-context line.

🔧 Fix: warn that away rollback exits away mode
1 warning still open:

  • ⚠️ bin/fm-turnend-guard.sh:134 - The banner's unconditional closing line — reworded by this branch — reads "after draining and restoring delivery, end silently", and it is printed in away mode too, directly under the away repair line that just told the session the daemon owns the durable queue. That contradicts the contract this branch establishes: docs/turnend-guard.md states "the away daemon reads the durable queue through its own cursor and the session must not drain it", tests/fm-turnend-guard.test.sh asserts on the whole hook output that the away path never tells the session to drain, and bin/fm-afk-return.sh owns the drain as a return-time action. The regression only passes on a substring technicality: it matches "After draining queued wakes, ", not this line's "after draining". An away agent that follows the closing line at 3am runs fm-wake-drain.sh, which empties state/.wake-queue and removes records the daemon's seq cursor has not read yet — losing exactly the away wakes this guard exists to protect. Concrete fix: branch the line on $afk (away: "after restoring away delivery, end silently…"; otherwise keep today's wording). No test pins the current phrasing, so the change is contained.

🔧 Fix: stop telling away turns to drain the queue
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-turnend-guard.test.sh (includes new cases: dead away pusher with queued wakes blocks, queued wakes with no state/*.meta keep the guard active, session-mode queued-wake repair names the drain, healthy identity-matched pusher permits the turn)
  • bash tests/fm-supervision-instructions.test.sh (harness-aware away repair line, native vs terminal launcher paths, stop exits-away-mode caveat, no drain wording under --queue-pending 1)
  • bash tests/fm-daemon.test.sh (97 cases, including test_pusher_healthy_accepts_real_daemon_publication pinning fm_pusher_healthy to the daemon's real pidfile/lock publication)
  • Manual end-to-end operator scenario: hermetic away home with the shipped bin/, real bin/fm-supervise-daemon.sh started as the away pusher, live identity-matched .watch.lock watcher holder, real record in state/.wake-queue, then printf &#39;{&#34;stop_hook_active&#34;:false}&#39; | FM_HOME=&lt;home&gt; bash bin/fm-turnend-guard.sh invoked as a harness Stop hook — before the kill (exit 0, silent), after kill -9 of the daemon (exit 2 + away banner), with stop_hook_active=true in the same turn (exit 0), and on a later independent turn (exit 2 again)
  • Verified queue durability by diffing state/.wake-queue content before and after all guard invocations, and confirmed the daemon pid is gone so the daemon-owned max-defer alarm cannot run
⚠️ **Document** - 2 infos
  • ℹ️ docs/turnend-guard.md:48 - docs/turnend-guard.md:48-50 restates the /afk skill's harness-specific relaunch procedure verbatim (start-native, FM_AFK_STATE_PREPARED=1 fm-afk-start.sh, fm-afk-launch.sh stop rollback, terminal-backed start). The /afk skill (.agents/skills/afk/SKILL.md) is the owner of that procedure, so the two copies can drift if the away entry path changes. I left the copy in place because it documents what the guard's own repair line emits, which is guard-owned behavior and the explicit point of this change; reducing it to a pointer is a judgment call for the author.
  • ℹ️ docs/watcher-continuity.md:28 - docs/watcher-continuity.md:28 says "The existing turn-end guard implementation and adapters are unchanged." That sentence is a scoped claim about the watcher-continuity change (adapters are indeed still unchanged), but read as current state it now conflicts with this change touching bin/fm-turnend-guard.sh. I did not edit it, since rewriting another change's narrative is outside this change's scope; if the author wants it unambiguous, it should say "unchanged by the continuity work".
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@Freudator86
Freudator86 merged commit ce8309f into main Jul 28, 2026
10 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.

1 participant