Skip to content
This repository was archived by the owner on Aug 25, 2026. It is now read-only.

fix: reserve Grok watcher follower ownership - #86

Merged
JTInventory merged 3 commits into
mainfrom
fm/fm-grok-watch-exclusivity-0731
Jul 31, 2026
Merged

JTInventory merged 3 commits into
mainfrom
fm/fm-grok-watch-exclusivity-0731

Conversation

@JTInventory

Copy link
Copy Markdown
Owner

Intent

Fix the captain-visible Grok Watcher badge gap by reserving the per-home follower wait for Grok's tracked background-notify arm. Document the status-to-badge contract, make fm-watch-session start/restart refuse unsafe overlap when GROK_AGENT=1 while preserving non-Grok fallback behavior, allow only the explicit FM_ALLOW_WATCH_SESSION_WITH_GROK=1 emergency override, add behavior coverage, ship through no-mistakes to a JTInventory/firstmate PR, and never merge.

What Changed

  • Reserve the per-home follower wait for Grok’s tracked background-notify arm and document the status-to-badge contract.
  • Make fm-watch-session start/restart refuse unsafe overlap when GROK_AGENT=1, with only the explicit emergency override allowed.
  • Preserve non-Grok fallback behavior and add coverage for refusal, override, restart, and badge provenance.

Risk Assessment

✅ Low: The follow-up commit addresses both prior findings: the badge table now distinguishes tracked Grok output from fallback inner-arm output, and restart refusal coverage verifies the existing fallback is not stopped.

Testing

The provided baseline command was rerun successfully; all 90 behavior tests passed. Focused watcher tests passed twice, and the CLI transcript directly demonstrates the requested end-user behavior. The worktree remained clean and no linters or formatters were run.

Evidence: End-user CLI transcript

$ GROK_AGENT=1 FM_HOME=.../evidence-grok bin/fm-watch-session.sh start
watch-session: refusing Grok primary; Grok's tracked background arm must own the watcher wait (set FM_ALLOW_WATCH_SESSION_WITH_GROK=1 only for emergency fallback)
exit_code=1
grok_runner_files=not-created

$ GROK_AGENT=1 FM_ALLOW_WATCH_SESSION_WITH_GROK=1 FM_HOME=.../evidence-grok bin/fm-watch-session.sh start
watch-session: started target=firstmate-watch:fm-watch-2387610675 home=/tmp/no-mistakes-evidence/01KYV4RM705XPV71G16JB5QSQV/manual-watch-session.N7ZCot/evidence-grok
exit_code=0

$ GROK_AGENT=1 FM_HOME=.../evidence-grok bin/fm-watch-session.sh restart
watch-session: refusing Grok primary; Grok's tracked background arm must own the watcher wait (set FM_ALLOW_WATCH_SESSION_WITH_GROK=1 only for emergency fallback)
exit_code=1
fallback_after_refused_restart=present
stop_marker_after_refused_restart=not-created
kill_window_after_refused_restart=no

$ FM_HOME=.../evidence-non-grok bin/fm-watch-session.sh start
watch-session: started target=firstmate-watch:fm-watch-617793032 home=/tmp/no-mistakes-evidence/01KYV4RM705XPV71G16JB5QSQV/manual-watch-session.N7ZCot/evidence-non-grok
exit_code=0

$ FM_HOME=.../evidence-non-grok bin/fm-watch-session.sh --status
watch-session: running target=firstmate-watch:fm-watch-617793032 home=/tmp/no-mistakes-evidence/01KYV4RM705XPV71G16JB5QSQV/manual-watch-session.N7ZCot/evidence-non-grok
exit_code=0
Evidence: Focused watcher behavior tests
ok - watch-session start/status/stop are scoped to one FM_HOME and never use broad pkill
ok - watch-session stop waits through delayed watcher lock startup
ok - watch-session stop fails closed for an unpinned legacy watcher
ok - watch-session delays failed and no-op re-arms but immediately re-arms after successful wakes
ok - watch-session status reports runner-window liveness, not inner arm health
ok - watch-session refuses to steal the Grok primary follower slot
ok - watch-session restart refuses Grok overlap before stopping the fallback
ok - watch-session emergency override remains available for Grok fallback

exit_code=0
Evidence: Aggregate behavior test log
ok: PR target repo jtinventory/firstmate verified
tmux 3.4
ok - gate refusal helper covers marker, empty marker, path backstop, and normal session
ok - spawn, send, and teardown refuse both gate signals before lifecycle work
ok - tracked gate-refusal wiring and trusted no-mistakes config are present
# all fm-gate-refuse tests passed
Running 90 behavior tests with 4 parallel job(s)
START: tests/fm-afk-inject-e2e.test.sh (TMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-afk-inject-e2e/tmp GOTMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-afk-inject-e2e/gotmp)
START: tests/fm-afk-inject-herdr-e2e.test.sh (TMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-afk-inject-herdr-e2e/tmp GOTMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-afk-inject-herdr-e2e/gotmp)
START: tests/fm-afk-launch.test.sh (TMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-afk-launch/tmp GOTMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-afk-launch/gotmp)
START: tests/fm-backend-herdr-presentation-e2e.test.sh (TMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-backend-herdr-presentation-e2e/tmp GOTMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-backend-herdr-presentation-e2e/gotmp)
PASS: tests/fm-afk-inject-e2e.test.sh
ok - Scenario A: partial input defers injection; digest arrives clean after idle
ok - Scenario B: swallowed Enter produces exactly one clean digest
ok - Scenario C: a normal captain status injects exactly one clean single-line sentinel digest
all e2e injection tests passed
PASS: tests/fm-afk-inject-herdr-e2e.test.sh
skip: set FM_HERDR_SMOKE=1 to opt into the real Herdr AFK e2e
PASS: tests/fm-afk-launch.test.sh
ok - AFK daemon survives harness process-group reap and stops on return
ok - AFK launch fails closed when the durable away flag cannot be created
ok - AFK launch is idempotent and return clears the away flag
ok - AFK launch forwards the resolved Herdr supervisor target and backend
ok - AFK refresh fails closed when the durable away flag cannot be written
ok - AFK return retains the away flag when identity verification fails
ok - AFK return retains the away flag when a live daemon record is missing
ok - AFK return retains the away flag when a stale daemon record remains live
ok - AFK status keeps a live unverified daemon visible
ok - AFK return fails closed when the durable away flag cannot be removed
ok - AFK transition lock rejects an invalid state path promptly
ok - AFK transition lock distinguishes I/O failure from contention
ok - AFK transition lock handoff preserves contention return code
ok - AFK transition lock bounds contention retries
ok - AFK transition lock preserves a replacement daemon and away flag
ok - AFK return clears the away flag after confirmed daemon absence
ok - AFK return retains the away flag when TERM does not stop the daemon
all fm-afk-launch tests passed
PASS: tests/fm-backend-herdr-presentation-e2e.test.sh
ok - real Herdr lab: flag-off spawn retains the Stage 1 Herdr command sequence with zero ordering calls
ok - real Herdr lab: every projected create, task-tab create, seeded prune, and move preserves active workspace and tab
warning: herdr presentation cleanup could not verify the exact pane; refusing focus-unsafe pane close
ok - real Herdr lab: active seeded-tab pruning refuses the exact pane and preserves exact focus
ok - real Herdr lab: bounded lock contention warns and falls back flat without projection or focus drift
ok - real Herdr lab: concurrent primary workers form one stable contiguous block without active workspace/tab drift
ok - real Herdr lab: forced workspace.move failure leaves a successful worker in default order with a warning and no cleanup
ok - real Herdr lab: concurrent post-create abort cleanup stays serialized with exact focus restoration
ok - real Herdr lab: Treehouse commands and metadata shape are byte-identical except for Herdr container IDs
ok - real Herdr lab: exact task-pane close restores the exact captain workspace/tab after Herdr's raw focus steal
ok - real Herdr lab: concurrent projected cleanup is serialized and leaves active workspace/tab unchanged
ok - real Herdr lab: three repeated concurrent create/order/cleanup waves have zero active workspace or tab drift
ok - real Herdr lab: primary presentation opt-in inherits into real secondmate homes
ok - real Herdr lab: primary and two secondmate homes each own a top-level contiguous child block
ok - real Herdr lab: concurrent primary/A/B spawns preserve parent order and exact focus
ok - real Herdr lab: session lock contention from a secondmate home falls back flat with no journal
ok - real Herdr lab: Hi Bit and Wheelhouse-style same-identity restarts reclaim one nested space with exact focus and idempotence
ok - real Herdr lab: secondmate restart binding and reclaim stay isolated to the exact child home and parent
ok - real Herdr lab: concurrent cross-home recoveries replace exact husks under one session lock with no focus drift
ok - real Herdr lab: legacy projection labels and flat secondmate tabs are left unmigrated
ok - real Herdr lab: multi-home exact-pane teardowns restore captain focus without workspace close authority
warning: no exact herdr presentation token match for missing1; leaving any stale space untouched and spawning flat
warning: no exact herdr presentation token match for renamed1; leaving any stale space untouched and spawning flat
warning: 2 exact herdr presentation token matches for duplicate1 are quarantined; inspecting only for duplicate-agent risk
warning: quarantined herdr presentation for duplicate1 is dead or agent-free; exact bound reclaim may proceed, otherwise spawning flat
warning: 2 exact herdr presentation token matches for duplicate1 are quarantined; inspecting only for duplicate-agent risk
error: quarantined herdr presentation for duplicate1 has a live pane; refusing duplicate launch
ok - real Herdr lab: missing, renamed, and duplicate tokens trigger zero destructive or adoptive calls, and live duplicate risk refuses launch
ok - real Herdr lab validation completed on Herdr 0.7.4 with the default-session tripwire intact
START: tests/fm-backend-herdr-prune-safety-e2e.test.sh (TMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-backend-herdr-prune-safety-e2e/tmp GOTMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-backend-herdr-prune-safety-e2e/gotmp)
START: tests/fm-backend-herdr-respawn-idem-e2e.test.sh (TMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-backend-herdr-respawn-idem-e2e/tmp GOTMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-backend-herdr-respawn-idem-e2e/gotmp)
START: tests/fm-backend-herdr-smoke.test.sh (TMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-backend-herdr-smoke/tmp GOTMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-backend-herdr-smoke/gotmp)
START: tests/fm-backend-herdr-workspace-per-home-e2e.test.sh (TMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-backend-herdr-workspace-per-home-e2e/tmp GOTMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-backend-herdr-workspace-per-home-e2e/gotmp)
PASS: tests/fm-backend-herdr-prune-safety-e2e.test.sh
skip: set FM_HERDR_E2E=1 to run the real prune-safety Herdr lab e2e
PASS: tests/fm-backend-herdr-respawn-idem-e2e.test.sh
skip: set FM_HERDR_E2E=1 to run the real respawn-idempotency Herdr lab e2e
PASS: tests/fm-backend-herdr-smoke.test.sh
skip: set FM_HERDR_SMOKE=1 to opt into the real Herdr smoke
PASS: tests/fm-backend-herdr-workspace-per-home-e2e.test.sh
skip: set FM_HERDR_E2E=1 to run the real workspace-per-home Herdr lab e2e
START: tests/fm-backend-herdr.test.sh (TMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-backend-herdr/tmp GOTMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-backend-herdr/gotmp)
START: tests/fm-backend-tmux-smoke.test.sh (TMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-backend-tmux-smoke/tmp GOTMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-backend-tmux-smoke/gotmp)
START: tests/fm-backend.test.sh (TMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-backend/tmp GOTMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-backend/gotmp)
START: tests/fm-backlog-audit.test.sh (TMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-backlog-audit/tmp GOTMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-backlog-audit/gotmp)
PASS: tests/fm-backend-herdr.test.sh
ok - fm_backend_herdr_version_check: accepts the current protocol (14)
ok - fm_backend_herdr_version_check: refuses an old protocol loudly
ok - fm_backend_herdr_version_check: refuses loudly when herdr is not instal

... [87869 bytes truncated] ...

ck is reclaimed
ok - live watcher lock with stale heartbeat is actionable
ok - guard banner leads when down with pending wakes (re-arm-after-drain) and stays silent when fresh+live
ok - concurrent fm_lock_try_acquire yields exactly one winner
ok - dead-pid stale lock is reclaimed by a single acquirer
ok - concurrent stale-lock steal yields exactly one winner
ok - live steal mutex is not reclaimed
ok - live-held lock is not stolen
ok - live-held lock with matching pid identity is not stolen
ok - live-held lock with mismatched pid identity is reclaimed
ok - live-held legacy identity remains protected during migration
ok - expired live-held legacy identity is reclaimed
ok - matching expired legacy watcher identity is migrated before expiry recovery
ok - live-held lock without pid identity remains live-held
ok - zombie follower lock is reclaimed using its stored process identity
ok - legacy zombie follower lock is reclaimed without new metadata
ok - fallback process identity includes stable process-group and command data
ok - fallback start identity accepts and distinguishes prior formats
ok - detach cleanup rejects legacy start tokens
ok - detached spawn waits for the target after exec
ok - detached spawn cleans the launcher after pid-file timeout
ok - detached spawn cleans the target after exec timeout
ok - legacy follower locks without home scope fail closed
ok - watcher health rejects a zombie lock owner
ok - watcher health rejects an unpinned legacy lock
ok - empty mid-acquire lock keeps a minimum grace
ok - late original claimant cannot claim a recreated lock
ok - paused mid-acquire claimant backs off to active stealer
ok - watch restart refuses to signal a reused pid
ok - plain arm fails closed on a reused-pid lock before PR-check migration
ok - watcher self-evicts when the lock pid no longer names it
ok - arm attaches to a live fresh watcher and exits only when that cycle ends
ok - arm migrates and attaches to a live legacy watcher lock
ok - arm rejects an unverified legacy watcher lock
ok - arm starts+confirms a fresh watcher on a clean lock and self-heals a dead-pid lock (never healthy off a dead pid)
ok - arm stands down on HUP while the detached watcher keeps its lock and beacon
Terminated
ok - watcher survives SIGTERM of the arm's entire process group
ok - a healthy cycle keeps one attach waiter and duplicate arms exit without stacking
ok - restart hands off the follower slot without stacking waiters
ok - process start identity distinguishes same-second processes
ok - arm propagates an immediate watcher wake before confirmation
ok - arm attaches to a peer watcher after child stands down and exits when peer dies
ok - arm reports FAILED and exits non-zero when no fresh watcher can be confirmed
PASS: tests/fm-watcher-protocol.test.sh
ok - legacy watcher creates a durable pending-reply fence
ok - protocol restart stays fenced until tracked re-arm
ok - plain arm performs verified legacy primary takeover
ok - nested pending-reply gate validates child owner scope
ok - protocol restart refuses AFK and preserves X cadence
ok - verified AFK daemon ownership satisfies protocol gate
# all watcher protocol tests passed
START: tests/fm-x-mode.test.sh (TMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-x-mode/tmp GOTMPDIR=/tmp/fm-behavior-tests.z5eVGp/fm-x-mode/gotmp)
START: tests/no-mistakes-required-workflow.test.sh (TMPDIR=/tmp/fm-behavior-tests.z5eVGp/no-mistakes-required-workflow/tmp GOTMPDIR=/tmp/fm-behavior-tests.z5eVGp/no-mistakes-required-workflow/gotmp)
PASS: tests/fm-x-mode.test.sh
ok - fm-x-poll is a hard no-op without a token (inert default)
ok - fm-x-poll treats an explicitly empty env token as configured
ok - fm-x-poll stays silent on HTTP 204 (the common case)
ok - fm-x-poll lets an explicitly empty relay env override .env
ok - fm-x-poll surfaces auth/config errors once and clears on recovery
ok - fm-x-poll stashes the question and prints the compact marker
ok - fm-x-poll preserves in_reply_to conversation context in the inbox
ok - fm-x-poll reports inbox commit failures without emitting a mention wake
ok - fm-x-poll requires a non-empty question before waking
ok - fm-x-poll rejects an unsafe request_id (path-traversal guard)
ok - fm-x-reply posts a request-bound answer and echoes only the request_id
ok - fm-x-reply accepts the reply via --text-file and stdin (safe, unexpanded)
ok - fm-x-reply exits non-zero on a non-2xx relay response
ok - fm-x-reply cleans up auth header temp files on interrupted posts
ok - fm-x-reply rejects missing arguments with a usage error
ok - fm-x-reply --help makes image support discoverable
ok - fm-x-reply rejects whitespace-only reply text
ok - fm-x-reply dry-run records the would-be reply and never posts
ok - fm-x-reply dry-run works without a token
ok - fm-x-reply honors FMX_DRY_RUN from .env
ok - fm-x-reply lets an explicitly empty dry-run env override .env
ok - fm-x-reply dry-run fails when it cannot record the preview
ok - fmx_split_thread: word-boundary, within-limit, numbered, lossless, capped
ok - fm-x-reply keeps a concise reply as a single unnumbered tweet
ok - fm-x-reply auto-splits a long reply into a numbered thread (texts[])
ok - fm-x-reply clamps a below-floor max to 50 characters
ok - fm-x-reply posts a thread payload (texts[]) to the relay
ok - fm-x-reply --image posts an image object on answer
ok - fm-x-reply streams large image payloads outside curl argv
ok - fm-x-reply dry-run records compact image metadata for threaded replies
ok - fm-x-reply cleans image and payload temp files
ok - fm-x-reply --image rejects missing and unsupported image paths clearly
ok - fm-x-reply --image rejects oversized files before encoding
ok - fm-x-reply --followup posts to /connector/followup with the same request-bound body
ok - fm-x-reply --followup --image posts an image object
ok - fm-x-reply --followup is accepted in any position and leaves the answer path default
ok - fm-x-reply --followup dry-run marks the endpoint without changing the answer path
ok - fm-x-reply --followup auto-splits a long follow-up into a marked thread
ok - fm-x-reply followup dry-run keeps endpoint marker and compact image metadata
ok - fm-x-dismiss posts a request-bound dismiss and echoes only the request_id
ok - fm-x-dismiss dry-run records the would-be body and never posts
ok - fm-x-dismiss dry-run works without a token
ok - fm-x-dismiss exits non-zero on a non-2xx relay response
ok - fm-x-dismiss exits non-zero on a transport failure
ok - fm-x-dismiss rejects an unsafe request_id (path-traversal guard)
ok - fm-x-dismiss rejects missing or extra arguments with a usage error
ok - fm-x-link records and refreshes the X-request link without disturbing meta
ok - meta rewrites are independent of TMPDIR
ok - fm-x-link rejects unsafe ids, missing meta, and missing arguments
ok - fm-x-followup --check reports postable / not-linked correctly
ok - fm-x-followup --check prunes a link past the 24h window
ok - fm-x-followup posts the follow-up and clears the link on success
ok - fm-x-followup --image forwards the attachment through fm-x-reply --followup
ok - fm-x-followup keeps the link when the post fails
ok - fm-x-followup skips silently and clears the link past the 24h window
ok - fm-x-followup is a no-op for a task with no X link
ok - fm-x-followup dry-run records the follow-up and clears the link
ok - fm-x-followup rejects malformed invocations
ok - bootstrap activates X mode from an .env token, idempotently
ok - bootstrap reports missing X-mode dependencies before arming
ok - bootstrap does not report X mode on when activation artifacts cannot be written
ok - bootstrap is inert without a non-empty .env token (non-X users unaffected)
ok - bootstrap cleans up X artifacts on opt-out and is silent once off
ok - bootstrap reports failed X artifact cleanup on opt-out
PASS: tests/no-mistakes-required-workflow.test.sh
ok - fixed-head signed opened, unsigned edited, signed edited yields 0/1/0
ok - body event groups are distinct while head changes remain coalesced
ok - run names expose monotonic numbers and immutable IDs
ok - fork, permission, check-name, marker, and bot-exemption contracts are preserved
All 90 behavior tests passed

exit_code=0

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed ✅
  • ⚠️ docs/supervision-protocols/grok.md:28 - The badge table treats any watcher: started/attached line as proof of a tracked Grok task, but the tmux fallback also prints those inner-arm lines. Clarify that the badge mapping applies only to output from Grok's tracked background call, and mark fallback arm output as non-badge.
  • ⚠️ tests/fm-watch-session.test.sh:260 - Coverage tests only the start refusal and override. Add a restart case proving refusal happens before stop_runner and leaves the existing fallback intact.

🔧 Fix: Clarified Grok badge provenance; added restart refusal coverage
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bash bin/fm-run-behavior-tests.sh
  • bash tests/fm-watch-session.test.sh
  • bash tests/fm-watch-session.test.sh (rerun)
  • Manual private-tmux CLI verification of Grok refusal, override, restart preservation, and non-Grok fallback
  • bash bin/fm-run-behavior-tests.sh
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@JTInventory
JTInventory merged commit 7aa07ab into main Jul 31, 2026
6 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant