Skip to content

feat(supervision): add resting vs active state for secondmate in-flight counting#391

Open
Freudator86 wants to merge 5 commits into
kunchenguid:mainfrom
Freudator86:fm/secondmate-rest-q4
Open

feat(supervision): add resting vs active state for secondmate in-flight counting#391
Freudator86 wants to merge 5 commits into
kunchenguid:mainfrom
Freudator86:fm/secondmate-rest-q4

Conversation

@Freudator86

Copy link
Copy Markdown

Intent

Add a 'resting vs active' supervision state for persistent secondmates, so a resting secondmate does not count as in-flight work for stop-hook/turnend-guard supervision, while an active one still does. Background: state/.meta records kind=secondmate for persistent secondmates; previously ANY secondmate meta counted as in-flight for supervision purposes exactly like an ordinary crewmate/scout task, so a secondmate that is deliberately idle-but-alive still blocked the primary's turn-end whenever there was no fresh watcher beacon. This is purely a supervision-COUNTING change, not a lifecycle change: persistent secondmates/bosuns stay alive regardless of this field. Implementation: bin/fm-supervision-lib.sh's fm_supervision_status (the shared predicate used by both fm-guard.sh and fm-turnend-guard.sh) now reads kind= and supervision= from each state/*.meta file via a new fm_sup_resting_secondmate helper, and skips a meta file only when it is BOTH kind=secondmate AND supervision=resting when computing FM_SUP_IN_FLIGHT. supervision=active and legacy meta with no supervision= field at all (the pre-existing case) still count exactly as before - this is a deliberate safe default so pre-existing secondmates are never silently dropped from supervision. Ordinary crewmate/scout meta (kind=ship, kind=scout, or no kind field) is completely unaffected. dormant is a distinct, pre-existing, unrelated concept (a registry-only entry in data/secondmates.md with no live state/.meta at all) and this change does not touch or conflate it. Deliberately out of scope (per explicit instruction from the task brief): no command or plumbing was added to actually set/toggle supervision=resting on a live secondmate - that lifecycle mechanism is left for a future task; this change only wires the counting predicate and documents the field. bin/fm-turnend-guard.sh itself was deliberately NOT modified: the predicate change in fm-supervision-lib.sh alone was sufficient to fix its behavior (verified by a new passing test), so per the brief's explicit guidance the hook script was left untouched to keep the change minimal. Secondmate liveness probing and respawn behavior in bin/fm-bootstrap.sh (SECONDMATE_LIVENESS sweep) is unrelated to this predicate and was verified unaffected by supervision=resting via a new test. Tests were written first (TDD), confirmed failing against the old predicate, then the implementation was added and confirmed passing. Added tests to the existing tests/fm-turnend-guard.test.sh (predicate-level: resting excluded, active counts, legacy counts, ordinary crewmate/scout unaffected, a resting secondmate plus real co-located in-flight work still counts; hook-level: turn-end guard silent with only a resting secondmate in flight, still blocks with an active one) and one new test in tests/fm-secondmate-liveness.test.sh proving the liveness/respawn sweep is unaffected by supervision=resting. Documentation: one clause added to AGENTS.md's existing state/.meta field-list line (section 2) describing the new optional supervision= field, one new sentence in AGENTS.md section 8 cross-referencing it, and docs/turnend-guard.md's 'Shared Predicate' and 'Tests' sections updated to describe the exception and the new test coverage - all one-line pointers per this repo's one-owner/inline-stub documentation conventions (loaded the firstmate-coding-guidelines skill before making these changes, per this repo's own CONTRIBUTING.md). No yolo: this is the core firstmate-core template repo, and PR merge decisions belong to the upstream maintainer (the captain), never auto-merged by me.

What Changed

  • bin/fm-supervision-lib.sh's fm_supervision_status (shared by fm-guard.sh and fm-turnend-guard.sh) now reads kind=/supervision= from each state/*.meta via a new fm_sup_resting_secondmate helper, excluding a meta only when it is both kind=secondmate and supervision=resting from FM_SUP_IN_FLIGHT; supervision=active, legacy meta with no supervision= field, and ordinary crewmate/scout meta all still count exactly as before.
  • Added predicate- and hook-level test coverage to tests/fm-turnend-guard.test.sh (resting excluded, active/legacy counts, ordinary crewmate/scout unaffected, resting-secondmate-plus-real-in-flight-work still counts, silent-vs-blocking hook behavior) and a new test in tests/fm-secondmate-liveness.test.sh proving the liveness/respawn sweep is unaffected by supervision=resting; also fixed portable test assertions in tests/fm-session-start.test.sh (mkdir wording, leaked CLAUDECODE env var) and a shellcheck SC1007 warning in the same file.
  • Documented the new optional state/<id>.meta supervision= field and the resting/active exception in AGENTS.md, CONTRIBUTING.md, docs/architecture.md, docs/scripts.md, and docs/turnend-guard.md.

Risk Assessment

✅ Low: Small, well-scoped change to a single shared predicate function with a safe default (absent/non-"resting" values still count as in-flight), backed by thorough new tests covering the predicate, the hook-level behavior, and non-interference with the unrelated liveness/respawn sweep; docs and AGENTS.md are updated consistently with the code.

Testing

Ran the full portable test suite (51 files via tests/*.test.sh) with tmux 3.6, and it passed end-to-end with a clean worktree afterward; targeted runs of tests/fm-turnend-guard.test.sh and tests/fm-secondmate-liveness.test.sh confirm the new fm_sup_resting_secondmate behavior end-to-end: a kind=secondmate meta with supervision=resting is excluded from FM_SUP_IN_FLIGHT (predicate) and does not block the turn-end hook when it's the only in-flight entry, while supervision=active or legacy meta with no field still counts and still blocks the hook exactly as before; a separate test confirms the secondmate liveness/respawn sweep in fm-bootstrap.sh is unaffected by supervision=resting (a dead resting secondmate is still killed and respawned). Ordinary crewmate/scout meta (kind=ship/kind=scout) is unaffected in all cases.

Evidence: fm-turnend-guard.test.sh output (predicate + hook resting-vs-active cases)
ok - fm_supervision_unhealthy: false with no state/*.meta at all
ok - fm_supervision_unhealthy: true with in-flight task and no beacon ever
ok - fm_supervision_unhealthy: true with in-flight task and a beacon far outside the grace window
ok - fm_supervision_unhealthy: false with in-flight task and a fresh beacon
ok - fm_supervision_status: FM_SUP_QUEUE_PENDING tracks state/.wake-queue
ok - fm_supervision_status: kind=secondmate supervision=resting is excluded from in-flight count
ok - fm_supervision_status: kind=secondmate supervision=active still counts as in-flight
ok - fm_supervision_status: legacy kind=secondmate meta with no supervision= field still counts as in-flight
ok - fm_supervision_status: ordinary crewmate/scout meta counts unchanged (no regression)
ok - fm_supervision_status: real in-flight work still counts regardless of a co-located resting secondmate's own supervision= value
ok - fm-turnend-guard: silent no-op with nothing in flight
ok - fm-turnend-guard: blocks when a fresh beacon has no live watcher lock
ok - fm-turnend-guard: blocks on a dead watcher lock even when the beacon is fresh
ok - fm-turnend-guard: silent no-op with a live watcher lock and fresh beacon
ok - fm-turnend-guard: blocks on a live watcher lock with an ancient beacon
ok - fm-turnend-guard: blocks with the exact required reason in the primary when unhealthy
ok - fm-turnend-guard: does not block turn-end when only resting secondmates are in flight
ok - fm-turnend-guard: still blocks turn-end for an active secondmate exactly as before
ok - fm-turnend-guard: blocks from active FM_HOME state, not only repo-root state
ok - fm-turnend-guard: X-mode repair reason sources the cadence config
ok - fm-turnend-guard: ignores stale repo-root state when FM_HOME is set
ok - fm-turnend-guard: uses FM_STATE_OVERRIDE ahead of FM_HOME/state
ok - fm-turnend-guard: stop_hook_active=true always allows the stop (never blocks twice in one turn)
ok - fm-turnend-guard: inert in a secondmate home (.fm-secondmate-home marker present) even when unhealthy
ok - fm-turnend-guard: inert in a crewmate/scout task worktree (linked git worktree) even when unhealthy
ok - fm-turnend-guard: fails open (never blocks) when jq is missing
ok - fm-turnend-guard: silent no-op on empty stdin
ok - fm-turnend-guard: runs well under the generous timing margin (0s)
ok - fm-turnend-guard-grok: forces one same-session resume when the shared predicate blocks
ok - fm-turnend-guard-grok: loop guard prevents a nested resume loop
ok - .claude/settings.json: Stop hook uses CLAUDE_PROJECT_DIR-anchored command
ok - .codex/hooks.json: Stop hook invokes the shared primary guard
ok - .codex/hooks.json: Stop hook uses hook process root when payload cwd is outside
ok - .codex/hooks.json: Stop hook ignores nested git root guard scripts
ok - .opencode primary plugin: session.idle forces one follow-up through the shared guard
ok - .opencode primary plugin: guard path is anchored to worktree, not directory
ok - .pi primary extension: turn_end forces one follow-up through the shared guard
ok - .grok primary hook: Stop hook invokes the grok adapter
Evidence: fm-secondmate-liveness.test.sh output (liveness sweep unaffected by supervision=resting)
ok - fm_backend_tmux_agent_alive: alive/dead/unknown classification
ok - fm_backend_herdr_agent_alive: dead/no-agent->dead, live->alive, unknown->unknown
ok - fm_backend_agent_alive: routes tmux/herdr correctly, unknown for an unverified backend
ok - sweep: a confirmed-dead secondmate endpoint is killed and respawned
ok - sweep: an already-live secondmate is left untouched (no kill, no respawn)
ok - sweep: a transient/unknown probe reading is reported but never acted on
ok - sweep: an unverified harness makes a dead-looking probe inconclusive
ok - sweep: idempotent by construction - a live secondmate is never re-touched on a later run
ok - sweep: skipped entirely under FM_BOOTSTRAP_DETECT_ONLY=1, exactly like the other mutating sweeps
ok - sweep: liveness probe and respawn behavior are unaffected by supervision=resting
ok - sweep: a silent no-op with no kind=secondmate meta present (a secondmate home's own natural scoping)
# all fm-secondmate-liveness tests passed
Evidence: Full test suite run (all 51 tests/*.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
skip: herdr not found
skip: herdr not found
skip: cmux CLI not found on PATH or at the bundle path
ok - fm_backend_cmux_version_check: accepts the verified minimum (0.64.17)
ok - fm_backend_cmux_version_check: accepts a newer version (0.70.0)
ok - fm_backend_cmux_version_check: refuses an old version loudly
ok - fm_backend_cmux_version_check: refuses loudly when cmux is not found on PATH or at the bundle path
ok - fm_backend_cmux_password: reads the first non-empty line of config/cmux-socket-password
ok - fm_backend_cmux_password: preserves spaces and tabs in config/cmux-socket-password
ok - fm_backend_cmux_password: respects FM_CONFIG_OVERRIDE
ok - fm_backend_cmux_password: empty when config/cmux-socket-password is absent
ok - fm_backend_cmux_cli: exports CMUX_SOCKET_PASSWORD when config/cmux-socket-password is set
ok - fm_backend_cmux_parse_target: splits '<workspace_uuid>:<surface_uuid>' on the first colon
ok - fm_backend_cmux_normalize_key: Enter/Escape/C-c map to cmux's verified enter/escape/ctrl-c
ok - fm_backend_cmux_scoped_title: scopes a primary task title with firstmate plus root hash
ok - fm_backend_cmux_scoped_title: scopes a secondmate task title with the home marker plus root hash
ok - fm_backend_cmux_scoped_title: includes the resolved FM_ROOT hash in the home label
ok - fm_backend_validate: cmux is a known backend
ok - fm_backend_busy_state: cmux (no native primitive) always reports unknown, same as tmux/zellij/orca
ok - fm_backend_composer_state: routes cmux to the cmux composer classifier
ok - fm_backend_cmux_ping_state: reports 'ok' on PONG
ok - fm_backend_cmux_ping_state: reports 'denied' when socketControlMode=cmuxOnly rejects the connection
ok - fm_backend_cmux_ping_state: reports 'unauth' when password mode rejects a missing/wrong password
ok - fm_backend_cmux_ping_state: reports 'unauth' when password mode rejects a wrong password (Invalid password)
ok - fm_backend_cmux_ping_state: reports 'down' when the app is not running yet
ok - fm_backend_cmux_ensure_running: returns immediately when cmux is already reachable
ok - fm_backend_cmux_ensure_running: fails fast on a denied socket without attempting to launch, naming every viable mode
ok - fm_backend_cmux_ensure_running: fails fast on an unauthenticated socket, naming the password config and the Automation mode alternative
ok - fm_backend_cmux_create_task: refuses a duplicate workspace title (cmux's own new-workspace has no uniqueness check)
ok - fm_backend_cmux_create_task: creates a workspace and parses workspace_id/surface_id from list responses
ok - fm_backend_cmux_target_ready: fails when the workspace/surface is not found (list-panes structural check)
ok - fm_backend_cmux_target_ready: verifies the workspace title against the expected label first
ok - fm_backend_cmux_target_ready: rejects a workspace id reused under a different title
ok - fm_backend_cmux_capture: fetches generously and trims to N lines locally
ok - fm_backend_cmux_capture: propagates a read-screen failure even when stdout is empty
ok - fm_backend_cmux_capture: fails when the target surface is absent
ok - fm_backend_cmux_send_key: normalizes the key (Escape -> escape) and targets the explicit workspace/surface
ok - fm_backend_cmux_send_key: recovers stale workspace/surface ids by expected label
ok - fm_backend_cmux_send_literal: calls send with an explicit workspace/surface and a -- separator
ok - fm_backend_cmux_current_path: actively probes with marked begin/end lines (zellij-shape frozen cwd)
ok - fm_backend_cmux_composer_state: a bare '❯' composer row reads empty
ok - fm_backend_cmux_composer_state: the ghost placeholder text reads empty, not pending
ok - fm_backend_cmux_composer_state: real composer text reads pending
ok - fm_backend_cmux_composer_state: a slash-command popup's argument-hint placeholder still reads pending (the incident fix)
ok - fm_backend_cmux_composer_state: reports unknown when the surface cannot be captured
ok - fm_backend_cmux_composer_state: reports unknown when no border-delimited composer row is found
ok - fm_backend_cmux_send_text_submit: reports 'empty' once the composer row reads empty after one Enter
ok - fm_backend_cmux_send_text_submit: reports 'pending' when the composer never clears after retried Enters (swallowed)
ok - fm_backend_cmux_send_text_submit: retries past a popup-placeholder-fill Enter and lands the real second Enter (the incident fix)
ok - fm_backend_cmux_send_text_submit: reports 'send-failed' when the target workspace/surface is absent
ok - fm_backend_cmux_kill: closes the whole task workspace directly
ok - fm_backend_cmux_kill: never fails even when close-workspace fails
ok - fm_backend_cmux_kill: recovers stale workspace/surface ids by expected label
ok - fm_backend_cmux_list_live: lists only this home's scoped task workspaces using plain fm-<id> labels
ok - fm-spawn.sh: refuses backend=cmux for --secondmate spawns (mirrors Orca's refusal; no secondmate launch design exists yet)
skip: herdr not found
skip: herdr not found
skip: herdr not found
skip: herdr not found
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 installed
ok - fm_backend_herdr_workspace_label: a primary home (no marker) resolves to 'firstmate'
ok - fm_backend_herdr_workspace_label: a secondmate home (.fm-secondmate-home) resolves to '2ndmate-<id>'
ok - fm_backend_herdr_workspace_label: trims whitespace around the marker's secondmate id
ok - fm_backend_herdr_workspace_label: an empty marker file falls back to the primary label 'firstmate'
ok - fm_backend_herdr_workspace_label: two different secondmate homes get two different, non-colliding labels
ok - fm_backend_herdr_cli: sets HERDR_SESSION AND appends a trailing --session flag on every call
ok - fm_backend_herdr_container_ensure: version-gates, starts the server, ensures the firstmate workspace, echoes session:workspace_id + the seeded default tab id
ok - fm_backend_herdr_container_ensure: reuses an existing firstmate workspace without recreating it, and reports no seeded default tab (adopted, not created)
ok - fm_backend_herdr_container_ensure: workspace create passes --no-focus
ok - fm_backend_herdr_container_ensure: creates the workspace under the SECONDMATE home's own label, not 'firstmate'
ok - fm_backend_herdr_create_task: prunes exactly the seeded default tab container_ensure identified, once the first real task tab exists
ok - herdr repeated spawn/teardown: one persistent firstmate workspace reused, zero orphans, default tab pruned, create ran once
ok - fm_backend_herdr_create_task: an ADOPTED workspace's pre-existing tab is never pruned (the created-vs-adopted gate)
ok - fm_backend_herdr_create_task: the label-collision startup-workspace scenario (2026-07-02 incident) leaves the captain's live tab untouched
ok - fm_backend_herdr_workspace_prune_seeded_default_tab: refuses to close the seeded default tab when its pane reports a working agent (defense in depth)
ok - no bin/ jq filter names a --arg/--argjson variable after a jq reserved keyword
ok - fm_backend_herdr_create_task: refuses a duplicate tab label (herdr's own tab create has no uniqueness check)
ok - fm_backend_herdr_create_task: a same-labeled tab with a live (even idle) registered agent still refuses exactly as before
ok - fm_backend_herdr_create_task: scans every same-labeled tab and refuses if any duplicate is live
ok - fm_backend_herdr_create_task: closes and replaces a same-labeled tab whose pane is dead (pane_not_found)
ok - fm_backend_herdr_create_task: closes and replaces a same-labeled tab whose pane is alive but hosts no registered agent (a restored plain shell)
ok - fm_backend_herdr_create_task: closes every confirmed same-labeled husk only after creating the replacement
ok - fm_backend_herdr_create_task: refuses 

... [53021 bytes truncated] ...

 sitting on a terminal status is surfaced (queue + exit)
ok - a stale terminal-looking status is overridden and absorbed while a run is actively working, then wedge-escalated
ok - provably-working non-terminal stale is absorbed on first sight, then wedge-escalated past the threshold
ok - consecutive wedge escalations on the same pane accumulate and demand deep inspection at the threshold
ok - a pane becoming active again resets the consecutive wedge-escalation counter
ok - a not-provably-working non-terminal stale is surfaced immediately (never left to wait out the timer)
ok - matching non-terminal stale suppressors repair missing or corrupt stale-since timers
ok - triage log capping handles wc byte counts with leading spaces
ok - a heartbeat with no captain-relevant change is absorbed and backs off the cadence
ok - heartbeat backstop fail-safe surfaces a captain-relevant status the per-wake path missed
ok - the liveness beacon stays fresh while the watcher absorbs benign wakes (fm-guard never false-alarms)
ok - with .afk present the watcher reverts to one-shot so the daemon owns triage (no double-triage)
ok - simultaneous watcher starts leave exactly one live process
ok - fm_pid_identity is locale-invariant across LC_ALL/LC_TIME
ok - killed watcher stale lock 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
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 - 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 - watch restart reports a healthy peer without attaching to it
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 starts+confirms a fresh watcher on a clean lock and self-heals a dead-pid lock (never healthy off a dead pid)
ok - arm cleans child watcher and temp output on HUP
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
watcher: lock held by live pid 815806 but heartbeat is stale for 836951829s (>300s); inspect or stop that watcher before re-arming.
ok - arm reports FAILED and exits non-zero when no fresh watcher can be confirmed
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, fence-aware, 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 uses the Discord inbox platform budget instead of the X tweet budget
ok - fm-x-reply keeps numeric X requests on the X tweet budget
ok - fm-x-reply prefers an explicit relay-provided reply limit
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 --followup posts to /connector/followup with the same request-bound body
ok - fm-x-reply maps a followup_unavailable follow-up 409 to exit 9
ok - fm-x-reply maps every follow-up 409 to exit 9 even without the marker
ok - fm-x-reply treats answer-endpoint 409 as a generic failure
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 - fm-x-link records Discord platform context so follow-ups keep the Discord budget
ok - fm-x-link paired carry flags preserve a prior task's follow-up binding onto a successor
ok - fm-x-link recovery relink preserves Discord platform context after inbox drain
ok - fm-x-link rejects malformed or unpaired carry flags
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 7-day window
ok - fm-x-followup --check prunes a link that already reached the follow-up cap
ok - fm-x-followup posts a follow-up, increments the counter, and keeps the link under the cap
ok - fm-x-followup --final clears the link after one post regardless of the remaining count
ok - fm-x-followup clears the link once the third follow-up reaches the cap
ok - fm-x-followup --image forwards the attachment through fm-x-reply --followup
ok - fm-x-followup keeps the link and counter when the post fails
ok - fm-x-followup tombstones the link when a post-success counter write fails
ok - fm-x-followup treats a relay cap/window rejection as an already-exhausted link, not a retry
ok - fm-x-followup skips silently and clears the link past the 7-day 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 increments the counter, keeping the link
ok - fm-x-followup dry-run --final clears the link just as a live post would
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
- Outcome: 🔧 1 issue found → auto-fixed (2) ✅ across 3 runs (58m45s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

🔧 **Test** - 1 issue found → auto-fixed (2) ✅
  • 🚨 tests failed with exit code 1
  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"

🔧 Fix: Fix portable test assertions for mkdir wording and leaked CLAUDECODE env var
1 error still open:

  • 🚨 tests failed with exit code 1
  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"

🔧 Fix: placeholder
✅ Re-checked - no issues remain.

  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"
  • bash tests/fm-turnend-guard.test.sh (38 assertions incl. resting/active/legacy predicate cases and hook-level silent-vs-block cases)
  • bash tests/fm-secondmate-liveness.test.sh (11 assertions incl. sweep-unaffected-by-supervision=resting)
  • for t in tests/*.test.sh; do bash &#34;$t&#34;; done full 51-file suite, exit code 0
  • git status --short before and after testing to confirm a clean worktree
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@Freudator86 Freudator86 force-pushed the fm/secondmate-rest-q4 branch 2 times, most recently from 3d5dade to 86dc12f Compare July 9, 2026 22:42
@kunchenguid

kunchenguid commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Thanks for the PR! This branch currently has a merge conflict with the base branch.

When you get a chance, please rebase onto (or merge) the latest base branch, resolve the conflict, and push. After that, checks will re-run and the PR will get looked at again.

Noted for firstmate#391 at 86dc12fc.

@Freudator86 Freudator86 force-pushed the fm/secondmate-rest-q4 branch from 86dc12f to e832ee4 Compare July 10, 2026 11:33
@kunchenguid

kunchenguid commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Thanks for the PR! This branch currently has a merge conflict with the base branch.

When you get a chance, please rebase onto (or merge) the latest base branch, resolve the conflict, and push. After that, checks will re-run and the PR will get looked at again.

Noted for firstmate#391 at e832ee4c.

…ht counting

A persistent secondmate's own state/<id>.meta counted as in-flight for
stop-hook/turnend-guard supervision exactly like a crewmate/scout, so a
secondmate that is deliberately idle-but-alive still blocked turn-end
whenever its supervising primary had no fresh watcher beacon. kind=secondmate
meta can now carry supervision=resting to exclude it from that count while
staying fully alive; supervision=active and legacy meta with no field still
count as before. Liveness probing/respawn and dormant registry entries are
untouched - this is purely a supervision-counting change.
@Freudator86 Freudator86 force-pushed the fm/secondmate-rest-q4 branch from e832ee4 to 54f8974 Compare July 10, 2026 17:43
@kunchenguid

kunchenguid commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Thanks for the PR! This branch currently has a merge conflict with the base branch.

When you get a chance, please rebase onto (or merge) the latest base branch, resolve the conflict, and push. After that, checks will re-run and the PR will get looked at again.

Noted for firstmate#391 at 54f89744.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants