Skip to content

fix(afk): deliver away-mode escalations through Telegram - #5

Merged
morris2spears merged 31 commits into
mainfrom
fm/firstmate-afk-telegram-escalations
Jul 30, 2026
Merged

fix(afk): deliver away-mode escalations through Telegram#5
morris2spears merged 31 commits into
mainfrom
fm/firstmate-afk-telegram-escalations

Conversation

@morris2spears

Copy link
Copy Markdown
Owner

Intent

The developer's goal was to fix a real delivery defect in Firstmate's away mode: while the captain is away, escalations the daemon surfaces (decisions, blockers, review-ready notices) were not reaching them on Telegram. They required the fix to extend the existing single Telegram relay owner rather than adding a second bot, poller, sender, or credential store; preserve all approval boundaries so a Telegram message never authorizes merges or privileged actions; deliver each escalation exactly once with no duplication between Telegram and chat; keep delivery status observable and durable without leaking tokens, chat IDs, or message contents; and fall back safely with a visible failure instead of claiming delivery. Validation was to run through the no-mistakes pipeline, driven gate by gate without --yes, with a PR opened against morris2spears/firstmate main after rebasing there, and no merging. Across repeated review rounds the developer refused narrow patches for the recurring duplicate-delivery and evidence-loss themes, first approving a single-owner ledger transaction that atomically snapshots/restores/retires buffer, digest, receipt, and rollback evidence, then on round 5 directing that the mechanism be replaced entirely: immutable versioned batch directories with one atomic active-batch pointer switch performed only before daemon start, with no in-place merging or adoption into live paths.

What Changed

  • Away-mode escalations surfaced by fm-supervise-daemon.sh (decisions, blockers, review-ready, stale/wedge and pause re-surface notices) are now relayed to Telegram through the existing single fm-tg-lib.sh relay owner, with in-session chat delivery deduplicated against a persisted confirmed-prefix receipt so each line is delivered exactly once and an unconfirmed send fails visibly instead of being reported as delivered.
  • New bin/fm-away-ledger-lib.sh holds the away batch state as an immutable versioned store: each lifecycle transition stages a complete successor version, validates it, and switches a single active pointer atomically under an owner lock, refusing to activate while a live daemon holds the lock; fm-afk-launch.sh, fm-afk-start.sh and fm-afk-return.sh publish a version before clearing live artifacts, fold retained versions into the return catch-up, and retire them only once the catch-up gate is acknowledged.
  • Bearings skill made chat-only by default, plus docs updates (README.md, docs/configuration.md, docs/architecture.md, docs/scripts.md, AGENTS.md, .agents/skills/) for the away-mode delivery state and env seams, and new/extended shell test suites (fm-tg-mode, fm-afk-launch, fm-afk-return, fm-daemon, fm-bearings-skill) covering the version store, pointer switch, exactly-once delivery and failure classes.

Risk Assessment

✅ Low: The round-7 finding is fixed at exactly the recommended owner boundary - a validated active-version identity that the store can vouch for when the ledger payload cannot be parsed - in 38 lines with a regression test that reproduces the reported sequence, and I could not substantiate any new reachable defect: the v- prefix cannot collide with a minted ledger id, the read-side match still refuses a receipt naming a different batch, an absent or dangling pointer fails closed with an honest log, and the unlocked pointer read can only degrade to that same honest refusal.

Testing

I ran the targeted away-mode/Telegram suites (fm-tg-mode's 136 cases, fm-afk-launch, fm-afk-return, fm-daemon, fm-bearings-skill - all pass) and then drove the feature end-to-end the way a captain experiences it: away flag on, three escalations (decision, blocker, review-ready) buffered, flushed through the real ledger owner and version store with only the outbound phone client and the captain-pane inject replaced by recorders. The phone received one message carrying all three events plus the approval-boundary notice; captain chat received only a text-free receipt naming the private digest, not the alert body; with the phone client removed the next escalation produced a visible "Telegram delivery unavailable; the captain was not contacted there" chat fallback and no send; after restoring the client only the new line went to the phone, and counted per-event occurrences show each escalation reached exactly one surface exactly once. Durable delivery status files contained only result class plus epoch (no text, token, or chat id), away dirs were 0700 and digests 0600, the store held immutable v.* versions behind one active pointer, and the real bin/fm-afk-return.sh catch-up listed every away escalation exactly once before retiring the store to empty. This change is shell scripts, skill markdown, and docs with no rendered UI surface, so the end-user surfaces are the Telegram message text and captain-chat text, which I captured verbatim instead of as screenshots. The only failure seen was one pre-existing fm-bearings-snapshot case that fails identically at the base commit and exercises code this change does not touch; the worktree was left clean.

Evidence: Away-mode Telegram escalation end-to-end transcript

### 3. batch flushes -> phone receives it === PHONE MESSAGE === Firstmate needs you: needs-decision: approve the privileged prod cutover for fm-task-3 | blocked: fm-task-7 needs a rotated deploy credential | review-ready: fm-task-9 has a green PR awaiting the captain Notice only. This message does not approve a merge, privileged change, destructive action, or security-sensitive action. ### 4. what the captain sees in session (no duplicate of the alert body) Telegram accepted away-mode alert 1785404629-ab351e0865099728-a0-0-6b7a8d517884deb0 (3 event(s)). Reconcile the durable fleet records now using the private away digests state/tg-away-digest/1785404629-ab351e0865099728-*.items; do not repeat this alert in captain chat. ### 5/6. durable evidence -rw------- tg-away-delivery/<id>.status accepted 1785404630 every delivery status is exactly <result class> <epoch> - no text, no token, no chat id drwx------ tg-away-delivery / tg-away-digest / tg-away-versions ### 7. phone client GONE -> visible failure, no false claim Supervisor escalate (1 event(s)): blocked: fm-task-11 lost its upstream API key (Telegram delivery unavailable; the captain was not contacted there. Use the existing in-session fallback.) phone messages so far: 1 ### 8. exactly-once accounting times "lost its upstream API key" reached the phone: 0 / captain chat: 1 times "privileged prod cutover" reached the phone: 1 / captain chat: 0 ### 9. immutable versioned batch store + single active pointer active / active.applied / v.1785404632-50b5d5c948badb9b active pointer -> v.1785404632-50b5d5c948badb9b ### 11. real return gate: bin/fm-afk-return.sh begin exit: 0; away flag now: cleared catch-up escalation: blocked: fm-task-15 is waiting on a captain decision that never flushed catch-up away-telegram: needs-decision: approve the privileged prod cutover for fm-task-3 catch-up away-telegram: blocked: fm-task-7 needs a rotated deploy credential catch-up away-telegram: review-ready: fm-task-9 has a green PR awaiting the captain catch-up away-telegram: needs-decision: approve the staging rollback for fm-task-12 fm-afk-return: catch-up clear; ordinary captain work may proceed each away escalation appears exactly once: fm-task-3:1 fm-task-7:1 fm-task-9:1 fm-task-11:0 (already delivered in chat) fm-task-12:1 fm-task-15:1 version store after the return retirement: 0 entries


### 1. captain goes away (state/.afk written by /afk)
away mode ACTIVE

### 2. daemon surfaces a decision + a blocker + a review-ready notice while away
     1	needs-decision: approve the privileged prod cutover for fm-task-3
     2	blocked: fm-task-7 needs a rotated deploy credential
     3	review-ready: fm-task-9 has a green PR awaiting the captain

### 3. batch flushes -> phone receives it
=== PHONE MESSAGE ===
Firstmate needs you: needs-decision: approve the privileged prod cutover for fm-task-3 | blocked: fm-task-7 needs a rotated deploy credential | review-ready: fm-task-9 has a green PR awaiting the captain Notice only. This message does not approve a merge, privileged change, destructive action, or security-sensitive action.

### 4. what the captain sees in session (no duplicate of the alert body)
Telegram accepted away-mode alert 1785404629-ab351e0865099728-a0-0-6b7a8d517884deb0 (3 event(s)). Reconcile the durable fleet records now using the private away digests state/tg-away-digest/1785404629-ab351e0865099728-*.items; do not repeat this alert in captain chat. (pre-read; re-arm not needed - watcher daemon-managed)

### 5. durable, observable delivery evidence (no text, no token, no chat id)
-rw-------  tg-away-delivery/1785404629-ab351e0865099728-a0-0-6b7a8d517884deb0.status  accepted 1785404630
private away digest:
-rw-------  tg-away-digest/1785404629-ab351e0865099728-a0-0-6b7a8d517884deb0.items
    needs-decision: approve the privileged prod cutover for fm-task-3
    blocked: fm-task-7 needs a rotated deploy credential
    review-ready: fm-task-9 has a green PR awaiting the captain

### 6. leak scan: delivery status evidence must carry no token, chat id, or alert text
every delivery status is exactly <result class> <epoch> - no text, no token, no chat id
alert text lives only in the 0600 private digest, and every away dir is owner-only:
  drwx------ tg-away-delivery
  drwx------ tg-away-digest
  drwx------ tg-away-versions
  drwx------ tg-away-versions/v.1785404630-4ac12517ad312a9e
  drwx------ tg-away-versions/v.1785404630-4ac12517ad312a9e/tg-away-digest

### 7. a new escalation while the phone client is GONE -> visible failure, no false claim
Supervisor escalate (1 event(s)): blocked: fm-task-11 lost its upstream API key (Telegram delivery unavailable; the captain was not contacted there. Use the existing in-session fallback. Pre-read; re-arm not needed - watcher daemon-managed)
phone messages so far: 1

### 8. next escalation with the client back -> only the NEW line reaches the phone
phone messages total: 2
times "lost its upstream API key" reached the phone: 0
times "lost its upstream API key" reached captain chat: 1
times "privileged prod cutover" reached the phone: 1
times "privileged prod cutover" reached captain chat: 0

### 9. immutable versioned batch store + single active pointer
  active
  active.applied
  v.1785404632-50b5d5c948badb9b
active pointer -> v.1785404632-50b5d5c948badb9b

### 10. one more escalation the daemon could not flush before the captain returned
buffer at return:
  blocked: fm-task-15 is waiting on a captain decision that never flushed

### 11. the real return gate: bin/fm-afk-return.sh begin (captain-facing output)
exit: 0; away flag now: cleared
  catch-up escalation: blocked: fm-task-15 is waiting on a captain decision that never flushed
  catch-up away-telegram: needs-decision: approve the privileged prod cutover for fm-task-3
  catch-up away-telegram: blocked: fm-task-7 needs a rotated deploy credential
  catch-up away-telegram: review-ready: fm-task-9 has a green PR awaiting the captain
  catch-up away-telegram: needs-decision: approve the staging rollback for fm-task-12
  fm-afk-return: catch-up clear; ordinary captain work may proceed
each away escalation appears exactly once in the catch-up report:
  fm-task-3: 1 row(s)
  fm-task-7: 1 row(s)
  fm-task-9: 1 row(s)
  fm-task-11: 0 row(s)
  fm-task-12: 1 row(s)
  fm-task-15: 1 row(s)
version store after the return retirement: 0 entr(y|ies)

### done
Evidence: Exact bytes the phone client received
=== PHONE MESSAGE ===
Firstmate needs you: needs-decision: approve the privileged prod cutover for fm-task-3 | blocked: fm-task-7 needs a rotated deploy credential | review-ready: fm-task-9 has a green PR awaiting the captain Notice only. This message does not approve a merge, privileged change, destructive action, or security-sensitive action.
=== PHONE MESSAGE ===
Firstmate needs you: needs-decision: approve the staging rollback for fm-task-12 Notice only. This message does not approve a merge, privileged change, destructive action, or security-sensitive action.
Evidence: Exact messages injected into captain chat

Telegram accepted away-mode alert <id> (3 event(s)). Reconcile the durable fleet records now using the private away digests state/tg-away-digest/<batch>-*.items; do not repeat this alert in captain chat. Supervisor escalate (1 event(s)): blocked: fm-task-11 lost its upstream API key (Telegram delivery unavailable; the captain was not contacted there. Use the existing in-session fallback.) Telegram accepted away-mode alert <id2> (1 event(s)). Reconcile ... do not repeat this alert in captain chat.

Telegram accepted away-mode alert 1785404629-ab351e0865099728-a0-0-6b7a8d517884deb0 (3 event(s)). Reconcile the durable fleet records now using the private away digests state/tg-away-digest/1785404629-ab351e0865099728-*.items; do not repeat this alert in captain chat. (pre-read; re-arm not needed - watcher daemon-managed)
Supervisor escalate (1 event(s)): blocked: fm-task-11 lost its upstream API key (Telegram delivery unavailable; the captain was not contacted there. Use the existing in-session fallback. Pre-read; re-arm not needed - watcher daemon-managed)
Telegram accepted away-mode alert 1785404631-0d6975bb118096e5-a0-0-3d006aa80875e49a (1 event(s)). Reconcile the durable fleet records now using the private away digests state/tg-away-digest/1785404631-0d6975bb118096e5-*.items; do not repeat this alert in captain chat. (pre-read; re-arm not needed - watcher daemon-managed)
Evidence: Reproducible end-to-end evidence script
#!/usr/bin/env bash
# End-to-end away-mode escalation walkthrough against the real daemon code.
#
# The only stubs are the two things a hermetic run cannot have: the outbound
# phone client (`tg`, recorded to tg-sent.log with the exact bytes it received)
# and the captain-pane inject (recorded to chat.log with the exact message the
# captain would see in session). Everything else - the ledger owner, the version
# store, the delivery evidence, the digests, the away return fold - is the real
# shipped code.
set -u
ROOT=${ROOT:?set ROOT to the repo root}
OUT=${OUT:?set OUT to the evidence dir}

. "$ROOT/bin/fm-tg-lib.sh"
. "$ROOT/bin/fm-supervise-daemon.sh"

HOME_DIR="$OUT/home"
rm -rf "$HOME_DIR"
mkdir -p "$HOME_DIR/config" "$HOME_DIR/state" "$HOME_DIR/inbox/pending"
STATE="$HOME_DIR/state"
: > "$HOME_DIR/config/telegram-mode"     # captain opted Telegram mode in
SENT="$HOME_DIR/tg-sent.log"
CHAT="$HOME_DIR/chat.log"

# Recording phone client, standing in for ~/dev/phone-inbox/tg.
cat > "$HOME_DIR/tg" <<SH
#!/usr/bin/env bash
printf '=== PHONE MESSAGE ===\n' >> '$SENT'
cat >> '$SENT'
printf '\n' >> '$SENT'
exit "\${FAKE_TG_EXIT:-0}"
SH
chmod +x "$HOME_DIR/tg"

say() { printf '\n### %s\n' "$*"; }

flush() {  # [tg-bin] [fake-exit]
  local bin=${1:-$HOME_DIR/tg} rc=${2:-0}
  (
    inject_msg() { printf '%s\n' "$1" >> "$CHAT"; return 0; }
    FM_HOME="$HOME_DIR" FM_CONFIG_OVERRIDE="$HOME_DIR/config" \
      FM_TG_AWAY_EXEC=live FMTG_TG_BIN="$bin" FAKE_TG_EXIT="$rc" \
      escalate_flush "$STATE"
  )
}

say "1. captain goes away (state/.afk written by /afk)"
: > "$STATE/.afk"
afk_active "$STATE" && echo "away mode ACTIVE"

say "2. daemon surfaces a decision + a blocker + a review-ready notice while away"
escalate_add "$STATE" 'needs-decision: approve the privileged prod cutover for fm-task-3'
escalate_add "$STATE" 'blocked: fm-task-7 needs a rotated deploy credential'
escalate_add "$STATE" 'review-ready: fm-task-9 has a green PR awaiting the captain'
nl -ba "$STATE/.subsuper-escalations"

say "3. batch flushes -> phone receives it"
flush || echo "flush returned non-zero"
cat "$SENT"

say "4. what the captain sees in session (no duplicate of the alert body)"
cat "$CHAT"

say "5. durable, observable delivery evidence (no text, no token, no chat id)"
for f in "$STATE"/tg-away-delivery/*.status; do
  printf '%s  %s  %s\n' "$(ls -l "$f" | cut -d' ' -f1)" "${f#$STATE/}" "$(cat "$f")"
done
printf 'private away digest:\n'
for f in "$STATE"/tg-away-digest/*.items; do
  printf '%s  %s\n' "$(ls -l "$f" | cut -d' ' -f1)" "${f#$STATE/}"
  sed 's/^/    /' "$f"
done

say "6. leak scan: delivery status evidence must carry no token, chat id, or alert text"
bad=0
for f in "$STATE"/tg-away-delivery/*.status; do
  if ! grep -Eq '^(accepted|attempting|failed) [0-9]+( [a-z_]+)?$' "$f"; then
    printf 'UNEXPECTED CONTENT in %s: %s\n' "${f##*/}" "$(cat "$f")"; bad=1
  fi
done
[ "$bad" -eq 0 ] && echo "every delivery status is exactly <result class> <epoch> - no text, no token, no chat id"
printf 'alert text lives only in the 0600 private digest, and every away dir is owner-only:\n'
find "$STATE/tg-away-delivery" "$STATE/tg-away-digest" "$STATE/tg-away-versions" -type d \
  -exec sh -c 'printf "  %s %s\n" "$(ls -ld "$1" | cut -d" " -f1)" "${1##*/state/}"' _ {} \;

say "7. a new escalation while the phone client is GONE -> visible failure, no false claim"
escalate_add "$STATE" 'blocked: fm-task-11 lost its upstream API key'
flush "$HOME_DIR/no-such-tg" || echo "flush returned non-zero"
tail -n 1 "$CHAT"
printf 'phone messages so far: %s\n' "$(grep -c '=== PHONE MESSAGE ===' "$SENT")"

say "8. next escalation with the client back -> only the NEW line reaches the phone"
escalate_add "$STATE" 'needs-decision: approve the staging rollback for fm-task-12'
flush || echo "flush returned non-zero"
printf 'phone messages total: %s\n' "$(grep -c '=== PHONE MESSAGE ===' "$SENT")"
printf 'times "lost its upstream API key" reached the phone: %s\n' \
  "$(grep -c 'lost its upstream API key' "$SENT")"
printf 'times "lost its upstream API key" reached captain chat: %s\n' \
  "$(grep -c 'lost its upstream API key' "$CHAT")"
printf 'times "privileged prod cutover" reached the phone: %s\n' \
  "$(grep -c 'privileged prod cutover' "$SENT")"
printf 'times "privileged prod cutover" reached captain chat: %s\n' \
  "$(grep -c 'privileged prod cutover' "$CHAT")"

say "9. immutable versioned batch store + single active pointer"
ls -1 "$STATE/tg-away-versions" | sed 's/^/  /'
printf 'active pointer -> %s\n' "$(cat "$STATE/tg-away-versions/active" 2>/dev/null)"

say "10. one more escalation the daemon could not flush before the captain returned"
escalate_add "$STATE" 'blocked: fm-task-15 is waiting on a captain decision that never flushed'
printf 'buffer at return:\n'; sed 's/^/  /' "$STATE/.subsuper-escalations"

say "11. the real return gate: bin/fm-afk-return.sh begin (captain-facing output)"
mkdir -p "$HOME_DIR/bin" "$HOME_DIR/data"
for f in fm-afk-return.sh fm-wake-lib.sh fm-classify-lib.sh fm-away-ledger-lib.sh fm-x-lib.sh; do
  cp "$ROOT/bin/$f" "$HOME_DIR/bin/"
done
cat > "$HOME_DIR/bin/fm-afk-launch.sh" <<'SH'
#!/usr/bin/env bash
[ "${1:-}" = stop ] || exit 2
rm -f "$FM_HOME/state/.afk" "$FM_HOME/state/.afk-daemon-terminal"
SH
printf '#!/usr/bin/env bash\nexit 0\n' > "$HOME_DIR/bin/fm-wake-drain.sh"
chmod +x "$HOME_DIR/bin/"*.sh
FM_HOME="$HOME_DIR" FM_STATE_OVERRIDE="$STATE" "$HOME_DIR/bin/fm-afk-return.sh" begin \
  > "$HOME_DIR/return.log" 2>&1
printf 'exit: %s; away flag now: %s\n' "$?" \
  "$([ -e "$STATE/.afk" ] && echo present || echo cleared)"
sed 's/^/  /' "$HOME_DIR/return.log"
printf 'each away escalation appears exactly once in the catch-up report:\n'
for k in fm-task-3 fm-task-7 fm-task-9 fm-task-11 fm-task-12 fm-task-15; do
  printf '  %s: %s row(s)\n' "$k" "$(grep -c "$k" "$HOME_DIR/return.log")"
done
printf 'version store after the return retirement: %s entr(y|ies)\n' \
  "$(ls -1A "$STATE/tg-away-versions" 2>/dev/null | wc -l | tr -d ' ')"

say "done"
- Outcome: ⚠️ 1 warning across 1 run (12m14s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 5 issues found → auto-fixed (7) ✅
  • 🚨 bin/fm-afk-launch.sh:534 - fm_afk_launch_start calls fm_afk_launch_reclaim_backups with group adoption enabled (no digests-only flag) after the launch has succeeded, and success means fm_afk_launch_wait_ready has already looped until daemon_lock_held_by_live_daemon returned 0 - the daemon is live. away_ledger_reclaim_backups (bin/fm-away-ledger-lib.sh:469-545) then adopts a leftover backup's buffer/sidecar/wedge group straight into the live paths whenever those three are absent, which is precisely the state a freshly started session is in after fm_afk_clear_stale_artifacts. That is the hazard the digests-only mode was added for: the lib comment states the daemon is the sole writer of the buffer and its sidecar with no shared lock, so adoption races escalate_add. Failing sequence: a prior transaction left an orphaned backup (away_ledger_restore reported failure, fm_afk_launch_restore_backup kept it); a new start succeeds; reclaim checks the three live paths as absent; the daemon escalates in that window, writing state/.subsuper-escalations plus a fresh ledger via escalate_add; reclaim's mktemp+mv then overwrites both, so the just-escalated captain-relevant line reaches neither Telegram nor captain chat and a retired batch's counts are re-installed live. The rollback branch is safe (no daemon), so the fix is at the shared boundary of this call: pass digests-only=1 whenever the transaction succeeded (daemon live), or perform group reclaim before the daemon is launched - never after it.
  • ⚠️ bin/fm-afk-return.sh:181 - return_reconcile calls away_ledger_reclaim_backups with group adoption enabled unconditionally, but the preceding fm-afk-launch.sh stop may have failed (that path only sets lifecycle_ok=0 and appends evidence, it does not return). With the daemon still alive, adoption into the live buffer/sidecar/wedge paths hits the same unlocked-writer race as the launch path: the daemon's concurrently appended escalation line is discarded by the adoption mv. Gate group adoption on the stop having actually succeeded (pass digests-only=1 when lifecycle_ok is 0), so only digest text is merged while a daemon may still be writing.
  • ⚠️ bin/fm-afk-return.sh:188 - return_reconcile appends the ENTIRE live escalation buffer as escalation evidence and then appends every digest item as away-telegram evidence, so lines already counted in the ledger's accounted appear twice in the catch-up gate. Reachable whenever an accepted+digested send's in-session receipt injection failed (inject_msg returns non-zero, escalate_flush preserves the buffer with accounted == n) and the away session then ends: the same escalation is presented to Firstmate as two separate catch-up items to act on. away_ledger_fold_retained_backups deliberately reads each backup's own accounted count and prints only lines past it, and the lib comment at bin/fm-away-ledger-lib.sh:578-582 asserts a caller-side dedupe "as it does at return" that does not exist here. Fix at the same boundary the backup path already uses: read away_ledger_read against the live buffer and emit only lines past accounted, falling back to the whole buffer when the record is unknown.
  • ⚠️ bin/fm-away-ledger-lib.sh:469 - The recorded intent for the final round was to replace this recovery mechanism entirely with immutable versioned batch directories and one atomic active-batch pointer switch performed only before daemon start, with no in-place merging or adoption into live paths. The delivered code still does per-file in-place digest merging into the live digest directory and per-artifact adoption of buffer/sidecar/wedge into live paths (away_ledger_reclaim_backups), and it is invoked after daemon start on the launch success path. If that directive is authoritative, this is the mechanism it asked to retire rather than harden further; if the directive was superseded, the surviving in-place design should be stated as the accepted approach. Author decision, not a mechanical fix.
  • ℹ️ bin/fm-tg-followup.sh:165 - fmtg_send_stdin returns 127 both for "no runnable client" (its own guard) and transparently when the client itself exits 127, so a client that exits 127 for its own reasons is reported as "phone-inbox tg client not runnable at <path>". The daemon avoids this by calling fmtg_client_runnable out-of-band; this caller could do the same (or have fmtg_send_stdin use a distinct out-of-band signal) so the diagnostic cannot be forged by the client's own status. Diagnostic wording only - no behavioral impact, both branches exit 1.

🔧 Fix: replace away batch reclaim with immutable versioned store
4 warnings still open:

  • ⚠️ bin/fm-away-ledger-lib.sh:666 - away_ledger_unaccounted_lines resolves the accounted count with away_ledger_read, and away_ledger_read MIGRATES IN PLACE: on any legacy record shape (bare epoch, 5-field, 6-field, or an id without a nonce) it calls away_ledger_write, which mktemps and mv's over "<buf>.since". away_ledger_fold_versions calls it with "$vdir/escalations", so the return catch-up's documented read-only fold writes into a published immutable version at $vdir/escalations.since. Concrete path: an away session running the pre-ledger daemon writes a bare-epoch sidecar; the daemon is upgraded mid-session (a scenario this branch explicitly supports and documents); the next entry publishes a version carrying that legacy sidecar; at return, away_ledger_fold_versions <state> escalations mutates that version. Two consequences: the store's stated immutability invariant ("Read-only: nothing is retired, renamed, or marked here") is false, and if the migration write fails (read-only dir, full disk) away_ledger_read returns unknown, unaccounted_lines falls back to the WHOLE version buffer, and lines that version's own digests already carry are appended under escalation while the same text is appended under away-telegram - the cross-kind duplication this round fixed. Fix at the shared boundary: give the owner a non-mutating record parse (read/normalise without the away_ledger_write migration) and use it for every version-directory read; keep the migrating read for live paths only. tests/fm-tg-mode.test.sh:1049 exercises fold_versions with a current 7-field sidecar and only asserts .complete still exists, so it cannot catch this.
  • ⚠️ bin/fm-afk-return.sh:235 - clear_delivery_artifacts is now the acknowledgement boundary and returns non-zero when away_ledger_retire_batch or away_ledger_versions_retire_all fails, but return_reconcile calls it bare (rc discarded) and after rm -f &#34;$GATE&#34; at line 234. Failing sequence: a version directory cannot be removed (permissions, or a file inside it held); versions_retire_all returns 1; the warning goes to stderr but the gate is already gone and return_reconcile returns 0, so the catch-up is reported acknowledged while retained versions survive. The next away session's catch-up folds those same versions again and presents already-acknowledged escalations as fresh items to act on - exactly the duplicate-presentation the store is meant to prevent. Retire before closing the gate, or keep the gate open (lifecycle blocker) and propagate the non-zero status when retirement fails.
  • ⚠️ bin/fm-afk-start.sh:157 - The non-prepared branch of fm_afk_start_main calls fm_afk_clear_stale_artifacts, which retires the live buffer, sidecar and wedge marker via away_ledger_retire_batch WITHOUT first publishing an immutable version, and it never runs away_ledger_version_gc / away_ledger_version_apply_pending. On this documented direct-start path (the script header's "direct, non-prepared start") a crashed prior away session's un-flushed escalation lines are deleted with no surviving copy, so nothing reaches the return catch-up - the same evidence-loss class the version store was introduced to close, which the launcher path avoids by publishing first (bin/fm-afk-launch.sh:518-530). Additionally, an interrupted pointer switch never converges when the daemon is started outside the launcher, since apply_pending is only invoked by fm_afk_launch_versions_prepare. Fix: in the non-prepared branch, publish a version (and sweep/replay) before clearing, so every away entry - launcher or not - retains the prior unit until the catch-up acknowledges it.
  • ⚠️ bin/fm-away-ledger-lib.sh:374 - The authoritative instruction stated that during daemon operation every ledger transition creates a successor complete immutable directory and switches atomically under the owner lock. As delivered, versions are published only at lifecycle transactions (away entry, crash rollback); the daemon's own transitions still write the live artifacts in place - away_ledger_write mv's the sidecar over itself, escalate_add appends to the live buffer, away_ledger_digest_record publishes into the live digest directory. The safety property the clause protects is instead enforced two other ways: the daemon is the sole writer of the live unit, and away_ledger_version_activate / away_ledger_version_apply_pending refuse (rc 3) while away_ledger_daemon_live sees the lock held, so no version content can land under a running daemon. No cross-pairing or lost-append path remains reachable that I can substantiate. Reporting it because the clause is explicit and unimplemented: either add per-transition successor versions, or confirm that in-place daemon writes plus the refuse-while-live guard are the accepted design and update the store's own doc block to say so.

🔧 Fix: immutable version reads, locked store, gated retirement
6 issues (3 warnings, 3 infos) still open:

  • ⚠️ bin/fm-away-ledger-lib.sh:618 - _away_ledger_version_publish_locked now returns 1 whenever the staged record parses as unknown, and that failure propagates all the way out: away_ledger_entry_capture returns 1, so fm_afk_launch_start / fm_afk_launch_start_native abort with "could not capture the pre-launch away batch" (bin/fm-afk-launch.sh:520,580) and a direct start refuses at bin/fm-afk-start.sh:167. Nothing clears the offending record, so away mode cannot be entered again until someone manually deletes state/.subsuper-escalations.since. Reachable state: a zero-byte or truncated sidecar - the pre-branch daemon wrote it with a plain _now &gt; &#34;${buf}.since&#34; redirect, so a crash or a full disk mid-write leaves an empty file, and away_ledger_parse reports unknown for it while [ -e &#34;$staging/escalations.since&#34; ] is still true. Any record shape outside the 1/5/6/7-field set (a downgrade after a future field is added) does the same. The refusal is also unnecessary: unknown is already designed to fail closed, and away_ledger_unaccounted_lines falls back to the whole buffer, so publishing the unit with the record left as-is (or omitting only the sidecar slot from the version) preserves every owed line and lets the fold over-report instead of wedging the lifecycle. Treat an unparseable staged record as "nothing to normalise" rather than a publication failure.
  • ⚠️ bin/fm-afk-return.sh:229 - clear_delivery_artifacts (which calls away_ledger_retire_batch, deleting the live buffer, sidecar, wedge marker and digest directory) now runs BEFORE write_gate at line 234. If write_gate then fails - its mktemp or mv cannot complete, e.g. an unwritable or full state dir - return_reconcile returns 1 with the gate still holding only write_pending_seed's preserved evidence, while this pass's freshly folded escalation and digest lines have already been deleted from disk and exist nowhere durable. Before this round retirement ran only after the gate had closed cleanly, so a write_gate failure left the live unit intact for the next pass to re-fold. Fix the ordering while keeping the new gating: write the gate with this pass's evidence first, then attempt retirement, then remove the gate on success (appending the lifecycle blocker and re-writing the gate when retirement fails), so nothing is deleted before its evidence is durable.
  • ⚠️ bin/fm-away-ledger-lib.sh:525 - away_ledger_lock_acquire reclaims a stale lock with a bare rm -rf -- &#34;$lock&#34; after observing a dead pid (line 525, and the same shape for an incomplete lock at line 518). Two acquirers that both read the same dead pid both remove it: A removes, loops, and its mkdir succeeds; B - still in the iteration where it decided to reclaim - then removes A's freshly created lock and its own mkdir succeeds too, so both believe they hold the store. Mutual exclusion is what stops a publish from interleaving with a materialise; with it broken, _away_ledger_version_publish_locked can copy the buffer, a concurrent _away_ledger_version_apply_locked can mv both live artifacts, and the published version ends up with a buffer paired with a foreign sidecar - the cross-pairing this store exists to make impossible. Preconditions are narrow (a holder that died mid-transaction plus two concurrent entry paths), but this repo already owns the hardened protocol: fm_lock_try_acquire in bin/fm-wake-lib.sh takes a &lt;lock&gt;.steal lock before removing a stale primary and adds a mid-acquire freshness grace. Adopt that reclaim protocol here - factoring the primitive into the dependency-free bin/fm-lock-lib.sh keeps fm-away-ledger-lib.sh out of fm-wake-lib.sh, which fm-afk-return.sh's read-only guard path deliberately does not source.
  • ℹ️ bin/fm-away-ledger-lib.sh:735 - _away_ledger_version_apply_locked returns 1 when active names a version that is missing or not provably complete, and away_ledger_entry_capture turns that rc 1 into a refused away entry on every path. Nothing clears such a pointer: away_ledger_version_gc sweeps incomplete version directories but never inspects the pointers, so a version lost out of band (a partial away_ledger_version_retire where the pointer rm failed, or an operator deletion) wedges away mode permanently. Cheap hardening: have version_gc drop an active / active.applied pointer that names a non-existent or incomplete version - the content it named is already gone, so clearing the pointer is the only forward path.
  • ℹ️ bin/fm-afk-start.sh:167 - The new capture refusal returns 1 at line 168, but state/.afk was already written at line 140, so a refused direct start leaves away mode flagged with no daemon supervising: afk_active is true for the daemon-less system and the captain's next message hits the return gate. The escalation buffer itself is preserved (the intended property, and what tests/fm-daemon.test.sh asserts), so nothing is lost - only the flag is inconsistent. Clearing state/.afk on this refusal when this invocation was the one that wrote it would make the failure fully honest.
  • ℹ️ bin/fm-away-ledger-lib.sh:416 - The round-2 instruction asked that every daemon transition (append, reservation, acceptance, digest, accounted acknowledgement, retry state, wedge evidence) acquire the owner lock, build a complete successor version, validate it, and switch the pointer, with no live artifact writes remaining. The delivered code keeps those transitions as in-place atomic writes to the live unit and instead documents that choice at lines 416-425 as the accepted design, with the rationale that the daemon is the sole writer for the whole session and the refuse-while-live guard means no version content can land under it. I could not substantiate a reachable cross-pairing or lost-append path under the current code, so I am not treating it as a defect - flagging it only so the deviation from an explicit instruction is a decision you make rather than one that passes silently.

🔧 Fix: version every away ledger transition under owner lock
5 issues (1 error, 3 warnings, 1 info) still open:

  • 🚨 bin/fm-supervise-daemon.sh:659 - escalate_add now returns 1 when away_ledger_append cannot commit a successor version, and its log line claims "The heartbeat scan and wake queue re-derive it." Neither call site honours that. At line 1597 the wake handler ignores the rc and unconditionally runs stale_marker_remove (removing the persistence marker housekeeping would re-escalate from) and mark_escalated_seen at line 1601, which writes state/.subsuper-seen-status-<task> with the very status line that was never buffered - and the heartbeat catch-all at line 1441-1443 skips any status whose seen marker already equals last. The catch-all scan repeats the same mistake at line 1444: escalate_add's rc is ignored and mark_status_seen runs anyway. Result: on a refused append the captain-relevant escalation exists in no buffer, no version and no digest, is permanently suppressed from re-derivation, and reaches neither Telegram nor captain chat - only a line in the daemon's own log the away captain cannot see. This is newly reachable because the append is now a full transaction (10s owner-lock timeout, stage copy, publish validation, materialise) rather than a &gt;&gt; redirect - and the lock can legitimately be busy, e.g. while the launcher's fm_afk_launch_versions_sweep holds it or during the shutdown reentrancy stall below. Fix at the two call sites: only remove the stale marker and record the seen marker when escalate_add succeeded, so the next scan re-derives the escalation.
  • ⚠️ bin/fm-supervise-daemon.sh:1023 - escalate_flush now ends with away_ledger_truncate &#34;$buf&#34; || return 1, so a truncate transaction that fails AFTER inject_msg confirmed the submit is reported to callers as a flush failure. The ledger and buffer still say the batch is undelivered, so the next flush (housekeeping's retry at line 1338/1342, or the max-defer path) re-injects the identical digest into captain chat - a duplicate in-session delivery of an escalation the captain already received - and housekeeping additionally treats the flush as wedged and fires inject_wedge_alarm. Reachable whenever the transaction fails at its tail: a 10s owner-lock timeout, a materialise copy failure, or the final active.applied pointer write failing after the projection already succeeded (_away_ledger_transact_locked returns 1 there even though the truncation landed). Distinguish the two outcomes: retry the truncation under the lock (or persist the confirmed-inject fact in the same version) so a post-inject bookkeeping failure never re-delivers the same batch; only a genuinely unsubmitted inject should return non-zero.
  • ⚠️ bin/fm-away-ledger-lib.sh:623 - away_ledger_lock_acquire is not reentrant, and the daemon installs trap cleanup TERM INT (bin/fm-supervise-daemon.sh:1800) whose cleanup calls escalate_flush at line 1787 - which now performs ledger transactions (reserve/confirm/digest via telegram_away_deliver, then truncate). A TERM arriving while a transaction holds state/tg-away-versions/.owner.lock runs the trap with that lock still held by this same process: the recorded pid is alive, so _away_ledger_lock_steal deliberately refuses to reclaim it, and every shutdown transaction spins the full 200 x 0.05s (~10s) before failing. The stop path in fm-afk-launch.sh SIGTERMs the daemon precisely so this flush runs, and the daemon documents a ~1s shutdown, so the observable effects are a shutdown that stalls for tens of seconds (potentially escalated to SIGKILL by the stop timeout) and a final flush that delivers nothing in session or to the phone. The buffered lines survive in the live unit and its version for the return fold, so this is a liveness and lost-final-flush problem rather than evidence loss. Fix: make the owner lock reentrant for the holding pid with depth tracking (so a nested release does not drop the outer transaction's lock), or block TERM/INT for the duration of a transaction and let the trap run between transitions.
  • ⚠️ bin/fm-away-ledger-lib.sh:879 - _away_ledger_transact_locked reads previous from the active pointer at line 867 and, after applying the successor, deletes it at line 879, justified by "the successor was built from it and is a complete superset". That holds only when the live unit is actually the predecessor's projection, but _away_ledger_stage_build copies the LIVE unit, not $dir/$previous. Divergence is reachable: if away_ledger_version_materialise fails partway (it removes the live buffer, sidecar and wedge first, then copies each back), transact returns non-zero with active = the new version and active.applied still stale, and nothing replays it - away_ledger_version_apply_pending only runs at an away entry, never inside a live session. The next escalate_add then sees an empty/absent buffer, mints a fresh batch via away_ledger_open, stages from that diverged live unit, publishes a successor that does NOT contain the earlier lines, and deletes the predecessor version that did - the only remaining copy if the entry-capture version has since been retired. Cheap guard that makes the stated invariant true: only retire previous when active.applied equalled previous at the start of the transaction (the live unit was provably that version's projection); otherwise leave it retained for the return fold, which already deduplicates identical lines.
  • ℹ️ bin/fm-away-ledger-lib.sh:475 - away_ledger_wedge_record creates state/.subsuper-inject-wedged.stage.XXXXXX and removes it on its own paths, but a crash between mktemp and the rm leaks it. away_ledger_retire_working_records enumerates the temp patterns to sweep (.since., .apply., .staging., .tg-away-digest.apply.) and does not include .subsuper-inject-wedged.stage.*, so the leak survives every lifecycle boundary. One-line addition to the same sweep list.

🔧 Fix: honour refused appends, unique mints, reentrant store lock
4 issues (1 error, 3 warnings) still open:

  • 🚨 bin/fm-supervise-daemon.sh:1408 - The housekeeping wedge path still ignores escalate_add's return code and removes the suppressor unconditionally: escalate_add &#34;$state&#34; &#34;stale persisted ${age}s (possible wedge): $win&#34; is followed by stale_marker_remove &#34;$win&#34; &#34;$state&#34; on the next line, regardless of rc. This is the exact contract round 4 established and fixed at the other two sites (handle_wake at line 1620 and the catch-all scan at line 1465) - a refused append must leave suppressors intact - and this sibling was missed. Failing sequence: the append transaction refuses (owner-lock timeout against a concurrent fm_afk_launch_versions_sweep / gc, a materialise failure, or a full disk), escalate_add logs and returns 1 with the line in no buffer, no version and no digest; stale_marker_remove then deletes state/.subsuper-stale-<key>, so the aged wedge that was about to be escalated is discarded. Recovery is only partial and delayed: the marker is recreated by a later stale wake (stale_marker_record at line 1669) with a FRESH epoch, so the pane must idle another full FM_STALE_ESCALATE_SECS before it can escalate again, and if the pane produces no further stale wake the possible-wedge escalation is gone for good - the 8.5h silent-wedge class this daemon exists to prevent. Fix at the same boundary as the two fixed sites: if escalate_add ...; then stale_marker_remove ...; fi, with an ERROR log on the else branch.
  • ⚠️ bin/fm-supervise-daemon.sh:1442 - The pause re-surface path has the same shape: escalate_add's rc is ignored and _now &gt; &#34;$marker&#34; on line 1443 resets the pause marker's epoch anyway. On a refused append (same triggers as above) the recheck escalation is buffered nowhere, yet the marker is re-armed, so the forgotten pause cannot re-surface for another full FM_PAUSE_RESURFACE_SECS window - the exact rot the block's own comment says must not happen. Less severe than the stale path because the marker survives and the pause does eventually re-surface, but the suppressor-only-on-success rule should hold here too: only reset the marker when escalate_add succeeded, and leave it aged so the next housekeeping tick retries immediately.
  • ⚠️ bin/fm-supervise-daemon.sh:965 - The new in-session dedupe guard matches on an EXACT line count: away_ledger_chat_confirmed compares the recorded &lt;batch-id&gt; &lt;lines&gt; confirmed against the buffer's current n. A batch only ever grows, so any append between the confirmed submit and a successful truncation defeats the guard. Failing sequence: escalate_flush injects and inject_msg confirms, chat_mark records confirmed n=3 (line 1039), away_ledger_truncate fails (owner-lock timeout, materialise copy failure, or the trailing active.applied pointer write) and escalate_flush returns 1; before housekeeping's retry a wake arrives and escalate_add appends line 4; the next flush sees n=4, chat_confirmed returns false, and chat_body (tail -n +$((accounted + 1))) carries all four lines back into inject_msg - re-delivering lines 1-3 that the captain already has in chat, which is the duplication the round-4 fix was accepted to eliminate. Fix at the same boundary: treat the confirmed record as a delivered PREFIX rather than an equality check - when the recorded batch id matches and recorded lines <= n, skip the first recorded lines when building chat_body (and skip the inject entirely when recorded == n), instead of re-injecting the whole buffer.
  • ⚠️ bin/fm-afk-start.sh:177 - fm_afk_clear_stale_artifacts now delegates to away_ledger_retire_batch, which returns non-zero when the version-store owner lock cannot be acquired (a 10s timeout against a concurrent launcher sweep or a stale lock) or when a live artifact cannot be removed. The call at line 177 is unguarded, and this script runs under set -eu (line 33) with fm_afk_start_main invoked unguarded at line 187, so that non-zero status now aborts the script before exec &#34;$FM_AFK_DAEMON&#34;. Before this change the function was a bare rm -f ... 2&gt;/dev/null and always returned 0, so the start always proceeded. Result: state/.afk has already been written at line 139, the unit has been partially retired, no daemon is started, and nothing is printed - away mode is flagged with no supervisor and the failure is silent. Note the capture-failure branch immediately above (lines 168-176) already does the honest thing: it prints a diagnostic and rolls back a flag this invocation wrote. Give the retire call the same treatment - capture its status, print the diagnostic, roll back the flag when had_afk is 0, and return 1 explicitly - rather than letting errexit end the run mid-way.

🔧 Fix: gate suppressors on committed appends, confirmed chat prefix
2 issues (1 error, 1 info) still open:

  • 🚨 bin/fm-supervise-daemon.sh:1051 - The new confirmed-prefix guard is defeated by this function's own pre-submit write. Line 1051 runs away_ledger_chat_mark &#34;$state&#34; &#34;$batch_id&#34; &#34;$n&#34; attempting before every inject, and _away_ledger_mut_chat writes the chat slot with a single &gt; redirect (bin/fm-away-ledger-lib.sh:250), so the record &lt;id&gt; &lt;prefix&gt; confirmed is replaced by &lt;id&gt; &lt;n&gt; attempting. away_ledger_chat_delivered only answers for phase confirmed, so the prefix drops to 0. Failing sequence: buffer has 3 lines; flush injects them; chat_mark records confirmed 3; away_ledger_truncate fails (owner-lock timeout, materialise failure, or the trailing active.applied write) and flush returns 1. A wake appends line 4. Next flush correctly computes chat_start=3 and builds a message containing only line 4 - then line 1051 clobbers the confirmed record with attempting 4, and inject_msg fails (a wedged pane, the documented condition inject_wedge_alarm exists for). The next flush sees chat_delivered=0, chat_start falls back to accounted, and lines 1-3 the captain already has are re-injected into captain chat - the exact duplication the round-5 fix was accepted to eliminate, reachable through a sibling state transition in the same function. The new regression test (tests/fm-tg-mode.test.sh:153) only exercises a successful inject, so it cannot catch this. Fix at the shared owner boundary rather than at this call site: make the chat record's confirmed count monotonic - away_ledger_chat_mark/_away_ledger_mut_chat must carry the existing confirmed count forward when writing an attempting record for the same batch id (or hold confirmed and attempting as separate fields), and away_ledger_chat_delivered must read the confirmed field regardless of the current phase. That also covers a crash between the attempting write and the submit.
  • ℹ️ bin/fm-supervise-daemon.sh:1030 - When the ledger record parses as unknown, escalate_flush forces batch_id to none, so the second away_ledger_chat_delivered call cannot match the chat record's real batch id and returns 0; chat_start collapses to accounted (0) and the unknown-status message re-injects the whole buffer, repeating a prefix captain chat already received. Precondition is a sidecar corrupted or truncated after a confirmed submit. This is the deliberate fail-open direction (an unreadable ledger over-reports to chat rather than dropping a captain-relevant line), so it is low severity - but state/.subsuper-chat-delivery names its own batch id and is independent of the sidecar, so reading the id from the chat record itself would preserve the prefix without weakening the fail-open behaviour.

🔧 Fix: monotonic confirmed chat prefix, independent attempt range
2 issues (1 warning, 1 info) still open:

  • ⚠️ bin/fm-away-ledger-lib.sh:300 - The confirmed-prefix receipt cannot be created at all when the ledger is unreadable and no receipt exists yet. escalate_flush forces batch_id=none on an unknown record (bin/fm-supervise-daemon.sh:980); away_ledger_chat_mark_attempt (line 300) and away_ledger_chat_mark_confirmed (line 318) resolve the identity via _away_ledger_chat_effective_id, which falls back to the RECORDED id when the requested one is none - and with no receipt on disk that is also none, which the following case arm rejects with return 1. Failing sequence: a batch's sidecar is corrupted or truncated (a full disk, a crash mid-write); the flush logs WARN for the unrecorded attempt, injects successfully, then logs the ERROR at bin/fm-supervise-daemon.sh:1057 because the confirmation cannot be recorded either; if away_ledger_truncate also fails - the same root cause typically produces both - the next flush finds no receipt, chat_start collapses to 0, and every line already injected is re-delivered into captain chat. The round-6 instruction explicitly asked for a batch identity/chat receipt that stays recoverable when the ledger payload is malformed; as delivered that holds only when a receipt already exists (the case tests/fm-tg-mode.test.sh:1441 covers), never for a batch that first goes unreadable. Fix at the same owner boundary: when neither the caller nor the receipt supplies a usable id, let the receipt mint and carry its own identity (away_ledger_mint_id) rather than refusing - a later flush passing none already adopts whichever id the receipt names, so a single write makes the prefix hold. Two faults are needed to reach the duplication, so this is a follow-up rather than a blocker.
  • ℹ️ bin/fm-away-ledger-lib.sh:293 - The new attempt-from/attempt-to fields are written on every flush and parsed back by _away_ledger_chat_parse, but nothing consumes them for a decision. The round-6 instruction wanted an uncertain submit to prevent retyping the attempted range; inject_msg is boolean (delivered or not), so there is no uncertain in-session outcome to gate on and a refused inject correctly retries the same suffix. Keeping the fields is defensible - separating them from confirmed is precisely what makes the prefix monotonic, and they make a crash between attempt and submit visible in the version - so this is informational only, no action needed.

🔧 Fix: open chat receipts from validated active version identity
✅ Re-checked - no issues remain.

⚠️ **Test** - 1 warning
  • ⚠️ tests/fm-bearings-snapshot.test.sh - tests/fm-bearings-snapshot.test.sh fails one case ("restoring the SSHHIP child did not clear only its narrow warning") in this environment. I verified it fails identically with the base commit's bin/ and base test file, and the case only exercises bin/fm-fleet-snapshot.sh which this change does not touch, so it is pre-existing and unrelated to the away-mode Telegram work. Flagging it so the author decides whether to chase it separately; no fix attempted here since it is out of this change's scope.
  • bash tests/fm-tg-mode.test.sh - 136 away/Telegram-mode cases including versioned batch store, pointer switch, exactly-once delivery, failure classes
  • bash tests/fm-afk-launch.test.sh
  • bash tests/fm-afk-return.test.sh
  • bash tests/fm-daemon.test.sh
  • bash tests/fm-bearings-skill.test.sh
  • bash tests/fm-bearings-snapshot.test.sh (1 pre-existing failure; re-ran 3x, then re-ran with base-commit bin/ and base test file to confirm it predates this change)
  • Manual end-to-end away session via ROOT=$PWD OUT=&lt;evidence&gt; bash away-telegram-e2e.sh: away flag set, 3 escalations buffered, escalate_flush delivering through a recording tg client, missing-client failure round, client-restored round, then real bin/fm-afk-return.sh begin catch-up and version-store retirement
  • Verified git status --porcelain clean after restoring the temporary base-commit checkout used for the pre-existing-failure comparison
✅ **Document** - passed

✅ No issues found.

⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 127)
✅ **Push** - passed

✅ No issues found.

deeto15 and others added 30 commits July 30, 2026 03:09
* Add internal status skill

* no-mistakes(document): register /status skill in documentation-audiences inventory

* no-mistakes(lint): replace grep|wc -l with grep -c in status skill test

* test: silence literal status skill patterns

* Refactor bearings default to chat-only

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
…tate, duplicated delivery dir, digest loss on restart
@morris2spears
morris2spears merged commit 566f88e into main Jul 30, 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.

2 participants